String(format:) にバックスラッシュを含んだ C 文字列を渡すと円マークに変換される問題

## :x: String(format:)

日本語環境の macOS では Foundation.framework の `NSString(format:)` およびこれを内部的に呼ぶ `String(format:)` で `%s` フォーマット指定子を利用して C 文字列を変換すると、バックスラッシュ (= `\` = `0x5C`) が円マーク (= `¥` = `0xA5`) になります。

```swift
print("\\".withCString { String(format: "%s", $0) })
// => ¥
```

フォーマット指定子を利用せずテンプレートにリテラルの `\` を書いた場合は変換されません。

```swift
print("\\".withCString { String(format: "\\%s", $0) })
// => \¥
```

ちなみに `\` の ASCII コードポイントを直接指定しても同じく `¥` に変換されます。

```swift
print("\u{5C}".withCString { String(format: "%s", $0) })
// => ¥
```

いずれも英語環境では問題なく `\` になります。

~~この挙動は [`NSString(format:)` のリファレンス](https://developer.apple.com/documentation/foundation/nsstring/1407827-init) や [String Programming Guide for Core Foundation](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFStrings/formatSpecifiers.html#//apple_ref/doc/uid/TP40004265) などには記載がありません。~~

[String Programming Guide](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Strings/Articles/formatSpecifiers.html#//apple_ref/doc/uid/TP40004265-SW4) には

> Because the %s specifier causes the characters to be interpreted in the system default encoding, the results can be variable, especially with right-to-left languages.

という記載があり、`%s` 指定子はシステムの default encoding の影響を受けると明記されています。
システムの default encoding は以下のコードで確認できます。
私の環境では以下のようになりました。

```swift
print(String.defaultCStringEncoding) // => Japanese (MacOS)
```

`Japanese (MacOS)` とは `CFStringEncodings.macJapanese` を指しています。

```swift
assert(String.defaultCStringEncoding.rawValue == CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.macJapanese.rawValue)))
```

MacJapanese は Shift_JIS の拡張なので、`0x5C` は ASCII と互換性がなく、`¥` となっています。
これをプログラム側から変更することはできません。

> https://developer.apple.com/documentation/foundation/nsstring/1410091-defaultcstringencoding
> The default C-string encoding is determined from system information and can’t be changed programmatically for an individual process.

これを変更するには、プログラム起動時に環境変数 `__CFSTRING_TEXT_ENCODING` の値を変更してあげる必要があります。
起動後にプログラム自身が環境変数を変更しても更新はされません。

```sh
$ cat main.swift
print("\\".withCString { String(format: "%s", $0) })
$ __CFSTRING_TEXT_ENCODING=0x1F5:0x08000100:0xE swift
\
```

試していませんが、おそらく MacJapanese だけでなく、`0x5C` が ASCII と互換性のない文字コード (e.g. KS X 1001) では同じ問題が起こりそうです。
プログラム側からユーザーの環境を制御することはできないので、ユーザーからの入力を `String(format: "%s")` に渡さないようにしなくてはなりません。

## :o: String(utf8String:)

これは `NSString(format:`) 特有の事象であるようで、`NSString(cString:encoding:)` や `NSString(utf8String:)` では発生しないようです。

```swift
print("\\".withCString { String(utf8CString: $0)! })
// => \

print("\\".withCString { String(cString: $0, encoding: .utf8)! })
// => \
```

## :o: printf()

また、似た機能の C の関数群 `printf` ファミリーでは同じ現象は発生しません。

```c
#include <stdio.h>

int main(int argc, char **argv) {
    printf("%s", "\\");
    // => \
    return 0;
}
```

## :x: String(format:locale:)

`String(format:)` にはロケールを設定できるので、システムロケールの問題かと思い POSIX ロケールを明示的にセットしてみましたが、効果はありませんでした。
一応 `setlocale()` もしてみましたが、効果はなさそうです。

```swift
let locale = Locale(identifier: "C")
print("\\".withCString { String(format: "%s", locale: locale, $0) })
// => ¥

setlocale(LC_ALL, "C");
print("\\".withCString { String(format: "%s", $0) })
// => ¥
```

## まとめ

文字列のフォーマットに `String(format:)` を使用した場合は、うまい回避方法が見つけられていません。
扱う文字列が ASCII だけで良い場合は `snprintf` など C 言語の層で変換を行ってから `String` に変換するのが良いでしょう。
もし ASCII 外の文字列を扱う場合は `String.replacingOccurrences(of:with:)` などを用いて、 `¥` を `\` に変換してあげる必要があるかもしれません。