const NEWLINE_NIBBLE: u8 = 0xD;
#[inline]
#[must_use]
pub const fn char_to_nibble(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'.' => Some(10),
b',' => Some(11),
b'/' => Some(12),
b'n' => Some(13),
b'-' => Some(14),
b'e' => Some(15),
_ => None,
}
}
#[inline]
#[must_use]
pub const fn nibble_to_char(n: u8) -> Option<u8> {
match n {
0..=9 => Some(b'0' + n),
10 => Some(b'.'),
11 => Some(b','),
12 => Some(b'/'),
13 => Some(b'n'),
14 => Some(b'-'),
15 => Some(b'e'),
_ => None,
}
}
#[must_use]
pub fn string_to_fie_line(input: &str) -> Vec<u8> {
match try_string_to_fie_line(input) {
Ok(v) => v,
Err(c) => panic!(
"string_to_fie_line: character {:?} (0x{:02X}) not in FIE alphabet",
c as char, c
),
}
}
pub fn try_string_to_fie_line(input: &str) -> Result<Vec<u8>, u8> {
let bytes = input.as_bytes();
let len = bytes.len();
if len == 0 {
return Ok(vec![(NEWLINE_NIBBLE << 4) | NEWLINE_NIBBLE]);
}
let mut out = Vec::with_capacity(len / 2 + 2);
let mut i = 0;
while i + 1 < len {
let hi = char_to_nibble(bytes[i]).ok_or(bytes[i])?;
let lo = char_to_nibble(bytes[i + 1]).ok_or(bytes[i + 1])?;
out.push((hi << 4) | lo);
i += 2;
}
if len % 2 == 0 {
out.push((NEWLINE_NIBBLE << 4) | NEWLINE_NIBBLE);
} else {
let hi = char_to_nibble(bytes[len - 1]).ok_or(bytes[len - 1])?;
out.push((hi << 4) | NEWLINE_NIBBLE);
}
Ok(out)
}
#[must_use]
pub fn fie_line_to_string(data: &[u8]) -> Option<String> {
let mut chars = Vec::with_capacity(data.len() * 2);
for &byte in data {
let hi = byte >> 4;
let lo = byte & 0x0F;
if hi == NEWLINE_NIBBLE {
break;
}
chars.push(nibble_to_char(hi)?);
if lo == NEWLINE_NIBBLE {
break;
}
chars.push(nibble_to_char(lo)?);
}
String::from_utf8(chars).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nibble_mapping_round_trip() {
for c in b"0123456789.,/n-e".iter() {
let n = char_to_nibble(*c).expect("should map");
let back = nibble_to_char(n).expect("should reverse");
assert_eq!(*c, back, "round-trip failed for char {:?}", *c as char);
}
}
#[test]
fn nibble_values_correct() {
assert_eq!(char_to_nibble(b'0'), Some(0));
assert_eq!(char_to_nibble(b'5'), Some(5));
assert_eq!(char_to_nibble(b'9'), Some(9));
assert_eq!(char_to_nibble(b'.'), Some(10));
assert_eq!(char_to_nibble(b','), Some(11));
assert_eq!(char_to_nibble(b'/'), Some(12));
assert_eq!(char_to_nibble(b'n'), Some(13));
assert_eq!(char_to_nibble(b'-'), Some(14));
assert_eq!(char_to_nibble(b'e'), Some(15));
}
#[test]
fn invalid_chars_return_none() {
assert_eq!(char_to_nibble(b'A'), None);
assert_eq!(char_to_nibble(b' '), None);
assert_eq!(char_to_nibble(b'x'), None);
assert_eq!(char_to_nibble(b'\n'), None);
}
#[test]
fn empty_string() {
let result = string_to_fie_line("");
assert_eq!(result, vec![0xDD]);
}
#[test]
fn single_char() {
let result = string_to_fie_line("5");
assert_eq!(result, vec![0x5D]);
}
#[test]
fn two_chars_even() {
let result = string_to_fie_line("12");
assert_eq!(result, vec![0x12, 0xDD]);
}
#[test]
fn three_chars_odd() {
let result = string_to_fie_line("123");
assert_eq!(result, vec![0x12, 0x3D]);
}
#[test]
fn four_chars_even() {
let result = string_to_fie_line("1234");
assert_eq!(result, vec![0x12, 0x34, 0xDD]);
}
#[test]
fn special_chars() {
let result = string_to_fie_line("1.2");
assert_eq!(result, vec![0x1A, 0x2D]);
}
#[test]
fn comma_separated() {
let result = string_to_fie_line("1,2");
assert_eq!(result, vec![0x1B, 0x2D]);
}
#[test]
fn negative_value() {
let result = string_to_fie_line("-5");
assert_eq!(result, vec![0xE5, 0xDD]);
}
#[test]
fn slash_and_dot() {
let result = string_to_fie_line("1/2.3");
assert_eq!(result, vec![0x1C, 0x2A, 0x3D]);
}
#[test]
fn all_special_chars() {
let result = string_to_fie_line(".,/n-e");
assert_eq!(result, vec![0xAB, 0xCD, 0xEF, 0xDD]);
}
#[test]
fn round_trip_even() {
let input = "12345678";
let encoded = string_to_fie_line(input);
let decoded = fie_line_to_string(&encoded).expect("decode should succeed");
assert_eq!(decoded, input);
}
#[test]
fn round_trip_odd() {
let input = "1234567";
let encoded = string_to_fie_line(input);
let decoded = fie_line_to_string(&encoded).expect("decode should succeed");
assert_eq!(decoded, input);
}
#[test]
fn round_trip_single() {
let input = "9";
let encoded = string_to_fie_line(input);
let decoded = fie_line_to_string(&encoded).expect("decode should succeed");
assert_eq!(decoded, input);
}
#[test]
fn round_trip_with_specials() {
let input = "100.50,-3/e";
let encoded = string_to_fie_line(input);
let decoded = fie_line_to_string(&encoded).expect("decode should succeed");
assert_eq!(decoded, input);
}
#[test]
fn n_char_encodes_as_newline_nibble() {
let encoded = string_to_fie_line("n");
assert_eq!(encoded, vec![0xDD]);
let decoded = fie_line_to_string(&encoded).expect("decode should succeed");
assert_eq!(decoded, "");
}
#[test]
fn round_trip_empty() {
let input = "";
let encoded = string_to_fie_line(input);
let decoded = fie_line_to_string(&encoded).expect("decode should succeed");
assert_eq!(decoded, input);
}
#[test]
fn try_version_rejects_bad_char() {
let result = try_string_to_fie_line("hello");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), b'h');
}
#[test]
#[should_panic(expected = "not in FIE alphabet")]
fn panicking_version_rejects_bad_char() {
let _ = string_to_fie_line("ABC");
}
#[test]
fn realistic_fpss_request() {
let input = "21,0,1,0,20240315,0,15000";
let encoded = string_to_fie_line(input);
let decoded = fie_line_to_string(&encoded).expect("decode should succeed");
assert_eq!(decoded, input);
assert_eq!(encoded[0], 0x21);
assert_eq!(encoded[1], 0xB0);
}
#[test]
fn fie_decode_partial_garbage_returns_none() {
let data = [0xFF]; let decoded = fie_line_to_string(&data).expect("should decode");
assert_eq!(decoded, "ee");
}
}