Split to crates

This commit is contained in:
2026-07-09 09:48:19 +05:30
parent f162dc7175
commit d3012165bc
13 changed files with 297 additions and 245 deletions
+4
View File
@@ -0,0 +1,4 @@
[package]
name = "wolz-lib"
version = "0.0.1"
edition = "2024"
+171
View File
@@ -0,0 +1,171 @@
use std::{
collections::HashMap,
fs::{self},
mem::take,
path::PathBuf,
string::FromUtf8Error,
};
use crate::{
error::{WError, WResult}, mac::{Device, Mac, Nick},
};
pub struct Config {
nick_map: HashMap<Nick, Mac>,
}
impl Config {
pub(crate) fn create() -> WResult<Self> {
let mut config_parent = Config::get_parent()?;
config_parent.push("woly.conf");
let config_file = config_parent;
if config_file.exists() {
return Config::read_existing_conf(config_file);
}
std::fs::File::create_new(&config_file).map_err(|e| {
WError::ConfigDirInitError(format!(
"Failed to create file at {} due to {}",
config_file.display(),
e
))
})?;
Ok(Config {
nick_map: HashMap::default(),
})
}
pub(crate) fn get_mac(&self, mac_or_nick: &str) -> WResult<Mac> {
match Device::create(mac_or_nick)? {
Device::Mac(mac) => Ok(mac),
Device::Nick(nick) => self
.nick_map
.get(&nick)
.ok_or(WError::NickNotMapped(format!(
"Nickname {} is not mapped to any MAC address in the config file",
&nick.name
)))
.map(|mac| mac.clone()),
}
}
fn read_existing_conf(config_file: PathBuf) -> WResult<Self> {
let bytes = fs::read(&config_file).map_err(|e| {
WError::ConfigDirInitError(format!(
"Unable to read the contents of {} because {}",
config_file.display(),
e
))
})?;
// name=mac, one per line
// Eg: potato=32:1a:ff:3e:10:ef
let mut key = Vec::new();
let mut value = Vec::new();
let mut parse_state = 0;
let mut line_number = 1;
let mut nick_map = HashMap::new();
for b in bytes.into_iter() {
if b == b'=' {
parse_state = 1;
continue;
}
// TODO: Also consider \r and \r\n
if b == b'\n' {
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| 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(WError::ConfigFileFormatError(format!(
"Duplicate key {} at line number {}",
nick.name, line_number
)));
}
nick_map.insert(nick, mac);
parse_state = 0;
line_number += 1;
continue;
}
match parse_state {
0 => key.push(b),
_ => value.push(b),
}
}
Ok(Config { nick_map })
}
fn get_parent() -> WResult<PathBuf> {
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(WError::InvalidConfigDir(format!(
"{} does not point to a valid directory {}",
config_dir_env_var_name,
path.display()
)));
}
return Ok(path);
}
// Only works for linux
// Read $XDG_CONFIG_DIR, then fallback to $HOME/.config
if let Ok(xdg_config_dir) = std::env::var("XDG_CONFIG_HOME") {
return Config::validate_config_dir(PathBuf::from(xdg_config_dir), "$XDG_CONFIG_HOME");
}
if let Ok(home_dir) = std::env::var("HOME") {
let mut config_dir = PathBuf::from(home_dir);
config_dir.push(".config");
return Config::validate_config_dir(config_dir, "$HOME/.config");
}
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) -> WResult<PathBuf> {
if !config_dir.exists() {
fs::create_dir(&config_dir).map_err(|e| {
WError::ConfigDirInitError(format!(
"Could not create the config directory at {} due to {}. Config source {}",
config_dir.display(),
e,
config_dir_source,
))
})?;
return Ok(config_dir);
}
if !config_dir.is_dir() {
return Err(WError::ConfigDirInitError(format!(
"config directory is not a valid directory: {}. Config source {}",
config_dir.display(),
config_dir_source,
)));
}
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))
}
+32
View File
@@ -0,0 +1,32 @@
use std::fmt::Display;
pub type WResult<T> = std::result::Result<T, WError>;
#[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)
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod error;
pub(crate) mod mac;
pub(crate) mod config;
pub mod wolz;
+128
View File
@@ -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<Self> {
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<Self> {
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<Self> {
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");
}
}
+33
View File
@@ -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<Self> {
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(())
}
}