use serde::Serialize;
pub fn chunked(ms1: &str) -> String {
const CHUNK: usize = 5;
const GROUPS_PER_LINE: usize = 10;
let groups: Vec<&str> = ms1
.as_bytes()
.chunks(CHUNK)
.map(|c| std::str::from_utf8(c).expect("ASCII codex32 chars only"))
.collect();
let mut out = String::new();
for (i, line_groups) in groups.chunks(GROUPS_PER_LINE).enumerate() {
if i > 0 {
out.push('\n');
}
out.push_str(&line_groups.join(" "));
}
out
}
#[derive(Serialize)]
pub struct EncodeJson<'a> {
pub schema_version: &'static str,
pub ms1: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<&'a str>,
pub word_count: usize,
pub entropy_hex: String,
}
#[derive(Serialize)]
pub struct DecodeJson<'a> {
pub schema_version: &'static str,
pub entropy_hex: String,
pub phrase: String,
pub language: &'a str,
pub word_count: usize,
pub language_defaulted: bool,
}
#[derive(Serialize)]
pub struct InspectReportJson {
pub hrp: String,
pub threshold: u8,
pub tag: String,
pub share_index: char,
pub prefix_byte: u8,
pub payload_bytes_hex: String,
pub checksum_valid: bool,
}
#[derive(Serialize)]
pub struct InspectJson {
pub schema_version: &'static str,
pub report: InspectReportJson,
pub would_decode: bool,
pub failure_reasons: Vec<&'static str>,
}
#[derive(Serialize)]
pub struct VerifySuccessJson<'a> {
pub schema_version: &'static str,
pub status: &'a str, pub message: &'a str,
}
#[derive(Serialize)]
pub struct ErrorEnvelopeJson {
pub schema_version: &'static str,
pub error: ErrorBodyJson,
}
#[derive(Serialize)]
pub struct ErrorBodyJson {
pub kind: &'static str,
pub message: String,
pub exit_code: u8,
pub details: Option<serde_json::Value>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chunked_50_char_string_is_one_line_of_10_groups() {
let ms1 = "ms10entrsqqqqqqqqqqqqqqqqqqqqqqqqqqqqcj9sxraq34v7f";
assert_eq!(ms1.len(), 50);
let out = chunked(ms1);
assert_eq!(out.lines().count(), 1);
let groups: Vec<&str> = out.split(' ').collect();
assert_eq!(groups.len(), 10);
assert!(groups.iter().all(|g| g.len() == 5));
}
#[test]
fn chunked_75_char_string_is_two_lines_10_plus_5() {
let ms1 = "ms10entrsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqcwugpdxtfme2w";
assert_eq!(ms1.len(), 75);
let out = chunked(ms1);
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 2);
let line1_groups: Vec<&str> = lines[0].split(' ').collect();
assert_eq!(line1_groups.len(), 10);
let line2_groups: Vec<&str> = lines[1].split(' ').collect();
assert_eq!(line2_groups.len(), 5);
}
#[test]
fn chunked_each_v01_length_produces_expected_layout() {
for (len, expected_groups) in [(50, 10), (56, 12), (62, 13), (69, 14), (75, 15)] {
let s: String = "x".repeat(len);
let out = chunked(&s);
let total: usize = out.split([' ', '\n']).count();
assert_eq!(
total, expected_groups,
"length {} expected {} groups",
len, expected_groups
);
}
}
#[test]
fn encode_json_serializes_correctly() {
let j = EncodeJson {
schema_version: "1",
ms1: "ms10entrs...",
language: Some("english"),
word_count: 12,
entropy_hex: "00".repeat(16),
};
let s = serde_json::to_string(&j).unwrap();
assert!(s.starts_with("{\"schema_version\":\"1\""));
assert!(s.contains("\"ms1\":\"ms10entrs...\""));
assert!(s.contains("\"language\":\"english\""));
}
#[test]
fn encode_json_omits_language_for_hex_input() {
let j = EncodeJson {
schema_version: "1",
ms1: "ms10...",
language: None,
word_count: 12,
entropy_hex: "00".repeat(16),
};
let s = serde_json::to_string(&j).unwrap();
assert!(!s.contains("language"));
}
}