Node.jsでPHPのセッションを読むというようなことをやっていた。あんまり筋の良いことではないし、当然同じことをやっている人は少なく、一応PHPの直列化した文字列を復号するパッケージは無いわけではないが片手で数えるほどで、大なり小なり問題を抱えていた。
問題の形は様々ではあったけど、躓いている箇所は同じで、みんな文字長を計算する箇所で間違えていた。つまりはこういうことだ。PHPの直列化した文字列はs:文字列長(バイト):"文字列"みたいになっているのだけど、PHP側ではUTF-8で文字列長を計算しているものが、JavaScript側ではUTF-16になっているので、長さを相互に調節しなければいけないということだ。
これは問題としては特に難しくないはずだけど、そもそもマルチバイトの文字列のことが想定されていなかったり、その想定されていない状態から無理に対応をしようとして片手落ちになっていたりした。本来PRでも投げるべきなのだろうけど、いくつかパッケージを試した時点でそもそもこの方法で対応するのは良くないかもと思ってやめたので、備忘録として書いている。
もうちょっと具体的に書いておくと、UTF-8ではUnicodeの符号位置を表すのはアスキーの場合1byte(これは完全にアスキーコードと同じ)、ウムラウト(Äとか)なんかの記号の場合2byte、日本語の標準的な文字の場合3byte、絵文字(🍰)や外字なんかの場合4byteみたいに可変長で文字を表している。これに対してJavaScriptの内部エンコード(これはJavaScriptのソースコードを記述してる文字コードとは関係ない)であるUTF-16の場合、2byteないし4byte(サロゲートペア)で表すことになる。
サロゲートペアの文字列はJavaScript上では二文字のように見える。たとえば、
"🍰".length は2を返すし、"🍰".charCodeAt(0)は55356、"🍰".charCodeAt(1)は57200を返すし、これを前半部・後半部とかだけ切り出してしまうと読めない文字に化けてしまう。
で、PHPで直列化されたものをJavaScript側で読むコードを書くと、サロゲートペア以外は一文字として読んでいくような処理になる。つまり最初の格納種別と、長さを読んで、その後長さ分の文字列を切り出すわけだけど、そのときに読んだ文字がUTF-8で何byteに相当するのかということをまず計算しなくちゃいけない。これは前出のString.charCodeAtとかString.codePointAtで調べれば良い。その上で、更に今読んだ一文字がサロゲートペアなら、その後半部(次の添字の文字)はうまいことスキップするような処理を入れてやればいい。
最終的に私はNode.jsで処理するのをやめて、phpで標準入力から入ってきた直列化文字列をそのままJSONエンコードを通して標準出力するだけの数行のプログラムを書き、Node.jsのプログラムからspawnで呼び出すようにした。あまりまともなやり方ではないかもしれないが、今回の仕事ではそれで十分だった。プログラムに色々な人が居るならば、私は綺麗に溶接してヤスリをかけ塗装するより、ダクトテープでぐるぐる巻きにするタイプであろうと思う。