use crate::{MultiUuError, PartCollection};
#[derive(Debug, PartialEq)]
pub struct ReassembledFile {
pub filename: String,
pub mode: u32,
pub data: Vec<u8>,
pub is_truncated: bool,
pub missing_parts: Vec<u32>,
}
pub fn reassemble(collection: &PartCollection) -> Result<ReassembledFile, MultiUuError> {
let present: Vec<u32> = collection.present_parts().filter(|&n| n >= 1).collect();
if present.is_empty() {
return Err(MultiUuError::EmptyCollection);
}
let missing_parts = collection.missing_parts();
let mut all_data: Vec<u8> = Vec::new();
let mut any_truncated = false;
let mut filename = String::new();
let mut mode = 0u32;
let mut first = true;
for part_num in &present {
let entry = collection
.get(*part_num)
.unwrap_or_else(|| unreachable!("present_parts listed a part that get() cannot find"));
let block = uuencoding::decode(&entry.body_bytes)?;
if first {
filename = block.metadata.filename;
mode = block.metadata.mode;
first = false;
}
if block.is_truncated {
any_truncated = true;
}
all_data.extend_from_slice(&block.data);
}
let is_truncated = any_truncated || !missing_parts.is_empty();
Ok(ReassembledFile {
filename,
mode,
data: all_data,
is_truncated,
missing_parts,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{PartCollection, PartEntry};
const PART1_BODY: &[u8] = b"begin 644 file.bin\n.2&5L;&\\L(%=O<FQD(2 \n \nend\n";
const PART2_BODY: &[u8] = b"begin 644 file.bin\n-5&AI<R!I<R!A(&UU; \n \nend\n";
const PART3_BODY: &[u8] = b"begin 644 file.bin\n-=&DM<&%R=\"!T97-T+@ \n \nend\n";
fn make_entry(part_number: u32, body: &[u8]) -> PartEntry {
PartEntry {
part_number,
body_bytes: body.to_vec(),
subject: None,
}
}
#[test]
fn single_part_correct_data_and_metadata() {
let mut c = PartCollection::with_total(1);
c.add(make_entry(1, PART1_BODY)).unwrap();
let result = reassemble(&c).unwrap();
assert_eq!(result.data, b"Hello, World! ");
assert_eq!(result.filename, "file.bin");
assert_eq!(result.mode, 0o644);
assert!(!result.is_truncated);
assert!(result.missing_parts.is_empty());
}
#[test]
fn three_parts_full_reassembly() {
let mut c = PartCollection::with_total(3);
c.add(make_entry(1, PART1_BODY)).unwrap();
c.add(make_entry(2, PART2_BODY)).unwrap();
c.add(make_entry(3, PART3_BODY)).unwrap();
let result = reassemble(&c).unwrap();
assert_eq!(result.data, b"Hello, World! This is a multi-part test.");
assert_eq!(result.filename, "file.bin");
assert_eq!(result.mode, 0o644);
assert!(!result.is_truncated);
assert!(result.missing_parts.is_empty());
}
#[test]
fn three_parts_out_of_order_still_correct() {
let mut c = PartCollection::with_total(3);
c.add(make_entry(3, PART3_BODY)).unwrap();
c.add(make_entry(1, PART1_BODY)).unwrap();
c.add(make_entry(2, PART2_BODY)).unwrap();
let result = reassemble(&c).unwrap();
assert_eq!(result.data, b"Hello, World! This is a multi-part test.");
assert!(!result.is_truncated);
}
#[test]
fn missing_middle_part_yields_truncated() {
let mut c = PartCollection::with_total(3);
c.add(make_entry(1, PART1_BODY)).unwrap();
c.add(make_entry(3, PART3_BODY)).unwrap();
let result = reassemble(&c).unwrap();
assert!(result.is_truncated);
assert_eq!(result.missing_parts, vec![2]);
assert_eq!(result.data, b"Hello, World! ti-part test.");
}
#[test]
fn empty_collection_returns_error() {
let c = PartCollection::new();
let err = reassemble(&c).unwrap_err();
assert!(matches!(err, MultiUuError::EmptyCollection));
}
#[test]
fn toc_only_is_empty_collection() {
let mut c = PartCollection::new();
c.add(PartEntry {
part_number: 0,
body_bytes: b"toc data".to_vec(),
subject: None,
})
.unwrap();
let err = reassemble(&c).unwrap_err();
assert!(matches!(err, MultiUuError::EmptyCollection));
}
#[test]
fn truncated_uu_body_with_all_parts_present() {
let truncated_part2: Vec<u8> = PART2_BODY[..PART2_BODY.len() - 6].to_vec();
let mut c = PartCollection::with_total(3);
c.add(make_entry(1, PART1_BODY)).unwrap();
c.add(PartEntry {
part_number: 2,
body_bytes: truncated_part2,
subject: None,
})
.unwrap();
c.add(make_entry(3, PART3_BODY)).unwrap();
let result = reassemble(&c).unwrap();
assert!(result.is_truncated, "body missing `end` must be truncated");
assert!(
result.missing_parts.is_empty(),
"all parts were present; missing_parts must be empty"
);
}
#[test]
fn decode_error_on_first_part_propagates() {
let mut c = PartCollection::with_total(1);
c.add(make_entry(1, b"this is not valid uu data\n"))
.unwrap();
let err = reassemble(&c).unwrap_err();
assert!(matches!(err, MultiUuError::DecodeError(_)));
}
#[test]
fn decode_error_on_second_part_propagates() {
let mut c = PartCollection::with_total(2);
c.add(make_entry(1, PART1_BODY)).unwrap();
c.add(make_entry(2, b"not valid uu\n")).unwrap();
let err = reassemble(&c).unwrap_err();
assert!(matches!(err, MultiUuError::DecodeError(_)));
}
}