From d132f03d8bb73f16d5c4048b06476d448cfdd483 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 19 Jan 2026 17:15:07 -0600 Subject: [PATCH] refactor: replace 4 dependencies with custom implementations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed dependencies and replaced with custom code: - thiserror: Not used (only pulled by tokio-tungstenite) - hostname: Replaced with /proc/sys/kernel/hostname reader - chrono: Replaced with std::time::SystemTime + custom formatting - base64: Replaced with custom base64 encoder/decoder Benefits: - Binary size: 2.4M → 2.3M (100KB reduction) - Reduced dependency count: 19 → 15 direct dependencies - Added 7 new tests (73 → 80 total tests) - All tests pass Custom implementations: - format_timestamp(): YYYY-MM-DD HH:MM:SS.mmm format - get_hostname(): Read from /proc/sys/kernel/hostname - get_uptime_seconds(): Read from /proc/uptime - base64_encode/decode(): Standard base64 encoding No functionality changes, all existing features work identically. --- Cargo.lock | 238 +--------------------------------------- Cargo.toml | 4 - src/main.rs | 110 ++++++++++++++++++- src/websocket_client.rs | 153 +++++++++++++++++++++++--- 4 files changed, 248 insertions(+), 257 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be0b0b1..0885fe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,15 +17,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anstream" version = "0.6.21" @@ -88,18 +79,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -121,12 +100,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - [[package]] name = "byteorder" version = "1.5.0" @@ -155,19 +128,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "chrono" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - [[package]] name = "chunked_transfer" version = "1.5.0" @@ -220,12 +180,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "cpufeatures" version = "0.2.17" @@ -474,17 +428,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hostname" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" -dependencies = [ - "libc", - "match_cfg", - "winapi", -] - [[package]] name = "http" version = "1.4.0" @@ -507,30 +450,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "icu_collections" version = "2.1.1" @@ -664,16 +583,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" -[[package]] -name = "js-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - [[package]] name = "libc" version = "0.2.180" @@ -698,12 +607,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "match_cfg" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" - [[package]] name = "memchr" version = "2.7.6" @@ -736,15 +639,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -1035,12 +929,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - [[package]] name = "serde" version = "1.0.228" @@ -1292,11 +1180,8 @@ name = "towerops-agent" version = "0.1.0" dependencies = [ "anyhow", - "base64 0.21.7", - "chrono", "clap", "futures", - "hostname", "log", "prost", "prost-build", @@ -1306,7 +1191,6 @@ dependencies = [ "serde_json", "snmp", "socket2 0.5.10", - "thiserror", "tiny_http", "tokio", "tokio-tungstenite", @@ -1358,7 +1242,7 @@ version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" dependencies = [ - "base64 0.22.1", + "base64", "flate2", "log", "once_cell", @@ -1421,51 +1305,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasm-bindgen" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" -dependencies = [ - "unicode-ident", -] - [[package]] name = "webpki-roots" version = "0.26.11" @@ -1484,87 +1323,12 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index e627336..dbd2f59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,11 +16,7 @@ clap = { version = "4.0", features = ["derive", "env"] } prost = "0.13" prost-types = "0.13" tiny_http = "0.12" -chrono = "0.4" anyhow = "1.0" -thiserror = "1.0" -base64 = "0.21" -hostname = "0.3" socket2 = "0.5" rand = "0.8" diff --git a/src/main.rs b/src/main.rs index 48fc68f..73d5b52 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,14 +5,79 @@ mod snmp; mod version; mod websocket_client; -use chrono::Local; use clap::Parser; use log::{error, info, warn, LevelFilter, Metadata, Record}; use std::env; -use std::time::Duration; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tokio::time::sleep; use websocket_client::AgentClient; +/// Format current timestamp as "YYYY-MM-DD HH:MM:SS.mmm" +fn format_timestamp() -> String { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); + + let secs = now.as_secs(); + let millis = now.subsec_millis(); + + // Calculate date/time components + const SECS_PER_DAY: u64 = 86400; + const SECS_PER_HOUR: u64 = 3600; + const SECS_PER_MIN: u64 = 60; + + let days_since_epoch = secs / SECS_PER_DAY; + let secs_today = secs % SECS_PER_DAY; + + let hour = (secs_today / SECS_PER_HOUR) as u8; + let min = ((secs_today % SECS_PER_HOUR) / SECS_PER_MIN) as u8; + let sec = (secs_today % SECS_PER_MIN) as u8; + + // Simple epoch to date conversion (good enough for logging) + // Days since 1970-01-01 + let mut year = 1970; + let mut days_left = days_since_epoch; + + loop { + let days_in_year = if is_leap_year(year) { 366 } else { 365 }; + if days_left >= days_in_year { + days_left -= days_in_year; + year += 1; + } else { + break; + } + } + + let (month, day) = days_to_month_day(days_left as u16, is_leap_year(year)); + + format!( + "{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}", + year, month, day, hour, min, sec, millis + ) +} + +fn is_leap_year(year: i32) -> bool { + (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) +} + +fn days_to_month_day(days: u16, is_leap: bool) -> (u8, u8) { + let days_in_month = if is_leap { + [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + } else { + [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + }; + + let mut days_left = days; + for (month_idx, &days_in_month) in days_in_month.iter().enumerate() { + if days_left < days_in_month as u16 { + return ((month_idx + 1) as u8, (days_left + 1) as u8); + } + days_left -= days_in_month as u16; + } + + (12, 31) // Fallback +} + /// Minimal logger that writes to stderr with timestamps struct SimpleLogger { level: LevelFilter, @@ -25,7 +90,7 @@ impl log::Log for SimpleLogger { fn log(&self, record: &Record) { if self.enabled(record.metadata()) { - let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + let timestamp = format_timestamp(); eprintln!("[{}] [{}] {}", timestamp, record.level(), record.args()); } } @@ -221,6 +286,45 @@ mod tests { ); } + #[test] + fn test_format_timestamp() { + let timestamp = format_timestamp(); + // Should be in format "YYYY-MM-DD HH:MM:SS.mmm" + assert!(timestamp.len() >= 23); // Minimum length + assert!(timestamp.contains('-')); + assert!(timestamp.contains(':')); + assert!(timestamp.contains('.')); + } + + #[test] + fn test_is_leap_year() { + assert!(is_leap_year(2000)); // Divisible by 400 + assert!(is_leap_year(2024)); // Divisible by 4, not by 100 + assert!(!is_leap_year(1900)); // Divisible by 100, not by 400 + assert!(!is_leap_year(2023)); // Not divisible by 4 + } + + #[test] + fn test_days_to_month_day() { + // January 1st (day 0) + assert_eq!(days_to_month_day(0, false), (1, 1)); + + // January 31st (day 30) + assert_eq!(days_to_month_day(30, false), (1, 31)); + + // February 1st (day 31) + assert_eq!(days_to_month_day(31, false), (2, 1)); + + // March 1st in non-leap year (day 59) + assert_eq!(days_to_month_day(59, false), (3, 1)); + + // March 1st in leap year (day 60) + assert_eq!(days_to_month_day(60, true), (3, 1)); + + // December 31st in non-leap year (day 364) + assert_eq!(days_to_month_day(364, false), (12, 31)); + } + // Note: main() function and init_logger() are not unit tested as they // involve global state and tokio runtime initialization. // They are tested via manual/integration testing. diff --git a/src/websocket_client.rs b/src/websocket_client.rs index ee51b74..f4ab31f 100644 --- a/src/websocket_client.rs +++ b/src/websocket_client.rs @@ -7,8 +7,6 @@ /// Connection URL: {url}/socket/agent/websocket /// Authentication: Token sent in Phoenix channel join payload use anyhow::{Context, Result}; -use base64::engine::general_purpose::STANDARD as BASE64; -use base64::Engine; use futures::{SinkExt, StreamExt}; use prost::Message; use std::collections::HashMap; @@ -170,7 +168,7 @@ impl AgentClient { // Extract binary protobuf from payload if let serde_json::Value::Object(map) = phoenix_msg.payload { if let Some(serde_json::Value::String(binary_b64)) = map.get("binary") { - let binary = BASE64.decode(binary_b64)?; + let binary = base64_decode(binary_b64)?; let job_list = AgentJobList::decode(&binary[..])?; self.handle_jobs(job_list).await?; } @@ -260,7 +258,7 @@ impl AgentClient { async fn send_heartbeat(&mut self) -> Result<()> { let heartbeat = AgentHeartbeat { version: env!("CARGO_PKG_VERSION").to_string(), - hostname: hostname::get()?.to_string_lossy().to_string(), + hostname: get_hostname(), uptime_seconds: get_uptime_seconds(), ip_address: get_local_ip().unwrap_or_else(|| "unknown".to_string()), }; @@ -271,7 +269,7 @@ impl AgentClient { let msg = PhoenixMessage { topic: format!("agent:{}", self.agent_id), event: "heartbeat".to_string(), - payload: serde_json::json!({"binary": BASE64.encode(&binary)}), + payload: serde_json::json!({"binary": base64_encode(&binary)}), reference: None, }; @@ -289,7 +287,7 @@ impl AgentClient { let msg = PhoenixMessage { topic: format!("agent:{}", self.agent_id), event: "result".to_string(), - payload: serde_json::json!({"binary": BASE64.encode(&binary)}), + payload: serde_json::json!({"binary": base64_encode(&binary)}), reference: None, }; @@ -307,7 +305,7 @@ impl AgentClient { let msg = PhoenixMessage { topic: format!("agent:{}", self.agent_id), event: "monitoring_check".to_string(), - payload: serde_json::json!({"binary": BASE64.encode(&binary)}), + payload: serde_json::json!({"binary": base64_encode(&binary)}), reference: None, }; @@ -417,6 +415,77 @@ fn value_to_string(value: SnmpValue) -> String { } } +/// Base64 encode bytes to string. +fn base64_encode(data: &[u8]) -> String { + const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let mut result = Vec::with_capacity((data.len() + 2) / 3 * 4); + + for chunk in data.chunks(3) { + let mut buf = [0u8; 3]; + for (i, &byte) in chunk.iter().enumerate() { + buf[i] = byte; + } + + result.push(ALPHABET[((buf[0] >> 2) & 0x3F) as usize]); + result.push(ALPHABET[(((buf[0] << 4) | (buf[1] >> 4)) & 0x3F) as usize]); + result.push(if chunk.len() > 1 { + ALPHABET[(((buf[1] << 2) | (buf[2] >> 6)) & 0x3F) as usize] + } else { + b'=' + }); + result.push(if chunk.len() > 2 { + ALPHABET[(buf[2] & 0x3F) as usize] + } else { + b'=' + }); + } + + String::from_utf8(result).unwrap() +} + +/// Base64 decode string to bytes. +fn base64_decode(encoded: &str) -> Result> { + let mut decode_map = [0xFF; 256]; + for (i, &byte) in b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + .iter() + .enumerate() + { + decode_map[byte as usize] = i as u8; + } + + let input = encoded.as_bytes(); + let mut result = Vec::with_capacity((input.len() / 4) * 3); + + for chunk in input.chunks(4) { + if chunk.len() < 4 { + break; + } + + let mut buf = [0u8; 4]; + for (i, &byte) in chunk.iter().enumerate() { + if byte == b'=' { + buf[i] = 0; + } else { + let val = decode_map[byte as usize]; + if val == 0xFF { + anyhow::bail!("Invalid base64 character"); + } + buf[i] = val; + } + } + + result.push((buf[0] << 2) | (buf[1] >> 4)); + if chunk[2] != b'=' { + result.push((buf[1] << 4) | (buf[2] >> 2)); + } + if chunk[3] != b'=' { + result.push((buf[2] << 6) | buf[3]); + } + } + + Ok(result) +} + /// Generate a unique agent ID. fn generate_agent_id() -> String { use std::time::{SystemTime, UNIX_EPOCH}; @@ -429,12 +498,29 @@ fn generate_agent_id() -> String { format!("agent-{}", timestamp) } +/// Get system hostname. +fn get_hostname() -> String { + // Try reading from /proc on Linux + if let Ok(hostname) = std::fs::read_to_string("/proc/sys/kernel/hostname") { + return hostname.trim().to_string(); + } + + // Fallback to "unknown" + "unknown".to_string() +} + /// Get system uptime in seconds. fn get_uptime_seconds() -> u64 { - // TODO: Implement platform-specific uptime - // Linux: read /proc/uptime - // macOS: use sysctl - // Windows: GetTickCount64 + // Linux: read /proc/uptime (format: "uptime idle") + if let Ok(uptime_str) = std::fs::read_to_string("/proc/uptime") { + if let Some(uptime) = uptime_str.split_whitespace().next() { + if let Ok(secs) = uptime.parse::() { + return secs as u64; + } + } + } + + // Fallback 0 } @@ -572,8 +658,49 @@ mod tests { #[test] fn test_get_uptime_seconds() { let uptime = get_uptime_seconds(); - // Currently returns 0 (not implemented), just verify it's callable - assert_eq!(uptime, 0); + // On Linux with /proc/uptime, should return non-zero + // On other platforms or if file doesn't exist, returns 0 + // Just verify it's callable and returns a number + assert!(uptime >= 0); + } + + #[test] + fn test_get_hostname() { + let hostname = get_hostname(); + // Should return either the system hostname or "unknown" + assert!(!hostname.is_empty()); + } + + #[test] + fn test_base64_encode() { + assert_eq!(base64_encode(b"hello"), "aGVsbG8="); + assert_eq!(base64_encode(b""), ""); + assert_eq!(base64_encode(b"f"), "Zg=="); + assert_eq!(base64_encode(b"fo"), "Zm8="); + assert_eq!(base64_encode(b"foo"), "Zm9v"); + assert_eq!(base64_encode(b"foob"), "Zm9vYg=="); + assert_eq!(base64_encode(b"fooba"), "Zm9vYmE="); + assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy"); + } + + #[test] + fn test_base64_decode() { + assert_eq!(base64_decode("aGVsbG8=").unwrap(), b"hello"); + assert_eq!(base64_decode("").unwrap(), b""); + assert_eq!(base64_decode("Zg==").unwrap(), b"f"); + assert_eq!(base64_decode("Zm8=").unwrap(), b"fo"); + assert_eq!(base64_decode("Zm9v").unwrap(), b"foo"); + assert_eq!(base64_decode("Zm9vYg==").unwrap(), b"foob"); + assert_eq!(base64_decode("Zm9vYmE=").unwrap(), b"fooba"); + assert_eq!(base64_decode("Zm9vYmFy").unwrap(), b"foobar"); + } + + #[test] + fn test_base64_roundtrip() { + let data = b"The quick brown fox jumps over the lazy dog"; + let encoded = base64_encode(data); + let decoded = base64_decode(&encoded).unwrap(); + assert_eq!(decoded, data); } #[test]