apt-mirror-check 1.0.0

Check errors for apt mirror
Documentation
use crate::file_attr::{RelativeFileAttr, RelativeFileAttrBuilder};
use relative_path::RelativePathBuf;
use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::path::Path;

#[derive(Debug, thiserror::Error)]
pub enum PackageError {
    #[error("{0}")]
    Io(#[from] io::Error),

    #[error("size error for line: {0}")]
    Size(String),
}

fn parse_packages_file_lines(
    lines: impl Iterator<Item = io::Result<String>>,
) -> Result<Vec<RelativeFileAttr>, PackageError> {
    let mut files = Vec::new();
    let mut current_package = RelativeFileAttrBuilder::default();

    for line in lines {
        let line = line?;
        if line.starts_with(' ') {
            continue; // ignore continuation lines, required fields won't be broken into multi-lines
        }

        if line.is_empty() {
            files.push(current_package.build().unwrap());
            current_package = RelativeFileAttrBuilder::default();
            continue;
        }

        if let Some((key, value)) = line.split_once(':') {
            let value = value.trim();

            match key {
                "Filename" => {
                    current_package.path(RelativePathBuf::from(value));
                }
                "Size" => {
                    let size = value
                        .parse::<usize>()
                        .map_err(|_| PackageError::Size(line))?;
                    current_package.size(size);
                }
                "MD5sum" => {
                    current_package.md5sum(value.to_string());
                }
                "SHA256" => {
                    current_package.sha256sum(value.to_string());
                }
                "SHA512" => {
                    current_package.sha512sum(value.to_string());
                }
                _ => {}
            }
        }
    }

    Ok(files)
}

fn parse_packages_file_none(path: &Path) -> Result<Vec<RelativeFileAttr>, PackageError> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);

    parse_packages_file_lines(reader.lines())
}

fn parse_packages_file_gz(path: &Path) -> Result<Vec<RelativeFileAttr>, PackageError> {
    let file = File::open(path)?;
    let dec = flate2::read::GzDecoder::new(file);
    let reader = BufReader::new(dec);

    parse_packages_file_lines(reader.lines())
}

fn parse_packages_file_xz(path: &Path) -> Result<Vec<RelativeFileAttr>, PackageError> {
    let file = File::open(path)?;
    let dec = xz2::read::XzDecoder::new(file);
    let reader = BufReader::new(dec);

    parse_packages_file_lines(reader.lines())
}

pub fn parse_packages_file(path: &Path) -> Result<Vec<RelativeFileAttr>, PackageError> {
    match path.extension().and_then(|s| s.to_str()) {
        None => parse_packages_file_none(path),
        Some("gz") => parse_packages_file_gz(path),
        Some("xz") => parse_packages_file_xz(path),
        _ => panic!("unsupported extension: {:?}", path) // impossible
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use flate2::write::GzEncoder;
    use flate2::Compression;
    use xz2::write::XzEncoder;
    use tempfile::NamedTempFile;

    #[test]
    fn test_parse_packages_file() {
        let packages_content = r#"Package: 0ad
Version: 0.0.26-3
Architecture: amd64
Description: Real-time strategy game of ancient warfare
Filename: pool/main/0/0ad/0ad_0.0.26-3_amd64.deb
Size: 7891488
MD5sum: 4d471183a39a3a11d00cd35bf9f6803d
SHA256: 3a2118df47bf3f04285649f0455c2fc6fe2dc7f0b237073038aa00af41f0d5f2

Package: 0ad-data
Version: 0.0.26-1
Architecture: all
Description: Real-time strategy game of ancient warfare (data files)
Filename: pool/main/0/0ad-data/0ad-data_0.0.26-1_all.deb
Size: 1377557908
MD5sum: fc5ed8a20ce1861950c7ed3a5a615be0
SHA256: 53745ae74d05bccf6783400fa98f3932b21729ab9d2e86151aa2c331c3455178

"#;

        let mut test_file = NamedTempFile::new().unwrap();
        test_file.write_all(packages_content.as_bytes()).unwrap();
        test_file.flush().unwrap();

        let result = parse_packages_file_none(test_file.path()).unwrap();

        assert_eq!(result.len(), 2);

        let first_file = &result[0];
        assert_eq!(first_file.path, "pool/main/0/0ad/0ad_0.0.26-3_amd64.deb");
        assert_eq!(first_file.size, Some(7891488));
        assert_eq!(
            first_file.md5sum,
            Some("4d471183a39a3a11d00cd35bf9f6803d".to_string())
        );
        assert_eq!(
            first_file.sha256sum,
            Some("3a2118df47bf3f04285649f0455c2fc6fe2dc7f0b237073038aa00af41f0d5f2".to_string())
        );
        assert_eq!(first_file.sha512sum, None);

        let second_file = &result[1];
        assert_eq!(
            second_file.path,
            "pool/main/0/0ad-data/0ad-data_0.0.26-1_all.deb"
        );
        assert_eq!(second_file.size, Some(1377557908));
        assert_eq!(
            second_file.md5sum,
            Some("fc5ed8a20ce1861950c7ed3a5a615be0".to_string())
        );
        assert_eq!(
            second_file.sha256sum,
            Some("53745ae74d05bccf6783400fa98f3932b21729ab9d2e86151aa2c331c3455178".to_string())
        );
        assert_eq!(second_file.sha512sum, None);
    }

    #[test]
    fn test_parse_packages_file_empty() {
        let packages_content = "";

        let mut test_file = NamedTempFile::new().unwrap();
        test_file.write_all(packages_content.as_bytes()).unwrap();
        test_file.flush().unwrap();

        let result = parse_packages_file_none(test_file.path()).unwrap();

        assert_eq!(result.len(), 0);
    }

    #[test]
    fn test_parse_packages_file_gz() {
        let packages_content = r#"Package: 0ad
Version: 0.0.26-3
Architecture: amd64
Description: Real-time strategy game of ancient warfare
Filename: pool/main/0/0ad/0ad_0.0.26-3_amd64.deb
Size: 7891488
MD5sum: 4d471183a39a3a11d00cd35bf9f6803d
SHA256: 3a2118df47bf3f04285649f0455c2fc6fe2dc7f0b237073038aa00af41f0d5f2

Package: 0ad-data
Version: 0.0.26-1
Architecture: all
Description: Real-time strategy game of ancient warfare (data files)
Filename: pool/main/0/0ad-data/0ad-data_0.0.26-1_all.deb
Size: 1377557908
MD5sum: fc5ed8a20ce1861950c7ed3a5a615be0
SHA256: 53745ae74d05bccf6783400fa98f3932b21729ab9d2e86151aa2c331c3455178

"#;

        // 创建 gzip 压缩的测试文件
        let mut test_file = NamedTempFile::new().unwrap();
        let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
        encoder.write_all(packages_content.as_bytes()).unwrap();
        let compressed_data = encoder.finish().unwrap();
        test_file.write_all(&compressed_data).unwrap();
        test_file.flush().unwrap();

        // 解析并验证结果
        let result = parse_packages_file_gz(test_file.path()).unwrap();

        assert_eq!(result.len(), 2);

        let first_file = &result[0];
        assert_eq!(first_file.path, "pool/main/0/0ad/0ad_0.0.26-3_amd64.deb");
        assert_eq!(first_file.size, Some(7891488));
        assert_eq!(
            first_file.md5sum,
            Some("4d471183a39a3a11d00cd35bf9f6803d".to_string())
        );
        assert_eq!(
            first_file.sha256sum,
            Some("3a2118df47bf3f04285649f0455c2fc6fe2dc7f0b237073038aa00af41f0d5f2".to_string())
        );
        assert_eq!(first_file.sha512sum, None);

        let second_file = &result[1];
        assert_eq!(
            second_file.path,
            "pool/main/0/0ad-data/0ad-data_0.0.26-1_all.deb"
        );
        assert_eq!(second_file.size, Some(1377557908));
        assert_eq!(
            second_file.md5sum,
            Some("fc5ed8a20ce1861950c7ed3a5a615be0".to_string())
        );
        assert_eq!(
            second_file.sha256sum,
            Some("53745ae74d05bccf6783400fa98f3932b21729ab9d2e86151aa2c331c3455178".to_string())
        );
        assert_eq!(second_file.sha512sum, None);
    }

    #[test]
    fn test_parse_packages_file_xz() {
        let packages_content = r#"Package: 0ad
Version: 0.0.26-3
Architecture: amd64
Description: Real-time strategy game of ancient warfare
Filename: pool/main/0/0ad/0ad_0.0.26-3_amd64.deb
Size: 7891488
MD5sum: 4d471183a39a3a11d00cd35bf9f6803d
SHA256: 3a2118df47bf3f04285649f0455c2fc6fe2dc7f0b237073038aa00af41f0d5f2

Package: 0ad-data
Version: 0.0.26-1
Architecture: all
Description: Real-time strategy game of ancient warfare (data files)
Filename: pool/main/0/0ad-data/0ad-data_0.0.26-1_all.deb
Size: 1377557908
MD5sum: fc5ed8a20ce1861950c7ed3a5a615be0
SHA256: 53745ae74d05bccf6783400fa98f3932b21729ab9d2e86151aa2c331c3455178

"#;

        // 创建 xz 压缩的测试文件
        let mut test_file = NamedTempFile::new().unwrap();
        let mut encoder = XzEncoder::new(Vec::new(), 6);
        encoder.write_all(packages_content.as_bytes()).unwrap();
        let compressed_data = encoder.finish().unwrap();
        test_file.write_all(&compressed_data).unwrap();
        test_file.flush().unwrap();

        // 解析并验证结果
        let result = parse_packages_file_xz(test_file.path()).unwrap();

        assert_eq!(result.len(), 2);

        let first_file = &result[0];
        assert_eq!(first_file.path, "pool/main/0/0ad/0ad_0.0.26-3_amd64.deb");
        assert_eq!(first_file.size, Some(7891488));
        assert_eq!(
            first_file.md5sum,
            Some("4d471183a39a3a11d00cd35bf9f6803d".to_string())
        );
        assert_eq!(
            first_file.sha256sum,
            Some("3a2118df47bf3f04285649f0455c2fc6fe2dc7f0b237073038aa00af41f0d5f2".to_string())
        );
        assert_eq!(first_file.sha512sum, None);

        let second_file = &result[1];
        assert_eq!(
            second_file.path,
            "pool/main/0/0ad-data/0ad-data_0.0.26-1_all.deb"
        );
        assert_eq!(second_file.size, Some(1377557908));
        assert_eq!(
            second_file.md5sum,
            Some("fc5ed8a20ce1861950c7ed3a5a615be0".to_string())
        );
        assert_eq!(
            second_file.sha256sum,
            Some("53745ae74d05bccf6783400fa98f3932b21729ab9d2e86151aa2c331c3455178".to_string())
        );
        assert_eq!(second_file.sha512sum, None);
    }
}