しょんぼり技術メモ

まいにちがしょんぼり

ファイルを開く際のモード指定の罠

いやぁハマった。

RubyでFile.open()する際のモードの話。結論から言うと、"r+"が万能で、"w+"はすでにファイルがあるときは0バイトにされ、"a+"は末尾にしか書けない。

リファレンスはこれ。読んだつもりだったけど理解が不十分だったらしい。

やりたいこと

1MiBのすでに存在するファイルのうち、128バイト目から136バイト目までを"DeAdBeEf"に書き換えたい。

$ dd if=/dev/zero of=1MiB.dat bs=1M count=1
$ hexdump -C 1MiB.dat
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00100000

r+の動作

"READ"というイメージに反して、ちゃんと書き込める。

irb(main):001:0> fp=File.open("1MiB.dat", "r+")
=> #<File:1MiB.dat>
irb(main):002:0> fp.pos
=> 0
irb(main):003:0> fp.seek(128)
=> 0
irb(main):004:0> fp.pos
=> 128
irb(main):005:0> fp.write("DeAdBeEf")
=> 8
irb(main):006:0> fp.pos
=> 136
irb(main):007:0> fp.close
=> nil


$ hexdump -C 1MiB.dat
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000080  44 65 41 64 42 65 45 66  00 00 00 00 00 00 00 00  |DeAdBeEf........|
00000090  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00100000 ### ←ファイルはぴったり1MiBある

w+の動作

うっかり間違えると悲劇。どんな大きなファイルでも、途端に0バイトにしてしまう。

irb(main):001:0> fp=File.open("1MiB.dat", "w+")
=> #<File:1MiB.dat>
irb(main):002:0> fp.pos
=> 0
irb(main):003:0> fp.seek(128)
=> 0
irb(main):004:0> fp.pos
=> 128
irb(main):005:0> fp.write("DeAdBeEf")
=> 8
irb(main):006:0> fp.pos
=> 136
irb(main):007:0> fp.close
=> nil


$ hexdump -C 1MiB.dat
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000080  44 65 41 64 42 65 45 66                           |DeAdBeEf|
00000088 ### ←0x88バイトでファイルが終わっている

a+の動作

一番のくせ者。"APPEND"は、末尾以外にデータを書き込もうとしても、書き込めたように見えて、末尾に書かれてしまう。
どうやらRubyの問題ではなく、Cのfopen()レベルの問題らしい。書き込み動作が行われる前に、必ず末尾へのseekが入るようだ。man fopenを参照。

irb(main):001:0> fp=File.open("1MiB.dat", "a+")
=> #<File:1MiB.dat>
irb(main):002:0> fp.pos
=> 0
irb(main):003:0> fp.seek(128)
=> 0
irb(main):004:0> fp.pos
=> 128     ### ←うn。
irb(main):005:0> fp.write("DeAdBeEf")
=> 8
irb(main):006:0> fp.pos
=> 1048584  ### ←うn!?
irb(main):007:0> fp.close
=> nil
irb(main):008:0> exit


$ hexdump -C 1MiB.dat
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00100000  44 65 41 64 42 65 45 66                           |DeAdBeEf|
00100008 ### ←ファイルサイズが8バイト増えている