use crate::ir_inner::model::program::Program;
pub const TEXT_FORMAT_HEADER: &str = "vyre_ir v0.1";
pub const MAX_TEXT_WIRE_BYTES: usize = 64 * 1024 * 1024;
pub const WIRE_BYTES_PER_LINE: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum TextParseError {
MissingHeader {
observed: String,
},
MissingWireBytesLine {
observed: String,
},
WireBytesTooLarge {
declared: usize,
},
InvalidHexCharacter {
line: usize,
character: char,
},
OddHexLineLength {
line: usize,
observed: usize,
},
DeclaredLengthMismatch {
declared: usize,
actual: usize,
},
WireDecodeFailed {
inner: crate::error::Error,
},
WireEncodeFailed {
inner: crate::error::Error,
},
}
impl TextParseError {
#[must_use]
#[inline]
pub fn message(&self) -> String {
match self {
Self::MissingHeader { observed } => format!(
"text format must start with `{TEXT_FORMAT_HEADER}` but saw `{observed}`. {}",
self.fix_hint()
),
Self::MissingWireBytesLine { observed } => format!(
"text format header must be followed by `wire_bytes <N>` but saw `{observed}`. {}",
self.fix_hint()
),
Self::WireBytesTooLarge { declared } => format!(
"declared wire_bytes = {declared} exceeds MAX_TEXT_WIRE_BYTES = {MAX_TEXT_WIRE_BYTES}. {}",
self.fix_hint()
),
Self::InvalidHexCharacter { line, character } => format!(
"invalid hex character `{character}` on body line {line}. {}",
self.fix_hint()
),
Self::OddHexLineLength { line, observed } => format!(
"hex body line {line} has {observed} characters, must be even. {}",
self.fix_hint()
),
Self::DeclaredLengthMismatch { declared, actual } => format!(
"declared wire_bytes = {declared} but decoded {actual}. {}",
self.fix_hint()
),
Self::WireDecodeFailed { inner } => {
format!("inner binary wire decoder rejected the body: {inner}")
}
Self::WireEncodeFailed { inner } => {
format!("inner binary wire encoder rejected the program: {inner}")
}
}
}
#[must_use]
#[inline]
pub fn fix_hint(&self) -> &'static str {
match self {
Self::MissingHeader { .. } => {
"Fix: re-emit the program with Program::to_text, or manually prepend `vyre_ir v0.1\\n`."
}
Self::MissingWireBytesLine { .. } => {
"Fix: re-emit the program with Program::to_text; the second line must read `wire_bytes N`."
}
Self::WireBytesTooLarge { .. } => {
"Fix: the program is too large to round-trip through the text format; use Program::to_wire directly or split the program."
}
Self::InvalidHexCharacter { .. } | Self::OddHexLineLength { .. } => {
"Fix: the text body must be lowercase hex with 64 characters per line (32 bytes). Re-emit with Program::to_text."
}
Self::DeclaredLengthMismatch { .. } => {
"Fix: the wire_bytes header does not match the body length. Recompute wire_bytes or re-emit with Program::to_text."
}
Self::WireDecodeFailed { .. } | Self::WireEncodeFailed { .. } => {
"Fix: see the wrapped error message for the underlying wire-format problem."
}
}
}
}
impl std::fmt::Display for TextParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message())
}
}
impl std::error::Error for TextParseError {}
impl Program {
#[inline]
#[must_use]
pub fn to_text(&self) -> Result<String, TextParseError> {
let bytes = self
.to_wire()
.map_err(|error| TextParseError::WireEncodeFailed { inner: error })?;
Ok(encode_text_body(&bytes))
}
#[inline]
#[must_use]
pub fn from_text(input: &str) -> Result<Self, TextParseError> {
let mut lines = input.lines();
let header = lines.next().unwrap_or("");
if header != TEXT_FORMAT_HEADER {
return Err(TextParseError::MissingHeader {
observed: truncate(header, 64),
});
}
let wire_line = lines.next().unwrap_or("");
let declared_bytes = parse_wire_bytes_line(wire_line)?;
if declared_bytes > MAX_TEXT_WIRE_BYTES {
return Err(TextParseError::WireBytesTooLarge {
declared: declared_bytes,
});
}
let mut body = Vec::with_capacity(declared_bytes);
for (offset, line) in lines.enumerate() {
let trimmed = line.trim_end_matches('\r');
if trimmed.is_empty() {
continue;
}
if trimmed.len() % 2 != 0 {
return Err(TextParseError::OddHexLineLength {
line: offset + 3,
observed: trimmed.len(),
});
}
let mut bytes = trimmed.as_bytes().chunks_exact(2);
for pair in &mut bytes {
let high =
hex_nibble(pair[0]).ok_or_else(|| TextParseError::InvalidHexCharacter {
line: offset + 3,
character: pair[0] as char,
})?;
let low =
hex_nibble(pair[1]).ok_or_else(|| TextParseError::InvalidHexCharacter {
line: offset + 3,
character: pair[1] as char,
})?;
body.push((high << 4) | low);
}
}
if body.len() != declared_bytes {
return Err(TextParseError::DeclaredLengthMismatch {
declared: declared_bytes,
actual: body.len(),
});
}
Program::from_wire(&body).map_err(|inner| TextParseError::WireDecodeFailed { inner })
}
}
#[inline]
#[must_use]
pub(crate) fn encode_text_body(bytes: &[u8]) -> String {
let hex_chars = bytes.len() * 2;
let line_count = bytes.len().div_ceil(WIRE_BYTES_PER_LINE);
let capacity = TEXT_FORMAT_HEADER.len() + 32 + hex_chars + line_count + 1;
let mut out = String::with_capacity(capacity);
out.push_str(TEXT_FORMAT_HEADER);
out.push('\n');
out.push_str("wire_bytes ");
push_usize(&mut out, bytes.len());
out.push('\n');
for chunk in bytes.chunks(WIRE_BYTES_PER_LINE) {
for byte in chunk {
push_hex_byte(&mut out, *byte);
}
out.push('\n');
}
out
}
#[inline]
pub(crate) fn push_usize(out: &mut String, value: usize) {
if value == 0 {
out.push('0');
return;
}
let mut digits = [0u8; 20];
let mut idx = 0;
let mut v = value;
while v > 0 {
digits[idx] = b'0' + (v % 10) as u8;
v /= 10;
idx += 1;
}
while idx > 0 {
idx -= 1;
out.push(digits[idx] as char);
}
}
#[inline]
pub(crate) fn push_hex_byte(out: &mut String, byte: u8) {
const HEX: &[u8; 16] = b"0123456789abcdef";
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
#[inline]
#[must_use]
pub(crate) fn parse_wire_bytes_line(line: &str) -> Result<usize, TextParseError> {
let trimmed = line.trim_end_matches('\r');
let Some(rest) = trimmed.strip_prefix("wire_bytes ") else {
return Err(TextParseError::MissingWireBytesLine {
observed: truncate(trimmed, 64),
});
};
rest.parse::<usize>()
.map_err(|_| TextParseError::MissingWireBytesLine {
observed: truncate(trimmed, 64),
})
}
#[inline]
#[must_use]
pub(crate) fn hex_nibble(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(10 + (byte - b'a')),
b'A'..=b'F' => Some(10 + (byte - b'A')),
_ => None,
}
}
#[inline]
#[must_use]
pub(crate) fn truncate(input: &str, max: usize) -> String {
if input.chars().count() <= max {
input.to_string()
} else {
let mut out = input.chars().take(max - 1).collect::<String>();
out.push('…');
out
}
}
#[cfg(test)]
mod tests;