use super::*;
#[test]
fn fixed_value_encodings_preserve_tags_bits_and_container_framing() {
for (value, encoded) in [
(Value::Null, "n"),
(Value::Bool(false), "b0"),
(Value::Bool(true), "b1"),
(Value::Int(i64::MIN), "i-9223372036854775808"),
(Value::Int(i64::MAX), "i9223372036854775807"),
(Value::Float(0.0), "f0"),
(Value::Float(-0.0), "f9223372036854775808"),
(Value::Float(f64::INFINITY), "f9218868437227405312"),
(
Value::Float(f64::from_bits(0x7ff8_0000_0000_0001)),
"f9221120237041090561",
),
(Value::String(String::new()), "s"),
(Value::String("\u{e9};=,\0".into()), "sc3a93b3d2c00"),
(
Value::Uuid(
hawdb_core::Uuid::parse_str("01890f3e-0e3c-7f9e-9b23-1bcdef012345").unwrap(),
),
"u01890f3e-0e3c-7f9e-9b23-1bcdef012345",
),
(Value::Binary(vec![]), "x"),
(Value::Binary(vec![0, 255]), "x00ff"),
(Value::List(vec![]), "l"),
(Value::List(vec![Value::Null, Value::Int(1)]), "l6e,6931"),
(Value::Map(BTreeMap::new()), "m"),
(
Value::Map(BTreeMap::from([
("z".into(), Value::Bool(false)),
("a".into(), Value::Int(1)),
])),
"m61=6931;7a=6230",
),
] {
assert_eq!(encode_value(&value), encoded, "{value:?}");
assert_eq!(decode_value(encoded).unwrap(), value, "{encoded}");
}
let properties = BTreeMap::from([
("z".into(), Value::Bool(false)),
("a".into(), Value::Int(1)),
]);
assert_eq!(encode_properties(&properties), "61=i1;7a=b0");
assert_eq!(decode_properties("61=i1;7a=b0").unwrap(), properties);
}
#[test]
fn decoding_preserves_legacy_tolerance_without_changing_binary_hex_rules() {
assert_eq!(decode_bytes("+fAF").unwrap(), vec![15, 175]);
assert_eq!(decode_value("xAF").unwrap(), Value::Binary(vec![175]));
assert!(decode_value("x+f").is_err());
assert_eq!(decode_value("i+1").unwrap(), Value::Int(1));
assert_eq!(decode_value("f+0").unwrap(), Value::Float(0.0));
assert_eq!(
decode_value("m61=6931;61=6932").unwrap(),
Value::Map(BTreeMap::from([("a".into(), Value::Int(2))]))
);
assert_eq!(
decode_properties("61=i1;61=i2").unwrap(),
BTreeMap::from([("a".into(), Value::Int(2))])
);
for encoded in [
"i9223372036854775808",
"f18446744073709551616",
"b2",
"n0",
"uinvalid",
] {
assert!(matches!(decode_value(encoded), Err(HawDBError::Storage(_))));
}
}
fn reference(input: &str) -> Option<Vec<u8>> {
fn nibble(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
if !input.len().is_multiple_of(2) {
return None;
}
input
.as_bytes()
.chunks_exact(2)
.map(|pair| {
let high = if pair[0] == b'+' { 0 } else { nibble(pair[0])? };
Some(high * 16 + nibble(pair[1])?)
})
.collect()
}
#[test]
fn hex_preserves_all_ascii_pairs_and_valid_byte_round_trips() {
for first in 0..128 {
for second in 0..128 {
let input = String::from_utf8(vec![first, second]).unwrap();
assert_eq!(decode_bytes(&input).ok(), reference(&input));
}
}
let bytes = (0..=255).collect::<Vec<u8>>();
assert_eq!(decode_bytes(&encode_bytes(&bytes)).unwrap(), bytes);
for text in ["", "\0\t\n", "ASCII \u{e9}\u{4e2d}\u{1f980}"] {
assert_eq!(decode_string(&encode_string(text)).unwrap(), text);
}
}
#[test]
fn malformed_hex_returns_storage_errors_without_panicking() {
for input in ["a\u{e9}a", "\u{1f980}", "00a\u{e9}a", "f", "gg"] {
let result = std::panic::catch_unwind(|| decode_bytes(input));
assert!(result.is_ok(), "hex decoder panicked for {input:?}");
assert!(matches!(result.unwrap(), Err(HawDBError::Storage(_))));
}
}
#[test]
fn bad_hex_diagnostics_do_not_copy_the_field() {
for input in [
format!("gg{}", "00".repeat(128 * 1024)),
format!("{}gg", "00".repeat(128 * 1024)),
] {
let error = decode_bytes(&input).unwrap_err().to_string();
assert!(error.contains("invalid hex"), "{error}");
assert!(error.len() < 128, "diagnostic copied {} bytes", error.len());
}
}
#[test]
fn malformed_value_tags_are_rejected_without_unbounded_diagnostics() {
for input in ["\u{e9}", "\u{4e2d}", "\u{1f980}", "", "?", "nextra"] {
let result = std::panic::catch_unwind(|| decode_value(input));
assert!(result.is_ok(), "value decoder panicked for {input:?}");
assert!(matches!(result.unwrap(), Err(HawDBError::Storage(_))));
}
let input = format!("?{}", "x".repeat(256 * 1024));
let error = decode_value(&input).unwrap_err().to_string();
assert!(error.len() < 128, "diagnostic copied {} bytes", error.len());
}
#[test]
fn nested_value_paths_propagate_corruption_and_preserve_valid_values() {
let bad_hex = "a\u{e9}a";
let bad_value = encode_string("\u{e9}");
for input in [
format!("s{bad_hex}"),
format!("l{bad_hex}"),
format!("m{bad_hex}=6931"),
format!("m6964={bad_hex}"),
format!("l{bad_value}"),
format!("m6964={bad_value}"),
] {
let result = std::panic::catch_unwind(|| decode_value(&input));
assert!(result.is_ok(), "nested decoder panicked for {input:?}");
assert!(matches!(result.unwrap(), Err(HawDBError::Storage(_))));
}
assert!(decode_properties("6964=sa\u{e9}a").is_err());
for value in [
Value::Null,
Value::Bool(true),
Value::Int(-7),
Value::Float(1.25),
Value::Uuid(hawdb_core::Uuid::parse_str("01890f3e-0e3c-7f9e-9b23-1bcdef012345").unwrap()),
Value::String("\u{4e2d}".into()),
Value::Binary(vec![0, 255]),
Value::List(vec![Value::String("\u{e9}".into()), Value::Null]),
Value::Map(BTreeMap::from([(
"\u{1f980}".into(),
Value::List(vec![Value::Int(8)]),
)])),
] {
assert_eq!(decode_value(&encode_value(&value)).unwrap(), value);
}
}
fn campaign(cases: usize) {
let mut seed = 0x0404_5afe_u64;
let mut next = || {
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
seed
};
for case in 0..cases {
let mut input = (0..next() % 128)
.map(|_| format!("{:02x}", (next() >> 32) as u8))
.collect::<String>();
match case % 8 {
1 => input.make_ascii_uppercase(),
2 => input.push_str("gg"),
3 => input.push('f'),
4 => input.insert_str(0, "a\u{e9}a"),
5 => input.push_str("+0+f"),
6 => input.truncate(input.len() / 2),
7 => input.push('\u{1f980}'),
_ => {}
}
let expected = reference(&input);
let decoded = decode_bytes(&input);
if let Err(error) = &decoded {
assert!(matches!(error, HawDBError::Storage(_)));
assert!(error.to_string().len() < 128, "case {case}");
}
assert_eq!(decoded.ok(), expected, "case {case}");
let expected = expected.and_then(|bytes| String::from_utf8(bytes).ok());
assert_eq!(decode_string(&input).ok(), expected, "case {case}");
assert_eq!(
decode_value(&format!("s{input}")).ok(),
expected.map(Value::String),
"case {case}"
);
let bad_tag = ["\u{e9}", "\u{4e2d}", "\u{1f980}"][case % 3];
assert!(decode_value(&format!("{bad_tag}{input}")).is_err());
let error = decode_value(&format!("?{input}")).unwrap_err();
assert!(matches!(error, HawDBError::Storage(_)));
assert!(error.to_string().len() < 128, "case {case}");
}
}
#[test]
fn recovery_text_differential_smoke() {
campaign(32);
}
#[test]
#[ignore = "explicit local recovery text differential campaign"]
fn recovery_text_differential_campaign() {
campaign(1024);
}