diff --git a/Cargo.lock b/Cargo.lock index 1da33aa..3083bdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -585,6 +585,7 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "base64", "clap", "hive-fr0nt", "hive-sh4re", diff --git a/Cargo.toml b/Cargo.toml index f41eac0..a53b5b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ must_use_candidate = "allow" [workspace.dependencies] anyhow = "1" axum = { version = "0.8", features = ["ws"] } +base64 = "0.22" clap = { version = "4", features = ["derive"] } hive-fr0nt = { path = "hive-fr0nt" } hive-sh4re = { path = "hive-sh4re" } diff --git a/hive-c0re/Cargo.toml b/hive-c0re/Cargo.toml index 50afeb2..169a50e 100644 --- a/hive-c0re/Cargo.toml +++ b/hive-c0re/Cargo.toml @@ -9,6 +9,7 @@ workspace = true [dependencies] anyhow.workspace = true axum.workspace = true +base64.workspace = true reqwest.workspace = true clap.workspace = true hive-fr0nt.workspace = true diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index 51cd6c0..392f5d1 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -20,6 +20,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; +use base64::Engine; use reqwest::StatusCode; use tokio::process::Command; @@ -287,7 +288,7 @@ async fn ensure_core_avatar(token: &str) -> Result<()> { } let body = format!( r#"{{"image":"{}"}}"#, - base64_encode(CORE_AVATAR_PNG), + base64::engine::general_purpose::STANDARD.encode(CORE_AVATAR_PNG), ); let url = format!("{FORGE_HTTP}/api/v1/admin/users/core/avatar"); let status = forge_http(reqwest::Method::POST, &url, token, &body).await?; @@ -302,63 +303,6 @@ async fn ensure_core_avatar(token: &str) -> Result<()> { Ok(()) } -/// Standard base64 encode (RFC 4648 §4, with `=` padding). Used by -/// `ensure_core_avatar` to embed the hyperhive logo PNG in the -/// Forgejo admin avatar JSON body — the payload is one small image -/// in one cold path, so pulling in a base64 crate isn't worth the -/// dep churn. -fn base64_encode(bytes: &[u8]) -> String { - const ALPHABET: &[u8; 64] = - b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4); - for chunk in bytes.chunks(3) { - let b0 = chunk[0]; - let b1 = chunk.get(1).copied().unwrap_or(0); - let b2 = chunk.get(2).copied().unwrap_or(0); - out.push(ALPHABET[(b0 >> 2) as usize] as char); - out.push(ALPHABET[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char); - if chunk.len() > 1 { - out.push(ALPHABET[(((b1 & 0x0F) << 2) | (b2 >> 6)) as usize] as char); - } else { - out.push('='); - } - if chunk.len() > 2 { - out.push(ALPHABET[(b2 & 0x3F) as usize] as char); - } else { - out.push('='); - } - } - out -} - -#[cfg(test)] -mod base64_tests { - use super::base64_encode; - - // RFC 4648 §10 test vectors. - #[test] - fn rfc4648_vectors() { - 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 full_byte_range_roundtrip_length() { - // 256 bytes → 256 / 3 = 85 chunks + 1 leftover (chunks_of_3 walks 85 - // full + 1 padded), 86 chunks * 4 chars = 344. Cheap sanity check - // that the size calc + padding don't drift on a non-multiple input. - let bytes: Vec = (0..=255u8).collect(); - let encoded = base64_encode(&bytes); - assert_eq!(encoded.len(), 344); - assert!(encoded.ends_with('=')); - } -} - /// Ensure the bootstrap `core` admin user + a token at /// `CORE_TOKEN_PATH`. The token is what hive-c0re uses for forgejo /// API calls (org creation now, meta-repo push later). Returns the