写一个 Rust 小应用 pngme - 2.实现数据块

5 阅读4分钟

实现了数据块类型 ChunkType 结构体之后,接下来就可以实现其余的数据块功能了。

引入模块和类型

use std::convert::TryFrom;
use std::fmt;
use anyhow::{Result, Error};
use crc::{Crc, Algorithm};

// 引入之前实现的 ChunkType 结构体
use crate::chunk_type::ChunkType;

crc 库被用来为 PNG 文件的数据块计算 CRC 值。PNG 文件的每个数据块都包含一个 CRC 字段,用于验证数据块内容的完整性。代码中定义了 PNG 文件使用的 CRC-32 算法的多项式和相关参数,通过这些参数可以创建一个 Crc 实例,进而对数据块中的数据(包括数据块类型和数据内容)进行 CRC 计算,以确保数据在传输或存储过程中没有被篡改或损坏。

定义 PNG 数据块的 CRC 多项式

// 定义 PNG 数据块的 CRC 多项式
const CRC_32_POLY: u32 = 0x04C11DB7;
const CRC_32_ALGO: Algorithm<u32> = Algorithm {
  poly: CRC_32_POLY,
  init: 0xFFFFFFFF,
  refin: true,
  refout: true,
  xorout: 0xFFFFFFFF,
  check: 0xCBF43926,
  residue: 0x00000000,
  width: 32,
};
  • const CRC_32_POLY: u32 = 0x04C11DB7;:定义一个常量 CRC_32_POLY,它是一个 32 位无符号整数,表示 PNG 文件使用的 CRC-32 算法的多项式。这个多项式是国际标准 ISO 3309 中定义的标准 CRC-32 多项式。
  • const CRC_32_ALGO: Algorithm<u32> = Algorithm {... };:定义一个常量 CRC_32_ALGO,它是 Algorithm<u32> 类型的实例,用于描述 CRC-32 算法的具体参数:
    • poly: CRC_32_POLY:指定 CRC 算法使用的多项式,这里使用之前定义的 CRC_32_POLY
    • init: 0xFFFFFFFF:指定 CRC 寄存器的初始值,初始值为全 1。
    • refin: true:表示在计算 CRC 之前需要对输入数据的每个字节进行按位反转。
    • refout: true:表示在计算完 CRC 之后需要对结果进行按位反转。
    • xorout: 0xFFFFFFFF:表示在计算完 CRC 并反转结果之后,需要与这个值进行异或操作。
    • check: 0xCBF43926:这是一个校验值,用于验证 CRC 算法实现的正确性。当对特定的测试数据进行 CRC 计算时,结果应该等于这个校验值。
    • residue: 0x00000000:表示在处理完所有数据后,CRC 寄存器中剩余的值,这里为 0。
    • width: 32:表示 CRC 算法的位数,这里是 32 位 CRC 算法。 通过上述代码,我们为后续计算 PNG 数据块的 CRC 值做好了算法参数的准备工作。

定义 Chunk 结构体

#[derive(Debug, Clone)]
pub struct Chunk {
    length: u32,
    chunk_type: ChunkType,
    data: Vec<u8>,
    crc: u32,
}

impl Chunk {
    // 创建新的 Chunk 实例
    pub fn new(chunk_type: ChunkType, data: Vec<u8>) -> Chunk {
        let length = data.len() as u32;
        let mut all_data = Vec::new();
        all_data.extend_from_slice(&chunk_type.bytes());
        all_data.extend_from_slice(&data);
        let crc_calculator = Crc::<u32>::new(&CRC_32_ALGO);
        let crc = crc_calculator.checksum(&all_data);
        Chunk {
            length,
            chunk_type,
            data,
            crc,
        }
    }

    // 返回数据块数据的长度
    pub fn length(&self) -> u32 {
        self.length
    }

    // 返回数据块类型的引用
    pub fn chunk_type(&self) -> &ChunkType {
        &self.chunk_type
    }

    // 返回数据块数据的引用
    pub fn data(&self) -> &[u8] {
        &self.data
    }

    // 返回数据块的 CRC 值
    pub fn crc(&self) -> u32 {
        self.crc
    }

    // 尝试将数据块的数据转换为字符串
    pub fn data_as_string(&self) -> Result<String> {
        String::from_utf8(self.data.clone()).map_err(|e| Error::msg(e.to_string()))
    }

    // 将整个数据块转换为字节序列
    pub fn as_bytes(&self) -> Vec<u8> {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&self.length.to_be_bytes());
        bytes.extend_from_slice(self.chunk_type.bytes().as_slice());
        bytes.extend_from_slice(&self.data);
        bytes.extend_from_slice(&self.crc.to_be_bytes());
        bytes
    }
}

实现从字节切片转换为 Chunk 实例的功能

具体步骤:

  1. 检查输入字节切片的长度是否足够形成一个有效的数据块。
  2. 从字节切片中提取数据块的长度、数据块类型和数据。
  3. 从字节切片中提取预期的 CRC 值,并重新计算数据的 CRC 值。
  4. 比较计算得到的 CRC 值和预期的 CRC 值,如果不相等则返回错误。
  5. 如果所有检查都通过,则返回一个新的 Chunk 实例。
impl TryFrom<&[u8]> for Chunk {
    type Error = Error;

    fn try_from(value: &[u8]) -> Result<Self> {
        if value.len() < 12 {
            return Err(Error::msg("Input bytes are too short to form a valid chunk"));
        }
        let length = u32::from_be_bytes([value[0], value[1], value[2], value[3]]);
        let chunk_type = ChunkType::try_from([value[4], value[5], value[6], value[7]])?;
        let data_end = 8 + length as usize;
        if value.len() < data_end + 4 {
            return Err(Error::msg("Input bytes do not contain enough data for the specified length"));
        }
        let data = value[8..data_end].to_vec();
        let expected_crc = u32::from_be_bytes([value[data_end], value[data_end + 1], value[data_end + 2], value[data_end + 3]]);
        let mut all_data = Vec::new();
        all_data.extend_from_slice(&chunk_type.bytes());
        all_data.extend_from_slice(&data);
        let crc_calculator = Crc::<u32>::new(&CRC_32_ALGO);
        let calculated_crc = crc_calculator.checksum(&all_data);
        if calculated_crc != expected_crc {
            return Err(Error::msg("CRC check failed"));
        }
        Ok(Chunk {
            length,
            chunk_type,
            data,
            crc: calculated_crc,
        })
    }
}

实现 Display 特性,用于格式化输出 Chunk 实例

impl fmt::Display for Chunk {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.data_as_string() {
            Ok(s) => write!(f, "Chunk {{ length: {}, type: {}, data: \"{}\", crc: {} }}", self.length, self.chunk_type, s, self.crc),
            Err(_) => write!(f, "Chunk {{ length: {}, type: {}, data: {:?}, crc: {} }}", self.length, self.chunk_type, self.data, self.crc),
        }
    }
}