Initial commit: base95/220 and some other basics

This commit is contained in:
2026-01-14 18:33:21 +02:00
commit 1727cb9c55
10 changed files with 1158 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/.cursor
/target

186
Cargo.lock generated Normal file
View File

@@ -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",
]

12
Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "rs-furc"
version = "0.1.0"
edition = "2024"
authors = ["IceDragon <icedragon@quickfox.org>"]
[lib]
name = "furc"
path = "src/lib.rs"
[dependencies]
clap = { version = "4.5.53", features = ["derive"] }

303
src/base220.rs Normal file
View File

@@ -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<u8> for Base220Digit {
type Error = String;
fn try_from(value: u8) -> Result<Self, Self::Error> {
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<Base220Digit> 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<u8> {
to_base220vec(self.0)
}
pub fn to_vec_size(&self, size: usize) -> Result<Vec<u8>, Base220Error> {
let mut b220: Vec<u8> = 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<Base220> for u32 {
fn from(value: Base220) -> Self {
value.0
}
}
impl From<u32> for Base220 {
fn from(value: u32) -> Self {
Self(value)
}
}
impl<const N: usize> 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<u8> {
if n == 0 {
return [B220_ZERO].to_vec();
}
let mut acc = n;
let mut b220: Vec<u8> = 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<u8> = 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<u8> = 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:?}");
}
}
}

276
src/base95.rs Normal file
View File

@@ -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<u8> {
to_base95vec(self.0)
}
pub fn to_vec_size(&self, size: usize) -> Result<Vec<u8>, Base95Error> {
let mut b95: Vec<u8> = 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<Base95> for u32 {
fn from(value: Base95) -> Self {
value.0
}
}
impl From<u32> for Base95 {
fn from(value: u32) -> Self {
Self(value)
}
}
impl<const N: usize> 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<u8> {
if n == 0 {
return [B95_ZERO].to_vec();
}
let mut acc = n;
let mut b95: Vec<u8> = 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<u8> = 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<u8> = 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:?}");
}
}
}

73
src/bin/base-convert.rs Normal file
View File

@@ -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<u8>, // <- 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::<u32>() {
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)");
}
}

166
src/colors.rs Normal file
View File

@@ -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<Gender> for u8 {
fn from(value: Gender) -> Self {
match value {
Gender::Female => 0,
Gender::Male => 1,
Gender::Unspecified => 2,
Gender::Unknown(value) => value,
}
}
}
impl From<u8> 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<BaseSpecies> 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<u8> 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<String> 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);
}
}
}

101
src/conn.rs Normal file
View File

@@ -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<u8> 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'<');
}
}

8
src/lib.rs Normal file
View File

@@ -0,0 +1,8 @@
pub mod base95;
pub mod base220;
pub mod conn;
pub mod colors;
// fn main() {
// println!("Hello, world!");
// }

31
tests/test-base95.rs Normal file
View File

@@ -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"! "));
}