写一个 Rust 小应用 pngme - 1.实现数据块类型

11 阅读5分钟

Rust 学了忘,忘了学,干脆跟着大佬推荐写一个小应用,比较懒,直接沿用原项目的名字了。

原项目地址:jrdngr.github.io/pngme_book/

要做什么

开发一个命令行程序,它能在 PNG 文件中隐藏秘密信息。这个小应用将有四个命令:

  • 将信息编码到 PNG 文件中
  • 解码存储在 PNG 文件中的信息
  • 从 PNG 文件中移除信息
  • 打印出可搜索信息的 PNG 数据块列表

关于PNG

我们看过很多 png 格式的图片,那么 png 文件是由什么组成的呢?

PNG 文件基本结构

PNG 文件的前 8 个字节是固定值,即 137 80 78 71 13 10 26 10 ,该签名表明文件剩余部分包含一个 PNG 图像,图像由一系列数据块组成,以 IHDR 块开头,以 IEND 块结尾。

数据块布局
  • 长度(Length):4 字节无符号整数,记录数据块数据字段的字节数,不包含自身、数据块类型代码和 CRC 的长度,长度取值范围不能超过字节。
  • 数据块类型(Chunk Type):4 字节代码,由大小写 ASCII 字母组成,但编码器和解码器将其视为固定二进制值。
  • 数据块数据(Chunk Data):依据数据块类型而定,长度可为 0。
  • CRC(Cyclic Redundancy Check):4 字节,对数据块中除长度字段外的其他字节(包含数据块类型代码和数据字段)进行计算,即便数据块无数据也存在。
  • 数据块数据长度可变,数据块顺序受类型限制(如 IHDR 必须首位,IEND 必须末尾),同一类型数据块在特定条件下可重复出现。
数据块命名约定

数据块类型代码的每个字节第 5 位(值为 32)用于传达属性。

  • 辅助位(Ancillary bit):首字节第 5 位,0(大写字母)代表关键数据块,1(小写字母)代表辅助数据块,解码器对未知数据块处理方式不同。
  • 私有位(Private bit):第二个字节第 5 位,0(大写字母)表示公共数据块,1(小写字母)表示私有数据块,解码器无需测试该位。
  • 保留位(Reserved bit):第三个字节第 5 位,当前版本必须为 0(大写字母),未来可能用于扩展。
  • 安全复制位(Safe-to-copy bit):第四个字节第 5 位,0(大写字母)表示不安全复制,1(小写字母)表示安全复制,主要用于 PNG 编辑器处理未识别数据块。

实现数据块类型

返回数据块类型的字节表示,检查整个数据块类型的有效性,以及检查 4 个字节中每个字节大小写的特殊含义。

// 引入标准库中用于尝试转换的模块
use std::convert::TryFrom;
// 引入标准库中用于从字符串解析类型的模块
use std::str::FromStr;
// 引入 fmt 模块,用于实现格式化输出相关的特质
use std::fmt;
use anyhow::{Result, Error};

// 定义一个结构,该结构将派生以下特质:
// - Debug: 允许结构用 {:?} 格式符进行调试输出
// - Clone: 允许结构通过 .clone() 方法复制自身
// - Copy: 允许结构实例在赋值时不进行深拷贝,即可以被多处使用而不需要克隆
// - PartialEq: 允许结构的实例可以相互比较是否相等
// - Eq: 表示 PartialEq 比较是自反的,即任何实例与其自身相等
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChunkType([u8; 4]);

impl ChunkType {
    // 返回 ChunkType 内部的 4 个字节数组
    pub fn bytes(&self) -> [u8; 4] {
        self.0
    }

    // 检查当前 ChunkType 是否有效
    // 有效性包括:保留位有效且所有字节为 ASCII 字母
    pub fn is_valid(&self) -> bool {
        self.is_reserved_bit_valid() && self.0.iter().all(|&b| b.is_ascii_alphabetic())
    }

    // 检查当前 ChunkType 是否为关键类型
    // 关键类型是指其 4 个字节中第一个字节为 ASCII 大写字母
    pub fn is_critical(&self) -> bool {
        self.0[0].is_ascii_uppercase()
    }

    // 检查当前 ChunkType 是否为公共类型
    // 公共类型是指其 4 个字节中第一个字节为 ASCII 大写字母
    pub fn is_public(&self) -> bool {
        self.0[0].is_ascii_uppercase()
    }

    // 检查当前 ChunkType 的保留位是否有效
    // 有效性规则:第三个字节的第6位必须为0
    pub fn is_reserved_bit_valid(&self) -> bool {
        (self.0[2] & 0x20) == 0
    }

    // 检查当前 ChunkType 是否可以安全复制
    // 安全复制是指其 4 个字节中第四个字节为 ASCII 小写字母
    pub fn is_safe_to_copy(&self) -> bool {
        self.0[3].is_ascii_lowercase()
    }
}

// 允许从长度为 4 的 u8 数组创建 ChunkType 实例,若数组元素不是 ASCII 字母则返回错误。
impl TryFrom<[u8; 4]> for ChunkType {
    type Error = Error;

    // 尝试从 [u8; 4] 类型转换为 ChunkType
    // 如果 value 中的所有字节都是 ASCII 字母,则创建并返回 ChunkType 实例
    // 否则返回错误信息
    fn try_from(value: [u8; 4]) -> Result<Self, Self::Error> {
        if value.iter().all(|&b| b.is_ascii_alphabetic()) {
            Ok(ChunkType(value))
        } else {
          Err(anyhow::anyhow!("Invalid chunk type: must consist of only ASCII alphabetic characters"))
        }
    }
}

// 允许从字符串创建 ChunkType 实例,要求字符串长度为 4 且全是 ASCII 字母
impl FromStr for ChunkType {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.len() != 4 {
            return Err(anyhow::anyhow!("Invalid chunk type: must be exactly 4 characters long"));
        }
        let bytes: [u8; 4] = match s.as_bytes().try_into() {
            Ok(bytes) => bytes,
            Err(_) => return Err(anyhow::anyhow!("Invalid chunk type: conversion to bytes failed")),
        };
        if bytes.iter().all(|&b| b.is_ascii_alphabetic()) {
            Ok(ChunkType(bytes))
        } else {
            Err(anyhow::anyhow!("Invalid chunk type: must consist of only ASCII alphabetic characters"))
        }
    }
}

// 格式化输出,将 ChunkType 实例转换为字符串表示
impl fmt::Display for ChunkType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = String::from_utf8_lossy(&self.0);
        write!(f, "{}", s)
    }
}