From bf7a88d63fb51a533f3f0840be6010998e300238 Mon Sep 17 00:00:00 2001 From: Nathan Adams Date: Sat, 6 Apr 2024 00:08:30 +0200 Subject: [PATCH] frontend-utils: Support .ruf files --- Cargo.lock | 152 ++++++++++++++++++ desktop/src/backends/navigator.rs | 6 +- desktop/src/player.rs | 47 +++--- desktop/src/util.rs | 2 +- frontend-utils/Cargo.toml | 1 + frontend-utils/src/bundle/source.rs | 34 +++- .../source/test-assets/bundle-and-content.xip | Bin 0 -> 580 bytes .../src/bundle/source/test-assets/empty.zip | Bin 0 -> 22 bytes .../bundle/source/test-assets/just-bundle.zip | Bin 0 -> 265 bytes frontend-utils/src/bundle/source/zip.rs | 109 +++++++++++++ 10 files changed, 320 insertions(+), 31 deletions(-) create mode 100644 frontend-utils/src/bundle/source/test-assets/bundle-and-content.xip create mode 100644 frontend-utils/src/bundle/source/test-assets/empty.zip create mode 100644 frontend-utils/src/bundle/source/test-assets/just-bundle.zip create mode 100644 frontend-utils/src/bundle/source/zip.rs diff --git a/Cargo.lock b/Cargo.lock index 0e55a0adb..95896621b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,6 +49,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.11" @@ -488,6 +499,12 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bindgen" version = "0.69.4" @@ -658,6 +675,27 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "calloop" version = "0.12.4" @@ -773,6 +811,16 @@ dependencies = [ "windows-targets 0.52.4", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.7.0" @@ -928,6 +976,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "convert_case" version = "0.6.0" @@ -1383,6 +1437,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -2492,6 +2547,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.9" @@ -2670,6 +2734,15 @@ dependencies = [ "libc", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "insta" version = "1.38.0" @@ -3696,6 +3769,17 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.14" @@ -3708,6 +3792,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -4290,6 +4386,7 @@ dependencies = [ "toml_edit 0.22.9", "tracing", "url", + "zip", ] [[package]] @@ -4980,6 +5077,12 @@ dependencies = [ "ruffle_core", ] +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "swf" version = "0.2.0" @@ -6683,6 +6786,55 @@ dependencies = [ "syn 2.0.58", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.10+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zvariant" version = "4.0.2" diff --git a/desktop/src/backends/navigator.rs b/desktop/src/backends/navigator.rs index 0483f3de3..d8f5e9f45 100644 --- a/desktop/src/backends/navigator.rs +++ b/desktop/src/backends/navigator.rs @@ -52,7 +52,7 @@ pub struct ExternalNavigatorBackend { open_url_mode: OpenURLMode, - content: Arc, + content: Rc, } impl ExternalNavigatorBackend { @@ -66,7 +66,7 @@ impl ExternalNavigatorBackend { open_url_mode: OpenURLMode, socket_allowed: HashSet, socket_mode: SocketMode, - content: Arc, + content: Rc, ) -> Self { let proxy = proxy.and_then(|url| url.as_str().parse().ok()); let builder = HttpClient::builder() @@ -702,7 +702,7 @@ mod tests { } else { SocketMode::Deny }, - Arc::new(PlayingContent::DirectFile(url)), + Rc::new(PlayingContent::DirectFile(url)), ) } diff --git a/desktop/src/player.rs b/desktop/src/player.rs index 6bbe1bfcc..d4faeae1b 100644 --- a/desktop/src/player.rs +++ b/desktop/src/player.rs @@ -15,14 +15,13 @@ use ruffle_core::{ DefaultFont, LoadBehavior, Player, PlayerBuilder, PlayerEvent, PlayerRuntime, StageAlign, StageScaleMode, }; -use ruffle_frontend_utils::bundle::info::BUNDLE_INFORMATION_FILENAME; -use ruffle_frontend_utils::bundle::Bundle; +use ruffle_frontend_utils::bundle::source::BundleSourceError; +use ruffle_frontend_utils::bundle::{Bundle, BundleError}; use ruffle_render::backend::RenderBackend; use ruffle_render::quality::StageQuality; use ruffle_render_wgpu::backend::WgpuRenderBackend; use ruffle_render_wgpu::descriptors::Descriptors; use std::collections::{HashMap, HashSet}; -use std::ffi::OsStr; use std::fmt::{Debug, Formatter}; use std::io::Read; use std::path::PathBuf; @@ -175,30 +174,26 @@ impl ActivePlayer { let mut content = PlayingContent::DirectFile(movie_url.clone()); if movie_url.scheme() == "file" { if let Ok(path) = movie_url.to_file_path() { - if path.is_file() - && path.file_name() == Some(OsStr::new(BUNDLE_INFORMATION_FILENAME)) - { - if let Some(bundle_dir) = path.parent() { - match Bundle::from_path(bundle_dir) { - Ok(bundle) => { - if bundle.warnings().is_empty() { - tracing::info!("Opening bundle at {bundle_dir:?}"); - } else { - // TODO: Show warnings to user (toast?) - tracing::warn!( - "Opening bundle at {bundle_dir:?} with warnings" - ); - for warning in bundle.warnings() { - tracing::warn!("{warning}"); - } - } - content = PlayingContent::Bundle(movie_url.clone(), bundle); - } - Err(e) => { - // TODO: Visible popup when a bundle (or regular file) fails to open - tracing::error!("Couldn't open bundle at {bundle_dir:?}: {e}"); + match Bundle::from_path(&path) { + Ok(bundle) => { + if bundle.warnings().is_empty() { + tracing::info!("Opening bundle at {path:?}"); + } else { + // TODO: Show warnings to user (toast?) + tracing::warn!("Opening bundle at {path:?} with warnings"); + for warning in bundle.warnings() { + tracing::warn!("{warning}"); } } + content = PlayingContent::Bundle(movie_url.clone(), bundle); + } + Err(BundleError::BundleDoesntExist) + | Err(BundleError::InvalidSource(BundleSourceError::UnknownSource)) => { + // Do nothing and carry on opening it as a swf - this likely isn't a bundle at all + } + Err(e) => { + // TODO: Visible popup when a bundle (or regular file) fails to open + tracing::error!("Couldn't open bundle at {path:?}: {e}"); } } } @@ -215,7 +210,7 @@ impl ActivePlayer { opt.open_url_mode, opt.socket_allowed.clone(), opt.tcp_connections, - Arc::new(content), + Rc::new(content), ); if cfg!(feature = "software_video") { diff --git a/desktop/src/util.rs b/desktop/src/util.rs index f77828f2b..4ac8b0e06 100644 --- a/desktop/src/util.rs +++ b/desktop/src/util.rs @@ -245,7 +245,7 @@ pub fn url_to_readable_name(url: &Url) -> Cow<'_, str> { fn actually_pick_file(dir: Option) -> Option { let mut dialog = FileDialog::new() - .add_filter("Flash Files", &["swf", "spl"]) + .add_filter("Flash Files", &["swf", "spl", "ruf"]) .add_filter("All Files", &["*"]) .set_title("Load a Flash File"); diff --git a/frontend-utils/Cargo.toml b/frontend-utils/Cargo.toml index 406cf609d..6a3e2c59a 100644 --- a/frontend-utils/Cargo.toml +++ b/frontend-utils/Cargo.toml @@ -15,6 +15,7 @@ toml_edit = { version = "0.22.9", features = ["parse"] } url = { workspace = true } tracing = { workspace = true } thiserror = { workspace = true } +zip = "0.6.6" [dev-dependencies] tempfile = "3" \ No newline at end of file diff --git a/frontend-utils/src/bundle/source.rs b/frontend-utils/src/bundle/source.rs index c1f25d4b7..ad4568993 100644 --- a/frontend-utils/src/bundle/source.rs +++ b/frontend-utils/src/bundle/source.rs @@ -1,7 +1,12 @@ +use crate::bundle::info::BUNDLE_INFORMATION_FILENAME; +use crate::bundle::source::zip::ZipSource; +use std::ffi::OsStr; +use std::fs::File; use std::io::{Error, Read}; use std::path::{Path, PathBuf}; pub mod directory; +mod zip; trait BundleSourceImpl { type Read: Read; @@ -15,21 +20,46 @@ trait BundleSourceImpl { pub enum BundleSource { Directory(PathBuf), + ZipFile(ZipSource), } #[derive(Debug, thiserror::Error)] pub enum BundleSourceError { #[error("Unknown bundle source")] UnknownSource, + + #[error("Invalid or corrupt zip")] + InvalidZip, + + #[error("IO error opening file")] + Io(#[from] Error), } impl BundleSource { pub fn from_path(path: impl AsRef) -> Result { let path = path.as_ref(); - if path.is_dir() { + + // Opening a directory which happens to contain a ruffle-bundle.toml file, the bundle is this directory + if path.is_dir() && path.join(BUNDLE_INFORMATION_FILENAME).is_file() { return Ok(Self::Directory(path.to_owned())); } + if path.is_file() { + // Opening a ruffle-bundle.toml, the bundle is the parent directory + if path.file_name() == Some(OsStr::new(BUNDLE_INFORMATION_FILENAME)) { + return Ok(Self::Directory(path.to_owned())); + } + + // Opening a .ruf file, the bundle is that file viewed as a zip + if path.extension() == Some(OsStr::new("ruf")) { + return if let Ok(zip) = ZipSource::open(File::open(path)?) { + Ok(Self::ZipFile(zip)) + } else { + Err(BundleSourceError::InvalidZip) + }; + } + } + Err(BundleSourceError::UnknownSource) } @@ -42,6 +72,7 @@ impl BundleSource { file.read_to_end(&mut data)?; Ok(data) } + BundleSource::ZipFile(zip) => zip.read_file(path).map(|cursor| cursor.into_inner()), } } @@ -54,6 +85,7 @@ impl BundleSource { file.read_to_end(&mut data)?; Ok(data) } + BundleSource::ZipFile(zip) => zip.read_content(path).map(|cursor| cursor.into_inner()), } } } diff --git a/frontend-utils/src/bundle/source/test-assets/bundle-and-content.xip b/frontend-utils/src/bundle/source/test-assets/bundle-and-content.xip new file mode 100644 index 0000000000000000000000000000000000000000..a9cdd1bf50cfaa3d6f106132230ba19fef7dc1b8 GIT binary patch literal 580 zcmWIWW@Zs#U|`^2Siie9qR-|Lw?B{<0>nZLG7LqfX=ypBx=E#ZDLJWnCHc8Ip&^_M z%$B(VX&_u$!Og(P@`9Ox0ZerJX`S^x>Dl}Al=m6m&<&vhTw1=~r#(YIob*2*UU260 z8DE`~XU+#d($YP}crtCu6cLf=m#@VeLtl!DzFxlM*|XQr8A4yHF@?SsoxWU?ks$!+ zG%lb+7#PgnZLG7LqfX=ypBx=E#ZDLJWnCHc8Ip&^_M z%$B(VX&_u$!Og(P@`9Ox0ZerJX`S^x>Dl}Al=m6m&<&vhTw1=~r#(YIob*2*UU260 z8DE`~XU+#d($YP}crtCu6cLf=m#@VeLtl!DzFxlM*|XQr8A4yHF@?SsoxWU?ks-jF tkx7mjm*XUWPGn$U1mZ1?AQm>KvO=7S=F9+ZRyL3hMj(s;(z8Jv1^~gSO&(RefCell>); + +impl ZipSource { + pub fn open(reader: R) -> Result { + Ok(Self(RefCell::new(ZipArchive::new(reader)?))) + } +} + +impl BundleSourceImpl for ZipSource { + type Read = Cursor>; + + fn read_file(&self, path: &str) -> Result { + let mut self_ref = self.0.borrow_mut(); + let mut result = self_ref + .by_name(path.strip_prefix('/').unwrap_or(path)) + .map_err(|e| match e { + ZipError::Io(e) => e, + ZipError::InvalidArchive(_) => e.into(), + ZipError::UnsupportedArchive(_) => e.into(), + ZipError::FileNotFound => Error::from(ErrorKind::NotFound), + })?; + let mut buf = vec![]; + result.read_to_end(&mut buf)?; + Ok(Cursor::new(buf)) + } + + fn read_content(&self, path: &str) -> Result { + let path = path.strip_prefix('/').unwrap_or(path); + self.read_file(&format!("content/{path}")) + } +} + +#[cfg(test)] +mod tests { + use std::io::{Cursor, ErrorKind, Read}; + + use zip::result::ZipError; + + use crate::bundle::source::zip::ZipSource; + use crate::bundle::source::BundleSourceImpl; + + #[test] + fn open_not_a_zip() { + assert!(matches!( + ZipSource::open(Cursor::new(&[0, 1, 2, 3])), + Err(ZipError::InvalidArchive(_)) + )) + } + + #[test] + fn read_file_not_found() { + let not_a_zip = include_bytes!("./test-assets/empty.zip"); + let source = ZipSource::open(Cursor::new(not_a_zip)).unwrap(); + assert!(matches!( + source.read_file("some_file"), + Err(e) if e.kind() == ErrorKind::NotFound + )) + } + + #[test] + fn read_file_valid() { + let not_a_zip = include_bytes!("./test-assets/just-bundle.zip"); + let source = ZipSource::open(Cursor::new(not_a_zip)).unwrap(); + let mut file = source.read_file("ruffle-bundle.toml").unwrap(); + let mut string = String::new(); + file.read_to_string(&mut string).unwrap(); + assert_eq!("[bundle]\nname = \"Ruffle Logo Animation\"\nurl = \"https://ruffle.rs/demo/logo-anim.swf\"", string); + } + + #[test] + fn read_content_not_found() { + let not_a_zip = include_bytes!("./test-assets/empty.zip"); + let source = ZipSource::open(Cursor::new(not_a_zip)).unwrap(); + + assert!(matches!( + source.read_content("some_file"), + Err(e) if e.kind() == ErrorKind::NotFound + )) + } + + #[test] + fn read_content_not_in_content_dir() { + let not_a_zip = include_bytes!("./test-assets/just-bundle.zip"); + let source = ZipSource::open(Cursor::new(not_a_zip)).unwrap(); + + assert!(matches!( + source.read_content("../ruffle-bundle.toml"), + Err(e) if e.kind() == ErrorKind::NotFound + )) + } + + #[test] + fn read_content_valid() { + let not_a_zip = include_bytes!("./test-assets/bundle-and-content.xip"); + let source = ZipSource::open(Cursor::new(not_a_zip)).unwrap(); + let mut file = source.read_content("/foo.txt").unwrap(); + let mut string = String::new(); + file.read_to_string(&mut string).unwrap(); + assert_eq!("Hello world!\n", string); + } +}