diff --git a/Cargo.lock b/Cargo.lock index ef8b80e..8e9ca57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -658,12 +658,16 @@ dependencies = [ name = "dtmm" version = "0.1.0" dependencies = [ + "bitflags", "clap", "color-eyre", "confy", "druid", + "futures", + "oodle-sys", "sdk", "serde", + "serde_sjson", "tokio", "toml", "tracing", diff --git a/crates/dtmm/Cargo.toml b/crates/dtmm/Cargo.toml index 64361c4..006aa9e 100644 --- a/crates/dtmm/Cargo.toml +++ b/crates/dtmm/Cargo.toml @@ -6,13 +6,17 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bitflags = "1.3.2" clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "unicode"] } color-eyre = "0.6.2" confy = "0.5.1" druid = { git = "https://github.com/linebender/druid.git", features = ["im"] } +futures = "0.3.25" sdk = { path = "../../lib/sdk", version = "0.2.0" } -serde = "1.0.152" -tokio = "1.23.0" +serde = { version = "1.0.152", features = ["derive"] } +serde_sjson = { path = "../../lib/serde_sjson", version = "*" } +oodle-sys = { path = "../../lib/oodle-sys", version = "*" } +tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] } toml = "0.5.10" tracing = "0.1.37" tracing-error = "0.2.0" diff --git a/crates/dtmm/src/controller.rs b/crates/dtmm/src/controller.rs new file mode 100644 index 0000000..a2bf429 --- /dev/null +++ b/crates/dtmm/src/controller.rs @@ -0,0 +1,47 @@ +use druid::widget::{Button, Controller}; +use druid::{Data, Env, Event, EventCtx, LifeCycle, LifeCycleCtx, UpdateCtx, Widget}; + +pub struct DisabledButtonController; + +impl Controller> for DisabledButtonController { + fn event( + &mut self, + child: &mut Button, + ctx: &mut EventCtx, + event: &Event, + data: &mut T, + env: &Env, + ) { + if !ctx.is_disabled() { + ctx.set_disabled(true); + ctx.request_paint(); + } + child.event(ctx, event, data, env) + } + + fn lifecycle( + &mut self, + child: &mut Button, + ctx: &mut LifeCycleCtx, + event: &LifeCycle, + data: &T, + env: &Env, + ) { + child.lifecycle(ctx, event, data, env) + } + + fn update( + &mut self, + child: &mut Button, + ctx: &mut UpdateCtx, + old_data: &T, + data: &T, + env: &Env, + ) { + if !ctx.is_disabled() { + ctx.set_disabled(true); + ctx.request_paint(); + } + child.update(ctx, old_data, data, env) + } +} diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs new file mode 100644 index 0000000..818bd8f --- /dev/null +++ b/crates/dtmm/src/engine.rs @@ -0,0 +1,373 @@ +use std::ffi::CString; +use std::io::{Cursor, ErrorKind}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::Arc; + +use color_eyre::eyre::Context; +use color_eyre::{eyre, Result}; +use futures::stream; +use futures::StreamExt; +use sdk::filetype::lua; +use sdk::filetype::package::Package; +use sdk::murmur::Murmur64; +use sdk::{ + Bundle, BundleDatabase, BundleFile, BundleFileType, BundleFileVariant, FromBinary, ToBinary, +}; +use tokio::io::AsyncWriteExt; +use tokio::{fs, try_join}; +use tracing::Instrument; + +use crate::state::{PackageInfo, State}; + +const MOD_BUNDLE_NAME: &str = "packages/mods"; +const BOOT_BUNDLE_NAME: &str = "packages/boot"; +const BUNDLE_DATABASE_NAME: &str = "bundle_database.data"; +const MOD_BOOT_SCRIPT: &str = "scripts/mod_main"; + +#[tracing::instrument] +async fn read_file_with_backup

(path: P) -> Result> +where + P: AsRef + std::fmt::Debug, +{ + let path = path.as_ref(); + let backup_path = { + let mut p = PathBuf::from(path); + let ext = if let Some(ext) = p.extension() { + ext.to_string_lossy().to_string() + ".bak" + } else { + String::from("bak") + }; + p.set_extension(ext); + p + }; + + let file_name = path + .file_name() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_else(|| String::from("file")); + + let bin = match fs::read(&backup_path).await { + Ok(bin) => bin, + Err(err) if err.kind() == ErrorKind::NotFound => { + // TODO: This doesn't need to be awaited here, yet. + // I only need to make sure it has finished before writing the changed bundle. + tracing::debug!( + "Backup does not exist. Backing up original {} to '{}'", + file_name, + backup_path.display() + ); + fs::copy(path, &backup_path).await.wrap_err_with(|| { + format!( + "failed to back up {} '{}' to '{}'", + file_name, + path.display(), + backup_path.display() + ) + })?; + + tracing::debug!("Reading {} from original '{}'", file_name, path.display()); + fs::read(path).await.wrap_err_with(|| { + format!("failed to read {} file: {}", file_name, path.display()) + })? + } + Err(err) => { + return Err(err).wrap_err_with(|| { + format!( + "failed to read {} from backup '{}'", + file_name, + backup_path.display() + ) + }); + } + }; + Ok(bin) +} + +#[tracing::instrument(skip_all)] +async fn patch_game_settings(state: Arc) -> Result<()> { + let settings_path = state + .get_game_dir() + .join("bundle/application_settings/settings_common.ini"); + + let settings = read_file_with_backup(&settings_path) + .await + .wrap_err("failed to read settings.ini")?; + let settings = String::from_utf8(settings).wrap_err("settings.ini is not valid UTF-8")?; + + let mut f = fs::File::create(&settings_path) + .await + .wrap_err_with(|| format!("failed to open {}", settings_path.display()))?; + + let Some(i) = settings.find("boot_script =") else { + eyre::bail!("couldn't find 'boot_script' field"); + }; + + f.write_all(settings[0..i].as_bytes()).await?; + f.write_all(b"boot_script = \"scripts/mod_main\"").await?; + + let Some(j) = settings[i..].find('\n') else { + eyre::bail!("couldn't find end of 'boot_script' field"); + }; + + f.write_all(settings[(i + j)..].as_bytes()).await?; + + tracing::info!("Patched game settings"); + Ok(()) +} + +#[tracing::instrument(skip_all, fields(package = info.get_name()))] +fn make_package(info: &PackageInfo) -> Result { + let mut pkg = Package::new(info.get_name().clone(), PathBuf::new()); + + for f in info.get_files().iter() { + let mut it = f.rsplit('.'); + let file_type = it + .next() + .ok_or_else(|| eyre::eyre!("missing file extension")) + .and_then(BundleFileType::from_str) + .wrap_err("invalid file name in package info")?; + let name: String = it.collect(); + pkg.add_file(file_type, name); + } + + Ok(pkg) +} + +#[tracing::instrument(skip_all)] +async fn build_bundles(state: Arc) -> Result<()> { + let mut bundle = Bundle::new(MOD_BUNDLE_NAME.into()); + let mut tasks = Vec::new(); + + let bundle_dir = Arc::new(state.get_game_dir().join("bundle")); + let database_path = bundle_dir.join(BUNDLE_DATABASE_NAME); + + let mut db = { + let bin = read_file_with_backup(&database_path) + .await + .wrap_err("failed to read bundle database")?; + let mut r = Cursor::new(bin); + let db = BundleDatabase::from_binary(&mut r).wrap_err("failed to parse bundle database")?; + tracing::trace!("Finished parsing bundle database"); + db + }; + + for mod_info in state.get_mods() { + let span = tracing::trace_span!("building mod packages", name = mod_info.get_name()); + let _enter = span.enter(); + + let mod_dir = state.get_mod_dir().join(mod_info.get_name()); + for pkg_info in mod_info.get_packages() { + let span = tracing::trace_span!("building package", name = pkg_info.get_name()); + let _enter = span.enter(); + + let pkg = make_package(pkg_info).wrap_err("failed to make package")?; + let mut variant = BundleFileVariant::new(); + let bin = pkg + .to_binary() + .wrap_err("failed to serialize package to binary")?; + variant.set_data(bin); + let mut file = BundleFile::new(pkg_info.get_name().clone(), BundleFileType::Package); + file.add_variant(variant); + + bundle.add_file(file); + + let src = mod_dir.join(pkg_info.get_name()); + let dest = bundle_dir.clone(); + let pkg_name = pkg_info.get_name().clone(); + let mod_name = mod_info.get_name().clone(); + + tracing::trace!( + "Adding package {} for mod {} to bundle database", + pkg_info.get_name(), + mod_info.get_name() + ); + + // Explicitely drop the guard, so that we can move the span + // into the async operation + drop(_enter); + + let task = async move { + tracing::debug!( + "Copying bundle {} for mod {}: {} -> {}", + pkg_name, + mod_name, + src.display(), + dest.display() + ); + fs::hard_link(&src, dest.as_ref()).await.wrap_err_with(|| { + format!("failed to hard link bundle {pkg_name} for mod {mod_name}") + }) + } + .instrument(span); + + tasks.push(task); + } + } + + tracing::debug!("Copying {} mod bundles", tasks.len()); + + let mut tasks = stream::iter(tasks).buffer_unordered(10); + + while let Some(res) = tasks.next().await { + res?; + } + + db.add_bundle(&bundle); + + { + let path = bundle_dir.join(format!("{:x}", Murmur64::hash(bundle.name()))); + tracing::trace!("Writing mod bundle to '{}'", path.display()); + fs::write(&path, bundle.to_binary()?) + .await + .wrap_err_with(|| format!("failed to write bundle to '{}'", path.display()))?; + } + + { + tracing::trace!("Writing bundle database to '{}'", database_path.display()); + let bin = db + .to_binary() + .wrap_err("failed to serialize bundle database")?; + fs::write(&database_path, bin).await.wrap_err_with(|| { + format!( + "failed to write bundle database to '{}'", + database_path.display() + ) + })?; + } + + Ok(()) +} + +#[tracing::instrument(skip_all)] +async fn patch_boot_bundle(state: Arc) -> Result<()> { + let bundle_dir = Arc::new(state.get_game_dir().join("bundle")); + + let bundle_path = bundle_dir.join(format!("{:x}", Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes()))); + let database_path = bundle_dir.join(BUNDLE_DATABASE_NAME); + + let (mut db, mut bundle) = try_join!( + async { + let bin = read_file_with_backup(&database_path) + .await + .wrap_err("failed to read bundle database")?; + let mut r = Cursor::new(bin); + + BundleDatabase::from_binary(&mut r).wrap_err("failed to parse bundle database") + } + .instrument(tracing::trace_span!("read bundle database")), + async { + let bin = read_file_with_backup(&bundle_path) + .await + .wrap_err("failed to read boot bundle")?; + + Bundle::from_binary(&state.get_ctx(), BOOT_BUNDLE_NAME.to_string(), bin) + .wrap_err("failed to parse boot bundle") + } + .instrument(tracing::trace_span!("read boot bundle")) + )?; + + { + tracing::trace!("Adding mod package file to boot bundle"); + let span = tracing::trace_span!("create mod package file"); + let _enter = span.enter(); + + let mut pkg = Package::new(MOD_BUNDLE_NAME.to_string(), PathBuf::new()); + + for mod_info in state.get_mods() { + for pkg_info in mod_info.get_packages() { + pkg.add_file(BundleFileType::Package, pkg_info.get_name()); + } + } + + let mut variant = BundleFileVariant::new(); + variant.set_data(pkg.to_binary()?); + let mut f = BundleFile::new(MOD_BUNDLE_NAME.to_string(), BundleFileType::Package); + f.add_variant(variant); + + bundle.add_file(f); + } + + { + tracing::trace!("Adding main mod Lua file to boot bundle"); + let span = tracing::trace_span!("create mod boot script file"); + let _enter = span.enter(); + + // TODO: Build actual boot script + let lua = CString::new( + r#" +print("dtmm says hello!") +require("scripts/main") +"#, + ) + .expect("invalid C string"); + let f = lua::compile(MOD_BOOT_SCRIPT.to_string(), &lua) + .wrap_err("failed to compile mod boot script")?; + + // TODO: + bundle.add_file(f); + } + + db.add_bundle(&bundle); + + try_join!( + async { + let bin = bundle + .to_binary() + .wrap_err("failed to serialize boot bundle")?; + fs::write(&bundle_path, bin) + .await + .wrap_err_with(|| format!("failed to write main bundle: {}", bundle_path.display())) + } + .instrument(tracing::trace_span!("write boot bundle")), + async { + let bin = db + .to_binary() + .wrap_err("failed to serialize bundle database")?; + fs::write(&database_path, bin).await.wrap_err_with(|| { + format!( + "failed to write bundle database to '{}'", + database_path.display() + ) + }) + } + .instrument(tracing::trace_span!("write bundle database")) + )?; + + Ok(()) +} + +#[tracing::instrument(skip_all, fields( + game_dir = %state.get_game_dir().display(), + mods = state.get_mods().len() +))] +pub(crate) async fn deploy_mods(state: State) -> Result<()> { + let state = Arc::new(state); + + tracing::info!( + "Deploying {} mods to {}", + state.get_mods().len(), + state.get_game_dir().join("bundle").display() + ); + + tracing::info!("Build mod bundles"); + build_bundles(state.clone()) + .await + .wrap_err("failed to build mod bundles")?; + + tracing::info!("Patch boot bundle"); + patch_boot_bundle(state.clone()) + .await + .wrap_err("failed to patch boot bundle")?; + + tracing::info!("Patch game settings"); + patch_game_settings(state.clone()) + .await + .wrap_err("failed to patch game settings")?; + + // TODO: Build mod order data + // TODO: Handle DMF + + tracing::info!("Finished deploying mods"); + Ok(()) +} diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index e3e8b29..300c6cd 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -1,24 +1,73 @@ +#![recursion_limit = "256"] +#![feature(let_chains)] + +use std::sync::Arc; + use clap::command; +use clap::Arg; use color_eyre::Report; use color_eyre::Result; use druid::AppLauncher; +use druid::ExtEventSink; +use druid::Target; +use tokio::runtime::Runtime; +use tokio::sync::mpsc::UnboundedReceiver; +use tokio::sync::RwLock; use tracing_error::ErrorLayer; use tracing_subscriber::prelude::*; use tracing_subscriber::EnvFilter; -use crate::state::State; +use crate::engine::deploy_mods; +use crate::state::{AsyncAction, Delegate, State, COMMAND_FINISH_DEPLOY}; +mod controller; +mod engine; mod main_window; mod state; mod theme; mod widget; +fn work_thread( + event_sink: Arc>, + action_queue: Arc>>, +) -> Result<()> { + let rt = Runtime::new()?; + + rt.block_on(async { + while let Some(action) = action_queue.write().await.recv().await { + let event_sink = event_sink.clone(); + match action { + AsyncAction::DeployMods(state) => tokio::spawn(async move { + if let Err(err) = deploy_mods(state).await { + tracing::error!("Failed to deploy mods: {:?}", err); + } + + event_sink + .write() + .await + .submit_command(COMMAND_FINISH_DEPLOY, (), Target::Auto) + .expect("failed to send command"); + }), + }; + } + }); + + Ok(()) +} + #[tracing::instrument] #[tokio::main] async fn main() -> Result<()> { color_eyre::install()?; - let _matches = command!().get_matches(); + let matches = command!() + .arg(Arg::new("oodle").long("oodle").help( + "The oodle library to load. This may either be:\n\ + - A library name that will be searched for in the system's default paths.\n\ + - A file path relative to the current working directory.\n\ + - An absolute file path.", + )) + .get_matches(); { let filter_layer = @@ -47,9 +96,27 @@ async fn main() -> Result<()> { } } + unsafe { + oodle_sys::init(matches.get_one::("oodle")); + } + let initial_state = State::new(); - AppLauncher::with_window(main_window::new()) - .launch(initial_state) - .map_err(Report::new) + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + let delegate = Delegate::new(sender); + + let launcher = AppLauncher::with_window(main_window::new()).delegate(delegate); + + let event_sink = launcher.get_external_handle(); + std::thread::spawn(move || { + let event_sink = Arc::new(RwLock::new(event_sink)); + let receiver = Arc::new(RwLock::new(receiver)); + loop { + if let Err(err) = work_thread(event_sink.clone(), receiver.clone()) { + tracing::error!("Work thread failed, restarting: {:?}", err); + } + } + }); + + launcher.launch(initial_state).map_err(Report::new) } diff --git a/crates/dtmm/src/main_window.rs b/crates/dtmm/src/main_window.rs index b8bf488..3dd37f0 100644 --- a/crates/dtmm/src/main_window.rs +++ b/crates/dtmm/src/main_window.rs @@ -7,7 +7,7 @@ use druid::{lens, Insets, LensExt, Widget, WidgetExt, WindowDesc}; use crate::state::{ ModInfo, PathBufFormatter, State, StateController, View, ACTION_DELETE_SELECTED_MOD, - ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD, + ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD, COMMAND_START_DEPLOY, }; use crate::theme; use crate::widget::ExtraWidgetExt; @@ -48,11 +48,13 @@ fn build_top_bar() -> impl Widget { ) .with_child( Flex::row() - .with_child(Button::new("Deploy Mods").on_click( - |_ctx, _state: &mut State, _env| { - todo!(); - }, - )) + .with_child( + Button::new("Deploy Mods") + .on_click(|ctx, _state: &mut State, _env| { + ctx.submit_command(COMMAND_START_DEPLOY); + }) + .disabled_if(|data, _| !data.can_deploy_mods()), + ) .with_default_spacer() .with_child( Button::new("Run Game").on_click(|_ctx, _state: &mut State, _env| { diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index c22bdd7..c963515 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -5,7 +5,7 @@ use druid::im::Vector; use druid::text::Formatter; use druid::widget::Controller; use druid::{ - AppDelegate, Data, DelegateCtx, Env, Event, EventCtx, Handled, Lens, Selector, Target, + AppDelegate, Command, Data, DelegateCtx, Env, Event, EventCtx, Handled, Lens, Selector, Target, Widget, }; use tokio::sync::mpsc::UnboundedSender; @@ -15,6 +15,9 @@ pub const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected pub const ACTION_SELECTED_MOD_DOWN: Selector = Selector::new("dtmm.action.selected-mod-down"); pub const ACTION_DELETE_SELECTED_MOD: Selector = Selector::new("dtmm.action.delete-selected-mod"); +pub const COMMAND_FINISH_DEPLOY: Selector = Selector::new("dtmm.command.finish-deploy"); +pub const COMMAND_START_DEPLOY: Selector = Selector::new("dtmm.command.start-deploy"); + #[derive(Copy, Clone, Data, Debug, PartialEq)] pub(crate) enum View { Mods, @@ -147,6 +150,10 @@ impl State { self.selected_mod_index.map(|i| i > 0).unwrap_or(false) } + pub fn can_deploy_mods(&self) -> bool { + !self.is_deployment_in_progress + } + pub(crate) fn get_game_dir(&self) -> &PathBuf { &self.game_dir } @@ -298,6 +305,52 @@ impl> Controller for StateController { } } +pub(crate) enum AsyncAction { + DeployMods(State), +} + +pub(crate) struct Delegate { + sender: UnboundedSender, +} + +impl Delegate { + pub fn new(sender: UnboundedSender) -> Self { + Self { sender } + } +} + +impl AppDelegate for Delegate { + #[tracing::instrument(name = "Delegate", skip_all)] + fn command( + &mut self, + _ctx: &mut DelegateCtx, + _target: Target, + cmd: &Command, + data: &mut State, + _env: &Env, + ) -> Handled { + if cmd.is(COMMAND_START_DEPLOY) { + if self + .sender + .send(AsyncAction::DeployMods(data.clone())) + .is_ok() + { + data.is_deployment_in_progress = true; + } else { + tracing::error!("Failed to queue action to deploy mods"); + } + + Handled::Yes + } else if cmd.is(COMMAND_FINISH_DEPLOY) { + data.is_deployment_in_progress = false; + Handled::Yes + } else { + tracing::debug!("Unknown command: {:?}", cmd); + Handled::No + } + } +} + pub(crate) struct PathBufFormatter; impl PathBufFormatter { diff --git a/crates/dtmm/src/widget/table_select.rs b/crates/dtmm/src/widget/table_select.rs new file mode 100644 index 0000000..00321f8 --- /dev/null +++ b/crates/dtmm/src/widget/table_select.rs @@ -0,0 +1,73 @@ +use druid::widget::{Controller, Flex}; +use druid::{Data, Widget}; + +pub struct TableSelect { + widget: Flex, + controller: TableSelectController, +} + +impl TableSelect { + pub fn new(values: impl IntoIterator + 'static)>) -> Self { + todo!(); + } +} + +impl Widget for TableSelect { + fn event( + &mut self, + ctx: &mut druid::EventCtx, + event: &druid::Event, + data: &mut T, + env: &druid::Env, + ) { + todo!() + } + + fn lifecycle( + &mut self, + ctx: &mut druid::LifeCycleCtx, + event: &druid::LifeCycle, + data: &T, + env: &druid::Env, + ) { + todo!() + } + + fn update(&mut self, ctx: &mut druid::UpdateCtx, old_data: &T, data: &T, env: &druid::Env) { + todo!() + } + + fn layout( + &mut self, + ctx: &mut druid::LayoutCtx, + bc: &druid::BoxConstraints, + data: &T, + env: &druid::Env, + ) -> druid::Size { + todo!() + } + + fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &T, env: &druid::Env) { + todo!() + } +} + +struct TableSelectController { + inner: T, +} + +impl TableSelectController {} + +impl Controller> for TableSelectController {} + +pub struct TableItem { + inner: dyn Widget, +} + +impl TableItem { + pub fn new(inner: impl Widget) -> Self { + todo!(); + } +} + +impl Widget for TableItem {} diff --git a/lib/sdk/src/bundle/database.rs b/lib/sdk/src/bundle/database.rs index b3d9296..8438b40 100644 --- a/lib/sdk/src/bundle/database.rs +++ b/lib/sdk/src/bundle/database.rs @@ -50,14 +50,15 @@ impl BundleDatabase { self.stored_files.entry(hash).or_default().push(file); - // TODO: Resource hashes - for f in bundle.files() { let file_name = FileName { extension: f.file_type(), name: Murmur64::hash(f.name(false, None).as_bytes()), }; + // TODO: Compute actual resource hash + self.resource_hashes.insert(hash, 0); + self.bundle_contents .entry(hash) .or_default() diff --git a/lib/sdk/src/bundle/file.rs b/lib/sdk/src/bundle/file.rs index 2efa9b2..a5ed380 100644 --- a/lib/sdk/src/bundle/file.rs +++ b/lib/sdk/src/bundle/file.rs @@ -12,8 +12,6 @@ use crate::binary::sync::*; use crate::filetype::*; use crate::murmur::{HashGroup, IdString64, Murmur64}; -use super::EntryHeader; - #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] pub enum BundleFileType { Animation, @@ -801,6 +799,12 @@ impl BundleFile { } } +impl PartialEq for BundleFile { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.file_type == other.file_type + } +} + pub struct UserFile { // TODO: Might be able to avoid some allocations with a Cow here data: Vec, diff --git a/lib/sdk/src/bundle/mod.rs b/lib/sdk/src/bundle/mod.rs index 9e1a699..ce3a23f 100644 --- a/lib/sdk/src/bundle/mod.rs +++ b/lib/sdk/src/bundle/mod.rs @@ -91,10 +91,6 @@ impl Bundle { } } - pub fn get_file>(&self, name: S) -> Option<&BundleFile> { - self.files.iter().find(|f| f.base_name().eq(name.as_ref())) - } - #[tracing::instrument(skip(ctx, binary), fields(len_binary = binary.as_ref().len()))] pub fn from_binary(ctx: &crate::Context, name: String, binary: B) -> Result where diff --git a/lib/sdk/src/filetype/package.rs b/lib/sdk/src/filetype/package.rs index 8b42116..2dc9c3c 100644 --- a/lib/sdk/src/filetype/package.rs +++ b/lib/sdk/src/filetype/package.rs @@ -104,6 +104,14 @@ impl DerefMut for Package { } impl Package { + pub fn new(name: String, root: PathBuf) -> Self { + Self { + _name: name, + _root: root, + inner: Default::default(), + } + } + fn len(&self) -> usize { self.values().fold(0, |total, files| total + files.len()) } diff --git a/lib/sdk/src/murmur/mod.rs b/lib/sdk/src/murmur/mod.rs index d054b48..9ea432d 100644 --- a/lib/sdk/src/murmur/mod.rs +++ b/lib/sdk/src/murmur/mod.rs @@ -66,6 +66,12 @@ impl fmt::UpperHex for Murmur64 { } } +impl fmt::LowerHex for Murmur64 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerHex::fmt(&self.0, f) + } +} + impl fmt::Display for Murmur64 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::UpperHex::fmt(&self.0, f)