frontend-utils: Support .ruf files

This commit is contained in:
Nathan Adams 2024-04-06 00:08:30 +02:00
parent 4632a14376
commit bf7a88d63f
10 changed files with 320 additions and 31 deletions

152
Cargo.lock generated
View File

@ -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"

View File

@ -52,7 +52,7 @@ pub struct ExternalNavigatorBackend<F: FutureSpawner> {
open_url_mode: OpenURLMode,
content: Arc<PlayingContent>,
content: Rc<PlayingContent>,
}
impl<F: FutureSpawner> ExternalNavigatorBackend<F> {
@ -66,7 +66,7 @@ impl<F: FutureSpawner> ExternalNavigatorBackend<F> {
open_url_mode: OpenURLMode,
socket_allowed: HashSet<String>,
socket_mode: SocketMode,
content: Arc<PlayingContent>,
content: Rc<PlayingContent>,
) -> 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)),
)
}

View File

@ -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") {

View File

@ -245,7 +245,7 @@ pub fn url_to_readable_name(url: &Url) -> Cow<'_, str> {
fn actually_pick_file(dir: Option<PathBuf>) -> Option<PathBuf> {
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");

View File

@ -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"

View File

@ -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<File>),
}
#[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<Path>) -> Result<Self, BundleSourceError> {
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()),
}
}
}

Binary file not shown.

View File

@ -0,0 +1,109 @@
use std::cell::RefCell;
use std::io::{Cursor, Error, ErrorKind, Read, Seek};
use zip::result::ZipError;
use zip::ZipArchive;
use crate::bundle::source::BundleSourceImpl;
pub struct ZipSource<R: Read + Seek>(RefCell<ZipArchive<R>>);
impl<R: Read + Seek> ZipSource<R> {
pub fn open(reader: R) -> Result<Self, ZipError> {
Ok(Self(RefCell::new(ZipArchive::new(reader)?)))
}
}
impl<R: Read + Seek> BundleSourceImpl for ZipSource<R> {
type Read = Cursor<Vec<u8>>;
fn read_file(&self, path: &str) -> Result<Self::Read, Error> {
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<Self::Read, Error> {
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);
}
}