From 1727cb9c55eeca93ed40e028823e319f66ab2f9d Mon Sep 17 00:00:00 2001 From: IceDragon Date: Wed, 14 Jan 2026 18:33:21 +0200 Subject: [PATCH] Initial commit: base95/220 and some other basics --- .gitignore | 2 + Cargo.lock | 186 ++++++++++++++++++++++++ Cargo.toml | 12 ++ src/base220.rs | 303 ++++++++++++++++++++++++++++++++++++++++ src/base95.rs | 276 ++++++++++++++++++++++++++++++++++++ src/bin/base-convert.rs | 73 ++++++++++ src/colors.rs | 166 ++++++++++++++++++++++ src/conn.rs | 101 ++++++++++++++ src/lib.rs | 8 ++ tests/test-base95.rs | 31 ++++ 10 files changed, 1158 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/base220.rs create mode 100644 src/base95.rs create mode 100644 src/bin/base-convert.rs create mode 100644 src/colors.rs create mode 100644 src/conn.rs create mode 100644 src/lib.rs create mode 100644 tests/test-base95.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eff95ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.cursor +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c0f01a6 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,186 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "clap" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rs-furc" +version = "0.1.0" +dependencies = [ + "clap", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c285419 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rs-furc" +version = "0.1.0" +edition = "2024" +authors = ["IceDragon "] + +[lib] +name = "furc" +path = "src/lib.rs" + +[dependencies] +clap = { version = "4.5.53", features = ["derive"] } diff --git a/src/base220.rs b/src/base220.rs new file mode 100644 index 0000000..c5b42af --- /dev/null +++ b/src/base220.rs @@ -0,0 +1,303 @@ +use core::fmt; +use std::fmt::Display; + +// char offset for the zero value +pub const B220_ZERO: u8 = '#' as u8; // 0x23 + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// ERRORS ///////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#[derive(Debug)] +pub enum Base220Error { + IntOverflow(u32, usize), +} + +impl fmt::Display for Base220Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Base220Error::IntOverflow(n, len) => write!(f, "input number {n} is too long for {len} bytes"), + } + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// STRUCTS //////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Base220Digit(u8); + +impl TryFrom for Base220Digit { + type Error = String; + + fn try_from(value: u8) -> Result { + const MAX_VALUE: u8 = 255 - B220_ZERO; + if value <= MAX_VALUE { + Ok(Self(value + B220_ZERO)) + } else { + Err(format!("base220 value {value} is too high (bigger than {MAX_VALUE})")) + } + } +} + +impl From for u8 { + fn from(value: Base220Digit) -> Self { + // precaution + if value.0 < B220_ZERO { + panic!("Base220Digit({}) stored value is too low (min: {})!", value.0, B220_ZERO); + } + + value.0 - B220_ZERO + } +} + + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Base220(pub u32); + +impl Base220 { + /// Returns the integer value of this base220 number + /// + /// # Examples + /// + /// ``` + /// use furc::base220::Base220; + /// let raw_b220_value = b"$$"; // 221 + /// let b220 = Base220::from(raw_b220_value); + /// assert_eq!(b220.value(), 221); + /// ``` + pub fn value(&self) -> u32 { + self.0 + } + + pub fn as_owned_str(&self) -> String { + to_base220vec(self.0).iter().map(|&c| c as char).collect() + } + + pub fn to_vec(&self) -> Vec { + to_base220vec(self.0) + } + + pub fn to_vec_size(&self, size: usize) -> Result, Base220Error> { + let mut b220: Vec = Vec::with_capacity(size); + match to_base220(self.0, &mut b220) { + Ok(_) => Ok(b220), + Err(e) => Err(e), + } + } + + pub fn store<'a>(&self, bytes: &'a mut [u8]) -> Result<&'a [u8], Base220Error> { + to_base220(self.0, bytes) + } +} + +impl Display for Base220 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_owned_str()) + } +} + +impl From for u32 { + fn from(value: Base220) -> Self { + value.0 + } +} + +impl From for Base220 { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From<&[u8; N]> for Base220 { + fn from(value: &[u8; N]) -> Self { + Self(from_base220(value)) + } +} + +impl From<&[u8]> for Base220 { + fn from(value: &[u8]) -> Self { + Self(from_base220(value)) + } +} + +impl From<&str> for Base220 { + fn from(value: &str) -> Self { + Self::from(value.as_bytes()) + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// FUNCTIONS ////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +pub fn from_base220(b220: &[u8]) -> u32 { + let mut acc = 0; + let mut multiplier = 1; + for ch in b220.iter() { + acc += multiplier * ((*ch - B220_ZERO) as u32); + multiplier *= 220; + } + return acc; +} + +pub fn to_base220(n: u32, dest: &mut [u8]) -> Result<&[u8], Base220Error> { + let mut acc = n; + + // least significant digit first + for i in 0..dest.len() { + dest[i] = ((acc % 220) as u8) + B220_ZERO; + acc /= 220; + } + + // if we processed all of the digits, acc will be 0 + match acc { + 0 => Ok(dest), + _ => Err(Base220Error::IntOverflow(n, dest.len())), + } +} + +pub fn to_base220vec(n: u32) -> Vec { + if n == 0 { + return [B220_ZERO].to_vec(); + } + + let mut acc = n; + let mut b220: Vec = Vec::new(); + while acc > 0 { + b220.push((acc % 220) as u8 + B220_ZERO); + acc /= 220; + } + + return b220; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// TESTS ////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// + + +#[cfg(test)] +mod tests { + use super::*; + + /////////////////////////////////////////////////////////////////////////// + // Base220Digit /////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + + #[test] + fn base220digit() { + assert!(Base220Digit::try_from(0).is_err()); + assert!(Base220Digit::try_from(1).is_err()); + assert!(Base220Digit::try_from(220).is_err()); + assert!(!Base220Digit::try_from(221).is_err()); + + // check value conversion + for value in vec![0, 1, 220] { + match Base220Digit::try_from(value) { + Ok(b220digit) => { + let v: u8 = b220digit.into(); + assert_eq!(v, value); + }, + Err(e) => panic!("Base220Digit::try_from({}) failed: {}", value, e), + } + } + + // check invalild values + for value in 221..256 { + match Base220Digit::try_from(value) { + Ok(b220digit) => panic!("Base220Digit::try_from({}) succeeded: {:?}", value, b220digit), + Err(e) => assert!(e.contains("too high")), + } + } + } + + + /////////////////////////////////////////////////////////////////////////// + // FUNCTIONS ////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + + /// Returns all *(bytes, u32)* example tuples for base220 conversion + fn get_base220_conversion_table() -> Vec<(&'static [u8], u32)> { + vec![ + // special case + (b"", 0), + + // 1 digit + (b"#", 0), + (b"$", 1), + (b"\xFE", 219), + + // 2 digits + (b"##", 0), + (b"$#", 1), + (b"#$", 220), + (b"$$", 221), + (b"4)", 1337), + (b"\xFE\xFE", 48399), + + // 3 digits + (b"$##", 1), + (b"##$", 48400), + ] + } + + #[test] + fn test_from_base220() { + for (b220, num) in get_base220_conversion_table() { + assert_eq!(num, from_base220(b220)) + } + } + + #[test] + fn test_to_base220() { + // test correct approach with properly sized buffer + for (b220, num) in get_base220_conversion_table() { + let mut buf: Vec = vec![0u8; b220.len()]; + assert_eq!(to_base220(num, &mut buf).unwrap(), b220); + } + + // test correct approach with over-sized buffer + for (b220, num) in get_base220_conversion_table() { + let mut buf: Vec = vec![0u8; b220.len() + 1]; + let b220result = to_base220(num, &mut buf).unwrap(); + + // most significant byte should change from 0 to B220_ZERO + assert_eq!(b220result[b220result.len() - 1], B220_ZERO); + assert_eq!( + &b220result[..b220result.len()-1], + b220, + "to_base220({num}, buf) -?-> {b220:?}"); + } + + // test undersized buffer & Base220Error::IntOverflow + let mut buf = [0u8; 1]; + assert!(to_base220(219, &mut buf).is_ok()); + + let result = to_base220(220, &mut buf); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), Base220Error::IntOverflow(220, 1))); + } + + #[test] + fn test_to_base220vec() { + for (b220, num) in get_base220_conversion_table() { + // skip zero padding, but expect at least one byte in the result! + let b220expected = match num { + 0 => &[B220_ZERO], + _ => match b220.iter().rev().position(|&c| c != B220_ZERO) { + Some(end_off) => &b220[..b220.len()-end_off], + None => b220, + } + }; + + assert_ne!(b220expected.len(), 0, "invalid expected b220 {:?} for input {}", b220, num); + assert_eq!(to_base220vec(num), b220expected, "input: {num}, expected: {b220expected:?}"); + } + } +} diff --git a/src/base95.rs b/src/base95.rs new file mode 100644 index 0000000..c98adc0 --- /dev/null +++ b/src/base95.rs @@ -0,0 +1,276 @@ +use core::fmt; +use std::fmt::Display; + +// char offset for the zero value +static B95_ZERO: u8 = ' ' as u8; // 0x20 + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// ERRORS ///////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#[derive(Debug)] +pub enum Base95Error { + IntOverflow(u32, usize), +} + +impl fmt::Display for Base95Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Base95Error::IntOverflow(n, len) => write!(f, "input number {n} is too long for {len} bytes"), + } + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// STRUCTS //////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#[derive(Debug, PartialEq, PartialOrd, Eq, Ord)] +pub struct Base95(pub u32); + +impl Base95 { + pub fn as_owned_str(&self) -> String { + to_base95vec(self.0).iter().map(|&c| c as char).collect() + } + + pub fn to_vec(&self) -> Vec { + to_base95vec(self.0) + } + + pub fn to_vec_size(&self, size: usize) -> Result, Base95Error> { + let mut b95: Vec = Vec::with_capacity(size); + match to_base95(self.0, &mut b95) { + Ok(_) => Ok(b95), + Err(e) => Err(e), + } + } + + pub fn store<'a>(&self, bytes: &'a mut [u8]) -> Result<&'a [u8], Base95Error> { + to_base95(self.0, bytes) + } +} + +impl Display for Base95 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_owned_str()) + } +} + +impl From for u32 { + fn from(value: Base95) -> Self { + value.0 + } +} + +impl From for Base95 { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From<&[u8; N]> for Base95 { + fn from(value: &[u8; N]) -> Self { + Self(from_base95(value)) + } +} + +impl From<&[u8]> for Base95 { + fn from(value: &[u8]) -> Self { + Self(from_base95(value)) + } +} + +impl From<&str> for Base95 { + fn from(value: &str) -> Self { + Self::from(value.as_bytes()) + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// FUNCTIONS ////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Converts a base95 number represented by a given slice - into a 32 +/// +/// # Examples +/// +/// ``` +/// use furc::base95::from_base95; +/// assert_eq!(1, from_base95(b" !")); +/// assert_eq!(1, from_base95(b"!")); +/// assert_eq!(95, from_base95(b"! ")); +/// assert_eq!(1337, from_base95(b".'")); +/// assert_eq!(9025, from_base95(b"! ")); +/// ``` +pub fn from_base95(b95: &[u8]) -> u32 { + let mut acc = 0; + let mut multiplier = 1; + for ch in b95.iter().rev() { + acc += multiplier * ((*ch - B95_ZERO) as u32); + multiplier *= 95; + } + return acc; +} + +/// Converts a number into a base95 number, and stores it in `dest` +/// +/// # Examples +/// ``` +/// use furc::base95::to_base95; +/// use furc::base95::Base95Error; +/// +/// let mut dest = [0; 2]; +/// assert_eq!(b" ", to_base95(0, &mut dest).unwrap()); +/// assert_eq!(b" !", to_base95(1, &mut dest).unwrap()); +/// assert_eq!(b"! ", to_base95(95, &mut dest).unwrap()); +/// assert_eq!(b"!!", to_base95(96, &mut dest).unwrap()); +/// assert_eq!(b".'", to_base95(1337, &mut dest).unwrap()); +/// assert_eq!(b"~~", to_base95(9024, &mut dest).unwrap()); +/// +/// let mut buf1 = [0; 1]; +/// assert!(to_base95(95, &mut buf1).is_err()); +/// assert!(matches!(to_base95(95, &mut buf1).unwrap_err(), Base95Error::IntOverflow(95, 1))); +/// +/// match to_base95(95, &mut buf1) { +/// Ok(_) => panic!("crammed a 2-digit number inside 1 byte, wtf?"), +/// Err(Base95Error::IntOverflow(n, len)) => println!("{n} is too big for an array of {len} :("), +/// Err(e) => panic!("unexpected error: {e:?}"), +/// } +/// ``` +pub fn to_base95(n: u32, dest: &mut [u8]) -> Result<&[u8], Base95Error> { + let mut acc = n; + + // least significant digit last + for i in (0..dest.len()).rev() { + dest[i] = ((acc % 95) as u8) + B95_ZERO; + acc /= 95; + } + + // if we processed all of the digits, acc will be 0 + match acc { + 0 => Ok(dest), + _ => Err(Base95Error::IntOverflow(n, dest.len())), + } +} + +/// Converts a number into a base95 vector of an arbitrary size +/// +/// # Examples +/// ``` +/// use furc::base95::to_base95vec; +/// +/// assert_eq!(to_base95vec(0), b" "); +/// assert_eq!(to_base95vec(1), b"!"); +/// assert_eq!(to_base95vec(95), b"! "); +/// assert_eq!(to_base95vec(96), b"!!"); +/// assert_eq!(to_base95vec(1337), b".'"); +/// assert_eq!(to_base95vec(9024), b"~~"); +/// ``` +pub fn to_base95vec(n: u32) -> Vec { + if n == 0 { + return [B95_ZERO].to_vec(); + } + + let mut acc = n; + let mut b95: Vec = Vec::new(); + while acc > 0 { + b95.push((acc % 95) as u8 + B95_ZERO); + acc /= 95; + } + + b95.reverse(); + return b95; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// TESTS ////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use super::*; + + /// Returns all *(bytes, u32)* example tuples for base95 conversion + fn get_base95_conversion_table() -> Vec<(&'static [u8], u32)> { + vec![ + // special case + (b"", 0), + + // 1 digit + (b" ", 0), + (b" ", 0), + (b"!", 1), + (b"~", 94), + + // 2 digits & order + (b" ", 0), + (b" !", 1), + (b"! ", 95), + (b"!!", 96), + (b".'", 1337), + (b"~~", 9024), + + // 3 digits + (b" !", 1), + (b"! ", 9025), + ] + } + + #[test] + fn test_from_base95() { + for (b95, num) in get_base95_conversion_table() { + assert_eq!(from_base95(b95), num); + } + } + + #[test] + fn test_to_base95() { + // test correct approach with properly sized buffer + for (b95, num) in get_base95_conversion_table() { + let mut buf: Vec = vec![0u8; b95.len()]; + assert_eq!(to_base95(num, &mut buf).unwrap(), b95); + } + + // test correct approach with over-sized buffer + for (b95, num) in get_base95_conversion_table() { + let mut buf: Vec = vec![0u8; b95.len() + 1]; + let b95result = to_base95(num, &mut buf).unwrap(); + + // most significant byte should change from 0 to B95_ZERO + assert_eq!(b95result[0], B95_ZERO); + assert_eq!( + &b95result[1..], + b95, + "to_base95({num}, buf) -?-> {b95:?}"); + } + + // test undersized buffer & Base95Error::IntOverflow + let mut buf = [0u8; 1]; + assert!(to_base95(94, &mut buf).is_ok()); + + let result = to_base95(95, &mut buf); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), Base95Error::IntOverflow(95, 1))); + } + + #[test] + fn test_to_base95vec() { + for (b95, num) in get_base95_conversion_table() { + // skip zero padding, but expect at least one byte in the result! + let b95expected = match num { + 0 => b" ", + _ => match b95.iter().position(|&c| c != B95_ZERO) { + Some(start) => &b95[start..], + None => b95, + }, + }; + + assert_ne!(b95expected.len(), 0, "invalid expected b95 {:?} for input {}", b95, num); + assert_eq!(to_base95vec(num), b95expected, "input: {num}, expected: {b95expected:?}"); + } + } +} diff --git a/src/bin/base-convert.rs b/src/bin/base-convert.rs new file mode 100644 index 0000000..34950da --- /dev/null +++ b/src/bin/base-convert.rs @@ -0,0 +1,73 @@ +use clap::Parser; + +use furc::base95::{from_base95, to_base95vec}; +use furc::base220::{from_base220, to_base220vec}; + +#[derive(Parser)] +#[command(name = "base-convert")] +#[command(version = "1.0")] +#[command(about = "Convert base95 and base220 Furcadia numbers", long_about = None)] +struct Cli { + #[clap(short, long, conflicts_with = "to", help = "Convert FROM base(...) to decimal (default)")] + from: bool, + + #[clap(short, long, default_value_t = true, conflicts_with = "from", help = "Convert TO base(...) from decimal")] + to: bool, + + #[clap(long, default_value_t = true, conflicts_with = "b220", help = "Convert to/from base95 (default)")] + b95: bool, + + #[clap(long, conflicts_with = "b95", help = "Convert to/from base220")] + b220: bool, + + #[clap(short, long, conflicts_with = "from")] + size: Option, // <- optional argumnet + + #[clap(short, long, help = "Input number")] + input: String, +} + +fn main() { + let mut cli = Cli::parse(); + if cli.b220 { cli.b95 = false } + if cli.from { cli.to = false } + + // ->base(...) + if cli.to { + // read input as decimal + let input = match cli.input.parse::() { + Ok(n) => n, + Err(e) => panic!("Error: invalid input {:?} ({:?})", cli.input, e), + }; + + // convert decimal input into base95 or base220 based on choice + let result: String = if cli.b95 { + to_base95vec(input) + } else if cli.b220 { + to_base220vec(input) + } else { + panic!("unknown conversion mode (neither --b95, nor --b220)"); + } + .into_iter() + .map(|c| c as char) + .collect(); + + print!("{}", result); + } else if cli.from { + // read input as bytes + let input = cli.input.as_bytes(); + + // convert into decimal + let result: u32 = if cli.b95 { + from_base95(input) + } else if cli.b220 { + from_base220(input) + } else { + panic!("unknown conversion mode (neither --b95, nor --b220)") + }; + + print!("{result}"); + } else { + panic!("unknown direction (neither --to, nor --from)"); + } +} diff --git a/src/colors.rs b/src/colors.rs new file mode 100644 index 0000000..c567040 --- /dev/null +++ b/src/colors.rs @@ -0,0 +1,166 @@ +use std::u8; + +use crate::base220::Base220Digit; + +use super::base220::B220_ZERO; + +enum Gender { + Male, + Female, + Unspecified, + Unknown(u8), +} + +impl From for u8 { + fn from(value: Gender) -> Self { + match value { + Gender::Female => 0, + Gender::Male => 1, + Gender::Unspecified => 2, + Gender::Unknown(value) => value, + } + } +} + +impl From for Gender { + fn from(value: u8) -> Self { + match value { + 0 => Gender::Female, + 1 => Gender::Male, + 2 => Gender::Unspecified, + v => Gender::Unknown(v), + } + } +} + +enum BaseSpecies { + Rodent, + Equine, + Feline, + Canine, + Mustline, + Lapine, + Squirrel, + Bovine, + Unknown(u8), +} + +impl From for u8 { + fn from(value: BaseSpecies) -> Self { + match value { + BaseSpecies::Rodent => 0, + BaseSpecies::Equine => 1, + BaseSpecies::Feline => 2, + BaseSpecies::Canine => 3, + BaseSpecies::Mustline => 4, + BaseSpecies::Lapine => 5, + BaseSpecies::Squirrel => 6, + BaseSpecies::Bovine => 7, + BaseSpecies::Unknown(v) => v, + } + } +} + +impl From for BaseSpecies { + fn from(value: u8) -> Self { + match value { + 0 => BaseSpecies::Rodent, + 1 => BaseSpecies::Equine, + 2 => BaseSpecies::Feline, + 3 => BaseSpecies::Canine, + 4 => BaseSpecies::Mustline, + 5 => BaseSpecies::Lapine, + 6 => BaseSpecies::Squirrel, + 7 => BaseSpecies::Bovine, + v => BaseSpecies::Unknown(v), + } + } +} + +struct Colors([u8; 14]); + +impl Colors { + pub const TYPE_BYTE: u8 = b't'; + + // Offsets //////////////////////////////////////////////////////////////// + pub const OFFSET_FUR: usize = 1; + pub const OFFSET_MARKINGS: usize = 2; + pub const OFFSET_HAIR: usize = 3; + pub const OFFSET_EYE: usize = 4; + pub const OFFSET_BADGE: usize = 5; + pub const OFFSET_VEST: usize = 6; + pub const OFFSET_BRACER: usize = 7; + pub const OFFSET_CAPE: usize = 8; + pub const OFFSET_BOOT: usize = 9; + pub const OFFSET_TROUSER: usize = 10; + pub const OFFSET_GENDER: usize = 11; + pub const OFFSET_SPECIES: usize = 12; + pub const OFFSET_SPECIAL: usize = 13; + + // Static ///////////////////////////////////////////////////////////////// + pub fn new() -> Colors { + let mut c = [B220_ZERO; 14]; + c[0] = Self::TYPE_BYTE; + Self(c) + } + + // Methods //////////////////////////////////////////////////////////////// + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + pub fn gender(&self) -> Gender { + Gender::from(self.0[Self::OFFSET_GENDER]) + } + pub fn set_gender(&mut self, gender: Gender) { + self.0[Self::OFFSET_GENDER] = gender.into() + } + + pub fn species(&self) -> BaseSpecies { + BaseSpecies::from(self.0[Self::OFFSET_SPECIES]) + } + pub fn set_species(&mut self, species: BaseSpecies) { + self.0[Self::OFFSET_SPECIES] = species.into() + } +} + +impl From<&[u8]> for Colors { + fn from(value: &[u8]) -> Self { + // ensure value is a compatible color type + if value[0] != Self::TYPE_BYTE { + panic!("Colors::from({:?}): input starts with {:?} and not with {:?}", value, value[0] as char, Self::TYPE_BYTE); + } + + // ensure value isn't too short + if value.len() < 14 { + panic!("Colors::from({:?}): expects a slice of at least 14 bytes, got {}", value, value.len()); + } + + let mut arr = [B220_ZERO; 14]; + arr.copy_from_slice(&value[..14]); + Colors(arr) + } +} + +impl From for Colors { + fn from(value: String) -> Self { + Self::from(value.as_bytes()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_colors_default() { + let col = Colors::new(); + + // ensure the raw value starts with 't' and is all base220 zeroes + let col_bytes = col.as_bytes(); + assert_eq!(col_bytes[0], b't'); + for &c in col_bytes.iter().skip(1) { + assert_eq!(c, B220_ZERO); + } + } +} \ No newline at end of file diff --git a/src/conn.rs b/src/conn.rs new file mode 100644 index 0000000..dd93f16 --- /dev/null +++ b/src/conn.rs @@ -0,0 +1,101 @@ + +static FURC_BUFSZ: i32 = 4096; + +pub trait FurcOutput { + fn send_bytes(&self, msg: &[u8]); + + fn send_str(&self, msg: String) { + self.send_bytes(msg.as_bytes()); + } +} + + +pub struct Protocol +{ + output: dyn FurcOutput, +} + +pub enum MoveDirection { + SW = 1, + SE = 3, + NW = 7, + NE = 9, +} + +#[repr(u8)] +pub enum TurnDirection { + CW = b'>', // clockwise + CCW = b'<', // counter-clockwise +} + +impl Into for TurnDirection { + fn into(self) -> u8 { + self as u8 + } +} + + +pub enum ClientMessage { + Raw(String), + Account { email: String, name: String, password: String }, + Connect { name: String, password: String }, + Turn(TurnDirection), + Move(MoveDirection), + Say(String), + Emote(String), + Shout(String), + ToggleShouts, + Whisper { name: String, msg: String }, + WhisperExact { name: String, msg: String }, + WhisperOffline { name: String, msg: String }, + MakeCookie { name: String, msg: String }, + GiveCookie { name: String, msg: String }, + Who, + Join(String), + JoinLast, + SummonLast, + Summon(String), + Decline, + Vascodagama, + Quit, + Desc(String), + Colors([u8; 13]), +} + +impl Protocol { + pub fn account(&self, email: &str, name: &str, password: &str) { + self.output.send_str(format!("account {email} {name} {password}")) + } + + pub fn connect(&self, name: &str, password: &str) { + self.output.send_str(format!("connect {name} {password}")) + } + + pub fn turn(&self, dir: TurnDirection) { + self.output.send_bytes(&[dir.into()]); + } + + pub fn turn_cw(&self) { + self.output.send_str(">".to_owned()); + } + + pub fn turn_ccw(&self) { + self.output.send_str("<".to_owned()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_turn_direction() { + let cw = TurnDirection::CW; + let cw_byte: u8 = cw.into(); + assert_eq!(cw_byte, b'>'); + + let ccw = TurnDirection::CCW; + let ccw_byte: u8 = ccw.into(); + assert_eq!(ccw_byte, b'<'); + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..46a4012 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,8 @@ + +pub mod base95; +pub mod base220; +pub mod conn; +pub mod colors; +// fn main() { +// println!("Hello, world!"); +// } diff --git a/tests/test-base95.rs b/tests/test-base95.rs new file mode 100644 index 0000000..bd8a22d --- /dev/null +++ b/tests/test-base95.rs @@ -0,0 +1,31 @@ +use furc::base95::*; + +#[test] +fn test_base95_from() { + assert_eq!(Base95(0), Base95::from(b"")); + + assert_eq!(Base95(0), Base95::from(b" ")); + assert_eq!(Base95(1), Base95::from(b"!")); + assert_eq!(Base95(94), Base95::from(b"~")); + + assert_eq!(Base95(0), Base95::from(b" ")); + assert_eq!(Base95(1), Base95::from(b" !")); + assert_eq!(Base95(95), Base95::from(b"! ")); + assert_eq!(Base95(96), Base95::from(b"!!")); + assert_eq!(Base95(9024), Base95::from(b"~~")); + + assert_eq!(Base95(0), Base95::from(b" ")); + assert_eq!(Base95(1), Base95::from(b" !")); + assert_eq!(Base95(9025), Base95::from(b"! ")); +} + +#[test] +fn test_base95_as_owned_str() { + assert_eq!(Base95(0).as_owned_str(), " "); + assert_eq!(Base95(1).as_owned_str(), "!"); + assert_eq!(Base95(94).as_owned_str(), "~"); + assert_eq!(Base95(95).as_owned_str(), "! "); + assert_eq!(Base95(96).as_owned_str(), "!!"); + assert_eq!(Base95(9024).as_owned_str(), "~~"); + assert_eq!(Base95(9025), Base95::from(b"! ")); +}