commit d3756e46456a0c7da4380ebbb430aab8c9c0b634 Author: Winter Hille Date: Thu Sep 25 23:30:38 2025 -0700 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d787b70 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/result diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5e27fde --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,292 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + +[[package]] +name = "clap" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "dollcode" +version = "0.1.0" +dependencies = [ + "clap", + "rug", +] + +[[package]] +name = "gmp-mpfr-sys" +version = "1.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60f8970a75c006bb2f8ae79c6768a116dd215fa8346a87aed99bf9d82ca43394" +dependencies = [ + "libc", + "windows-sys", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "libc" +version = "0.2.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rug" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad2e973fe3c3214251a840a621812a4f40468da814b1a3d6947d433c2af11f" +dependencies = [ + "az", + "gmp-mpfr-sys", + "libc", + "libm", +] + +[[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.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.53.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..197aa15 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "dollcode" +description = "A dollcode encoder/decoder" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[[bin]] +name = "dollcode-cli" +path = "src/cli.rs" +required-features = ["cli"] + +[dependencies] +rug = "1.27" + +[dependencies.clap] +version = "4.5.30" +features = ["derive"] +optional = true + +[features] +default = [] +cli = ["dep:clap"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..63e3564 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Winter Hille + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..53b6b67 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1758690382, + "narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "e643668fd71b949c53f8626614b21ff71a07379d", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d41ae7e --- /dev/null +++ b/flake.nix @@ -0,0 +1,40 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + }; + + outputs = + { self, nixpkgs }: + let + pkgs = nixpkgs.legacyPackages.x86_64-linux; + nativeBuildInputs = with pkgs; [ + pkg-config + m4 + ]; + buildInputs = [ pkgs.gmp ]; + in + with pkgs; + { + packages.x86_64-linux.default = rustPlatform.buildRustPackage { + name = "dollcode-cli"; + + src = ./.; + + cargoLock.lockFile = ./Cargo.lock; + + buildNoDefaultFeatures = true; + buildFeatures = [ "cli" ]; + + inherit nativeBuildInputs buildInputs; + }; + + devShells.x86_64-linux.default = mkShell { + packages = [ + cargo + clippy + rustc + ]; + inherit nativeBuildInputs buildInputs; + }; + }; +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..2805587 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,104 @@ +use std::{ + env, + io::{self, BufRead}, + str::FromStr, +}; + +use clap::{CommandFactory, Parser}; +use rug::{integer as int, Integer as Int}; + +use dollcode::Dollcode; + +#[derive(Debug, Default, Copy, Clone, clap::ValueEnum)] +enum Encoding { + #[default] + Ascii, + UTF8, + Decimal, + Hexadecimal, +} + +#[derive(Debug, clap::Parser)] +#[command(version)] +struct Args { + /// Encode input into dollcode [cannot be used with --decode] + #[arg(short, long, conflicts_with = "decode")] + encode: bool, + + /// Decode input from dollcode [cannot be used with --encode] + #[arg(short, long, conflicts_with = "encode")] + decode: bool, + + /// The encoding of input data + #[arg(long, value_enum, default_value_t = Encoding::Ascii)] + encoding: Encoding, + + /// The input data + input: Option, +} + +fn encode(input: String, encoding: Encoding) -> Result> { + Ok(match encoding { + Encoding::Ascii => Dollcode::encode_str(&input), + Encoding::UTF8 => { + let bytes = input.as_bytes(); + Dollcode::encode(Int::from_digits(bytes, int::Order::Msf)) + } + Encoding::Decimal => Dollcode::encode(Int::from_str(&input)?), + Encoding::Hexadecimal => { + let input = input.trim_start_matches("0x"); + Dollcode::encode(Int::from_str_radix(input, 16)?) + } + }) +} + +fn decode(input: Dollcode, encoding: Encoding) -> Result> { + Ok(match encoding { + Encoding::Ascii => input.decode_str(), + Encoding::UTF8 => { + let digits = input.decode().to_digits(int::Order::Msf); + String::from_utf8(digits)? + } + Encoding::Decimal => input.decode().to_string(), + Encoding::Hexadecimal => input.decode().to_string_radix(16), + }) +} + +fn main() -> Result<(), Box> { + let args = Args::parse(); + + if env::args().len() == 1 { + Args::command().print_help()?; + return Ok(()); + } + + let input = args.input.unwrap_or_default(); + if input.is_empty() { + return Err("No input provided".into()); + } + + let input = input.replace("\\", ""); + + if input == "-" { + let stdin = io::stdin(); + if args.decode { + for line in stdin.lock().lines() { + let dc = Dollcode::from_str(&line?)?; + println!("{}", decode(dc, args.encoding)?); + } + } else { + for line in stdin.lock().lines() { + println!("{}", encode(line?, args.encoding)?.as_str()); + } + } + } else { + if args.decode { + let dc = Dollcode::from_str(&input)?; + println!("{}", decode(dc, args.encoding)?); + } else { + println!("{}", encode(input, args.encoding)?.as_str()); + } + } + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ed22002 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,166 @@ +use rug::Integer as Int; + +#[derive(Debug)] +pub struct InvalidCharError { + ch: char, + pos: usize, +} + +impl std::error::Error for InvalidCharError {} + +impl std::fmt::Display for InvalidCharError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Invalid character {:?} as {:?}", self.ch, self.pos) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Dollcode(String); + +impl Dollcode { + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn encode(n: N) -> Self + where + N: Into, + { + let mut n = n.into(); + + if n.is_zero() { + return Dollcode(String::new()); + } + + let mut out = Vec::new(); + while n > Int::ZERO { + n -= 1; + out.push(match Int::from(&n % 3).to_u8_wrapping() { + 0 => '▖', + 1 => '▘', + 2 => '▌', + _ => unreachable!(), + }); + n /= 3; + } + + out.reverse(); + Dollcode(out.into_iter().collect()) + } + + pub fn encode_str(s: &str) -> Self { + if s.is_empty() { + return Self::encode(Int::ZERO); + } + + let mut n = Int::ZERO; + for byte in s.bytes() { + n = (n << 8) + byte; + } + + Self::encode(n) + } + + pub fn decode(&self) -> Int { + if self.0.is_empty() { + return Int::ZERO; + } + + let mut n = Int::ZERO; + for ch in self.0.chars() { + n *= 3; + n += match ch { + '▖' => 0, + '▘' => 1, + '▌' => 2, + _ => unreachable!(), + }; + n += 1; + } + + n + } + + pub fn decode_str(&self) -> String { + let mut n = self.decode(); + if n.is_zero() { + return String::new(); + } + + let mut out = Vec::new(); + while n > Int::ZERO { + out.push(char::from(Int::from(&n & u8::MAX).to_u8_wrapping())); + n >>= 8; + } + + out.reverse(); + out.into_iter().collect() + } +} + +impl std::str::FromStr for Dollcode { + type Err = InvalidCharError; + + fn from_str(s: &str) -> Result { + if let Some((pos, ch)) = s + .chars() + .enumerate() + .find(|&(_, ch)| !matches!(ch, '▖' | '▘' | '▌')) + { + return Err(InvalidCharError { ch, pos }); + } + + Ok(Dollcode(s.to_string())) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + + mod int { + use super::*; + + #[test] + fn test_encode() { + let dc = Dollcode::encode(Int::from(1234)); + assert_eq!(dc.as_str(), "▖▖▘▌▖▌▖") + } + + #[test] + fn test_decode() { + let dc = Dollcode::from_str("▖▖▘▌▖▌▖").unwrap(); + assert_eq!(dc.decode(), Int::from(1234)) + } + } + + mod str { + use super::*; + + #[test] + fn test_encode() { + let dc = Dollcode::encode_str("test"); + assert_eq!(dc.as_str(), "▖▖▘▘▌▘▌▘▖▖▘▖▌▌▘▖▘▘▖▖") + } + + #[test] + fn test_decode() { + let dc = Dollcode::from_str("▖▖▘▘▌▘▌▘▖▖▘▖▌▌▘▖▘▘▖▖").unwrap(); + assert_eq!(dc.decode_str(), "test") + } + } + + #[test] + fn test_parse_valid() { + let dc = "▖▖▘▘▌▘▌▘▖▖▘▖▌▌▘▖▘▘▖▖".parse::(); + assert!(dc.is_ok()) + } + + #[test] + fn test_parse_invalid() { + let dc = "test".parse::(); + assert!(dc.is_err()) + } +}