desktop: Allow opening folder based bundles
This commit is contained in:
parent
961bc0a7c5
commit
4632a14376
|
@ -1,6 +1,7 @@
|
|||
//! Navigator backend for web
|
||||
|
||||
use crate::executor::FutureSpawner;
|
||||
use crate::player::PlayingContent;
|
||||
use async_channel::{Receiver, Sender, TryRecvError};
|
||||
use async_io::Timer;
|
||||
use async_net::TcpStream;
|
||||
|
@ -21,9 +22,9 @@ use ruffle_core::indexmap::IndexMap;
|
|||
use ruffle_core::loader::Error;
|
||||
use ruffle_core::socket::{ConnectionState, SocketAction, SocketHandle};
|
||||
use std::collections::HashSet;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::ErrorKind;
|
||||
use std::io::{self, Read};
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
@ -50,6 +51,8 @@ pub struct ExternalNavigatorBackend<F: FutureSpawner> {
|
|||
upgrade_to_https: bool,
|
||||
|
||||
open_url_mode: OpenURLMode,
|
||||
|
||||
content: Arc<PlayingContent>,
|
||||
}
|
||||
|
||||
impl<F: FutureSpawner> ExternalNavigatorBackend<F> {
|
||||
|
@ -63,6 +66,7 @@ impl<F: FutureSpawner> ExternalNavigatorBackend<F> {
|
|||
open_url_mode: OpenURLMode,
|
||||
socket_allowed: HashSet<String>,
|
||||
socket_mode: SocketMode,
|
||||
content: Arc<PlayingContent>,
|
||||
) -> Self {
|
||||
let proxy = proxy.and_then(|url| url.as_str().parse().ok());
|
||||
let builder = HttpClient::builder()
|
||||
|
@ -86,6 +90,7 @@ impl<F: FutureSpawner> ExternalNavigatorBackend<F> {
|
|||
open_url_mode,
|
||||
socket_allowed,
|
||||
socket_mode,
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -170,7 +175,7 @@ impl<F: FutureSpawner> NavigatorBackend for ExternalNavigatorBackend<F> {
|
|||
fn fetch(&self, request: Request) -> OwnedFuture<Box<dyn SuccessResponse>, ErrorResponse> {
|
||||
enum DesktopResponseBody {
|
||||
/// The response's body comes from a file.
|
||||
File(File),
|
||||
File(Result<Vec<u8>, std::io::Error>),
|
||||
|
||||
/// The response's body comes from the network.
|
||||
///
|
||||
|
@ -195,13 +200,9 @@ impl<F: FutureSpawner> NavigatorBackend for ExternalNavigatorBackend<F> {
|
|||
#[allow(clippy::await_holding_lock)]
|
||||
fn body(self: Box<Self>) -> OwnedFuture<Vec<u8>, Error> {
|
||||
match self.response_body {
|
||||
DesktopResponseBody::File(mut file) => Box::pin(async move {
|
||||
let mut body = vec![];
|
||||
file.read_to_end(&mut body)
|
||||
.map_err(|e| Error::FetchError(e.to_string()))?;
|
||||
|
||||
Ok(body)
|
||||
}),
|
||||
DesktopResponseBody::File(file) => {
|
||||
Box::pin(async move { file.map_err(|e| Error::FetchError(e.to_string())) })
|
||||
}
|
||||
DesktopResponseBody::Network(response) => Box::pin(async move {
|
||||
let mut body = vec![];
|
||||
response
|
||||
|
@ -228,17 +229,16 @@ impl<F: FutureSpawner> NavigatorBackend for ExternalNavigatorBackend<F> {
|
|||
fn next_chunk(&mut self) -> OwnedFuture<Option<Vec<u8>>, Error> {
|
||||
match &mut self.response_body {
|
||||
DesktopResponseBody::File(file) => {
|
||||
let mut buf = vec![0; 4096];
|
||||
let res = file.read(&mut buf);
|
||||
let res = file
|
||||
.as_mut()
|
||||
.map(std::mem::take)
|
||||
.map_err(|e| Error::FetchError(e.to_string()));
|
||||
|
||||
Box::pin(async move {
|
||||
match res {
|
||||
Ok(count) if count > 0 => {
|
||||
buf.resize(count, 0);
|
||||
Ok(Some(buf))
|
||||
}
|
||||
Ok(bytes) if !bytes.is_empty() => Ok(Some(bytes)),
|
||||
Ok(_) => Ok(None),
|
||||
Err(e) => Err(Error::FetchError(e.to_string())),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -276,7 +276,9 @@ impl<F: FutureSpawner> NavigatorBackend for ExternalNavigatorBackend<F> {
|
|||
|
||||
fn expected_length(&self) -> Result<Option<u64>, Error> {
|
||||
match &self.response_body {
|
||||
DesktopResponseBody::File(file) => Ok(Some(file.metadata()?.len())),
|
||||
DesktopResponseBody::File(file) => {
|
||||
Ok(file.as_ref().map(|file| file.len() as u64).ok())
|
||||
}
|
||||
DesktopResponseBody::Network(response) => {
|
||||
let response = response.lock().expect("no recursive locks");
|
||||
let content_length = response.headers().get("Content-Length");
|
||||
|
@ -306,41 +308,46 @@ impl<F: FutureSpawner> NavigatorBackend for ExternalNavigatorBackend<F> {
|
|||
let client = self.client.clone();
|
||||
|
||||
match processed_url.scheme() {
|
||||
"file" => Box::pin(async move {
|
||||
// We send the original url (including query parameters)
|
||||
// back to ruffle_core in the `Response`
|
||||
let response_url = processed_url.clone();
|
||||
// Flash supports query parameters with local urls.
|
||||
// SwfMovie takes care of exposing those to ActionScript -
|
||||
// when we actually load a filesystem url, strip them out.
|
||||
processed_url.set_query(None);
|
||||
"file" => {
|
||||
let content = self.content.clone();
|
||||
Box::pin(async move {
|
||||
// We send the original url (including query parameters)
|
||||
// back to ruffle_core in the `Response`
|
||||
let response_url = processed_url.clone();
|
||||
// Flash supports query parameters with local urls.
|
||||
// SwfMovie takes care of exposing those to ActionScript -
|
||||
// when we actually load a filesystem url, strip them out.
|
||||
processed_url.set_query(None);
|
||||
|
||||
let path = match processed_url.to_file_path() {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
return create_specific_fetch_error(
|
||||
"Unable to create path out of URL",
|
||||
response_url.as_str(),
|
||||
"",
|
||||
);
|
||||
let path = match processed_url.to_file_path() {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
return create_specific_fetch_error(
|
||||
"Unable to create path out of URL",
|
||||
response_url.as_str(),
|
||||
"",
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let contents = std::fs::File::open(&path).or_else(|e| {
|
||||
let contents = content.get_local_file(&path).or_else(|e| {
|
||||
if cfg!(feature = "sandbox") {
|
||||
use rfd::FileDialog;
|
||||
let parent_path = Path::new(&path).parent().unwrap_or_else(|| Path::new(&path));
|
||||
|
||||
if e.kind() == ErrorKind::PermissionDenied {
|
||||
let attempt_sandbox_open = MessageDialog::new()
|
||||
.set_level(MessageLevel::Warning)
|
||||
.set_description(format!("The current movie is attempting to read files stored in {}.\n\nTo allow it to do so, click Yes, and then Open to grant read access to that directory.\n\nOtherwise, click No to deny access.", path.parent().unwrap_or(&path).to_string_lossy()))
|
||||
.set_description(format!("The current movie is attempting to read files stored in {}.\n\nTo allow it to do so, click Yes, and then Open to grant read access to that directory.\n\nOtherwise, click No to deny access.", parent_path.to_string_lossy()))
|
||||
.set_buttons(MessageButtons::YesNo)
|
||||
.show() == MessageDialogResult::Yes;
|
||||
|
||||
if attempt_sandbox_open {
|
||||
FileDialog::new().set_directory(&path).pick_folder();
|
||||
FileDialog::new().set_directory(parent_path).pick_folder();
|
||||
|
||||
return std::fs::File::open(&path);
|
||||
return content.get_local_file(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -348,26 +355,16 @@ impl<F: FutureSpawner> NavigatorBackend for ExternalNavigatorBackend<F> {
|
|||
Err(e)
|
||||
});
|
||||
|
||||
let file = match contents {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
return create_specific_fetch_error(
|
||||
"Can't open file",
|
||||
response_url.as_str(),
|
||||
e,
|
||||
);
|
||||
}
|
||||
};
|
||||
let response: Box<dyn SuccessResponse> = Box::new(DesktopResponse {
|
||||
url: response_url.to_string(),
|
||||
response_body: DesktopResponseBody::File(contents),
|
||||
status: 0,
|
||||
redirected: false,
|
||||
});
|
||||
|
||||
let response: Box<dyn SuccessResponse> = Box::new(DesktopResponse {
|
||||
url: response_url.to_string(),
|
||||
response_body: DesktopResponseBody::File(file),
|
||||
status: 0,
|
||||
redirected: false,
|
||||
});
|
||||
|
||||
Ok(response)
|
||||
}),
|
||||
Ok(response)
|
||||
})
|
||||
}
|
||||
_ => Box::pin(async move {
|
||||
let client = client.ok_or_else(|| ErrorResponse {
|
||||
url: processed_url.to_string(),
|
||||
|
@ -692,8 +689,9 @@ mod tests {
|
|||
}
|
||||
|
||||
fn new_test_backend(socket_allow: bool) -> ExternalNavigatorBackend<TestFutureSpawner> {
|
||||
let url = Url::parse("https://example.com/path/").unwrap();
|
||||
ExternalNavigatorBackend::new(
|
||||
Url::parse("https://example.com/path/").unwrap(),
|
||||
url.clone(),
|
||||
TestFutureSpawner,
|
||||
None,
|
||||
false,
|
||||
|
@ -704,6 +702,7 @@ mod tests {
|
|||
} else {
|
||||
SocketMode::Deny
|
||||
},
|
||||
Arc::new(PlayingContent::DirectFile(url)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -15,11 +15,16 @@ 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_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;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
|
@ -89,6 +94,54 @@ impl From<&GlobalPreferences> for PlayerOptions {
|
|||
}
|
||||
}
|
||||
|
||||
pub enum PlayingContent {
|
||||
DirectFile(Url),
|
||||
Bundle(Url, Bundle),
|
||||
}
|
||||
|
||||
impl Debug for PlayingContent {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PlayingContent::DirectFile(url) => f
|
||||
.debug_tuple("PlayingContent::DirectFile")
|
||||
.field(url)
|
||||
.finish(),
|
||||
PlayingContent::Bundle(url, _) => f
|
||||
.debug_tuple("PlayingContent::Bundle")
|
||||
.field(url)
|
||||
.field(&"_")
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlayingContent {
|
||||
pub fn initial_swf_url(&self) -> &Url {
|
||||
match self {
|
||||
PlayingContent::DirectFile(url) => url,
|
||||
PlayingContent::Bundle(_, bundle) => &bundle.information().url,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
match self {
|
||||
PlayingContent::DirectFile(url) => crate::util::url_to_readable_name(url).to_string(),
|
||||
PlayingContent::Bundle(_, bundle) => bundle.information().name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_local_file(&self, path: &str) -> Result<Vec<u8>, std::io::Error> {
|
||||
match self {
|
||||
PlayingContent::DirectFile(_) => {
|
||||
let mut result = vec![];
|
||||
std::fs::File::open(path)?.read_to_end(&mut result)?;
|
||||
Ok(result)
|
||||
}
|
||||
PlayingContent::Bundle(_, bundle) => bundle.source().read_content(path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a current Player and any associated state with that player,
|
||||
/// which may be lost when this Player is closed (dropped)
|
||||
struct ActivePlayer {
|
||||
|
@ -119,7 +172,41 @@ 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (executor, future_spawner) = WinitAsyncExecutor::new(event_loop.clone());
|
||||
let movie_url = content.initial_swf_url().clone();
|
||||
let readable_name = content.name();
|
||||
let navigator = ExternalNavigatorBackend::new(
|
||||
opt.base.to_owned().unwrap_or_else(|| movie_url.clone()),
|
||||
future_spawner,
|
||||
|
@ -128,6 +215,7 @@ impl ActivePlayer {
|
|||
opt.open_url_mode,
|
||||
opt.socket_allowed.clone(),
|
||||
opt.tcp_connections,
|
||||
Arc::new(content),
|
||||
);
|
||||
|
||||
if cfg!(feature = "software_video") {
|
||||
|
@ -189,11 +277,9 @@ impl ActivePlayer {
|
|||
.with_avm2_optimizer_enabled(opt.avm2_optimizer_enabled);
|
||||
let player = builder.build();
|
||||
|
||||
let readable_name = crate::util::url_to_readable_name(movie_url);
|
||||
|
||||
window.set_title(&format!("Ruffle - {readable_name}"));
|
||||
|
||||
SWF_INFO.with(|i| *i.borrow_mut() = Some(readable_name.into_owned()));
|
||||
SWF_INFO.with(|i| *i.borrow_mut() = Some(readable_name));
|
||||
|
||||
let on_metadata = move |swf_header: &ruffle_core::swf::HeaderExt| {
|
||||
let _ = event_loop.send_event(RuffleEvent::OnMetadata(swf_header.clone()));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Ruffle Bundle (.ruf) format specification
|
||||
A Ruffle Bundle is an easy way to package and share Flash games and any assets that are required to make the game work.
|
||||
|
||||
A bundle can be a directory or a renamed zip file, and must contain at minimum a `bundle.toml` file.
|
||||
A bundle can be a directory or a renamed zip file, and must contain at minimum a `ruffle-bundle.toml` file.
|
||||
|
||||
<!-- TOC -->
|
||||
* [Ruffle Bundle (.ruf) format specification](#ruffle-bundle-ruf-format-specification)
|
||||
|
|
|
@ -7,7 +7,7 @@ impl BundleSourceImpl for Path {
|
|||
type Read = File;
|
||||
|
||||
fn read_file(&self, path: &str) -> Result<Self::Read, Error> {
|
||||
let potential_path = self.join(path);
|
||||
let potential_path = self.join(path.strip_prefix('/').unwrap_or(path));
|
||||
if !potential_path.starts_with(self) {
|
||||
return Err(Error::from(ErrorKind::NotFound));
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ impl BundleSourceImpl for Path {
|
|||
|
||||
fn read_content(&self, path: &str) -> Result<Self::Read, Error> {
|
||||
let root = self.join("content");
|
||||
let potential_path = root.join(path);
|
||||
let potential_path = root.join(path.strip_prefix('/').unwrap_or(path));
|
||||
if !potential_path.starts_with(root) {
|
||||
return Err(Error::from(ErrorKind::NotFound));
|
||||
}
|
||||
|
@ -136,6 +136,25 @@ mod tests {
|
|||
assert_eq!(result.as_deref(), Ok("Fancy!"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_content_works_with_absolute_path() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let _ = std::fs::create_dir(tmp_dir.path().join("content"));
|
||||
let _ = std::fs::write(tmp_dir.path().join("content/some_file.txt"), "Fancy!");
|
||||
let result = tmp_dir
|
||||
.path()
|
||||
.read_content("/some_file.txt")
|
||||
.map_err(|e| e.to_string())
|
||||
.map(|mut f| {
|
||||
let mut result = String::new();
|
||||
let _ = f.read_to_string(&mut result);
|
||||
result
|
||||
});
|
||||
drop(tmp_dir);
|
||||
|
||||
assert_eq!(result.as_deref(), Ok("Fancy!"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_content_outside_content_directory() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
|
|
Loading…
Reference in New Issue