From d3012165bc0a0d2a0ba9cf543c397378558ddf1e Mon Sep 17 00:00:00 2001 From: cool-mist Date: Thu, 9 Jul 2026 09:48:19 +0530 Subject: [PATCH] Split to crates --- Cargo.lock | 11 ++- Cargo.toml | 11 +-- crates/wolz-client/Cargo.lock | 7 ++ crates/wolz-client/Cargo.toml | 7 ++ crates/wolz-client/src/main.rs | 40 ++++++++ crates/wolz-lib/Cargo.toml | 4 + {src => crates/wolz-lib/src}/config.rs | 129 ++++++------------------- crates/wolz-lib/src/error.rs | 32 ++++++ crates/wolz-lib/src/lib.rs | 4 + crates/wolz-lib/src/mac.rs | 128 ++++++++++++++++++++++++ crates/wolz-lib/src/wolz.rs | 33 +++++++ src/error.rs | 38 -------- src/main.rs | 98 ------------------- 13 files changed, 297 insertions(+), 245 deletions(-) create mode 100644 crates/wolz-client/Cargo.lock create mode 100644 crates/wolz-client/Cargo.toml create mode 100644 crates/wolz-client/src/main.rs create mode 100644 crates/wolz-lib/Cargo.toml rename {src => crates/wolz-lib/src}/config.rs (58%) create mode 100644 crates/wolz-lib/src/error.rs create mode 100644 crates/wolz-lib/src/lib.rs create mode 100644 crates/wolz-lib/src/mac.rs create mode 100644 crates/wolz-lib/src/wolz.rs delete mode 100644 src/error.rs delete mode 100644 src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 09a378d..9c22eb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,5 +3,12 @@ version = 4 [[package]] -name = "woly" -version = "0.1.0" +name = "wolz" +version = "0.0.1" +dependencies = [ + "wolz-lib", +] + +[[package]] +name = "wolz-lib" +version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 9892f3c..a7cff58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,5 @@ -[package] -name = "woly" -version = "0.1.0" -edition = "2024" - -[dependencies] +[workspace] +members = [ + "crates/*", +] +resolver = "2" diff --git a/crates/wolz-client/Cargo.lock b/crates/wolz-client/Cargo.lock new file mode 100644 index 0000000..938bee6 --- /dev/null +++ b/crates/wolz-client/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "wolz" +version = "0.0.1" diff --git a/crates/wolz-client/Cargo.toml b/crates/wolz-client/Cargo.toml new file mode 100644 index 0000000..a113b73 --- /dev/null +++ b/crates/wolz-client/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "wolz" +version = "0.0.1" +edition = "2024" + +[dependencies] +wolz-lib = { path = "../wolz-lib" } diff --git a/crates/wolz-client/src/main.rs b/crates/wolz-client/src/main.rs new file mode 100644 index 0000000..5686566 --- /dev/null +++ b/crates/wolz-client/src/main.rs @@ -0,0 +1,40 @@ +use std::env::args; + +use wolz_lib::{error::WResult, wolz::Wolz}; + +fn main() -> WResult<()> { + let arg_1 = args().nth(1); + let Some(arg_1) = arg_1 else { + print_usage(); + return Ok(()); + }; + + let wolz = Wolz::create()?; + wolz.wake_up(&arg_1)?; + Ok(()) +} + +fn print_usage() { + println!( + "{}", + r#"usage: woly ARG + +Options: + ARG Either the mac address of the device to send the wol magic + packet to, or a nickname specified in woly.conf to lookup + the mac. + Eg: woly aa:bb:cc:dd:ee:ff + Eg: woly potato + where potato is configured in the woly.conf file as + potato=aa:bb:cc:dd:ee:ff + +Config file: + Filename woly.conf + Location $WOLY_CONFIG_DIR/woly.conf, then $XDG_CONFIG_DIR/woly.conf, + then $HOME/woly.conf + Format one KEY=VALUE per line without spaces + Example potato=aa:bb:cc:dd:ee:ff + idly=12:34:23:e2:3d:ff +"# + ); +} diff --git a/crates/wolz-lib/Cargo.toml b/crates/wolz-lib/Cargo.toml new file mode 100644 index 0000000..f5d6de8 --- /dev/null +++ b/crates/wolz-lib/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "wolz-lib" +version = "0.0.1" +edition = "2024" diff --git a/src/config.rs b/crates/wolz-lib/src/config.rs similarity index 58% rename from src/config.rs rename to crates/wolz-lib/src/config.rs index 0255137..756521d 100644 --- a/src/config.rs +++ b/crates/wolz-lib/src/config.rs @@ -1,101 +1,21 @@ use std::{ collections::HashMap, - fmt::Display, fs::{self}, mem::take, path::PathBuf, + string::FromUtf8Error, }; -use crate::error::{self, AppError, EResult}; - -#[derive(Clone)] -pub struct Mac { - pub mac_bytes: [u8; 6], - original_string: String, -} - -impl Display for Mac { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.original_string) - } -} - -impl Mac { - pub fn create(mac: &str) -> EResult { - let mut mac_bytes = [0u8; 6]; - let mut mac_bytes_index = 0; - let split: Vec<&str> = mac.split(':').collect(); - if split.len() != 6 { - return Err(AppError::NotAMac); - } - - for i in &split { - mac_bytes[mac_bytes_index] = u8::from_str_radix(i, 16).map_err(|e| { - AppError::ParseMacError(format!( - "'{} is not a valid mac string, failed to parse '{}' as hex due to {}", - &mac, &i, e - )) - })?; - mac_bytes_index += 1; - } - - Ok(Self { - mac_bytes, - original_string: mac.to_string(), - }) - } -} - -#[derive(Eq, PartialEq, Hash)] -pub struct Nick { - name: String, -} - -impl Nick { - pub fn create(nick: &str) -> EResult { - for b in nick.bytes() { - if !b.is_ascii_alphanumeric() { - return Err(AppError::NickNotAlphanumeric(format!( - "Nickname should be alphanumeric for a valid nick, Invalid char {}", - &b - ))); - } - } - - Ok(Self { name: nick.to_string() }) - } -} - -pub enum Device { - Mac(Mac), - Nick(Nick), -} - -impl Device { - pub fn create(mac_or_nick: &str) -> EResult { - let as_mac = Mac::create(mac_or_nick); - let Err(AppError::NotAMac) = as_mac else { - return as_mac.map(|m| Device::mac(m.mac_bytes, mac_or_nick.to_string())); - }; - - let as_nick = Nick::create(mac_or_nick)?; - Ok(Device::Nick(as_nick)) - } - - fn mac(mac_bytes: [u8; 6], original_string: String) -> Self { - Self::Mac(Mac { - mac_bytes, - original_string, - }) - } -} +use crate::{ + error::{WError, WResult}, mac::{Device, Mac, Nick}, +}; pub struct Config { nick_map: HashMap, } impl Config { - pub fn create() -> EResult { + pub(crate) fn create() -> WResult { let mut config_parent = Config::get_parent()?; config_parent.push("woly.conf"); let config_file = config_parent; @@ -104,7 +24,7 @@ impl Config { } std::fs::File::create_new(&config_file).map_err(|e| { - AppError::ConfigDirInitError(format!( + WError::ConfigDirInitError(format!( "Failed to create file at {} due to {}", config_file.display(), e @@ -116,13 +36,13 @@ impl Config { }) } - pub fn get_mac(&self, mac_or_nick: &str) -> EResult { + pub(crate) fn get_mac(&self, mac_or_nick: &str) -> WResult { match Device::create(mac_or_nick)? { Device::Mac(mac) => Ok(mac), Device::Nick(nick) => self .nick_map .get(&nick) - .ok_or(AppError::NickNotMapped(format!( + .ok_or(WError::NickNotMapped(format!( "Nickname {} is not mapped to any MAC address in the config file", &nick.name ))) @@ -130,9 +50,9 @@ impl Config { } } - fn read_existing_conf(config_file: PathBuf) -> EResult { + fn read_existing_conf(config_file: PathBuf) -> WResult { let bytes = fs::read(&config_file).map_err(|e| { - AppError::ConfigDirInitError(format!( + WError::ConfigDirInitError(format!( "Unable to read the contents of {} because {}", config_file.display(), e @@ -154,15 +74,14 @@ impl Config { // TODO: Also consider \r and \r\n if b == b'\n' { - let key_str = - String::from_utf8(take(&mut key)).map_err(|e| error::map_config_error_utf8(&e, line_number))?; + let key_str = String::from_utf8(take(&mut key)).map_err(|e| map_config_error_utf8(&e, line_number))?; let value_str = - String::from_utf8(take(&mut value)).map_err(|e| error::map_config_error_utf8(&e, line_number))?; - let mac = Mac::create(&value_str).map_err(|e| error::map_config_error(&e, line_number))?; + String::from_utf8(take(&mut value)).map_err(|e| map_config_error_utf8(&e, line_number))?; + let mac = Mac::create(&value_str).map_err(|e| map_config_error(&e, line_number))?; let nick = Nick { name: key_str }; if nick_map.contains_key(&nick) { - return Err(AppError::ConfigFileFormatError(format!( + return Err(WError::ConfigFileFormatError(format!( "Duplicate key {} at line number {}", nick.name, line_number ))); @@ -183,13 +102,13 @@ impl Config { Ok(Config { nick_map }) } - fn get_parent() -> EResult { + fn get_parent() -> WResult { let config_dir_env_var_name = "WOLY_CONFIG_HOME"; let config_dir_name = std::env::var(config_dir_env_var_name); if let Ok(config_dir_name) = config_dir_name { let path = PathBuf::from(config_dir_name); if !path.is_dir() { - return Err(AppError::InvalidConfigDir(format!( + return Err(WError::InvalidConfigDir(format!( "{} does not point to a valid directory {}", config_dir_env_var_name, path.display() @@ -211,16 +130,16 @@ impl Config { return Config::validate_config_dir(config_dir, "$HOME/.config"); } - return Err(AppError::ConfigDirInitError(format!( + return Err(WError::ConfigDirInitError(format!( "Set one of {}, {} or {} environment variable to determine the config directory", "$WOLY_CONFIG_HOME", "$XDG_CONFIG_HOME", "$HOME" ))); } - fn validate_config_dir(config_dir: PathBuf, config_dir_source: &str) -> EResult { + fn validate_config_dir(config_dir: PathBuf, config_dir_source: &str) -> WResult { if !config_dir.exists() { fs::create_dir(&config_dir).map_err(|e| { - AppError::ConfigDirInitError(format!( + WError::ConfigDirInitError(format!( "Could not create the config directory at {} due to {}. Config source {}", config_dir.display(), e, @@ -232,7 +151,7 @@ impl Config { } if !config_dir.is_dir() { - return Err(AppError::ConfigDirInitError(format!( + return Err(WError::ConfigDirInitError(format!( "config directory is not a valid directory: {}. Config source {}", config_dir.display(), config_dir_source, @@ -242,3 +161,11 @@ impl Config { return Ok(config_dir); } } + +fn map_config_error_utf8(error: &FromUtf8Error, line_number: usize) -> WError { + WError::ConfigFileFormatError(format!("On line {}, {}", line_number, error)) +} + +fn map_config_error(error: &WError, line_number: usize) -> WError { + WError::ConfigFileFormatError(format!("On line {}, {}", line_number, error)) +} diff --git a/crates/wolz-lib/src/error.rs b/crates/wolz-lib/src/error.rs new file mode 100644 index 0000000..20fccab --- /dev/null +++ b/crates/wolz-lib/src/error.rs @@ -0,0 +1,32 @@ +use std::fmt::Display; + +pub type WResult = std::result::Result; + +#[derive(Debug)] +pub enum WError { + ParseMacError(String), + NotAMac(String), + NickNotAlphanumeric(String), + NickNotMapped(String), + InvalidConfigDir(String), + ConfigDirInitError(String), + ConfigFileFormatError(String), + UdpSocketError(String), +} + +impl Display for WError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let to_write = match self { + WError::ParseMacError(s) => format!("{}", s), + WError::NotAMac(s) => format!("'{}' is not a valid MAC", s), + WError::NickNotAlphanumeric(s) => format!("{}", s), + WError::NickNotMapped(s) => format!("{}", s), + WError::InvalidConfigDir(s) => format!("{}", s), + WError::ConfigDirInitError(s) => format!("{}", s), + WError::ConfigFileFormatError(s) => format!("{}", s), + WError::UdpSocketError(s) => format!("{}", s), + }; + + write!(f, "{}", to_write) + } +} diff --git a/crates/wolz-lib/src/lib.rs b/crates/wolz-lib/src/lib.rs new file mode 100644 index 0000000..8b70e83 --- /dev/null +++ b/crates/wolz-lib/src/lib.rs @@ -0,0 +1,4 @@ +pub mod error; +pub(crate) mod mac; +pub(crate) mod config; +pub mod wolz; diff --git a/crates/wolz-lib/src/mac.rs b/crates/wolz-lib/src/mac.rs new file mode 100644 index 0000000..a2a756e --- /dev/null +++ b/crates/wolz-lib/src/mac.rs @@ -0,0 +1,128 @@ +use std::fmt::Display; + +use crate::error::{WError, WResult}; + +#[derive(Clone)] +pub(crate) struct Mac { + mac_bytes: [u8; 6], + original_string: String, +} + +impl Mac { + pub(crate) fn create(mac: &str) -> WResult { + let mut mac_bytes = [0u8; 6]; + let mut mac_bytes_index = 0; + let split: Vec<&str> = mac.split(':').collect(); + if split.len() != 6 { + return Err(WError::NotAMac(mac.to_string())); + } + + for i in &split { + mac_bytes[mac_bytes_index] = u8::from_str_radix(i, 16).map_err(|e| { + WError::ParseMacError(format!( + "'{} is not a valid mac string, failed to parse '{}' as hex due to {}", + &mac, &i, e + )) + })?; + mac_bytes_index += 1; + } + + Ok(Self { + mac_bytes, + original_string: mac.to_string(), + }) + } + + pub(crate) fn copy_magic_bytes(&self, magic_bytes: &mut [u8; 102]) { + magic_bytes[..6].fill(0xFF); + for i in 0..16 { + let start = 6 + i * 6; + magic_bytes[start..start + 6].copy_from_slice(&self.mac_bytes); + } + } +} + +impl Display for Mac { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.original_string) + } +} + +#[derive(Eq, PartialEq, Hash)] +pub(crate) struct Nick { + pub(crate) name: String, +} + +impl Nick { + pub(crate) fn create(nick: &str) -> WResult { + for b in nick.bytes() { + if !b.is_ascii_alphanumeric() { + return Err(WError::NickNotAlphanumeric(format!( + "Nickname should be alphanumeric for a valid nick, Invalid char {}", + &b + ))); + } + } + + Ok(Self { name: nick.to_string() }) + } +} + +pub(crate) enum Device { + Mac(Mac), + Nick(Nick), +} + +impl Device { + pub(crate) fn create(mac_or_nick: &str) -> WResult { + let as_mac = Mac::create(mac_or_nick); + let Err(WError::NotAMac(mac_or_nick)) = as_mac else { + return as_mac.map(|m| Device::mac(m.mac_bytes, mac_or_nick.to_string())); + }; + + let as_nick = Nick::create(&mac_or_nick)?; + Ok(Device::Nick(as_nick)) + } + + fn mac(mac_bytes: [u8; 6], original_string: String) -> Self { + Self::Mac(Mac { + mac_bytes, + original_string, + }) + } +} + +#[cfg(test)] +mod tests { + use super::Mac; + + #[test] + fn test_magic_bytes_for_mac() { + let mac = Mac::create("32:1a:ff:3e:10:ef").expect("Test data is curated to be valid"); + let mut magic_bytes = [0u8; 102]; + mac.copy_magic_bytes(&mut magic_bytes); + + #[rustfmt::skip] + let expected = [ + 255, 255, 255, 255, 255, 255, + 50, 26, 255, 62, 16, 239, + 50, 26, 255, 62, 16, 239, + 50, 26, 255, 62, 16, 239, + 50, 26, 255, 62, 16, 239, + 50, 26, 255, 62, 16, 239, + 50, 26, 255, 62, 16, 239, + 50, 26, 255, 62, 16, 239, + 50, 26, 255, 62, 16, 239, + 50, 26, 255, 62, 16, 239, + 50, 26, 255, 62, 16, 239, + 50, 26, 255, 62, 16, 239, + 50, 26, 255, 62, 16, 239, + 50, 26, 255, 62, 16, 239, + 50, 26, 255, 62, 16, 239, + 50, 26, 255, 62, 16, 239, + 50, 26, 255, 62, 16, 239, + ]; + + assert_eq!(magic_bytes, expected, "Magic bytes are not created correctly"); + } +} diff --git a/crates/wolz-lib/src/wolz.rs b/crates/wolz-lib/src/wolz.rs new file mode 100644 index 0000000..7c82afe --- /dev/null +++ b/crates/wolz-lib/src/wolz.rs @@ -0,0 +1,33 @@ +use std::net::UdpSocket; + +use crate::{ + config::Config, + error::{WError, WResult}, +}; + +pub struct Wolz { + config: Config, +} + +impl Wolz { + pub fn create() -> WResult { + let config = Config::create()?; + Ok(Self { config }) + } + + pub fn wake_up(&self, mac_or_nick: &str) -> WResult<()> { + let mac = self.config.get_mac(mac_or_nick)?; + let mut magic_bytes = [0u8; 102]; + mac.copy_magic_bytes(&mut magic_bytes); + + let socket = UdpSocket::bind("0.0.0.0:0") + .map_err(|e| WError::UdpSocketError(format!("couldn't bind to socket because {}", e)))?; + socket.set_broadcast(true) + .map_err(|e| WError::UdpSocketError(format!("couldn't set broadcast because {}", e)))?; + socket + .send_to(&magic_bytes, "255.255.255.255:9") + .map_err(|e| WError::UdpSocketError(format!("couldn't send data because {}", e)))?; + println!("Magic packet sent to 255.255.255.255:9 for Mac {:}", mac); + Ok(()) + } +} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 20b96ce..0000000 --- a/src/error.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::{fmt::Display, string::FromUtf8Error}; - -pub type EResult = std::result::Result; - -#[derive(Debug)] -pub enum AppError { - ParseMacError(String), - NotAMac, - NickNotAlphanumeric(String), - NickNotMapped(String), - InvalidConfigDir(String), - ConfigDirInitError(String), - ConfigFileFormatError(String), -} - -impl Display for AppError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let to_write = match self { - AppError::ParseMacError(s) => format!("{}", s), - AppError::NotAMac => "Input is not a valid MAC".to_string(), - AppError::NickNotAlphanumeric(s) => format!("{}", s), - AppError::NickNotMapped(s) => format!("{}", s), - AppError::InvalidConfigDir(s) => format!("{}", s), - AppError::ConfigDirInitError(s) => format!("{}", s), - AppError::ConfigFileFormatError(s) => format!("{}", s), - }; - - write!(f, "{}", to_write) - } -} - -pub fn map_config_error_utf8(error: &FromUtf8Error, line_number: usize) -> AppError { - AppError::ConfigFileFormatError(format!("On line {}, {}", line_number, error)) -} - -pub fn map_config_error(error: &AppError, line_number: usize) -> AppError { - AppError::ConfigFileFormatError(format!("On line {}, {}", line_number, error)) -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index fbbb910..0000000 --- a/src/main.rs +++ /dev/null @@ -1,98 +0,0 @@ -mod config; -mod error; - -use std::env::args; -use std::net::UdpSocket; - -use crate::config::{Config, Mac}; -use crate::error::EResult; - -fn main() -> EResult<()> { - let config = Config::create()?; - - let arg_1 = args().nth(1); - let Some(arg_1) = arg_1 else { - print_usage(); - return Ok(()); - }; - - let mac = config.get_mac(&arg_1)?; - let mut magic_bytes = [0u8; 102]; - get_magic_pkt(&mac, &mut magic_bytes); - - let socket = UdpSocket::bind("0.0.0.0:0").expect("couldnt' bind socket"); - socket.set_broadcast(true).expect("couldn't set broadcast"); - socket - .send_to(&magic_bytes, "255.255.255.255:9") - .expect("couldn't send data"); - println!("Magic packet sent to 255.255.255.255:9 for Mac {:}", mac); - Ok(()) -} - -fn get_magic_pkt(mac: &Mac, magic_bytes: &mut [u8; 102]) { - magic_bytes[..6].fill(0xFF); - for i in 0..16 { - let start = 6 + i * 6; - magic_bytes[start..start + 6].copy_from_slice(&mac.mac_bytes); - } -} - -fn print_usage() { - println!( - "{}", - r#"usage: woly ARG - -Options: - ARG Either the mac address of the device to send the wol magic - packet to, or a nickname specified in woly.conf to lookup - the mac. - Eg: woly aa:bb:cc:dd:ee:ff - Eg: woly potato - where potato is configured in the woly.conf file as - potato=aa:bb:cc:dd:ee:ff - -Config file: - Filename woly.conf - Location $WOLY_CONFIG_DIR/woly.conf, then $XDG_CONFIG_DIR/woly.conf, - then $HOME/woly.conf - Format one KEY=VALUE per line without spaces - Example potato=aa:bb:cc:dd:ee:ff - idly=12:34:23:e2:3d:ff -"# - ); -} - -#[cfg(test)] -mod tests { - use crate::{config::Mac, get_magic_pkt}; - - #[test] - fn test_magic_bytes_for_mac() { - let mut magic_bytes = [0u8; 102]; - let mac = Mac::create("32:1a:ff:3e:10:ef").expect("Test data is curated to be valid"); - get_magic_pkt(&mac, &mut magic_bytes); - - #[rustfmt::skip] - let expected = [ - 255, 255, 255, 255, 255, 255, - 50, 26, 255, 62, 16, 239, - 50, 26, 255, 62, 16, 239, - 50, 26, 255, 62, 16, 239, - 50, 26, 255, 62, 16, 239, - 50, 26, 255, 62, 16, 239, - 50, 26, 255, 62, 16, 239, - 50, 26, 255, 62, 16, 239, - 50, 26, 255, 62, 16, 239, - 50, 26, 255, 62, 16, 239, - 50, 26, 255, 62, 16, 239, - 50, 26, 255, 62, 16, 239, - 50, 26, 255, 62, 16, 239, - 50, 26, 255, 62, 16, 239, - 50, 26, 255, 62, 16, 239, - 50, 26, 255, 62, 16, 239, - 50, 26, 255, 62, 16, 239, - ]; - - assert_eq!(magic_bytes, expected, "Magic bytes are not created correctly"); - } -}