use crate::error::YencError;
#[derive(Debug, Default)]
pub(crate) struct YbeginFields {
pub size: Option<u64>,
pub line_length: Option<u8>,
pub part: Option<u32>,
pub total: Option<u32>,
pub name: Option<String>,
}
#[derive(Debug, Default)]
pub(crate) struct YpartFields {
pub begin: Option<u64>,
pub end: Option<u64>,
}
#[derive(Debug, Default)]
pub(crate) struct YendFields {
pub size: Option<u64>,
pub pcrc32: Option<u32>,
pub crc32: Option<u32>,
}
pub(crate) fn parse_ybegin(payload: &str) -> Result<YbeginFields, YencError> {
let mut f = YbeginFields::default();
if let Some(name_pos) = find_name_param(payload) {
let before_name = payload[..name_pos].trim();
parse_kv_pairs(before_name, &mut f)?;
let name_start = name_pos + "name=".len();
f.name = Some(
payload[name_start..]
.trim_end_matches(['\r', '\n'])
.to_string(),
);
} else {
parse_kv_pairs(payload, &mut f)?;
}
Ok(f)
}
pub(crate) fn parse_ypart(payload: &str) -> Result<YpartFields, YencError> {
let mut f = YpartFields::default();
for (key, val) in kv_iter(payload) {
match key {
"begin" => {
f.begin = Some(parse_u64(val, "begin")?);
}
"end" => {
f.end = Some(parse_u64(val, "end")?);
}
_ => {} }
}
Ok(f)
}
pub(crate) fn parse_yend(payload: &str) -> Result<YendFields, YencError> {
let mut f = YendFields::default();
for (key, val) in kv_iter(payload) {
match key {
"size" => {
f.size = Some(parse_u64(val, "size")?);
}
"pcrc32" => {
f.pcrc32 = Some(parse_crc32(val, "pcrc32")?);
}
"crc32" => {
f.crc32 = Some(parse_crc32(val, "crc32")?);
}
_ => {}
}
}
Ok(f)
}
fn find_name_param(payload: &str) -> Option<usize> {
if payload.starts_with("name=") {
return Some(0);
}
if let Some(pos) = payload.find(" name=") {
return Some(pos + 1); }
if let Some(pos) = payload.find("\tname=") {
return Some(pos + 1); }
None
}
fn parse_kv_pairs(s: &str, f: &mut YbeginFields) -> Result<(), YencError> {
for (key, val) in kv_iter(s) {
match key {
"size" => f.size = Some(parse_u64(val, "size")?),
"line" => {
let n: u64 = parse_u64(val, "line")?;
f.line_length = Some(n.min(255) as u8);
}
"part" => f.part = Some(parse_u32(val, "part")?),
"total" => f.total = Some(parse_u32(val, "total")?),
_ => {} }
}
Ok(())
}
fn kv_iter(s: &str) -> impl Iterator<Item = (&str, &str)> {
s.split_ascii_whitespace()
.take_while(|tok| !tok.starts_with("name="))
.filter_map(|tok| {
let eq = tok.find('=')?;
Some((&tok[..eq], &tok[eq + 1..]))
})
}
fn parse_u64(s: &str, field: &str) -> Result<u64, YencError> {
s.parse::<u64>().map_err(|_| YencError::InvalidHeader {
field: field.to_string(),
})
}
fn parse_u32(s: &str, field: &str) -> Result<u32, YencError> {
s.parse::<u32>().map_err(|_| YencError::InvalidHeader {
field: field.to_string(),
})
}
fn parse_crc32(s: &str, field: &str) -> Result<u32, YencError> {
u32::from_str_radix(s, 16).map_err(|_| YencError::InvalidHeader {
field: field.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ybegin_single_part() {
let f = parse_ybegin("line=128 size=64 name=test.bin").unwrap();
assert_eq!(f.line_length, Some(128));
assert_eq!(f.size, Some(64));
assert_eq!(f.name.as_deref(), Some("test.bin"));
assert!(f.part.is_none());
assert!(f.total.is_none());
}
#[test]
fn ybegin_multi_part() {
let f = parse_ybegin("part=1 total=2 line=128 size=128 name=test.bin").unwrap();
assert_eq!(f.part, Some(1));
assert_eq!(f.total, Some(2));
assert_eq!(f.line_length, Some(128));
assert_eq!(f.size, Some(128));
assert_eq!(f.name.as_deref(), Some("test.bin"));
}
#[test]
fn ybegin_name_with_spaces() {
let f = parse_ybegin("size=100 name=my cool file.bin").unwrap();
assert_eq!(f.name.as_deref(), Some("my cool file.bin"));
assert_eq!(f.size, Some(100));
}
#[test]
fn ybegin_unknown_fields_ignored() {
let f = parse_ybegin("line=128 size=64 newfield=42 name=test.bin").unwrap();
assert_eq!(f.size, Some(64));
assert_eq!(f.name.as_deref(), Some("test.bin"));
}
#[test]
fn ybegin_field_order_independent() {
let a = parse_ybegin("size=10 line=128 name=f.bin").unwrap();
let b = parse_ybegin("line=128 size=10 name=f.bin").unwrap();
assert_eq!(a.name, b.name);
assert_eq!(a.size, b.size);
assert_eq!(a.line_length, b.line_length);
}
#[test]
fn ybegin_invalid_size_is_error() {
let err = parse_ybegin("size=notanumber name=f.bin").unwrap_err();
assert!(matches!(err, YencError::InvalidHeader { field } if field == "size"));
}
#[test]
fn ypart_basic() {
let f = parse_ypart("begin=1 end=64").unwrap();
assert_eq!(f.begin, Some(1));
assert_eq!(f.end, Some(64));
}
#[test]
fn ypart_unknown_fields_ignored() {
let f = parse_ypart("begin=1 end=64 extra=99").unwrap();
assert_eq!(f.begin, Some(1));
assert_eq!(f.end, Some(64));
}
#[test]
fn yend_single_part() {
let f = parse_yend("size=64 crc32=100ece8c").unwrap();
assert_eq!(f.size, Some(64));
assert_eq!(f.crc32, Some(0x100e_ce8c));
assert!(f.pcrc32.is_none());
}
#[test]
fn yend_multi_part() {
let f = parse_yend("size=64 part=1 pcrc32=100ece8c crc32=24650d57").unwrap();
assert_eq!(f.size, Some(64));
assert_eq!(f.pcrc32, Some(0x100e_ce8c));
assert_eq!(f.crc32, Some(0x2465_0d57));
}
#[test]
fn yend_invalid_crc_is_error() {
let err = parse_yend("size=64 crc32=GGGGGGGG").unwrap_err();
assert!(matches!(err, YencError::InvalidHeader { field } if field == "crc32"));
}
#[test]
fn ybegin_tab_before_name_is_handled() {
let f = parse_ybegin("line=128 size=3\tname=hello.bin").unwrap();
assert_eq!(
f.name.as_deref(),
Some("hello.bin"),
"tab-separated name= must be parsed correctly"
);
assert_eq!(f.size, Some(3));
assert_eq!(f.line_length, Some(128));
}
#[test]
fn parse_ybegin_line_length_clamped_to_255() {
let f = parse_ybegin("line=300 size=10 name=test.bin").unwrap();
assert_eq!(
f.line_length,
Some(255),
"line= value 300 must be clamped to 255"
);
}
}