对三行样本执行 import,读完、解析、关闭后输出 IMPORTED\t3\n。但“打开成功”不保证“读取成功”:在本机 Linux,open my $input, '<:raw', '.../fixtures' 对目录可成功,第一次 <$input> 随后返回 undef,$! 是 Is a directory。原来的 while (<$input>) 把这一回当 EOF,甚至可能输出 IMPORTED\t0。这不是解析错误,而是 I/O 错误被错误地翻译为有效空文件。

现在逐次 readline 前清 $!;若返回 undef,先检查 $!,有错误则停止并诊断,只有没有错误才视为 EOF。读完仍检查 close,不能将关闭失败当作成功。针对目录的反例实测 STDOUT 0 字节、STDERR cannot read input: Is a directory\n、退出 2;普通合成文件正例返回 0、STDERR 0 字节。目录的这一行为只在冻结 Linux 环境测过,别的平台不能由此推定。

1
2
3
4
5
6
$! = 0;
my $bytes = <$input>;
if (!defined $bytes) {
die "cannot read input: $!\n" if $!;
last;
}

练习:把一个不存在的路径传给 import,错误发生在读循环还是 open?发生在 open;证据里区分 cannot open input 和 cannot read input,两者均不得返回有效导入计数。参考 Perl 5.38.2 pod/perlfunc.pod 中 open/readline/close,以及 pod/perlvar.pod 的 $!;命令和断言见 examples/perl-language/evidence/26-31.md。

五字段入口的读取故障

上面的 IMPORTED 输出属于旧四字段 bin/logtool.pl。五字段 bin/logtool 的 import 需要输出路径和拒绝清单,成功计数是 INPUT、ACCEPTED、REJECTED。两个入口都必须先区分 I/O 失败和合法空输入;输出字段改变不能改变这一原则。

新 Course.pm 用 sysread 读原始字节块。返回正整数表示得到这些字节,返回 0 表示 EOF,返回 undef 表示失败;判断顺序不能写成“假值一律结束”。原始句柄避免在底层隐式解码,读取完成后才按行边界做 UTF-8 解码。每次块读取最多 4096 字节,行长和总量分别受限;块边界可能落在中文字符的三个字节中间,因此不能对每个块独立 decode。

读取循环正常结束还需显式 close。只依赖词法句柄离开作用域时关闭,会失去在业务成功前检查 close 返回值的机会。close 返回假时,本次导入没有成功结果;调用者只能收到异常,不能收到半个 accepted 数组。输出端的 close 更需单独检查,因为缓冲写入可能到关闭时才报告失败。

course-input-faults.t 在独立 Perl 进程中,于加载模块前覆盖 CORE::GLOBAL::sysread 和 close,分别制造 EIO。四个断言判断读取失败不能当 EOF、关闭失败不能当成功、缺文件在 open 阶段失败、后续正常输入仍得到 5 行。覆盖只在测试进程存在,生产模块没有“测试开关”。这类受控注入用于证明分支和传播规则;实际文件系统的所有失败方式仍需平台探针。

[PATTERN] 资源边界保留 open、read、close 三次独立判断,捕获失败阶段及原始原因。文件存在、打开成功、读完和关闭成功是不同事实。

练习:若 sysread 返回 undef,已经缓冲的两条记录能否作为成功 import 发布?不能。I/O 失败表示输入完整性未知,整个调用失败;普通格式坏行才按课程契约进入拒绝清单,二者不能合并处理。运行 tools/replay.pl --case all 可重放读取注入和正常输入,原始输出位于当前 completion/evidence/。

来源:Perl 5.38.2 sysread、close、PerlIO。