APFSの秘密を解く
前回予告で今回は

なぜこうなってしまうのか。その秘密はファイル名にあります。
#!/usr/bin/env perl
use strict;
use warnings;
use Encode;
use feature 'say';
use utf8;
binmode STDOUT, ':utf8';
sub listdir {
my $dn = shift;
opendir my $dh, $dn or die "$dn: $!";
my @fn = map { decode_utf8($_) }
grep !/\A\./, readdir $dh;
closedir $dh;
@fn;
}
my $dir = shift or die "usage: $0 dirname";
my $fn_nfc = "\x{304b}\x{3070}\x{3093}.txt"; # かばん
my $fn_nfd = "\x{304b}\x{306f}\x{3099}\x{3093}.txt";
eval {
my $path = "$dir/$fn_nfc";
open my $fh, '>:utf8', $path
or die "$path:$!";
say $fh "食べないでくださーい";
close $fh;
};
say $@ if $@;
eval {
my $path = "$dir/$fn_nfd";
open my $fh, '<:utf8', $path
or die "$path:$!";
say <$fh>;
close $fh;
};
say $@ if $@;
eval {
my $path = "$dir/$fn_nfd";
open my $fh, '>>:utf8', $path
or die "$path:$!";
say $fh "食べないよー";
close $fh;
};
say $@ if $@;
eval {
my $path = "$dir/$fn_nfc";
open my $fh, '<:utf8', $path
or die "$path:$!";
say <$fh>;
close $fh;
};
say $@ if $@;
for (listdir($dir)) {
say "$_:", encode 'ascii', $_, Encode::FB_PERLQQ;
}
unlink "$dir/$fn_nfc","$dir/$fn_nfd"
Perlで書かれたリスト1の検証コードでHFS+の空ディレクトリを指定して実行すると、
食べないでくださーい 食べないでくださーい 食べないよー かばん.txt:\x{304b}\x{306f}\x{3099}\x{3093}.txt
ところがAPFSだとこうなるのです。
/Volumes/apfs/utf8test/test.d/かばん.txt:Nosuch file or directory at test.pl line 31. 食べないでくださーい かばん.txt:\x{304b}\x{306f}\x{3099}\x{3093}.txt かばん.txt:\x{304b}\x{3070}\x{3093}.txt
いったい何が起きているのでしょう?
HFS+では\x{304b}\x{3070}\x{3093}.txt
と\x{304b}\x{306f}\x{3099}\x{3093}.txt
は同じファイル名として扱われていますが、
そしてSwiftも、\u{304b}\u{3070}\u{3093}
と\u{304b}\u{306f}\u{3099}\u{3093}
を等しいとみなしているのです。
1> "\u{304b}\u{3070}\u{3093}" == "\u{304b}\u{306f}\u{3099}\u{3093}" $R0: Bool = true
文字列が単なるバイト列だった前世紀には驚きの結果ですが、==
を実装しているのは、
SwiftのUnicode原理主義ぶりは、==
にとどまりません。先のPerlスクリプトをFoundationを使って移植した検証コード
#!/usr/bin/env swift
// utility functions and methods
#if os(Linux)
import Glibc
#else
import Darwin
#endif
import Foundation
func listDir(_ path:String)->[String] {
let fm = FileManager.default
do {
let items = try fm
.contentsOfDirectory(atPath:path)
return items
} catch let e {
fatalError("\(e)")
}
}
func slurpFile(_ path:String) throws ->String {
do {
let str = try String(
contentsOfFile:path,
encoding:.utf8
)
return str
} catch let e {
throw e
}
}
extension String {
func append(path: String) throws {
let fm = FileManager.default
if fm.isWritableFile(atPath:path) {
do {
let url = URL(fileURLWithPath:path)
let fh = try FileHandle(
forWritingTo: url
)
_ = fh.seekToEndOfFile()
_ = fh.write(self.data(using:.utf8)!)
} catch let e {
throw e
}
} else {
let created = fm.createFile(
atPath:path,
contents:self.data(using:.utf8)!,
attributes:nil
)
if !created {
throw NSError(
domain:
"failed to create \"\(path)\"",
code:500
)
}
}
}
}
// main part
let args = CommandLine.arguments
if args.count < 2 {
fatalError("\(args[0]) directory")
}
let dir = args[1]
let fn_nfc = "\u{304b}\u{3070}\u{3093}.txt" // かばん
let fn_nfd = "\u{304b}\u{306f}\u{3099}\u{3093}.txt"
do {
let path = "\(dir)/\(fn_nfc)"
try "食べないでくださーい\n".append(path:path)
} catch let e {
print("\(#line):\(e)")
}
do {
let path = "\(dir)/\(fn_nfd)"
try print(slurpFile(path))
} catch let e {
print("\(#line):\(e)")
}
do {
let path = "\(dir)/\(fn_nfd)"
try "食べないよー\n".append(path:path)
} catch let e {
print("\(#line):\(e)")
}
do {
let path = "\(dir)/\(fn_nfc)"
try print(slurpFile(path))
} catch let e {
print("\(#line):\(e)")
}
for item in listDir(dir) {
print("\(item):", Array(item.unicodeScalars))
}
let fm = FileManager.default
for item in [fn_nfc, fn_nfd] {
let path = "\(dir)/\(item)"
if fm.fileExists(atPath:path) {
try fm.removeItem(atPath:path)
}
}
ただしそうなるのはmacOSの場合。Linux上ではPerlと同じように振る舞います。
てんでんばらばらちんぷんかんぷんまとまらない?
なんともややこしそうですが、
SwiftでString
をNFCにするには、import Foundation
してから.precomposedStringWithCanonicalMapping
にアクセスするだけです。Swift 3.
import Foundation
extension String {
var nfc:String {
return self.precomposedStringWithCanonicalMapping
}
var nfd:String {
return self.decomposedStringWithCanonicalMapping
}
var nfkc:String {
return self.precomposedStringWithCompatibilityMapping
}
var nfkd:String {
return self.decomposedStringWithCompatibilityMapping
}
}
これで"\u{304b}\u{306f}\u{3099}\u{3093}".nfc
とすればかばんは3文字で収まります。
もうひとつ念のために、===
を次のように定義しておくのです。
func ===(_ lhs:String, _ rhs:String) -> Bool {
return Array(lhs.utf8) == Array(rhs.utf8)
}
そうすると、
let ue9 = "\u{E9}" // é let u65u301 = "\u{65}\u{301}" // é + ́ ue9 == u65u301 // true ue9 === u65u301 // false ue9.nfd === u65u301 // true ue9 === u64u301.nfc // true
Perlに慣れた筆者から見ると、
次号予告
ところで==は、
let a:Any = 1
let b:Any = 1
ここでa == b
としても、error: binary ope rator '==' cannot be applied to two 'Any' operands
と怒られてしまいます。Equatable
プロトコルに準拠している型だけが等しさを享受できるのです。
プロトコル? 準拠? 次回はいよいよSwift最大の特長であるプロトコルを、
本誌最新号をチェック!
Software Design 2022年9月号
2022年8月18日発売
B5判/192ページ
定価1,342円
(本体1,220円+税10%)
- 第1特集
MySQL アプリ開発者の必修5科目
不意なトラブルに困らないためのRDB基礎知識 - 第2特集
「知りたい」「使いたい」「発信したい」をかなえる
OSSソースコードリーディングのススメ - 特別企画
企業のシステムを支えるOSとエコシステムの全貌
[特別企画]Red Hat Enterprise Linux 9最新ガイド - 短期連載
今さら聞けないSSH
[前編]リモートログインとコマンドの実行 - 短期連載
MySQLで学ぶ文字コード
[最終回]文字コードのハマりどころTips集 - 短期連載
新生「Ansible」徹底解説
[4]Playbookの実行環境(基礎編)