From d3756e46456a0c7da4380ebbb430aab8c9c0b634 Mon Sep 17 00:00:00 2001 From: Winter Hille Date: Thu, 25 Sep 2025 23:30:38 -0700 Subject: [PATCH] initial --- .gitignore | 2 + Cargo.lock | 292 +++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 23 +++++ LICENSE | 21 ++++ README.md | 0 flake.lock | 27 +++++ flake.nix | 40 ++++++++ src/cli.rs | 104 +++++++++++++++++++ src/lib.rs | 166 ++++++++++++++++++++++++++++++ 9 files changed, 675 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 src/cli.rs create mode 100644 src/lib.rs 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()) + } +}