Chapter 09Project Hexdump

项目

概述

本项目将原始字节转换为整洁、对齐感知的十六进制视图。我们将增量读取文件,将每行格式化为OFFSET: HEX ASCII,并保持输出在不同平台上的稳定性。写入器接口通过std.fs.File.writerstd.Io.Writer使用缓冲的stdout,如File.zigIo.zig中所述。

格式化程序默认每行打印16个字节,并可通过--width N(4..32)进行配置。字节分组为8|8以便于扫描,不可打印的ASCII字符在右侧边栏中变为点号,如fmt.zig#Command-line-flags中所述。

学习目标

  • 使用std.fmt.parseInt解析CLI标志并验证数字。
  • 使用固定缓冲区流式传输文件并组装精确宽度的输出行。
  • 使用非弃用的File.Writer + Io.Writer来缓冲stdout并干净地刷新。

构建转储

我们将连接三个部分:一个微小的CLI解析器、一个行格式化程序以及一个以精确宽度块向格式化程序提供数据的循环。实现依赖于Zig的切片和显式生命周期(在释放args之前复制路径)以保持健壮性;参见process.zig#Error-Handling

Zig
const std = @import("std");

// Chapter 9 – Project: Hexdump
// 第9章 - 项目:十六进制转储
//
// A small, alignment-aware hexdump that prints:
// 一个小的、对齐感知的十六进制转储,打印:
// OFFSET: 16 hex bytes (grouped 8|8)  ASCII
// OFFSET: 16 hex bytes (按 8|8 分组) ASCII
// Default width is 16 bytes per line; override with --width N (4..32).
// 默认宽度为每行 16 字节;使用 --width N (4..32) 覆盖。
//
// Usage:
// zig run hexdump.zig -- <path>
// zig run hexdump.zig -- <路径>
// zig run hexdump.zig -- --width 8 <path>
// zig run hexdump.zig -- --width 8 <路径>

const Cli = struct {
    width: usize = 16,
    path: []const u8 = &[_]u8{},
};

fn printUsage() void {
    std.debug.print("usage: hexdump [--width N] <path>\n", .{});
}

fn parseArgs(allocator: std.mem.Allocator) !Cli {
    var cli: Cli = .{};
    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);

    if (args.len == 1 or (args.len == 2 and std.mem.eql(u8, args[1], "--help"))) {
        printUsage();
        std.process.exit(0);
    }

    var i: usize = 1;
    while (i + 1 < args.len and std.mem.eql(u8, args[i], "--width")) : (i += 2) {
        const val = args[i + 1];
        cli.width = std.fmt.parseInt(usize, val, 10) catch {
            std.debug.print("error: invalid width '{s}'\n", .{val});
            std.process.exit(2);
        };
        if (cli.width < 4 or cli.width > 32) {
            std.debug.print("error: width must be between 4 and 32\n", .{});
            std.process.exit(2);
        }
    }

    if (i >= args.len) {
        std.debug.print("error: expected <path>\n", .{});
        printUsage();
        std.process.exit(2);
    }

    // Duplicate the path so it remains valid after freeing args.
    // Duplicate 路径 so it remains valid after freeing 参数.
    cli.path = try allocator.dupe(u8, args[i]);
    return cli;
}

fn isPrintable(c: u8) bool {
    // Printable ASCII (space through tilde)
    return c >= 0x20 and c <= 0x7E;
}

fn dumpLine(stdout: *std.Io.Writer, offset: usize, bytes: []const u8, width: usize) !void {
    // OFFSET (8 hex digits), colon and space
    // OFFSET (8 hex digits), colon 和 space
    try stdout.print("{X:0>8}: ", .{offset});

    // Hex bytes with grouping at 8
    // Hex bytes 使用 grouping 在 8
    var i: usize = 0;
    while (i < width) : (i += 1) {
        if (i < bytes.len) {
            try stdout.print("{X:0>2} ", .{bytes[i]});
        } else {
            // 填充缺失的字节以保持ASCII列对齐
            try stdout.print("   ", .{});
        }
        if (i + 1 == width / 2) {
            try stdout.print(" ", .{}); // extra gap between 8|8
        }
    }

    // Two spaces before ASCII gutter
    // 两个 spaces before ASCII gutter
    try stdout.print("  ", .{});

    i = 0;
    while (i < width) : (i += 1) {
        if (i < bytes.len) {
            const ch: u8 = if (isPrintable(bytes[i])) bytes[i] else '.';
            try stdout.print("{c}", .{ch});
        } else {
            try stdout.print(" ", .{});
        }
    }
    try stdout.print("\n", .{});
}

pub fn main() !void {
    const allocator = std.heap.page_allocator;
    const cli = try parseArgs(allocator);

    var file = std.fs.cwd().openFile(cli.path, .{ .mode = .read_only }) catch {
        std.debug.print("error: unable to open '{s}'\n", .{cli.path});
        std.process.exit(1);
    };
    defer file.close();

    // Buffered stdout using the modern File.Writer + Io.Writer interface.
    // 缓冲 stdout 使用 modern 文件.Writer + Io.Writer 接口.
    var out_buf: [16 * 1024]u8 = undefined;
    var file_writer = std.fs.File.writer(std.fs.File.stdout(), &out_buf);
    const stdout = &file_writer.interface;

    var offset: usize = 0;
    var carry: [64]u8 = undefined; // enough for max width 32
    var carry_len: usize = 0;

    var buf: [64 * 1024]u8 = undefined;
    while (true) {
        const n = try file.read(buf[0..]);
        if (n == 0 and carry_len == 0) break;

        var idx: usize = 0;
        while (idx < n) {
            // fill a line from carry + buffer bytes
            // fill 一个 line 从 carry + 缓冲区 bytes
            const need = cli.width - carry_len;
            const take = @min(need, n - idx);
            @memcpy(carry[carry_len .. carry_len + take], buf[idx .. idx + take]);
            carry_len += take;
            idx += take;

            if (carry_len == cli.width) {
                try dumpLine(stdout, offset, carry[0..carry_len], cli.width);
                offset += carry_len;
                carry_len = 0;
            }
        }

        if (n == 0 and carry_len > 0) {
            try dumpLine(stdout, offset, carry[0..carry_len], cli.width);
            offset += carry_len;
            carry_len = 0;
        }
    }
    try file_writer.end();
}
运行
Shell
$ zig run hexdump.zig -- sample.txt
输出
Shell
00000000: 48 65 6C 6C 6F 2C 20 48  65 78 64 75 6D 70 21 0A   Hello, Hexdump!.

ASCII边栏将不可打印的字节替换为.;文件末尾的换行符显示为0A和右侧的点号。

宽度和分组

传递--width N来更改每行的字节数。分组仍然将行分成两半(N/2)以保持视觉锚定。

运行
Shell
$ zig run hexdump.zig -- --width 8 sample.txt
输出
Shell
00000000: 48 65 6C 6C  6F 2C 20 48   Hello, H
00000008: 65 78 64 75  6D 70 21 0A   exdump!.

行格式化程序填充十六进制和ASCII区域,以便列在最后一行对齐,其中字节可能无法填满完整宽度。

注意事项

  • 避免使用已弃用的I/O接口;此示例使用File.writer加上Io.Writer缓冲区,并调用end()来刷新并设置最终位置。
  • 十六进制格式化保持简单——除了偏移量外没有-C风格的索引列。扩展格式化程序是一个简单的后续练习。
  • 参数生命周期很重要:如果在使用cli.path之前释放args,请复制路径字符串。

练习

  • 添加--group N来控制额外空格的位置(当前为N = width/2)。
  • 支持--offset 0xNN以从非零基地址开始地址。
  • 包括每行的右侧十六进制校验和和最终页脚(例如,总字节数)。

替代方案与边缘情况

  • 大文件:代码以固定大小的块流式传输并组装行;调整缓冲区大小以匹配你的I/O环境。
  • 非ASCII编码:ASCII边栏故意保持简陋。对于UTF-8感知,你需要一个更仔细的渲染器;参见unicode.zig
  • 二进制管道:当未提供路径时从stdin读取;如果你想支持管道,请相应地调整打开/循环。

Help make this chapter better.

Found a typo, rough edge, or missing explanation? Open an issue or propose a small improvement on GitHub.