use std::fmt::Write;
use std::path::Path;
use bee::file::hash_directory;
use bee::postage::{
EFFECTIVE_SIZE_BREAKPOINTS, get_stamp_effective_bytes, get_stamp_theoretical_bytes,
};
use bee::swarm::{
CidType, FileChunker, GSOC_DEFAULT_PROXIMITY, Identifier, Reference, convert_reference_to_cid,
gsoc_mine,
};
const PSS_TARGET_HEX_LENGTH_MAX: usize = 4;
pub fn hash_path(path: &str) -> Result<String, String> {
let p = Path::new(path);
let meta = std::fs::metadata(p).map_err(|e| format!("stat {path:?}: {e}"))?;
if meta.is_dir() {
let r = hash_directory(p).map_err(|e| format!("hash_directory: {e}"))?;
return Ok(r.to_hex());
}
let bytes = std::fs::read(p).map_err(|e| format!("read {path:?}: {e}"))?;
let mut chunker = FileChunker::new();
chunker
.write(&bytes)
.map_err(|e| format!("chunker write: {e}"))?;
let root = chunker
.finalize()
.map_err(|e| format!("chunker finalize: {e}"))?;
Ok(root.address.to_hex())
}
pub fn cid_for_ref(ref_hex: &str, kind: CidType) -> Result<String, String> {
let r = Reference::from_hex(ref_hex.trim()).map_err(|e| format!("ref parse: {e}"))?;
convert_reference_to_cid(&r, kind).map_err(|e| format!("cid encode: {e}"))
}
pub fn parse_cid_kind(arg: Option<&str>) -> Result<CidType, String> {
match arg.unwrap_or("manifest").to_ascii_lowercase().as_str() {
"manifest" | "m" => Ok(CidType::Manifest),
"feed" | "f" => Ok(CidType::Feed),
other => Err(format!(
"unknown CID kind: {other:?} (expected 'manifest' or 'feed')"
)),
}
}
pub fn depth_table() -> String {
let mut out = String::new();
out.push_str("depth theoretical effective eff %\n");
out.push_str("----- ------------ ------------------- -----\n");
for (depth, _gb) in EFFECTIVE_SIZE_BREAKPOINTS {
let theoretical = get_stamp_theoretical_bytes(*depth);
let effective = get_stamp_effective_bytes(*depth);
let pct = if theoretical > 0 {
(effective as f64) / (theoretical as f64) * 100.0
} else {
0.0
};
let _ = writeln!(
&mut out,
"{depth:>5} {:>12} {:>19} {:>4.1}%",
human_bytes(theoretical),
human_bytes(effective),
pct,
);
}
out
}
pub fn gsoc_mine_for(overlay_hex: &str, identifier_str: &str) -> Result<String, String> {
let overlay = parse_hex_32(overlay_hex)?;
let id = Identifier::from_string(identifier_str);
let pk = gsoc_mine(&overlay, &id, GSOC_DEFAULT_PROXIMITY)
.map_err(|e| format!("gsoc_mine: {e}"))?;
Ok(format!(
"private_key=0x{}\nproximity≥{} (default)",
pk.to_hex(),
GSOC_DEFAULT_PROXIMITY,
))
}
pub fn pss_target_for(overlay_hex: &str) -> Result<String, String> {
let cleaned = overlay_hex.trim().trim_start_matches("0x");
if cleaned.len() < PSS_TARGET_HEX_LENGTH_MAX {
return Err(format!(
"overlay hex too short: need ≥{PSS_TARGET_HEX_LENGTH_MAX} chars, got {}",
cleaned.len()
));
}
if !cleaned.chars().all(|c| c.is_ascii_hexdigit()) {
return Err("overlay must be hex".into());
}
Ok(cleaned[..PSS_TARGET_HEX_LENGTH_MAX].to_ascii_lowercase())
}
fn parse_hex_32(s: &str) -> Result<[u8; 32], String> {
let cleaned = s.trim().trim_start_matches("0x");
if cleaned.len() != 64 {
return Err(format!(
"expected 64 hex chars (32 bytes), got {}",
cleaned.len()
));
}
let mut out = [0u8; 32];
for i in 0..32 {
let byte = u8::from_str_radix(&cleaned[2 * i..2 * i + 2], 16)
.map_err(|e| format!("hex char {}: {e}", 2 * i))?;
out[i] = byte;
}
Ok(out)
}
fn human_bytes(n: i64) -> String {
const KIB: i64 = 1 << 10;
const MIB: i64 = 1 << 20;
const GIB: i64 = 1 << 30;
const TIB: i64 = 1 << 40;
if n >= TIB {
format!("{:.2} TiB", n as f64 / TIB as f64)
} else if n >= GIB {
format!("{:.2} GiB", n as f64 / GIB as f64)
} else if n >= MIB {
format!("{:.2} MiB", n as f64 / MIB as f64)
} else if n >= KIB {
format!("{:.2} KiB", n as f64 / KIB as f64)
} else {
format!("{n} B")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn depth_table_renders_each_breakpoint_row() {
let out = depth_table();
for (depth, _) in EFFECTIVE_SIZE_BREAKPOINTS {
assert!(
out.contains(&format!("{depth:>5}")),
"depth {depth} missing from rendered table:\n{out}"
);
}
assert!(out.contains("theoretical"));
assert!(out.contains("effective"));
}
#[test]
fn cid_round_trips_a_known_reference() {
let ref_hex = "0".repeat(64);
let cid = cid_for_ref(&ref_hex, CidType::Manifest).expect("encode");
assert!(cid.starts_with('b'), "CID must use multibase prefix b");
}
#[test]
fn cid_kind_parses_aliases() {
assert!(matches!(parse_cid_kind(None), Ok(CidType::Manifest)));
assert!(matches!(
parse_cid_kind(Some("manifest")),
Ok(CidType::Manifest)
));
assert!(matches!(parse_cid_kind(Some("M")), Ok(CidType::Manifest)));
assert!(matches!(parse_cid_kind(Some("feed")), Ok(CidType::Feed)));
assert!(matches!(parse_cid_kind(Some("f")), Ok(CidType::Feed)));
assert!(parse_cid_kind(Some("garbage")).is_err());
}
#[test]
fn cid_rejects_short_hex() {
let err = cid_for_ref("deadbeef", CidType::Manifest).unwrap_err();
assert!(err.contains("ref parse"), "got: {err}");
}
#[test]
fn pss_target_returns_first_four_hex_lowercased() {
let overlay = "AABBCCDDeeff00112233445566778899aabbccddeeff00112233445566778899";
let out = pss_target_for(overlay).unwrap();
assert_eq!(out, "aabb");
}
#[test]
fn pss_target_strips_0x_prefix() {
let overlay = "0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899";
let out = pss_target_for(overlay).unwrap();
assert_eq!(out, "aabb");
}
#[test]
fn pss_target_rejects_short_input() {
let err = pss_target_for("aa").unwrap_err();
assert!(err.contains("too short"), "got: {err}");
}
#[test]
fn pss_target_rejects_non_hex() {
let err = pss_target_for("zzzz_not_hex").unwrap_err();
assert!(err.contains("hex"), "got: {err}");
}
#[test]
fn parse_hex_32_round_trips_zero_overlay() {
let hex = "0".repeat(64);
let arr = parse_hex_32(&hex).unwrap();
assert_eq!(arr, [0u8; 32]);
}
#[test]
fn parse_hex_32_rejects_wrong_length() {
assert!(parse_hex_32("aa").is_err());
assert!(parse_hex_32(&"f".repeat(63)).is_err());
assert!(parse_hex_32(&"f".repeat(65)).is_err());
}
#[test]
fn human_bytes_picks_the_right_unit() {
assert_eq!(human_bytes(0), "0 B");
assert_eq!(human_bytes(1023), "1023 B");
assert_eq!(human_bytes(2048), "2.00 KiB");
assert_eq!(human_bytes(2 * 1024 * 1024), "2.00 MiB");
assert_eq!(human_bytes(3 * 1024 * 1024 * 1024), "3.00 GiB");
}
#[test]
fn hash_path_round_trips_a_temp_file() {
let tmp = std::env::temp_dir().join("bee-tui-utility-verbs-hash-test.bin");
let payload = b"hello bee-tui";
std::fs::write(&tmp, payload).unwrap();
let r = hash_path(tmp.to_str().unwrap()).expect("hash_path");
assert_eq!(r.len(), 64, "ref hex must be 32 bytes ({r})");
let r2 = hash_path(tmp.to_str().unwrap()).unwrap();
assert_eq!(r, r2);
let _ = std::fs::remove_file(tmp);
}
#[test]
fn hash_path_errors_on_missing_file() {
let err = hash_path("/definitely/not/a/path/anywhere/u8s9d8f").unwrap_err();
assert!(err.contains("stat"), "got: {err}");
}
}