長い話を短く

長い話を短く

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"))

おすすめ記事