長い話を短く

長い話を短く

JSON文字列は、任意のファイルパス、プロセス名、コマンドライン引数、およびより一般的なC文字列(さまざまな文字セットでエンコードされたテキストを含むか、テキストがまったくないことを意味する可能性があります)を直接表すことはできません。

たとえば、多くのutil-linux、Linux LVM、systemdutilities curl、GNU parallel、ripgrep、sqlite3、tree、さまざまなFreeBSDユーティリティとその--libxo=jsonオプション...JSON形式でデータを出力し、プログラムで「信頼できるように」解析できます。

ただし、出力したい一部の文字列(ファイル名など)にUTF-8でエンコードされていないテキストが含まれていると、すべてが中断されるように見えます。

この場合、ユーティリティ間でさまざまな種類の動作が表示されます。

  • デコードできないバイトを次に置き換える文字を置き換えるたとえば、(たとえば?exiftoolまたはU + FFFD(�)、または時には元に戻せない方法でいくつかのエンコード形式を使用します(例"\\x80"column
  • "json-string"fromから[65, 234]バイト配列injournalctlまたはfrom{"text":"foo"}から{"bytes":"base64-encoded"}inなど、別の表現に切り替えることですrg
  • これを間違った方法で処理する人。 curl
  • ほとんどは、有効なUTF-8をそのまま形成しないバイト、つまり無効なUTF-8を含むJSON文字列をダンプします。

ほとんどのutil-linuxユーティリティは最後のカテゴリに属します。例えばlsfd:

$ sh -c 'lsfd -Joname -p "$$" --filter "(ASSOC == \"3\")"' 3> $'\x80' | sed -n l
{$
   "lsfd": [$
      {$
         "name": "/home/chazelas/tmp/\200"$
      }$
   ]$
}$

これは誤ったUTF-8を出力するので、間違ったJSONを出力することを意味します。

この出力は厳密には有効ではありませんが、まだ明確で理論的には後処理が可能です。

ところで、JSON処理ユーティリティをたくさん確認してみましたが、そのどれも処理できませんでした。彼らは次のいずれかを行います。

  • デコードエラーによるエラー
  • このバイトをU + FFFDに置き換えます。
  • 悲劇的な方法で失敗しました

何か抜けたような気がします。間違いなくこの形式を選択するときは、この点を考慮する必要がありますか?

長い話を短く

だから私の質問は次のようになります

  • UTF-8でエンコードされた文字列を誤って含むJSON形式の名前はありますか(一部のバイト値> = 0x80は有効なUTF-8エンコーディング文字の一部を形成しません)?
  • perlこの形式を確実に処理できるツールやプログラミング言語モジュール(好ましくは他の人にも開いています)はありますか?
  • jqまたは、JSON処理ユーティリティ(たとえば、、、json_xs...)で処理できるように、型を有効なJSONに変換するか、その逆に変換することもできます。mlr好ましくは、情報を失うことなく有効なJSON文字列を保存する方法で処理しますか?

追加情報

以下は私が直接調査した内容です。これは役に立つと考えられるサポートデータです。これは簡単なダンプであり、コマンドはzsh構文を取り、Debian不安定システム(およびいくつかのFreeBSD 12.4-RELEASE-p5)で実行されます。混乱させて申し訳ありません。

lsfd(およびほとんどのutil-linuxユーティリティ):生データを出力します。

$ sh -c 'lsfd -Joname -p "$$" --filter "(ASSOC == \"3\")"' 3> $'\x80' | sed -n l
{$
   "lsfd": [$
      {$
         "name": "/home/chazelas/\200"$
      }$
   ]$
}$

列: 明示的にエスケープされない:

$ printf '%s\n' $'St\351phane' 'St\xe9phane' $'a\0b' | column -JC name=firstname
{
   "table": [
      {
         "firstname": "St\\xe9phane"
      },{
         "firstname": "St\\xe9phane"
      },{
         "firstname": "a"
      }
   ]
}

latin1(または全バイト範囲をカバーする単一バイト文字セット)を使用してロケールに切り替えると、元の形式を取得するのに役立ちます。

$ printf '%s\n' $'St\351phane' $'St\ue9phane' | LC_ALL=C.iso88591 column -JC name=firstname  | sed -n l
{$
   "table": [$
      {$
         "firstname": "St\351phane"$
      },{$
         "firstname": "St\303\251phane"$
      }$
   ]$
}$

Journalctl:バイト配列:

$ logger $'St\xe9phane'
$ journalctl -r -o json | jq 'select(._COMM == "logger").MESSAGE'
[
  83,
  116,
  233,
  112,
  104,
  97,
  110,
  101
]

カール:フェイク

$ printf '%s\r\n' 'HTTP/1.0 200' $'Test: St\xe9phane' '' |  socat -u - tcp-listen:8000,reuseaddr &
$ curl -w '%{header_json}' http://localhost:8000
{"test":["St\uffffffe9phane"]
}

\Uこれで、Unicodeがコードポイントに制限されていることを除いて言葉になります\U0010FFFF


cvtsudoers: オリジナル

$ printf 'Defaults secure_path="/home/St\351phane/bin"' | cvtsudoers -f json  | sed -n l
{$
    "Defaults": [$
        {$
            "Options": [$
                { "secure_path": "/home/St\351phane/bin" }$
            ]$
        }$
    ]$
}$

dmesg:オリジナル

$ printf 'St\351phane\n' | sudo tee /dev/kmsg
$ sudo dmesg -J | sed -n /phane/l
         "msg": "St\351phane"$

iproute2:プリミティブでバグがあります

少なくともip link制御文字0x1 .. 0x1f(そのうちの一部だけがインターフェース名には許可されていません)も生の出力であるため、JSONでは無効です。

$ ifname=$'\1\xe9'
$ sudo ip link add name $ifname type dummy
$ sudo ip link add name $ifname type dummy

(2回追加されました!最初は名前が変更されました__。)

$ ip l
[...]
14: __: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 12:22:77:40:6f:8c brd ff:ff:ff:ff:ff:ff
15: �: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 12:22:77:40:6f:8c brd ff:ff:ff:ff:ff:ff
$ ip -j l | sed -n l
[...]
dcast":"ff:ff:ff:ff:ff:ff"},{"ifindex":14,"ifname":"__","flags":["BRO\
ADCAST","NOARP"],"mtu":1500,"qdisc":"noop","operstate":"DOWN","linkmo\
de":"DEFAULT","group":"default","txqlen":1000,"link_type":"ether","ad\
dress":"12:22:77:40:6f:8c","broadcast":"ff:ff:ff:ff:ff:ff"},{"ifindex\
":15,"ifname":"\001\351","flags":["BROADCAST","NOARP"],"mtu":1500,"qd\
isc":"noop","operstate":"DOWN","linkmode":"DEFAULT","group":"default"\
,"txqlen":1000,"link_type":"ether","address":"12:22:77:40:6f:8c","bro\
adcast":"ff:ff:ff:ff:ff:ff"}]$
$ ip -V
ip utility, iproute2-6.5.0, libbpf 1.2.2

Exiftool:バイトを次に変更しますか?

$ exiftool -j $'St\xe9phane.txt'
[{
  "SourceFile": "St?phane.txt",
  "ExifToolVersion": 12.65,
  "FileName": "St?phane.txt",
  "Directory": ".",
  "FileSize": "0 bytes",
  "FileModifyDate": "2023:09:30 10:04:21+01:00",
  "FileAccessDate": "2023:09:30 10:04:26+01:00",
  "FileInodeChangeDate": "2023:09:30 10:04:21+01:00",
  "FilePermissions": "-rw-r--r--",
  "Error": "File is empty"
}]

lsar:バイト値をtarのUnicodeコードポイントとして解釈します。

$ tar cf f.tar $'St\xe9phane.txt' $'St\ue9phane.txt'
$ lsar --json f.tar| grep FileNa
      "XADFileName": "Stéphane.txt",
      "XADFileName": "Stéphane.txt",

zipの場合:URIエンコーディング

$ bsdtar --format=zip -cf a.zip St$'\351'phane.txt Stéphane.txt
$ lsar --json a.zip | grep FileNa
      "XADFileName": "St%e9phane.txt",
      "XADFileName": "Stéphane.txt",

lsipc:オリジナル

$ ln -s /usr/lib/firefox-esr/firefox-esr $'St\xe9phane'
$ ./$'St\xe9phane' -new-instance
$ lsipc -mJ | grep -a phane | sed -n l
         "command": "./St\351phane -new-instance"$
         "command": "./St\351phane -new-instance"$

GNUパラレル:オリジナル

$ parallel --results -.json echo {} ::: $'\xe9' | sed -n l
{ "Seq": 1, "Host": ":", "Starttime": 1696068481.231, "JobRuntime": 0\
.001, "Send": 0, "Receive": 2, "Exitval": 0, "Signal": 0, "Command": \
"echo '\351'", "V": [ "\351" ], "Stdout": "\351\\u000a", "Stderr": ""\
 }$

rg: "text": "..." から "bytes": "base64..." に切り替えます。

$ echo $'St\ue9phane' | rg --json '.*'
{"type":"begin","data":{"path":{"text":"<stdin>"}}}
{"type":"match","data":{"path":{"text":"<stdin>"},"lines":{"text":"Stéphane\n"},"line_number":1,"absolute_offset":0,"submatches":[{"match":{"text":"Stéphane"},"start":0,"end":9}]}}
{"type":"end","data":{"path":{"text":"<stdin>"},"binary_offset":null,"stats":{"elapsed":{"secs":0,"nanos":137546,"human":"0.000138s"},"searches":1,"searches_with_match":1,"bytes_searched":10,"bytes_printed":235,"matched_lines":1,"matches":1}}}
{"data":{"elapsed_total":{"human":"0.002445s","nanos":2445402,"secs":0},"stats":{"bytes_printed":235,"bytes_searched":10,"elapsed":{"human":"0.000138s","nanos":137546,"secs":0},"matched_lines":1,"matches":1,"searches":1,"searches_with_match":1}},"type":"summary"}
$ echo $'St\xe9phane' | LC_ALL=C rg --json '.*'
{"type":"begin","data":{"path":{"text":"<stdin>"}}}
{"type":"match","data":{"path":{"text":"<stdin>"},"lines":{"bytes":"U3TpcGhhbmUK"},"line_number":1,"absolute_offset":0,"submatches":[{"match":{"text":"St"},"start":0,"end":2},{"match":{"text":"phane"},"start":3,"end":8}]}}
{"type":"end","data":{"path":{"text":"<stdin>"},"binary_offset":null,"stats":{"elapsed":{"secs":0,"nanos":121361,"human":"0.000121s"},"searches":1,"searches_with_match":1,"bytes_searched":9,"bytes_printed":275,"matched_lines":1,"matches":2}}}
{"data":{"elapsed_total":{"human":"0.002471s","nanos":2471435,"secs":0},"stats":{"bytes_printed":275,"bytes_searched":9,"elapsed":{"human":"0.000121s","nanos":121361,"secs":0},"matched_lines":1,"matches":2,"searches":1,"searches_with_match":1}},"type":"summary"}

興味深い「x-カスタム」エンコーディング:

$ echo $'St\xe9\xeaphane' | rg -E x-user-defined --json '.*'  | jq -a .data.lines.text
null
"St\uf7e9\uf7eaphane\n"
null
null

ASCII 以外のテキスト専用領域の文字を含みます。https://www.w3.org/International/docs/encoding/#x-ユーザー定義


sqlite3:オリジナル

$ sqlite3 -json a.sqlite3 'select * from a' | sed -n l
[{"a":"a"},$
{"a":"\351"}]$

木: オリジナル

$ tree -J | sed -n l
[$
  {"type":"directory","name":".","contents":[$
    {"type":"file","name":"\355\240\200\355\260\200"},$
    {"type":"file","name":"a.zip"},$
    {"type":"file","name":"f.tar"},$
    {"type":"file","name":"St\303\251phane.txt"},$
    {"type":"link","name":"St\351phane","target":"/usr/lib/firefox-es\
r/firefox-esr"},$
    {"type":"file","name":"St\351phane.txt"}$
  ]}$
,$
  {"type":"report","directories":1,"files":6}$
]$

lslock:オリジナル

$ lslocks --json | sed -n /phane/l
         "path": "/home/chazelas/1/St\351phane.txt"$

@raf~の生皮:生の

$ rh -j | sed -n l
[...]
{"path":"./St\351phane", "name":"St\351phane", "start":".", "depth":1\
[...]

FreeBSD ps --libxo=json: エスケープ:

$ sh -c 'sleep 1000; exit' $'\xe9' &
$ ps --libxo=json -o args -p $!
{"process-information": {"process": [{"arguments":"sh -c sleep 1000; exit \\M-i"}]}
}
$ sh -c 'sleep 1000; exit' '\M-i' &
$ ps --libxo=json -o args -p $!
{"process-information": {"process": [{"arguments":"sh -c sleep 1000; exit \\\\M-i"}]}
}

FreeBSD wc --libxo=json: 生

$ wc --libxo=json  $'\xe9' | LC_ALL=C sed -n l
{"wc": {"file": [{"lines":10,"words":10,"characters":21,"filename":"\351"}]}$
}$

また、見ることができますこのバグレポートは次のとおりです。sesutil map --libxo報告者と開発者の両方が出力がUTF-8であることを期待しています。そしてlibxo議論の紹介コーディングの問題は議論されますが、実際の結論は導出されません。


JSON処理ツール

jsec:受け入れるがU + FFFDに変換

$ jsesc  -j $'\xe9'
"\uFFFD"

jq:受け入れてU + FFFDに変換しますが、偽:

$ print '"a\351b"' | jq -a .
"a\ufffd"
$ print '"a\351bc"' | jq -a .
"a\ufffdbc"

gojq:エラーもありません

$ echo '"\xe9ab"' | gojq -j . | uconv -x hex
\uFFFD\u0061\u0062

json_pp:受け入れ、U + FFFDに変換

$ print '"a\351b"' | json_pp -json_opt ascii,pretty
"a\ufffdb"

json_xs:同じ

$ print '"a\351b"' | json_xs | uconv -x hex
\u0022\u0061\uFFFD\u0062\u0022\u000A

-e同じ

$ print '"\351"' | PERL_UNICODE= json_xs -t none -e 'printf "%x\n", ord($_)'
fffd

JSON:エラー

$ printf '{"file":"St\351phane"}' | jshon -e file -u
json read error: line 1 column 11: unable to decode byte 0xe9 near '"St'

json5: 承諾、U+FFFDに変換

$ echo '"\xe9"' | json5 | uconv -x hex
\u0022\uFFFD\u0022

jc:エラー

$ echo 'St\xe9phane' | jc --ls
jc:  Error - ls parser could not parse the input data.
             If this is the correct parser, try setting the locale to C (LC_ALL=C).
             For details use the -d or -dd option. Use "jc -h --ls" for help.

mlr: 承諾, U+FFFDに変換

$ echo '{"f":"St\xe9phane"}' | mlr --json cat | sed -n l
[$
{$
  "f": "St\357\277\275phane"$
}$
]$

vd: エラー

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 1: invalid continuation byte

JSON::分析:エラー

$ echo '"\xe9"'| perl -MJSON::Parse=parse_json -l -0777 -ne 'print parse_json($_)'
JSON error at line 1, byte 3/4: Unexpected character '"' parsing string starting from byte 1: expecting bytes in range 80-bf: 'x80-\xbf' at -e line 1, <> chunk 1.

ジョー:間違っている

$ echo '\xe9' | jo -a
jo: json.c:1209: emit_string: Assertion `utf8_validate(str)' failed.
zsh: done             echo '\xe9' |
zsh: IOT instruction  jo -a

base64が利用可能です。

$ echo '\xe9' | jo a=@-
jo: json.c:1209: emit_string: Assertion `utf8_validate(str)' failed.
zsh: done             echo '\xe9' |
zsh: IOT instruction  jo a=@-
$ echo '\xe9' | jo a=%-
{"a":"6Qo="}

jsed:受け入れてU + FFFDに変換

$ echo '{"a":"\xe9"}' | ./jsed get --path a | uconv -x hex
\uFFFD%

zgrep -li json ${(s[:])^"$(man -w)"}/man[18]*/*(N)1 JSONを処理できるコマンドのリストについては、参考資料を参照してください。

²C文字列はJSON文字列とは異なり、NULを含めることができないため、任意のJSON文字列を表すことはできません。

3処理が問題になる可能性がありますが、2つの文字列を連結すると、有効な文字で終わり、いくつかの仮定が壊れる可能性があります。

答え1

このトピックについて調査するほど、そのようなlsfd行動が正しくないという確信が高まりました。RFC 8259 8.1説明する:

クローズドエコシステムに属さないシステム間で交換されるJSONテキストは、UTF-8を使用してエンコードする必要があります。

問題があるという事実は、これらの出力が閉じたエコシステムにカプセル化されていないため、JSONテキストがRFC 8259に違反していることを意味します。

私の考えでは、個々のプロジェクトのバグレポートを開いて問題を知らせるのが良い習慣です。その後、問題を処理するかどうか、およびどのように処理するかを決定することは、プロジェクトメンテナンスに依存します。

私はこれがプロジェクトメンテナンスの観点から解決可能でなければならないと思います。lsfdLC_CTYPE / LANG環境変数を尊重し、入力がそのロケールから来ると仮定し、それをUTF-8に変換できます。


UTF-8でエンコードされた文字列を誤って含むJSON形式の名前はありますか(一部のバイト値> = 0x80は有効なUTF-8エンコーディング文字の一部を形成しません)?

答え:「壊れた」

冗談ですが少しだけです。実際にここで何が起こるのかは、JSONがUTF-8で書かれていますが、すべての入力もUTF-8であることを確認するためのチェックが行われないことです。技術的にあなたが見ているのはミックス文字セットは、非標準の文字セットでエンコードされたjsonファイルではありません。

この形式を確実に処理できるツールやプログラミング言語モジュール(Perlを好むが他のユーザーにも開いている)はありますか?

一部は、入力が完全にLATIN-1という(間違った)仮定に基づく特別な処理など、特定の場合に満足のいく結果を得ることがあります。これは、JSONのすべての特殊文字が単一のUTF-8バイトコード値(128以下のASKII文字コードと同じ)であるために機能します。多くのシングルバイト文字セットの最初の127バイトコードは同じ意味を持ちます。

しかし、明らかにしておくと、私たちはUTF-8でなければなりませんが、UTF-8ではなく出力を処理することについて話しています。したがって、ここでの解決策はデザインではなく運に依存します!これは「未定義の動作」に似ています。


特定の文字セットに対する回避策がある可能性があります。これらの回避策が成功するには、文字セットがすべてのバイトコードをUnicodeにマッピングする必要があること、または実際にマッピングされていないバイトコードが使用されることを確認する必要があります。文字セットはUTF-8、特に1バイトの文字コードも共有する必要があります[]{}:""''\

LATIN-1は私が知っている唯一のものですが、これはUnicodeにLATIN-1という特別なブロックがあるので特に機能します。ラテン語-1サプリメント。これにより、バイト値をUnicodeコードポイントにコピーするだけでLATIN-1をUnicodeに変換できます。

ただし、同様のcp1252にはUnicodeにマッピングできないスペースがあり、ソリューションが急速に中断されます。


この破損した動作を処理するために私が提案する方法はPython3を使用することです。 Python3は、特にテキストを表すために使用されるバイトシーケンスと文字列の違いを理解しています。

Python3から生のバイトを読み取り、選択したエンコーディングを想定して文字列にデコードできます。

import sys
import json

data = sys.stdin.buffer.read()
string_data = data.decode("LATIN1")
decoded_structure = json.loads(string_data)

その後、主に演算子を使用してjsonを操作できます[]。例:latin-1を使用するjsonの場合Ç

{
   "lsfd": [
      {
         "name": "/home/chazelas/tmp/Ç"
      }
   ]
}

次のコマンドを使用して名前を印刷できます。

import sys
import json

data = sys.stdin.buffer.read()
string_data = data.decode("LATIN1")
decoded_structure = json.loads(string_data)
print(decoded_structure["lsfd"]["name"].encode("LATIN1"))

このアプローチを使用すると、データを文字列として処理する前にバイトとして処理することもできます。これは状況が非常に汚れているときに便利です。たとえば、入力を次のようにエンコードする必要があります。cp1252ただし、cp1252に無効なバイトが含まれています。

import sys
import json

data = sys.stdin.buffer.read()
data = data.replace(b'\x90', b'\\x90')
data = data.replace(b'\x9D', b'\\x9D')
string_data = data.decode("cp1252")
decoded_structure = json.loads(string_data)
print(decoded_structure["lsfd"]["name"].encode("cp1252"))

答え2

JSONの文字列をテキストとして処理する必要がない場合、可能な(完全に満足のいくものではない)アプローチは、JSON処理ツール(jqmlr...)の入力を前処理iconv -f latin1 -t utf-8し、出力をに後処理するiconv -f utf-8 -t latinことです。 0x80より大きいすべてのバイトを対応するUnicodeコードポイントを持つ文字に変換します。つまり、入力をlatin1でエンコードされたものとして扱います。

$ exec 3> $'\x80\xff'
$ ls -ld "$(lsfd -Jp "$$" | jq -r '.lsfd[]|select(.assoc=="3").name')"
ls: cannot access '/home/chazelas/1/��': No such file or directory

jqこのバイトを U+FFFD に変換するので動作しませんが、次のようになります。

$ ls -ld "$(lsfd -Jp "$$" | iconv -fl1  | jq -r '.lsfd[]|select(.assoc=="3").name' | iconv -tl1)"
-rw-r--r-- 1 chazelas chazelas 0 Sep 30 15:51 '/home/chazelas/tmp/'$'\200\377'

働く今、競合が発生する方法はいくつかあります。

  • このプロセス中に文字列の長さ(バイト単位と文字単位)が変更されるため、実行する長さの確認は正確ではありません(JSON文字列の文字長はファイル名の長さと一致しますが)。
  • JSON処理ツールが文字をエスケープしないことを確認する必要があります(例:inを\uxxxx使用しない)。それ以外の場合、文字は後でバイトに変換されません。-ajq
  • さらに、JSON処理ツールは、コードポイントが0x80を超える文字を含む新しい文字列を生成しないでください。生成する場合は、デュアルエンコーディングが必要です。例:彼らが通り過ぎたくjq -r '"Fichier trouvé : " + .file'ないならば。jq -r '"Fichier trouvé : " + .file'iconv -f utf-8 -t latin1
  • テキストベースの検証や操作(文字クラスのテスト、並べ替えなど)は効果がありません。

latin1の代わりにHTMLで使用できる文字セットを使用すると、x-user-definedこれらの問題の一部を回避できます。 0x80より大きいすべてのバイトがプライベート領域の連続文字にマップされるため、アルファ/スペースとして誤って分類されず、一部の/...範囲に含まれますが、[a-z]私が知っている限り[0-9]//両方はその文字セットをサポートしません。iconvuconvrecode

latin1を使用すると、利点はコードポイントでバイト値を確認できることです。たとえば、名前にバイト0x80を含むオープンファイルを見つけるには、次のようにします。

$ lsfd -Jp "$$" | iconv -fl1 -tu8 | jq -r '.lsfd[]|select(.name|contains("\u0080"))' | iconv -fu8 -tl1
{
  "command": "zsh",
  "pid": 8127,
  "user": "chazelas",
  "assoc": "3",
  "mode": "-w-",
  "type": "REG",
  "source": "0:38",
  "mntid": 42,
  "inode": 2501864,
  "name": "/home/chazelas/tmp/��"
}

(��これは私のUTF-8ターミナルエミュレータがここでこれらのバイトをレンダリングする方法ですiconv

binaryksh(またはすぐにpipefail標準オプションをサポートするすべてのシェル、つまり以下を除くほとんどのシェル)を定義できます。dash

#! /bin/ksh -
set -o pipefail
iconv -f latin1 -t utf-8 |
  "$@" |
  iconv -f utf-8 -t latin1

次に、次のように使用します。

lsfd -J |
  binary jq -j '
    .lsfd[] |
    select(
      .assoc=="1" and
      .type=="REG" and
      (.name|match("[^\u0000-\u007f]"))
    ) | (.name + "\u0000")' |
  LC_ALL=C sort -zu |
  xargs -r0 ls -ldU --

パスにビット8が設定されている(0x7f / 127より大きい)バイトを含むすべてのプロセスの標準出力で開かれた一般ファイルを一覧表示します。


同様に、オブジェクト指向インタフェースを使用するJSONPerlモジュール(およびその基本およびJSON::XS実装)は、テキスト自体をデコード/エンコードすることなく、すでにデコードされたテキストで機能します。JSON::PPデフォルトでは、PERL_UNICODE環境変数が設定されていない限り、入出力はlatin1にデコード/エンコードされます。

json_xs/などのユーティリティは、json_ppUTF-8で明示的にデコード/エンコードするためのコマンドラインツールとしてこれらのモジュールを公開しますが、これらのモジュールを直接使用する場合は、その手順をスキップしてlatin1で作業できます。

$ exec 3> $'\x80\xff'
$ lsfd -Jp "$$" | perl -MJSON -l -0777 -ne '
   $_ = JSON->new->decode($_);
   print $_->{name} for grep {$_->{assoc} == 3} @{$_->{lsfd}}' |
   sed -n l
/home/chazelas/tmp/\200\377$

それらは、生成されたJSONがlatin1外部文字でエンコードされたときにU + 0000 .. U + 00FFの範囲を表現して表現できるようにするために、latin1同様の明示的なフラグも持っています。このフラグがない場合、これらの文字は警告メッセージとともにUTF-8でエンコードされます。ascii\uxxxx

journalctllatin1を使用すると、メッセージ表現を比較的簡単に処理できます。これは[1, 2, 3]、これらのバイト値を対応するUnicodeコードポイントを持つ文字に変換するためです(そしてlatin1でエンコードすると正しいバイトが得られます)。

上記の制限のいくつかはここでも適用されます。これはiconv内部で実行されるコマンドと同じです。perlあるいは、より正確に言うと、バイトをutf-8とutf-8に渡すことなく、バイトから同じ値を持つ文字に直接移動します。キャラクターの足跡。

$ logger $'St\xe9phane'
$ journalctl --since today -o json | perl -MJSON -MData::Dumper -lne '
   BEGIN{$j = JSON->new}
   $j->incr_parse($_);
   while ($obj = $j->incr_parse) {
     $msg = $obj->{MESSAGE};
     # handle array of integer representation
     $msg = join "", map(chr, @$msg) if ref $msg eq "ARRAY";
     print $msg
   }' |
   sed -n '/phane/l'
St\351phane$

このレンズを通して、私たちはすべての質問に答えることができます。

  • この形式の名前は何ですか?これは、UTF-8でエンコードされたJSONではなくlatin1でエンコードされたJSONまたはASCIIの上位セットであるシングルバイト文字セットであり、使用することを決定した全バイト範囲をカバーするUnicodeマッピングです(入力を次のように解釈して生成します。 )。出力)。

    UTF-8に比べて利点は、すべてのバイトシーケンスがこれらのエンコーディングで有効なテキストであるため、これらのユーティリティで生成されたすべてのUnixファイル名、コマンド引数、C文字列をtextで表すために使用できることです。

    JSON RFC は、UTF-8 以外のエンコーディングの使用を厳しく禁止しません。閉鎖された生態系。相互運用性には何の影響もありません。以前のバージョンのRFCはこれについてもっと寛大でした。。型を生成するツールが実行する操作を正しく文書化すると、これはバグではないと見なすことができます。

  • この形式を処理できるツールは何ですか?すべての文字セット(UTF-8だけでなく)からJSONをデコード/エンコードできるすべて。上記のように、現在のバージョンはjelloする。JSON// JSON::XSPerl モジュールはJSON::PPlatin1 を明示的にサポートします。

  • 一般的なJSONユーティリティが処理できるように、この形式をどのように前処理できますか? Latin1(または他のシングルバイト文字セット)からUTF-8に再コーディングして入力を前処理し、再コーディングして出力を後処理します。

答え3

python3(少なくとも私がテストしているバージョン3.11.5)では、そのモジュールはJSONモジュールのjsonように動作します。perl入力/出力はPythonモジュールの外部でデコード/エンコードされます。この場合、ロケールの文字セットに従って文字エンコーディングをオーバーライドできます。環境PYTHONIOENCODING変数

CおよびC.UTF-8ロケール(UTF-8を文字セットとして使用する他のロケールとは異なり)は、入力/出力がUTF-8にデコード/エンコードされる特別な場合に見えます(Cロケールの文字セットは常にASCII )有効なUTF-8の一部を形成しないバイトは、0xDC80から0xDCFFの範囲のコードポイントを使用してデコードされます(このコードポイントはUTF-16置換の後半に使用されるコードポイントに属します)ペアなので、有効な文字コードポイントではないので、ここで使用しても安全です)。

設定により、ロケールを変更せずに同じ効果を得ることができます。

PYTHONIOENCODING=utf-8:surrogateescape

その後、完全にUTF-8でエンコードされているがUTF-8以外の文字列を含むことができるJSONを処理できます。

$ printf '"\xe9"' | PYTHONIOENCODING=utf-8:surrogateescape  python3 -c '
import json, sys; _ = json.load(sys.stdin); print(hex(ord(_)))'
0xdce9

0xe9 バイトは文字 0xdce9 でデコードされます。

$ printf '"\xe9"' | PYTHONIOENCODING=utf-8:surrogateescape  python3 -c '
import json, sys; _ = json.load(sys.stdin); print(_)' | od -An -vtx1
 e9 0a

0xdce9は、出力時に0xe9バイトに再エンコードされます。

出力処理の例lsfd:

$ exec 3> $'\x80\xff'
$ lsfd -Jp "$$" | PYTHONIOENCODING=utf-8:surrogateescape python3 -c '
import json, sys
_ = json.load(sys.stdin)
for e in _["lsfd"]:
  if e["assoc"] == "3":
    print(e["name"])' | sed -n l
/home/chazelas/tmp/\200\377$

注:出力から一部のJSONを生成し、ensure_ascii=FalseUTF-8にデコードできないバイトを渡す必要がある場合は、次のようになります。

$ printf '"\xe9"' | PYTHONIOENCODING=utf-8:surrogateescape python3 -c '
import json, sys; _ = json.load(sys.stdin); print(json.dumps(_))'
"\udce9"

外部世界のほとんどのものはpythonこれを拒否します。

$ printf '"\xe9"' | PYTHONIOENCODING=utf-8:surrogateescape python3 -c '
import json, sys
_ = json.load(sys.stdin)
print(json.dumps(_, ensure_ascii=False))' | sed -n l
"\351"$

また、質問で述べたように、UTF-8でエンコードされた文字列が文字の途中に分割された結果である2つのJSON文字列がある場合、JSONで接続すると、エンコードされるまでそのバイトシーケンスを文字にマージしません。 UTF-8に戻る:

$ printf '{"a":"St\xc3","b":"\xa9phane"}' | PYTHONIOENCODING=utf-8:surrogateescape python3 -c '
import json, sys
_ = json.load(sys.stdin)
myname = _["a"] + _["b"]; print(len(myname), myname)'
9 Stéphane

私の名前は出力でうまく再構成されていますが、文字が再構築された代わりに含まれてmynameエスケープされ\udcc3ているので、長さが間違っていることを確認してください。\udca9\u00e9

IOエンコーディングを使用して、次の手順を実行して強制的にマージできますencodedecode

$ printf '{"a":"St\xc3","b":"\xa9phane"}' |
   PYTHONIOENCODING=utf-8:surrogateescape python3 -c '
import json,sys
_ = json.load(sys.stdin)
myname = (_["a"] + _["b"]).encode(sys.stdout.encoding,sys.stdout.errors).decode(sys.stdout.encoding,sys.stdout.errors)
print(len(myname), myname)'
8 Stéphane

perlいずれにせよ、その文字セットを使用するロケールから呼び出すことも、latin1でエンコード/デコードすることもできますPYTHONIOENCODING=latin1

vd(visidata)はで書かれていますが、python3入力がstdinから来る場合を考慮していないようで、$PYTHONIOENCODINGCまたはC.UTF-8ロケールで代理エスケープを実行していないようです(参照)この問題--encoding=latin1)、バージョン2.5以降で呼び出します(ここでその質問変更された)またはlatin1文字セットを使用するロケールで動作しているように見えるので、次のことができます。

lsfd -J | binary jq .lsfd | LC_CTYPE=C.iso88591 vd -f json

出力にUTF-8でエンコードされたテキスト以外のコマンドまたはファイル名がある場合、視覚lsfd効果は競合しません。lsfd -J

JSONファイルをファイルパスとして引数として渡すと、デフォルトでオプションに基づいて入力をデコードし、--encoding ロケールの出力文字セットを尊重するようです。--encoding-errorsutf-8surrogateescape

rcしたがって、ksh、zsh、bash(または他の構文を使用するes、)などのプロセス置換をサポートするシェルでは、akanga次のことができます。

vd -f json <(lsfd -J | binary jq .lsfd)

しかし、非正規ファイル(パイプなど)の場合、ランダムに失敗することがあることがわかりました。別の問題)。 json perl行()でフォーマットを使用する方がjsonl効果的です。

vd -f jsonl <(lsfd -J | binary jq -c '.lsfd[]')

または、パイプの代わりに一時ファイルを使用して、=(...)zsh(または現在のバージョンと同様に(...|psub -f)zsh)でプロセス置換形式を使用します。fish(...|psub)

vd -f json =(lsfd -J | binary jq .lsfd)

関連情報