refactor: replace 4 dependencies with custom implementations

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.
This commit is contained in:
Graham McIntire 2026-01-19 17:15:07 -06:00
parent c0fe57cd44
commit d132f03d8b
No known key found for this signature in database
4 changed files with 248 additions and 257 deletions

238
Cargo.lock generated
View file

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

View file

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

View file

@ -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.

View file

@ -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<Vec<u8>> {
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::<f64>() {
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]