nte_patcher 0.2.3

Rust implementation of NTE PatcherSDK
Documentation
#![allow(missing_docs)]
use crate::error::Error;
use md5::{Digest, Md5};
use std::path::Path;

fn parse_expected_md5(hex_str: &str) -> Option<[u8; 16]> {
    let mut bytes = [0u8; 16];
    hex::decode_to_slice(hex_str, &mut bytes).ok()?;
    Some(bytes)
}

pub async fn check_file_md5(path: &Path, expected_md5: &str) -> Result<bool, Error> {
    let expected_bytes = match parse_expected_md5(expected_md5) {
        Some(b) => b,
        None => return Ok(false),
    };

    let path_buf = path.to_path_buf();

    let is_match = tokio::task::spawn_blocking(move || -> Result<bool, std::io::Error> {
        let mut file = match std::fs::File::open(&path_buf) {
            Ok(f) => f,
            Err(_) => return Ok(false),
        };

        let metadata = match file.metadata() {
            Ok(m) => m,
            Err(_) => return Ok(false),
        };

        if metadata.len() == 0 {
            let empty_md5 = Md5::digest([]);
            return Ok(empty_md5.as_slice() == expected_bytes);
        }

        #[cfg(feature = "mmap")]
        let hash = {
            let mmap = unsafe { memmap2::Mmap::map(&file)? };
            Md5::digest(&mmap)
        };

        #[cfg(not(feature = "mmap"))]
        let hash = {
            use std::io::Read;
            let mut hasher = Md5::new();
            let mut buffer = vec![0u8; 65536];
            loop {
                let n = file.read(&mut buffer)?;
                if n == 0 {
                    break;
                }
                hasher.update(&buffer[..n]);
            }
            hasher.finalize()
        };

        Ok(hash.as_slice() == expected_bytes)
    })
    .await
    .unwrap_or(Ok(false))?;

    Ok(is_match)
}

pub async fn check_slice_md5(
    path: &Path,
    start: u64,
    size: u64,
    expected_md5: &str,
) -> Result<bool, Error> {
    let expected_bytes = match parse_expected_md5(expected_md5) {
        Some(b) => b,
        None => return Ok(false),
    };

    let path_buf = path.to_path_buf();

    let is_match = tokio::task::spawn_blocking(move || -> Result<bool, std::io::Error> {
        let mut file = match std::fs::File::open(&path_buf) {
            Ok(f) => f,
            Err(_) => return Ok(false),
        };

        let metadata = match file.metadata() {
            Ok(m) => m,
            Err(_) => return Ok(false),
        };

        if metadata.len() < start + size {
            return Ok(false);
        }

        if size == 0 {
            let empty_md5 = Md5::digest([]);
            return Ok(empty_md5.as_slice() == expected_bytes);
        }

        #[cfg(feature = "mmap")]
        let hash = {
            let mmap = unsafe {
                memmap2::MmapOptions::new()
                    .offset(start)
                    .len(size as usize)
                    .map(&file)?
            };
            Md5::digest(&mmap)
        };

        #[cfg(not(feature = "mmap"))]
        let hash = {
            use std::io::{Read, Seek, SeekFrom};
            file.seek(SeekFrom::Start(start))?;
            let mut hasher = Md5::new();
            let mut buffer = vec![0u8; 65536];
            let mut remaining = size;
            while remaining > 0 {
                let to_read = std::cmp::min(remaining, buffer.len() as u64) as usize;
                let n = file.read(&mut buffer[..to_read])?;
                if n == 0 {
                    break;
                }
                hasher.update(&buffer[..n]);
                remaining -= n as u64;
            }
            hasher.finalize()
        };

        Ok(hash.as_slice() == expected_bytes)
    })
    .await
    .unwrap_or(Ok(false))?;

    Ok(is_match)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_empty_file_md5() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("empty.txt");
        std::fs::File::create(&path).unwrap();

        let res = check_file_md5(&path, "d41d8cd98f00b204e9800998ecf8427e")
            .await
            .unwrap();
        assert!(res);
    }
}