use crate::{
consts::HEADER_LENGTH,
decode,
encode::Encoding,
file_type::FileType,
header::{Header, HeaderParseError},
};
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum JoinError {
#[error("No data found")]
Empty,
#[error("Conflicting/variable file type/encodings/sizes")]
ConflictingHeaders,
#[error("Too many parts, expected {0}, got {1}")]
TooManyParts(usize, usize),
#[error("Duplicated part index {0} has wrong content")]
DuplicatePartWrongContent(usize),
#[error("Part with index {0} has no data")]
PartWithNoData(usize),
#[error("Missing part, with index {0}")]
MissingPart(usize),
#[error(transparent)]
HeaderParseError(#[from] HeaderParseError),
#[error(transparent)]
DecodeError(#[from] decode::DecodeError),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Joined {
pub encoding: Encoding,
pub file_type: FileType,
pub data: Vec<u8>,
}
impl Joined {
pub fn try_from_parts(parts: Vec<String>) -> Result<Self, JoinError> {
let (header, data) = join_qrs(parts)?;
Ok(Self {
encoding: header.encoding,
file_type: header.file_type,
data,
})
}
}
fn join_qrs(input_parts: Vec<String>) -> Result<(Header, Vec<u8>), JoinError> {
let header = get_and_verify_headers(input_parts.as_slice())?;
let mut orderered_parts = vec![String::new(); header.num_parts];
for part in input_parts {
if part.is_empty() {
continue;
}
let index = get_index_from_part(&part, &header)?;
let current_part_content = &orderered_parts[index];
let part_data = &part[HEADER_LENGTH..];
if !current_part_content.is_empty() && current_part_content != part_data {
return Err(JoinError::DuplicatePartWrongContent(index));
}
if part_data.is_empty() {
return Err(JoinError::PartWithNoData(index));
}
orderered_parts[index] = part_data.to_string();
}
for (index, part) in orderered_parts.iter().enumerate() {
if part.is_empty() {
return Err(JoinError::MissingPart(index));
}
}
let data = decode::decode_ordered_parts(&orderered_parts, header.encoding)?;
Ok((header, data))
}
fn get_and_verify_headers(parts: &[String]) -> Result<Header, JoinError> {
if parts.is_empty() {
return Err(JoinError::Empty);
}
let first_header = parts
.iter()
.find(|line| !line.is_empty())
.ok_or(JoinError::Empty)?;
let header = Header::try_from_str(first_header)?;
for part in parts.iter().skip(1) {
if part.trim().is_empty() {
continue;
}
if part.len() < HEADER_LENGTH {
return Err(JoinError::ConflictingHeaders);
}
if part[0..6] != first_header[0..6] {
return Err(JoinError::ConflictingHeaders);
}
}
Ok(header)
}
pub(crate) fn get_index_from_part(part: &str, header: &Header) -> Result<usize, JoinError> {
let index = usize::from_str_radix(&part[6..8], 36).unwrap();
if index >= header.num_parts {
return Err(JoinError::TooManyParts(header.num_parts, index + 1));
};
Ok(index)
}
#[cfg(test)]
mod tests {
use crate::{encode::Encoding, file_type::FileType};
use super::*;
#[test]
fn test_verify_header() {
let parts = vec!["", "B$ZU0801", "B$ZU0801", "B$ZU0801", ""]
.into_iter()
.map(String::from)
.collect::<Vec<String>>();
let header = get_and_verify_headers(&parts);
assert!(header.is_ok());
assert_eq!(
header.unwrap(),
Header {
encoding: Encoding::Zlib,
file_type: FileType::UnicodeText,
num_parts: 8
}
);
}
#[test]
fn test_catches_empty() {
let parts = vec!["", "", "", "", ""]
.into_iter()
.map(String::from)
.collect::<Vec<String>>();
let header = get_and_verify_headers(&parts);
assert!(header.is_err());
assert_eq!(header.unwrap_err(), JoinError::Empty);
}
#[test]
fn test_catches_conflicting_headers() {
let parts = vec!["", "B$ZU0801", "B$ZU0902", "B$ZU0803", ""]
.into_iter()
.map(String::from)
.collect::<Vec<String>>();
let header = get_and_verify_headers(&parts);
assert!(header.is_err());
assert_eq!(header.unwrap_err(), JoinError::ConflictingHeaders);
}
}