// OmegaUpload Web Frontend // Copyright (C) 2021 Edward Shen // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . use std::io::Cursor; use std::sync::Arc; use gloo_console::log; use js_sys::{Array, Uint8Array}; use omegaupload_common::crypto::{open_in_place, Error, Key}; use omegaupload_common::secrecy::{Secret, SecretVec}; use serde::Serialize; use wasm_bindgen::JsCast; use web_sys::{Blob, BlobPropertyBag}; #[derive(Clone, Serialize)] pub struct ArchiveMeta { name: String, file_size: u64, } #[derive(Clone)] pub enum DecryptedData { String(Arc), Blob(Arc), Image(Arc, usize), Audio(Arc), Video(Arc), Archive(Arc, Vec), } fn now() -> f64 { web_sys::window() .expect("should have a Window") .performance() .expect("should have a Performance") .now() } pub struct MimeType(pub String); pub fn decrypt( mut container: Vec, key: &Secret, maybe_password: Option>, name_hint: Option<&str>, ) -> Result<(DecryptedData, MimeType), Error> { open_in_place(&mut container, key, maybe_password)?; let mime_type = guess_mime_type(name_hint, &container); log!("[rs] Mime type:", mime_type); log!("[rs] Blob conversion started."); let start = now(); let blob_chunks = Array::new_with_length(container.chunks(65536).len().try_into().unwrap()); for (i, chunk) in container.chunks(65536).enumerate() { let array = Uint8Array::new_with_length(chunk.len().try_into().unwrap()); array.copy_from(chunk); blob_chunks.set(i.try_into().unwrap(), array.dyn_into().unwrap()); } let mut blob_props = BlobPropertyBag::new(); blob_props.type_(mime_type); let blob = Arc::new( Blob::new_with_u8_array_sequence_and_options(blob_chunks.dyn_ref().unwrap(), &blob_props) .unwrap(), ); log!(format!( "[rs] Blob conversion completed in {}ms", now() - start )); let data = match container.content_type() { ContentType::Text => DecryptedData::String(Arc::new( // SAFETY: ContentType::Text is guaranteed to be valid UTF-8. unsafe { String::from_utf8_unchecked(container) }, )), ContentType::Image => DecryptedData::Image(blob, container.len()), ContentType::Audio => DecryptedData::Audio(blob), ContentType::Video => DecryptedData::Video(blob), ContentType::ZipArchive => handle_zip_archive(blob, container), ContentType::Gzip => handle_gzip(blob, container), ContentType::Unknown => DecryptedData::Blob(blob), }; Ok((data, MimeType(mime_type.to_owned()))) } fn handle_zip_archive(blob: Arc, container: Vec) -> DecryptedData { let mut entries = vec![]; let cursor = Cursor::new(container); if let Ok(mut zip) = zip::ZipArchive::new(cursor) { for i in 0..zip.len() { match zip.by_index(i) { Ok(file) => entries.push(ArchiveMeta { name: file.name().to_string(), file_size: file.size(), }), Err(err) => match err { zip::result::ZipError::UnsupportedArchive(s) => { log!("Unsupported: ", s.to_string()); } _ => { log!(format!("Error: {err}")); } }, } } } entries.sort_by(|a, b| a.name.cmp(&b.name)); DecryptedData::Archive(blob, entries) } fn handle_gzip(blob: Arc, container: Vec) -> DecryptedData { let mut entries = vec![]; let cursor = Cursor::new(container); let gzip_dec = flate2::read::GzDecoder::new(cursor); let mut archive = tar::Archive::new(gzip_dec); if let Ok(files) = archive.entries() { for file in files.flatten() { let file_path = if let Ok(file_path) = file.path() { file_path.display().to_string() } else { "".to_string() }; entries.push(ArchiveMeta { name: file_path, file_size: file.size(), }); } } if entries.is_empty() { DecryptedData::Blob(blob) } else { DecryptedData::Archive(blob, entries) } } fn guess_mime_type(name_hint: Option<&str>, data: &[u8]) -> &'static str { if let Some(name) = name_hint { let guesses = mime_guess::from_path(name); if let Some(mime_type) = guesses.first_raw() { // Found at least one, but generally speaking this crate only // uses authoritative sources (RFCs), so generally speaking // there's only one association, and multiple are due to legacy // support. As a result, we can probably just get the first one. log!("[rs] Mime type inferred from extension."); return mime_type; } log!("[rs] No mime type found for extension, falling back to introspection."); } tree_magic_mini::from_u8(data) } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] enum ContentType { Text, Image, Audio, Video, ZipArchive, Gzip, Unknown, } trait ContentTypeExt { fn mime_type(&self) -> &str; fn content_type(&self) -> ContentType; } impl> ContentTypeExt for T { fn mime_type(&self) -> &str { tree_magic_mini::from_u8(self.as_ref()) } fn content_type(&self) -> ContentType { let mime_type = self.mime_type(); // check image first; tree magic match_u8 matches SVGs as plain text if mime_type.starts_with("image/") // application/x-riff is WebP || mime_type == "application/x-riff" { ContentType::Image } else if tree_magic_mini::match_u8("text/plain", self.as_ref()) { if std::str::from_utf8(self.as_ref()).is_ok() { ContentType::Text } else { ContentType::Unknown } } else if mime_type.starts_with("audio/") { ContentType::Audio } else if mime_type.starts_with("video/") // application/x-matroska is mkv || mime_type == "application/x-matroska" { ContentType::Video } else if mime_type == "application/zip" { ContentType::ZipArchive } else if mime_type == "application/gzip" { ContentType::Gzip } else { ContentType::Unknown } } } #[cfg(test)] mod content_type { use super::*; macro_rules! test_content_type { ($($name:ident, $path:literal, $type:expr),*) => { $( #[test] fn $name() { let data = include_bytes!(concat!("../../test/", $path)); assert_eq!(data.content_type(), $type); } )* }; } test_content_type!(license_is_text, "LICENSE.md", ContentType::Text); test_content_type!(code_is_text, "code.rs", ContentType::Text); test_content_type!(patch_is_text, "0000-test-patch.patch", ContentType::Text); test_content_type!(png_is_image, "image.png", ContentType::Image); test_content_type!(webp_is_image, "image.webp", ContentType::Image); test_content_type!(svg_is_image, "image.svg", ContentType::Image); test_content_type!(mp3_is_audio, "music.mp3", ContentType::Audio); test_content_type!(mp4_is_video, "movie.mp4", ContentType::Video); test_content_type!(mkv_is_video, "movie.mkv", ContentType::Video); test_content_type!(zip_is_zip, "archive.zip", ContentType::ZipArchive); test_content_type!(gzip_is_gzip, "image.png.gz", ContentType::Gzip); test_content_type!(binary_is_unknown, "omegaupload", ContentType::Unknown); test_content_type!(pgp_is_text, "text.pgp", ContentType::Text); }