From bdb6b4ceee08df6996fe901647a273041005b03b Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Fri, 17 Oct 2025 14:19:26 +0800 Subject: [PATCH] write shit --- Cargo.lock | 86 +++++++++++++++++++ wfassoc/src/lib.rs | 42 +++++++--- wfassoc_exec/Cargo.toml | 2 + wfassoc_exec/src/cli.rs | 52 ++++++++++++ wfassoc_exec/src/main.rs | 154 +++++++++++------------------------ wfassoc_exec/src/manifest.rs | 30 +++++++ 6 files changed, 251 insertions(+), 115 deletions(-) create mode 100644 wfassoc_exec/src/cli.rs create mode 100644 wfassoc_exec/src/manifest.rs diff --git a/Cargo.lock b/Cargo.lock index 43715af..f776354 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,6 +377,45 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -420,6 +459,45 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + [[package]] name = "unicode-ident" version = "1.0.19" @@ -539,7 +617,9 @@ version = "0.1.0" dependencies = [ "clap", "comfy-table", + "serde", "thiserror", + "toml", "wfassoc", ] @@ -718,6 +798,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" + [[package]] name = "winreg" version = "0.55.0" diff --git a/wfassoc/src/lib.rs b/wfassoc/src/lib.rs index 56bf815..7c3a9a3 100644 --- a/wfassoc/src/lib.rs +++ b/wfassoc/src/lib.rs @@ -4,8 +4,8 @@ #[cfg(not(target_os = "windows"))] compile_error!("Crate wfassoc is only supported on Windows."); -pub(crate) mod assoc; -pub(crate) mod utilities; +pub mod assoc; +pub mod utilities; use indexmap::{IndexMap, IndexSet}; use std::ffi::OsStr; @@ -26,6 +26,8 @@ pub enum Error { BadRegOper(#[from] std::io::Error), #[error("{0}")] CastOsStr(#[from] utilities::CastOsStrError), + #[error("{0}")] + ParseExt(#[from] assoc::ParseExtError), #[error("no administrative privilege")] NoPrivilege, @@ -85,7 +87,7 @@ impl TryFrom for Scope { impl Scope { /// Check whether we have enough privilege when operating in current scope. /// If we have, return true, otherwise false. - fn has_privilege(&self) -> bool { + pub fn has_privilege(&self) -> bool { // If we operate on System, and we do not has privilege, // we think we do not have privilege, otherwise, // there is no privilege required. @@ -127,7 +129,7 @@ pub struct Manner { } impl Manner { - pub fn new(argv: &str) -> Self { + fn new(argv: &str) -> Self { Self { argv: argv.to_string(), } @@ -167,23 +169,25 @@ impl Program { /// /// - `My App` /// - `3DViewer` - /// - `我的Qt最时尚` + /// - `我的Qt程序` (means "My Qt App" in English) /// /// More preciously, `identifier` will be used as the vendor part of ProgId. /// /// `full_path` is the fully qualified path to the application. - pub fn new(identifier: &str, full_path: &Path) -> Self { + pub fn new(identifier: &str, full_path: &Path) -> Result { // TODO: Add checker for identifier - Self { + Ok(Self { identifier: identifier.to_string(), full_path: full_path.to_path_buf(), manners: IndexSet::new(), exts: IndexMap::new(), - } + }) } /// Add manner provided by this program. - pub fn add_manner(&mut self, manner: Manner) -> Result { + pub fn add_manner(&mut self, manner: &str) -> Result { + // Create manner from string + let manner = Manner::new(manner); // Backup a stringfied manner for error output. let manner_str = manner.to_string(); // Insert manner. @@ -201,11 +205,14 @@ impl Program { } /// Add file extension supported by this program and its associated manner. - pub fn add_ext(&mut self, ext: assoc::Ext, token: Token) -> Result { + pub fn add_ext(&mut self, ext: &str, token: Token) -> Result { // Check manner token if let None = self.get_manner(token) { return Err(Error::InvalidAssocManner); } + + // Create extension from string + let ext = assoc::Ext::new(ext)?; // Backup a stringfied extension for error output. let ext_str = ext.to_string(); // Insert file extension @@ -353,4 +360,19 @@ impl Program { } } +impl Program { + /// Set the default "open with" of given extension to this program. + pub fn link_ext(&self, ext: Token) -> Result<()> { + todo!() + } + + /// Remove this program from the default "open with" of given extension. + /// + /// If the default "open with" of given extension is not our program, + /// this function do nothing. + pub fn unlink_ext(&self, ext: Token) -> Result<()> { + todo!() + } +} + // endregion diff --git a/wfassoc_exec/Cargo.toml b/wfassoc_exec/Cargo.toml index ee2c440..4ea0559 100644 --- a/wfassoc_exec/Cargo.toml +++ b/wfassoc_exec/Cargo.toml @@ -10,4 +10,6 @@ license = "SPDX:MIT" thiserror = { workspace = true } wfassoc = { path="../wfassoc" } clap = { version="4.5.48", features=["derive"]} +serde = { version = "1.0.228", features=["derive"]} +toml = "0.9.8" comfy-table = "7.2.1" diff --git a/wfassoc_exec/src/cli.rs b/wfassoc_exec/src/cli.rs new file mode 100644 index 0000000..6642ccb --- /dev/null +++ b/wfassoc_exec/src/cli.rs @@ -0,0 +1,52 @@ +use clap::{Parser, Subcommand}; + +/// Simple program to manage Windows file associations +#[derive(Parser)] +#[command(name = "Windows File Association Operator", version, about)] +pub struct Cli { + /// The toml file introducing the complete program + #[arg( + short = 'c', + long = "config", + value_name = "PROG_CONFIG", + required = true + )] + pub(crate) config_file: String, + + /// The scope where wfassoc operate + #[arg(short = 'f', long = "for", value_name = "TARGET", value_enum, default_value_t = ForTarget::User)] + pub(crate) for_which: ForTarget, + + #[command(subcommand)] + pub(crate) command: Commands, +} + +#[derive(clap::ValueEnum, Clone)] +pub enum ForTarget { + #[value(name = "user")] + User, + #[value(name = "system")] + System, +} + +// impl From for RegisterKind { +// fn from(target: ForTarget) -> Self { +// match target { +// ForTarget::User => RegisterKind::User, +// ForTarget::System => RegisterKind::System, +// } +// } +// } + +#[derive(Subcommand)] +pub enum Commands { + /// Register the program + #[command(name = "register")] + Register, + /// Unregister the program + #[command(name = "unregister")] + Unregister, + /// Query file associations + #[command(name = "query")] + Query, +} diff --git a/wfassoc_exec/src/main.rs b/wfassoc_exec/src/main.rs index 1f352c7..8a697b6 100644 --- a/wfassoc_exec/src/main.rs +++ b/wfassoc_exec/src/main.rs @@ -1,8 +1,12 @@ -use clap::{Parser, Subcommand}; -use comfy_table::Table; -use std::process; +pub(crate) mod cli; +pub(crate) mod manifest; + +use clap::Parser; +use std::{collections::HashMap, path::Path, process}; use thiserror::Error as TeError; -use wfassoc::{Error as WfError, Ext, Scope, View}; +use wfassoc::{Program, Token}; +use cli::{Cli, Commands}; +use manifest::Manifest; // region: Basic Types @@ -11,7 +15,17 @@ use wfassoc::{Error as WfError, Ext, Scope, View}; enum Error { /// Error from wfassoc core. #[error("{0}")] - Core(#[from] WfError), + Core(#[from] wfassoc::Error), + /// Error when parsing manifest TOML file. + #[error("invalid manifest file: {0}")] + Manifest(#[from] manifest::Error), + + /// Error when specifying invalid manner name for extension. + #[error("extension {ext} associated manner {manner} is invalid in manifest file")] + InvalidMannerName { + manner: String, + ext: String + }, } /// Result type used in this executable. @@ -19,116 +33,46 @@ type Result = std::result::Result; // endregion -// region: Command Line Parser - -/// Simple program to manage Windows file associations -#[derive(Parser)] -#[command(name = "Windows File Association Operator", version, about)] -struct Cli { - /// The toml file introducing the complete program - #[arg( - short = 'c', - long = "config", - value_name = "PROG_CONFIG", - required = true - )] - config_file: String, - - /// The scope where wfassoc operate - #[arg(short = 'f', long = "for", value_name = "TARGET", value_enum, default_value_t = ForTarget::User)] - for_which: ForTarget, - - #[command(subcommand)] - command: Commands, -} - -#[derive(clap::ValueEnum, Clone)] -enum ForTarget { - #[value(name = "user")] - User, - #[value(name = "system")] - System, -} - -// impl From for RegisterKind { -// fn from(target: ForTarget) -> Self { -// match target { -// ForTarget::User => RegisterKind::User, -// ForTarget::System => RegisterKind::System, -// } -// } -// } - -#[derive(Subcommand)] -enum Commands { - /// Register the program - #[command(name = "register")] - Register, - /// Unregister the program - #[command(name = "unregister")] - Unregister, - /// Query file associations - #[command(name = "query")] - Query, -} - -// endregion - // region: Correponding Runner -fn run_register(cli: Cli) -> Result<()> { +fn build_program(cli: &Cli) -> Result { + // Open file and read manifest TOML file + let mf = Manifest::from_file(&cli.config_file)?; + // Create instance + let rv = Program::new(&mf.identifier, &Path::from(mf.path.as_str()))?; + // Setup manner + let manners: HashMap<&str, Token> = HashMap::new(); + for (k, v) in mf.manners.iter() { + let token = rv.add_manner(v.as_str())?; + manners.insert(k.as_str(), token); + } + // Setup extension + for (k, v) in mf.exts.iter() { + let token = match manners.get(v.as_str()) { + Some(v) => v, + None => return Err(Error::InvalidMannerName { manner: v.to_string(), ext: k.clone() }), + }; + rv.add_ext(k.as_str(), *token)?; + } + // Okey + Ok(rv) +} + +fn run_register(cli: &Cli) -> Result<()> { // let program = Program::new(); // let kind: RegisterKind = cli.for_which.into(); // program.register(kind)?; Ok(()) } -fn run_unregister(cli: Cli) -> Result<()> { +fn run_unregister(cli: &Cli) -> Result<()> { // let program = Program::new(); // program.unregister()?; Ok(()) } -fn run_query(cli: Cli) -> Result<()> { - let exts = [ - ".jpg", ".jfif", ".gif", ".bmp", ".png", ".ico", ".jpeg", ".tif", ".tiff", ".webp", ".svg", - ".kra", ".xcf", ".avif", ".qoi", ".apng", ".exr", - ]; - - for ext in exts.iter().map(|e| Ext::new(e).unwrap()) { - if let Some(ext_assoc) = ext.query(View::Hybrid) { - println!("{:?}", ext_assoc) - } - } - - // let mut table = Table::new(); - // table.set_header(["Extension", "Default Open", "Open With"]); - // for ext in exts.iter().map(|e| FileExt::new(e).unwrap()) { - // if let Some(ext_assoc) = ext.query(View::Hybrid) { - // if ext_assoc.len_open_with_progid() == 0 { - // table.add_row([ext.to_string().as_str(), ext_assoc.get_default(), ""]); - // } else { - // for (i, open_with_entry) in ext_assoc.iter_open_with_progids().enumerate() { - // if i == 0 { - // table.add_row([ - // ext.to_string().as_str(), - // ext_assoc.get_default(), - // open_with_entry, - // ]); - // } else { - // table.add_row(["", "", open_with_entry]); - // } - // } - // } - // } else { - // table.add_row([ext.to_string().as_str(), "", ""]); - // } - // } - // println!("{table}"); - - // let program = Program::new(); - // program.query()?; - //println!("Has privilege: {}", wfassoc::has_privilege()); +fn run_query(cli: &Cli) -> Result<()> { + Ok(()) } @@ -138,9 +82,9 @@ fn main() { let cli = Cli::parse(); let rv = match &cli.command { - Commands::Register => run_register(cli), - Commands::Unregister => run_unregister(cli), - Commands::Query => run_query(cli), + Commands::Register => run_register(&cli), + Commands::Unregister => run_unregister(&cli), + Commands::Query => run_query(&cli), }; rv.unwrap_or_else(|e| { eprintln!("Runtime error: {}.", e); diff --git a/wfassoc_exec/src/manifest.rs b/wfassoc_exec/src/manifest.rs new file mode 100644 index 0000000..892a19e --- /dev/null +++ b/wfassoc_exec/src/manifest.rs @@ -0,0 +1,30 @@ +use serde::Deserialize; +use std::collections::HashMap; +use thiserror::Error as TeError; +use toml; + +#[derive(Debug, TeError)] +#[error("{0}")] +pub enum Error { + Io(#[from] std::io::Error), + Toml(#[from] toml::de::Error), +} + +pub type Result = std::result::Result; + +#[derive(Debug, Deserialize)] +pub struct Manifest { + pub(crate) identifier: String, + pub(crate) path: String, + pub(crate) clsid: String, + pub(crate) manners: HashMap, + pub(crate) exts: HashMap, +} + +impl Manifest { + pub fn from_file(path: &str) -> Result { + let contents = std::fs::read_to_string(path)?; + let config: Manifest = toml::from_str(&contents)?; + Ok(config) + } +} \ No newline at end of file