mirror of
https://github.com/touchHLE/touchHLE.git
synced 2026-01-31 01:25:24 +01:00
Allow opening an IPA bundle file directly
This commit is contained in:
committed by
hikari_no_yume
parent
1e887c6488
commit
32d12cc697
56
Cargo.lock
generated
56
Cargo.lock
generated
@@ -8,6 +8,12 @@ version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
|
||||
|
||||
[[package]]
|
||||
name = "adler"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
@@ -191,6 +197,24 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.1.6"
|
||||
@@ -213,6 +237,16 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.21"
|
||||
@@ -340,6 +374,15 @@ version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
|
||||
dependencies = [
|
||||
"adler",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.17.0"
|
||||
@@ -616,6 +659,7 @@ dependencies = [
|
||||
"touchHLE_gl_bindings",
|
||||
"touchHLE_openal_soft_wrapper",
|
||||
"touchHLE_stb_image_wrapper",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -719,3 +763,15 @@ name = "xml-rs"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0445d0fbc924bb93539b4316c11afb121ea39296f99a3c4c9edad09e3658cdef"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"flate2",
|
||||
]
|
||||
|
||||
@@ -30,6 +30,7 @@ caf = "0.1.0"
|
||||
hound = "3.5.0"
|
||||
mach_object = "0.1.17"
|
||||
plist = "1.3.1"
|
||||
zip = { version = "0.6.4", default-features = false, features = ["deflate"] }
|
||||
rusttype = "0.9.3"
|
||||
# sdl2 crates pinned at 0.35.1 because static linking seems to be broken for
|
||||
# 0.35.2 on macOS (build errors about undefined symbols for
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
//! * [Anatomy of an iOS Application Bundle](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html)
|
||||
//! * [Bundle Resources](https://developer.apple.com/documentation/bundleresources?language=objc)
|
||||
|
||||
use crate::fs::{Fs, GuestPath, GuestPathBuf};
|
||||
use crate::fs::{BundleData, Fs, GuestPath, GuestPathBuf};
|
||||
use plist::dictionary::Dictionary;
|
||||
use plist::Value;
|
||||
use std::io::Cursor;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Bundle {
|
||||
@@ -25,32 +24,21 @@ pub struct Bundle {
|
||||
|
||||
impl Bundle {
|
||||
pub fn new_bundle_and_fs_from_host_path(
|
||||
host_path: PathBuf,
|
||||
) -> Result<(Bundle, Fs), &'static str> {
|
||||
if !host_path.is_dir() {
|
||||
return Err("Bundle path is not a directory");
|
||||
}
|
||||
|
||||
let plist_path = host_path.join("Info.plist");
|
||||
|
||||
if !plist_path.is_file() {
|
||||
return Err("Bundle does not contain an Info.plist file");
|
||||
}
|
||||
|
||||
let plist_bytes =
|
||||
std::fs::read(plist_path).map_err(|_| "Could not read Info.plist file")?;
|
||||
mut bundle_data: BundleData,
|
||||
) -> Result<(Bundle, Fs), String> {
|
||||
let plist_bytes = bundle_data.read_plist()?;
|
||||
|
||||
let plist = Value::from_reader(Cursor::new(plist_bytes))
|
||||
.map_err(|_| "Could not deserialize plist data")?;
|
||||
.map_err(|_| "Could not deserialize plist data".to_string())?;
|
||||
|
||||
let plist = plist
|
||||
.into_dictionary()
|
||||
.ok_or("plist root value is not a dictionary")?;
|
||||
.ok_or_else(|| "plist root value is not a dictionary".to_string())?;
|
||||
|
||||
let bundle_name = plist["CFBundleName"].as_string().unwrap();
|
||||
let bundle_id = plist["CFBundleIdentifier"].as_string().unwrap();
|
||||
|
||||
let (fs, guest_path) = Fs::new(&host_path, format!("{}.app", bundle_name), bundle_id);
|
||||
let (fs, guest_path) = Fs::new(bundle_data, format!("{bundle_name}.app"), bundle_id);
|
||||
|
||||
let bundle = Bundle {
|
||||
path: guest_path,
|
||||
|
||||
218
src/fs.rs
218
src/fs.rs
@@ -20,15 +20,25 @@
|
||||
//! Directories only need a corresponding directory in the host filesystem if
|
||||
//! they are writeable (i.e. if new files can be created in them).
|
||||
|
||||
mod bundle;
|
||||
|
||||
pub use bundle::BundleData;
|
||||
|
||||
use crate::fs::bundle::{IpaFile, IpaFileRef};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum FsNode {
|
||||
File {
|
||||
HostFile {
|
||||
host_path: PathBuf,
|
||||
writeable: bool,
|
||||
},
|
||||
IpaBundleFile {
|
||||
file: IpaFileRef,
|
||||
},
|
||||
Directory {
|
||||
children: HashMap<String, FsNode>,
|
||||
writeable: Option<PathBuf>,
|
||||
@@ -55,7 +65,7 @@ impl FsNode {
|
||||
if kind.is_file() {
|
||||
children.insert(
|
||||
name,
|
||||
FsNode::File {
|
||||
FsNode::HostFile {
|
||||
host_path,
|
||||
writeable,
|
||||
},
|
||||
@@ -91,12 +101,15 @@ impl FsNode {
|
||||
assert!(children.insert(String::from(name), child).is_none());
|
||||
self
|
||||
}
|
||||
fn file(host_path: PathBuf) -> Self {
|
||||
FsNode::File {
|
||||
fn host_file(host_path: PathBuf) -> Self {
|
||||
FsNode::HostFile {
|
||||
host_path,
|
||||
writeable: false,
|
||||
}
|
||||
}
|
||||
fn bundle_zip_file(file: IpaFileRef) -> Self {
|
||||
FsNode::IpaBundleFile { file }
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [Path] but for the virtual filesystem.
|
||||
@@ -104,7 +117,7 @@ impl FsNode {
|
||||
#[derive(Debug)]
|
||||
pub struct GuestPath(str);
|
||||
impl GuestPath {
|
||||
pub fn new<S: AsRef<str>>(s: &S) -> &GuestPath {
|
||||
pub fn new<S: AsRef<str> + ?Sized>(s: &S) -> &GuestPath {
|
||||
unsafe { &*(s.as_ref() as *const str as *const GuestPath) }
|
||||
}
|
||||
pub fn as_str(&self) -> &str {
|
||||
@@ -120,12 +133,24 @@ impl GuestPath {
|
||||
GuestPathBuf::from(format!("{}/{}", self.as_str(), path.as_ref()))
|
||||
}
|
||||
|
||||
/// Splits the path into a parent path and a file name.
|
||||
pub fn parent_and_file_name(&self) -> Option<(&GuestPath, &str)> {
|
||||
// FIXME: this should do the same resolution as `std::path::file_name()`
|
||||
let (parent_name, file_name) = self.as_str().rsplit_once('/')?;
|
||||
Some((GuestPath::new(parent_name), file_name))
|
||||
}
|
||||
|
||||
/// Get the final component of the path.
|
||||
pub fn file_name(&self) -> Option<&str> {
|
||||
// FIXME: this should do the same resolution as `std::path::file_name()`
|
||||
let (_, file_name) = self.as_str().rsplit_once('/')?;
|
||||
let (_, file_name) = self.parent_and_file_name()?;
|
||||
Some(file_name)
|
||||
}
|
||||
|
||||
/// Get the parent directory of the path.
|
||||
pub fn parent(&self) -> Option<&GuestPath> {
|
||||
let (parent_name, _) = self.parent_and_file_name()?;
|
||||
Some(parent_name)
|
||||
}
|
||||
}
|
||||
impl AsRef<GuestPath> for GuestPath {
|
||||
fn as_ref(&self) -> &Self {
|
||||
@@ -142,7 +167,7 @@ impl AsRef<GuestPath> for str {
|
||||
unsafe { &*(self as *const str as *const GuestPath) }
|
||||
}
|
||||
}
|
||||
impl std::borrow::ToOwned for GuestPath {
|
||||
impl ToOwned for GuestPath {
|
||||
type Owned = GuestPathBuf;
|
||||
|
||||
fn to_owned(&self) -> GuestPathBuf {
|
||||
@@ -276,6 +301,68 @@ fn handle_open_err<T>(open_result: std::io::Result<T>, host_path: &Path) -> T {
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [File] but for the guest filesystem.
|
||||
#[derive(Debug)]
|
||||
pub enum GuestFile {
|
||||
HostFile(File),
|
||||
IpaBundleFile(IpaFile),
|
||||
}
|
||||
|
||||
impl GuestFile {
|
||||
fn from_host_file(file: File) -> GuestFile {
|
||||
GuestFile::HostFile(file)
|
||||
}
|
||||
|
||||
fn from_ipa_file(file: &IpaFileRef) -> GuestFile {
|
||||
GuestFile::IpaBundleFile(file.open())
|
||||
}
|
||||
|
||||
pub fn sync_all(&self) -> std::io::Result<()> {
|
||||
match self {
|
||||
GuestFile::HostFile(file) => file.sync_all(),
|
||||
GuestFile::IpaBundleFile(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for GuestFile {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
match self {
|
||||
GuestFile::HostFile(file) => file.read(buf),
|
||||
GuestFile::IpaBundleFile(file) => file.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for GuestFile {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
match self {
|
||||
GuestFile::HostFile(file) => file.write(buf),
|
||||
GuestFile::IpaBundleFile(file) => {
|
||||
panic!("Attempt to write to a read-only file: {:?}", file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
match self {
|
||||
GuestFile::HostFile(file) => file.flush(),
|
||||
GuestFile::IpaBundleFile(file) => {
|
||||
panic!("Attempt to flush a read-only file: {:?}", file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for GuestFile {
|
||||
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
|
||||
match self {
|
||||
GuestFile::HostFile(file) => file.seek(pos),
|
||||
GuestFile::IpaBundleFile(file) => file.seek(pos),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The type that owns the guest filesystem and provides accessors for it.
|
||||
#[derive(Debug)]
|
||||
pub struct Fs {
|
||||
@@ -299,7 +386,7 @@ impl Fs {
|
||||
/// sandbox directory, where documents can be stored. A directory will be
|
||||
/// created at that path if it does not already exist.
|
||||
pub fn new(
|
||||
bundle_host_path: &Path,
|
||||
app_bundle: BundleData,
|
||||
bundle_dir_name: String,
|
||||
bundle_id: &str,
|
||||
) -> (Fs, GuestPathBuf) {
|
||||
@@ -325,16 +412,16 @@ impl Fs {
|
||||
let usr_lib = FsNode::dir()
|
||||
.with_child(
|
||||
"libgcc_s.1.dylib",
|
||||
FsNode::file(dylibs_host_path.join("libgcc_s.1.dylib")),
|
||||
FsNode::host_file(dylibs_host_path.join("libgcc_s.1.dylib")),
|
||||
)
|
||||
.with_child(
|
||||
// symlink
|
||||
"libstdc++.6.dylib",
|
||||
FsNode::file(dylibs_host_path.join("libstdc++.6.0.4.dylib")),
|
||||
FsNode::host_file(dylibs_host_path.join("libstdc++.6.0.4.dylib")),
|
||||
)
|
||||
.with_child(
|
||||
"libstdc++.6.0.4.dylib",
|
||||
FsNode::file(dylibs_host_path.join("libstdc++.6.0.4.dylib")),
|
||||
FsNode::host_file(dylibs_host_path.join("libstdc++.6.0.4.dylib")),
|
||||
);
|
||||
|
||||
let root = FsNode::dir()
|
||||
@@ -346,13 +433,7 @@ impl Fs {
|
||||
FAKE_UUID,
|
||||
FsNode::Directory {
|
||||
children: HashMap::from([
|
||||
(
|
||||
bundle_dir_name,
|
||||
FsNode::from_host_dir(
|
||||
bundle_host_path,
|
||||
/* writeable: */ false,
|
||||
),
|
||||
),
|
||||
(bundle_dir_name, app_bundle.into_fs_node()),
|
||||
(
|
||||
"Documents".to_string(),
|
||||
FsNode::from_host_dir(
|
||||
@@ -416,42 +497,43 @@ impl Fs {
|
||||
Some((parent, final_component.to_string()))
|
||||
}
|
||||
|
||||
/// Like [std::path::Path::is_file] but for the guest filesystem.
|
||||
/// Like [Path::is_file] but for the guest filesystem.
|
||||
pub fn is_file(&self, path: &GuestPath) -> bool {
|
||||
matches!(self.lookup_node(path), Some(FsNode::File { .. }))
|
||||
matches!(
|
||||
self.lookup_node(path),
|
||||
Some(FsNode::HostFile { .. } | FsNode::IpaBundleFile { .. })
|
||||
)
|
||||
}
|
||||
|
||||
/// Like [std::fs::read] but for the guest filesystem.
|
||||
pub fn read<P: AsRef<GuestPath>>(&self, path: P) -> Result<Vec<u8>, ()> {
|
||||
let node = self.lookup_node(path.as_ref()).ok_or(())?;
|
||||
let FsNode::File {
|
||||
host_path,
|
||||
writeable: _,
|
||||
} = node else {
|
||||
return Err(())
|
||||
};
|
||||
Ok(handle_open_err(std::fs::read(host_path), host_path))
|
||||
let mut file = self.open(path.as_ref())?;
|
||||
let mut result = Vec::new();
|
||||
file.read_to_end(&mut result).map_err(|_| ())?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Like [std::fs::File::open] but for the guest filesystem.
|
||||
/// Like [File::open] but for the guest filesystem.
|
||||
#[allow(dead_code)]
|
||||
pub fn open<P: AsRef<GuestPath>>(&self, path: P) -> Result<std::fs::File, ()> {
|
||||
pub fn open<P: AsRef<GuestPath>>(&self, path: P) -> Result<GuestFile, ()> {
|
||||
// it would be nice to delegate to self.open_with_options, but currently it wants a mutable reference to self
|
||||
let node = self.lookup_node(path.as_ref()).ok_or(())?;
|
||||
let FsNode::File {
|
||||
host_path,
|
||||
writeable: _,
|
||||
} = node else {
|
||||
return Err(())
|
||||
};
|
||||
Ok(handle_open_err(std::fs::File::open(host_path), host_path))
|
||||
match node {
|
||||
FsNode::HostFile { host_path, .. } => {
|
||||
let host_file = handle_open_err(File::open(host_path), host_path);
|
||||
Ok(GuestFile::from_host_file(host_file))
|
||||
}
|
||||
FsNode::IpaBundleFile { file } => Ok(GuestFile::from_ipa_file(file)),
|
||||
FsNode::Directory { .. } => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [std::fs::File::options] but for the guest filesystem.
|
||||
/// Like [File::options] but for the guest filesystem.
|
||||
pub fn open_with_options<P: AsRef<GuestPath>>(
|
||||
&mut self,
|
||||
path: P,
|
||||
options: GuestOpenOptions,
|
||||
) -> Result<std::fs::File, ()> {
|
||||
) -> Result<GuestFile, ()> {
|
||||
let GuestOpenOptions {
|
||||
read,
|
||||
write,
|
||||
@@ -474,26 +556,38 @@ impl Fs {
|
||||
// Open an existing file if possible
|
||||
|
||||
if let Some(existing_file) = children.get(&new_filename) {
|
||||
let FsNode::File {
|
||||
host_path,
|
||||
writeable,
|
||||
} = existing_file else {
|
||||
return Err(());
|
||||
};
|
||||
if !writeable && (append || write) {
|
||||
log!("Warning: attempt to write to read-only file {:?}", path);
|
||||
return Err(());
|
||||
match existing_file {
|
||||
FsNode::HostFile {
|
||||
host_path,
|
||||
writeable,
|
||||
} => {
|
||||
if !writeable && (append || write) {
|
||||
log!("Warning: attempt to write to read-only file {:?}", path);
|
||||
return Err(());
|
||||
}
|
||||
let file = handle_open_err(
|
||||
File::options()
|
||||
.read(read)
|
||||
.write(write)
|
||||
.append(append)
|
||||
.create(false)
|
||||
.truncate(truncate)
|
||||
.open(host_path),
|
||||
host_path,
|
||||
);
|
||||
return Ok(GuestFile::from_host_file(file));
|
||||
}
|
||||
FsNode::IpaBundleFile { file } => {
|
||||
if write || append || truncate {
|
||||
log!("Warning: attempt to write to read-only file {:?}", path);
|
||||
return Err(());
|
||||
}
|
||||
return Ok(GuestFile::from_ipa_file(file));
|
||||
}
|
||||
FsNode::Directory { .. } => {
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
return Ok(handle_open_err(
|
||||
std::fs::File::options()
|
||||
.read(read)
|
||||
.write(write)
|
||||
.append(append)
|
||||
.create(false)
|
||||
.truncate(truncate)
|
||||
.open(host_path),
|
||||
host_path,
|
||||
));
|
||||
};
|
||||
|
||||
// Create a new file otherwise
|
||||
@@ -516,7 +610,7 @@ impl Fs {
|
||||
let host_path = dir_host_path.join(&new_filename);
|
||||
|
||||
let file = handle_open_err(
|
||||
std::fs::File::options()
|
||||
File::options()
|
||||
.read(read)
|
||||
.write(write)
|
||||
.append(append)
|
||||
@@ -532,11 +626,11 @@ impl Fs {
|
||||
);
|
||||
children.insert(
|
||||
new_filename,
|
||||
FsNode::File {
|
||||
FsNode::HostFile {
|
||||
host_path,
|
||||
writeable: true,
|
||||
},
|
||||
);
|
||||
Ok(file)
|
||||
Ok(GuestFile::from_host_file(file))
|
||||
}
|
||||
}
|
||||
|
||||
232
src/fs/bundle.rs
Normal file
232
src/fs/bundle.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
//! IPA file format support, allowing it to be used as part of the guest filesystem.
|
||||
use crate::fs::{FsNode, GuestPath};
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Debug;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use zip::result::ZipError;
|
||||
use zip::ZipArchive;
|
||||
|
||||
/// A helper struct to build an FsNode with files and directories coming in arbitrary order.
|
||||
/// This is required, because ZIP files are allowed to store entries in arbitrary order.
|
||||
struct FsNodeBuilder {
|
||||
root: FsNode,
|
||||
}
|
||||
|
||||
impl FsNodeBuilder {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
root: FsNode::dir(),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_or_make_directory(&mut self, path: &GuestPath) -> &mut FsNode {
|
||||
let mut current = &mut self.root;
|
||||
for part in path.as_str().split('/') {
|
||||
if part.is_empty() {
|
||||
continue;
|
||||
}
|
||||
assert_ne!(part, "..", "unexpected .. in path: {path:?}");
|
||||
let FsNode::Directory { children, .. } = current else {
|
||||
panic!("expected directory, got {current:?}");
|
||||
};
|
||||
|
||||
let next = children.entry(part.to_string()).or_insert_with(FsNode::dir);
|
||||
current = next;
|
||||
}
|
||||
current
|
||||
}
|
||||
|
||||
fn add_file(&mut self, path: &GuestPath, node: FsNode) {
|
||||
let (parent_name, file_name) = path.parent_and_file_name().unwrap();
|
||||
assert_ne!(file_name, "..", "unexpected .. in path: {path:?}");
|
||||
let dir = self.find_or_make_directory(parent_name);
|
||||
let FsNode::Directory { children, .. } = dir else {
|
||||
panic!("expected directory, got {dir:?}");
|
||||
};
|
||||
|
||||
children.insert(file_name.to_string(), node);
|
||||
}
|
||||
|
||||
fn add_directory(&mut self, path: &GuestPath) {
|
||||
self.find_or_make_directory(path);
|
||||
}
|
||||
|
||||
fn build(self) -> FsNode {
|
||||
self.root
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an open app bundle, either a directory or a zip file.
|
||||
pub enum BundleData {
|
||||
HostDirectory(PathBuf),
|
||||
Zip {
|
||||
zip: ZipArchive<std::fs::File>,
|
||||
/// Path to the app bundle inside the zip file.
|
||||
/// It should be "Payload/<app name>.app" (no trailing slash!).
|
||||
bundle_path: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl BundleData {
|
||||
fn find_bundle_path_in_archive(zip: &mut ZipArchive<std::fs::File>) -> Result<String, String> {
|
||||
for i in 0..zip.len() {
|
||||
let file = zip
|
||||
.by_index(i)
|
||||
.map_err(|e| format!("Could not open IPA archive entry: {e}"))?;
|
||||
let path = file.name();
|
||||
if let Some(name) = path
|
||||
.strip_prefix("Payload/")
|
||||
.and_then(|path| path.split_once('/'))
|
||||
.and_then(|(name, _)| name.strip_suffix(".app"))
|
||||
{
|
||||
return Ok(format!("Payload/{name}.app"));
|
||||
}
|
||||
}
|
||||
Err("no app bundle found in the IPA archive".to_string())
|
||||
}
|
||||
|
||||
pub fn open_host_dir(path: &Path) -> Result<BundleData, String> {
|
||||
Ok(BundleData::HostDirectory(path.to_path_buf()))
|
||||
}
|
||||
|
||||
pub fn open_ipa(path: &Path) -> Result<BundleData, String> {
|
||||
let file =
|
||||
std::fs::File::open(path).map_err(|e| format!("Could not open IPA file: {e}"))?;
|
||||
let mut zip =
|
||||
ZipArchive::new(file).map_err(|e| format!("Could not open IPA archive: {e}"))?;
|
||||
let bundle_path = Self::find_bundle_path_in_archive(&mut zip)?;
|
||||
Ok(BundleData::Zip { zip, bundle_path })
|
||||
}
|
||||
|
||||
pub fn open_any(path: &Path) -> Result<BundleData, String> {
|
||||
if path.is_file()
|
||||
&& path
|
||||
.extension()
|
||||
.map(|ext| ext.eq_ignore_ascii_case("ipa"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Ok(Self::open_ipa(path)?)
|
||||
} else if path.is_dir() {
|
||||
Ok(Self::open_host_dir(path)?)
|
||||
} else {
|
||||
Err(format!(
|
||||
"{} is not a directory or an IPA file",
|
||||
path.display()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn into_fs_node(self) -> FsNode {
|
||||
match self {
|
||||
BundleData::HostDirectory(path) => FsNode::from_host_dir(&path, false),
|
||||
BundleData::Zip { zip, bundle_path } => {
|
||||
let archive = Rc::new(RefCell::new(zip));
|
||||
|
||||
let mut archive_guard = (*archive).borrow_mut();
|
||||
|
||||
let mut builder = FsNodeBuilder::new();
|
||||
for i in 0..archive_guard.len() {
|
||||
let file = archive_guard.by_index(i).unwrap(); // TODO: report IO error?
|
||||
let name = file.name();
|
||||
if let Some(path) = name.strip_prefix(&bundle_path) {
|
||||
let path = GuestPath::new(path);
|
||||
if file.is_dir() {
|
||||
builder.add_directory(path);
|
||||
} else {
|
||||
builder.add_file(
|
||||
path,
|
||||
FsNode::bundle_zip_file(IpaFileRef {
|
||||
archive: archive.clone(),
|
||||
index: i,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_plist(&mut self) -> Result<Vec<u8>, String> {
|
||||
match self {
|
||||
BundleData::HostDirectory(path) => {
|
||||
std::fs::read(path.join("Info.plist")).map_err(|e| {
|
||||
format!("Could not read Info.plist from the app bundle directory: {e}")
|
||||
})
|
||||
}
|
||||
BundleData::Zip { zip, bundle_path } => {
|
||||
let mut file = zip
|
||||
.by_name(&format!("{bundle_path}/Info.plist"))
|
||||
.map_err(|e| format!("Could not open Info.plist from the IPA archive: {e}"))?;
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf)
|
||||
.map_err(|e| format!("Could not read Info.plist from the IPA archive: {e}"))?;
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a file inside an IPA bundle that can be opened.
|
||||
#[derive(Debug)]
|
||||
pub struct IpaFileRef {
|
||||
archive: Rc<RefCell<ZipArchive<std::fs::File>>>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl IpaFileRef {
|
||||
pub fn open(&self) -> IpaFile {
|
||||
let mut archive = (*self.archive).borrow_mut();
|
||||
let mut file = match archive.by_index(self.index) {
|
||||
Ok(file) => file,
|
||||
Err(ZipError::Io(e)) => {
|
||||
// this is a runtime error, which we __probably__ should not bubble up to the guest
|
||||
panic!("IO error while opening file from IPA bundle: {e}")
|
||||
}
|
||||
// anything other than IO error is a bug in the code, we should always have a valid index
|
||||
Err(e) => panic!("BUG: could not open file from IPA bundle: {e}"),
|
||||
};
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf).unwrap();
|
||||
IpaFile {
|
||||
file: std::io::Cursor::new(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an opened file in an IPA bundle.
|
||||
pub struct IpaFile {
|
||||
// we need to use a cursor because zip::read::ZipFile doesn't implement Seek
|
||||
// and, generally, seeking in compressed files is hard to achieve
|
||||
// the simplest way to do it is to read the whole file into memory
|
||||
// the target apps should be small enough to fit in memory, right?
|
||||
file: std::io::Cursor<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Debug for IpaFile {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("IpaFile")
|
||||
.field("size", &self.file.get_ref().len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for IpaFile {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
self.file.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Seek for IpaFile {
|
||||
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
|
||||
self.file.seek(pos)
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ struct FILE {
|
||||
unsafe impl SafeRead for FILE {}
|
||||
|
||||
struct FileHostObject {
|
||||
file: std::fs::File,
|
||||
file: crate::fs::GuestFile,
|
||||
}
|
||||
|
||||
fn fopen(env: &mut Environment, filename: ConstPtr<u8>, mode: ConstPtr<u8>) -> MutPtr<FILE> {
|
||||
|
||||
@@ -282,10 +282,12 @@ impl Environment {
|
||||
fn new(bundle_path: PathBuf, options: Options) -> Result<Environment, String> {
|
||||
let startup_time = std::time::Instant::now();
|
||||
|
||||
let (bundle, fs) = match bundle::Bundle::new_bundle_and_fs_from_host_path(bundle_path) {
|
||||
let bundle_data = fs::BundleData::open_any(&bundle_path)
|
||||
.map_err(|e| format!("Could not open app bundle: {e}"))?;
|
||||
let (bundle, fs) = match bundle::Bundle::new_bundle_and_fs_from_host_path(bundle_data) {
|
||||
Ok(bundle) => bundle,
|
||||
Err(err) => {
|
||||
return Err(format!("Application bundle error: {}. Check that the path is to a .app directory. If this is a .ipa file, you need to extract it as a ZIP file to get the .app directory.", err));
|
||||
return Err(format!("Application bundle error: {err}. Check that the path is to an .app directory or an .ipa file."));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user