From 017a3ce0011545104c319ae41be318ec61750553 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 11 Nov 2026 09:18:40 +0100 Subject: [PATCH 01/21] ci: revert importlib-metadata to 8.7.1 to fix Semgrep install (#6064) The Dependabot bump to 10.1.0 (#6059) conflicts with the pinned opentelemetry-api 1.37.0 (importlib-metadata<8.8.0), so the Semgrep job failed with ResolutionImpossible. Ignore importlib-metadata in Dependabot since semgrep pins it indirectly. Signed-off-by: Niels Lohmann --- .github/dependabot.yml | 1 + cmake/requirements/requirements-semgrep.txt | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 727429823..9f1f7dd8d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -67,3 +67,4 @@ updates: - dependency-name: mcp - dependency-name: boltons - dependency-name: "opentelemetry-*" + - dependency-name: importlib-metadata diff --git a/cmake/requirements/requirements-semgrep.txt b/cmake/requirements/requirements-semgrep.txt index 0ed7fad86..ba66d32c4 100644 --- a/cmake/requirements/requirements-semgrep.txt +++ b/cmake/requirements/requirements-semgrep.txt @@ -391,9 +391,9 @@ httpx-sse==0.4.3 \ idna==3.20 \ --hash=sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44 \ --hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c -importlib-metadata==10.1.0 \ - --hash=sha256:82b04047fa09792c1a8cfeddfe08bec79e4b13c29c8469b0d3949860f61017e8 \ - --hash=sha256:cf45ce66a929c64e994185a809c04840d0501db17e663810d993d0f4829265c7 +importlib-metadata==8.7.1 \ + --hash=sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb \ + --hash=sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151 jsonschema==4.25.1 \ --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \ --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85 From 93d2dd2c69a3d70a1c4afb9a3604ba82e3dc23ef Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 11 Nov 2026 22:31:15 +0100 Subject: [PATCH 02/21] ci: pin Semgrep version without hashes (#6072) Signed-off-by: Niels Lohmann --- .github/dependabot.yml | 8 - .github/workflows/semgrep.yml | 6 +- cmake/requirements/requirements-semgrep.txt | 992 +------------------- 3 files changed, 6 insertions(+), 1000 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9f1f7dd8d..5336d0df5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -60,11 +60,3 @@ updates: interval: daily cooldown: default-days: 7 - ignore: - # semgrep pins these packages to exact or compatible-release versions (see its METADATA); - # bumping them on their own makes the Semgrep job fail with ResolutionImpossible, so - # they only move together with a semgrep release - - dependency-name: mcp - - dependency-name: boltons - - dependency-name: "opentelemetry-*" - - dependency-name: importlib-metadata diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 3703646a9..30f8b931a 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -52,8 +52,12 @@ jobs: # the old action fell back to, running community rules with no token. # SEMGREP_APP_TOKEN is still passed through so registry auth works if a # token is ever added. + # The version is pinned, but not by hash: Semgrep pins its own dependencies + # to exact or compatible-release versions, so a hash-pinned lock file broke + # on almost every Dependabot bump of a transitive dependency. Dependabot + # only bumps the semgrep line; pip resolves the rest. - name: Install Semgrep - run: python3 -m pip install --user --require-hashes -r cmake/requirements/requirements-semgrep.txt + run: python3 -m pip install --user -r cmake/requirements/requirements-semgrep.txt # `semgrep scan --sarif` always exits 0 even with findings; continue-on-error # is a safety net so the SARIF upload still runs if the scan itself errors. diff --git a/cmake/requirements/requirements-semgrep.txt b/cmake/requirements/requirements-semgrep.txt index ba66d32c4..028580449 100644 --- a/cmake/requirements/requirements-semgrep.txt +++ b/cmake/requirements/requirements-semgrep.txt @@ -1,991 +1 @@ -# Semgrep and all its dependencies, pinned by hash for the Semgrep workflow. A pip install with -# hashes must list every dependency, so this file was generated with -# uv pip compile requirements.in --generate-hashes --python-version 3.14 \ -# --python-platform x86_64-manylinux_2_39 --no-header --no-annotate -# from a requirements.in containing only "semgrep==1.184.0". Dependabot keeps it current. -annotated-types==0.8.0 \ - --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ - --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 -anyio==4.16.0 \ - --hash=sha256:59bb03251907983d1603d27c3087ddb1e93299ea2163adcdba37899a4fb12ffb \ - --hash=sha256:c66ae634e0b8228396b801741beb3294326d4b6102f7bba4041c0dfb58c29026 -attrs==26.1.0 \ - --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ - --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 -boltons==21.0.0 \ - --hash=sha256:65e70a79a731a7fe6e98592ecfb5ccf2115873d01dbc576079874629e5c90f13 \ - --hash=sha256:b9bb7b58b2b420bbe11a6025fdef6d3e5edc9f76a42fb467afe7ca212ef9948b -bracex==3.2.2 \ - --hash=sha256:284d59c4656e5a0421ff691402074bd5847049d50410535764a2c522a47095bc \ - --hash=sha256:d18976160b3ba01a57d5c53a5c8c3aa2506efa9e0a3a62467251f82b8f563fde -certifi==2026.10.23 \ - --hash=sha256:a9895e6e2758b371e0545e321a3a8fd1fcfd9829ada8824a936dfb16aaaf36cd \ - --hash=sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1 -cffi==2.1.1 \ - --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ - --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ - --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ - --hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \ - --hash=sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6 \ - --hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \ - --hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \ - --hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \ - --hash=sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9 \ - --hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \ - --hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \ - --hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \ - --hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \ - --hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \ - --hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \ - --hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \ - --hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \ - --hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \ - --hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \ - --hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \ - --hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \ - --hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \ - --hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \ - --hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \ - --hash=sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659 \ - --hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \ - --hash=sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004 \ - --hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \ - --hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \ - --hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \ - --hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \ - --hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \ - --hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \ - --hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \ - --hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \ - --hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \ - --hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \ - --hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \ - --hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \ - --hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \ - --hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \ - --hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \ - --hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \ - --hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \ - --hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \ - --hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \ - --hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \ - --hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \ - --hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \ - --hash=sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9 \ - --hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \ - --hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \ - --hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \ - --hash=sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41 \ - --hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \ - --hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \ - --hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \ - --hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \ - --hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \ - --hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \ - --hash=sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98 \ - --hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \ - --hash=sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1 \ - --hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \ - --hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \ - --hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \ - --hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \ - --hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \ - --hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \ - --hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \ - --hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \ - --hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \ - --hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \ - --hash=sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be \ - --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \ - --hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \ - --hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \ - --hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \ - --hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \ - --hash=sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b \ - --hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \ - --hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \ - --hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \ - --hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \ - --hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \ - --hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \ - --hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \ - --hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \ - --hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \ - --hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \ - --hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \ - --hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \ - --hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \ - --hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \ - --hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \ - --hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \ - --hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \ - --hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \ - --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ - --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 -charset-normalizer==3.5.2 \ - --hash=sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e \ - --hash=sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf \ - --hash=sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5 \ - --hash=sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56 \ - --hash=sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26 \ - --hash=sha256:0891b9d3903c5571c03771ca669a4b0ec5618ca722a5c957d3d29cd4e5062848 \ - --hash=sha256:0c951d5e6dd9c2ff60609476752bee49da4206adde960ebc247766937f72e718 \ - --hash=sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93 \ - --hash=sha256:114e4d0c92d618409ed82a99e22b5c5e768fe995f2973f78265f4524f49d4640 \ - --hash=sha256:11912e4bb14baae7c5d8791aa55ba0a3a03ec6729073307b0f57270abaa713d3 \ - --hash=sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875 \ - --hash=sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e \ - --hash=sha256:1461ac396c4fdb983a675f20aa555624f0ee18ac83d832b9244ffff3d8055275 \ - --hash=sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204 \ - --hash=sha256:15bb4005af6320d259dc7593ca84a38d7fe06a421dbcf7b910ae23979101e787 \ - --hash=sha256:15c44f7edfd477b06f517a5cc317fc1707edb9de2c865f43d4b6513907473234 \ - --hash=sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3 \ - --hash=sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98 \ - --hash=sha256:195c26fb65950f8fce54e26349852b7bdd7c5f120aeefbcc440b8a20faaed4a3 \ - --hash=sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187 \ - --hash=sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d \ - --hash=sha256:1bc0baf5ef96b6ede57d47f4b8fe4d9d84019c3bfcbeb20a41edc6a6ee341f1f \ - --hash=sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7 \ - --hash=sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011 \ - --hash=sha256:211d5a3eb6af8f513b8d4ca19a8c1b7accab1b5f0d3175f9826b03c1a920dc1f \ - --hash=sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869 \ - --hash=sha256:254eb48b9fa5ee9898a3c445825a1f340fe53712a098904b39b0bddba8ea3cb1 \ - --hash=sha256:2625388c6c754520c37abaf3b41eb34d1cc4a373f457898f08606c8e362b891d \ - --hash=sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847 \ - --hash=sha256:28a15fdad492a99b6eccfaaed66ef3f74050680545ea61ec8b2f4c538f1f1320 \ - --hash=sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9 \ - --hash=sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93 \ - --hash=sha256:2b7b3bbfb4fe8ef40600792d762fbaa9057559f9d3fad209525b7a22b99e91fd \ - --hash=sha256:2c9ad19a6cfcd5ea5c0d41161d22f9df1dcc277e9bef2751391334546a314c00 \ - --hash=sha256:2cc961b171b3f3440f410489ab3573e86aea8736134ebbb40ea1338b7f0831bc \ - --hash=sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0 \ - --hash=sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09 \ - --hash=sha256:304d5463e65a35d7bb0850550e0780395395f6fcf452f04db7d5ca7cecc425ac \ - --hash=sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621 \ - --hash=sha256:30fcd120b732aa79317f08dee04d7de0847822e4cf7ee0e9f445bb958832252c \ - --hash=sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8 \ - --hash=sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a \ - --hash=sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51 \ - --hash=sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0 \ - --hash=sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef \ - --hash=sha256:3d14b50de6bf4d0edf857a9386836846f982b8f524e188e2e68b96d702bcf4aa \ - --hash=sha256:3d21b8b13c7592db2ac5e544a6d83187b995257472b0c9e8351b6d507ae37ed6 \ - --hash=sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649 \ - --hash=sha256:3ddacd27458c45bdacd6bd6db644bfb730efbf9e830310186e3045c9c5be8fb2 \ - --hash=sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229 \ - --hash=sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e \ - --hash=sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd \ - --hash=sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115 \ - --hash=sha256:447441e76ec720b15e64418d32e092297340387053047c7c694f579efb0ee1d9 \ - --hash=sha256:4495c5002a7b28557e7e222e77e0b661183e432b7d6d2e788101e3f240e05b8c \ - --hash=sha256:44bd4fbb29dfbeba60e7d2bd000c59e4b21ddb3cc53912b14048d37092706d7c \ - --hash=sha256:4685902cf26edf013ed7a3da0f426ebba7a00ebb9541386d835afbf002c11cab \ - --hash=sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253 \ - --hash=sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995 \ - --hash=sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438 \ - --hash=sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0 \ - --hash=sha256:50e3adfb96fc189eb27b1cf62d3b598b89b4bb0420d93a3d3e42e137409011be \ - --hash=sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b \ - --hash=sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7 \ - --hash=sha256:55ea99acb17b9325618de155a0cd6a2e8f5d10be008113e1d433bbb58db543b2 \ - --hash=sha256:56bc200a365efb37383b7852e4cc5898d3b2da5987289b543956cf8cad71018a \ - --hash=sha256:588461c2e8384d309bd63e5826019b6977bc66d629b99ac8737bb795d7b2cb5a \ - --hash=sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a \ - --hash=sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c \ - --hash=sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5 \ - --hash=sha256:59f63901b0031c3136cf64704dcb21de0bbae62ce2c9529bc39d27665463de37 \ - --hash=sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e \ - --hash=sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4 \ - --hash=sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800 \ - --hash=sha256:6045373d5a89a5ec71afde535db987ca28e76dfa276c2d4c818265b375d4b055 \ - --hash=sha256:619799369eeef6366ed3e8755a5670f4f2f0fb6b30a0fd7264dc0fdc2357058e \ - --hash=sha256:62588a277bfb59def052abd940703fa35107152bf479781a878617d60faf8fb5 \ - --hash=sha256:62603db9a7caa0802eaa28c1c46fecd7b3a263a774069c24c3c28c302448721c \ - --hash=sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b \ - --hash=sha256:68eb192d85ab8e5f6ec69c2bc6ac0179fbf04a5ac1569d12fbef74883fe102d0 \ - --hash=sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80 \ - --hash=sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a \ - --hash=sha256:7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4 \ - --hash=sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2 \ - --hash=sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58 \ - --hash=sha256:75a3ceed0724d625d64b86ca20aba182e4df462e04c2414fc941c0f523f06aac \ - --hash=sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc \ - --hash=sha256:78456a747de8dc58360ffa581f30a002baf5aa28cb262536545e91f113ed7639 \ - --hash=sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf \ - --hash=sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d \ - --hash=sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f \ - --hash=sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c \ - --hash=sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc \ - --hash=sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4 \ - --hash=sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253 \ - --hash=sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade \ - --hash=sha256:849df64e889b2e17230d58410a03dba311a65b163508fd33679b2b737d4b7858 \ - --hash=sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26 \ - --hash=sha256:87e50a3e7cb90af586b6c5faf23e302a970415ac73bd7bd90a515a04b427ef96 \ - --hash=sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8 \ - --hash=sha256:8a893cc101149f80a653f82062ebc95b34525a2614382e1da5458fe7c6997249 \ - --hash=sha256:8b2bfab86aa71ae13aa41a6a26aab338e0db2b8bc75434b05aea89e011ff35a4 \ - --hash=sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13 \ - --hash=sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1 \ - --hash=sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03 \ - --hash=sha256:93223adc95033dd47133a46ccfc316a0139176fd79085762e27202ec56018f03 \ - --hash=sha256:9373ad13ef0d2c0fb761e04e55bfdee5a08b52cef2c882c8fbe9935b1517152e \ - --hash=sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364 \ - --hash=sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4 \ - --hash=sha256:9bde855991b7e362c146535e3136a50bfaffc0487d38b33ca7e5edefc6e23849 \ - --hash=sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0 \ - --hash=sha256:9cf9b1a857e25c4baceeb3624e92a56df3668f398c4acba74e174d81fb4d1d3a \ - --hash=sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036 \ - --hash=sha256:a090bb2c68df85450502e3e20d665e3a5af9c65a84d6508ed477badd49166fd3 \ - --hash=sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21 \ - --hash=sha256:a19a731138fc27d5682277d3b9df22855cea1239bce7fcec5f78f42ef2d1f3c3 \ - --hash=sha256:a66c3bc5ab1f0ff2164fc9965ddd611ff0802173f4b9d24554c563f6ab7e1d6e \ - --hash=sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413 \ - --hash=sha256:a89012d6d5476ee112d20d998570ed58df2260a852afb1758809cd6900411d21 \ - --hash=sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346 \ - --hash=sha256:b6856554c4f44d79fc2307d5768854310a8f0096e501c75637542c82292b0429 \ - --hash=sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685 \ - --hash=sha256:b736353c0a625bbd5fcec108576e2385db3496f4f771f785ff32e108d3c3bc45 \ - --hash=sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f \ - --hash=sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c \ - --hash=sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d \ - --hash=sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad \ - --hash=sha256:bd16aabe4a02a297c23417aa17ac6299dbd8c49f673bcd645b4929b11f5a4400 \ - --hash=sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb \ - --hash=sha256:c6708715abcf3c73b99508253e961a9967f02fe536532834149574eda6de0d1c \ - --hash=sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc \ - --hash=sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c \ - --hash=sha256:c9790464842f85f437dbbb54417eda1e0e6bfc52dd8d22d6fd1c994b73b2dc74 \ - --hash=sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf \ - --hash=sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604 \ - --hash=sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f \ - --hash=sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105 \ - --hash=sha256:d2374b62878abb00cd8309b32af6c0b715cd02dec0ca74ef12e5069bdc64144a \ - --hash=sha256:d376bbd28b3a8999db1a103b3b388aee6f1ddeb3e51bc2172993efdcd86e064d \ - --hash=sha256:d4a7319f304a774bed22115bc891618e45f85065ab44ea6acd07d274e750519a \ - --hash=sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1 \ - --hash=sha256:d760fe2a4d7c3b226cb9026d6a842868d52a7901bd98420e1baf14e80da85cf5 \ - --hash=sha256:d913de495d90407cd859d263bee2e5d1a4ed3eb6573c04e70d9ec619a7cbed7f \ - --hash=sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e \ - --hash=sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709 \ - --hash=sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874 \ - --hash=sha256:ddf19c062bea7a0cc80f519243d2c01dd091be0cf952a0750d4ad576709559f5 \ - --hash=sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc \ - --hash=sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95 \ - --hash=sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd \ - --hash=sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0 \ - --hash=sha256:e243bd13217235fc7290c621941c3f5cc8b66e4872495be821d7436ba2fb838d \ - --hash=sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3 \ - --hash=sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c \ - --hash=sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3 \ - --hash=sha256:e80e6c2f55656b4824d72065abb4ddd6a525c74bd78a0aab5d9fc2cf4fb5af50 \ - --hash=sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491 \ - --hash=sha256:ed905975ab14056a2e5eb1c376cb2e1ebc5396baf84163939c518556fccde9f5 \ - --hash=sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5 \ - --hash=sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655 \ - --hash=sha256:ef4fcbf3327382cd4c9f540babd61248208af7b93eec4de397b4d5f58a09e288 \ - --hash=sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd \ - --hash=sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084 \ - --hash=sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d \ - --hash=sha256:f5833ad231be5eb6553de524a70f48d71b2c8563101750531e0b80184e175cd4 \ - --hash=sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915 \ - --hash=sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1 \ - --hash=sha256:fb9e68df06293761f9fe66ade60a9bc6d0f5e42b8acf2939a9158af86ab0e5bd \ - --hash=sha256:fc14a032f813bf5fe624d991960ea83e9715adc27e4c1830a2361eb1d02ac341 \ - --hash=sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424 \ - --hash=sha256:fd1fbe0f116b6e55da77aca2c6ddcddcfac2186cbf78bdebf40fc156efca389d \ - --hash=sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f -click==8.4.2 \ - --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ - --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 -click-option-group==0.5.9 \ - --hash=sha256:ad2599248bd373e2e19bec5407967c3eec1d0d4fc4a5e77b08a0481e75991080 \ - --hash=sha256:f94ed2bc4cf69052e0f29592bd1e771a1789bd7bfc482dd0bc482134aff95823 -colorama==0.4.6 \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 -cryptography==50.0.2 \ - --hash=sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602 \ - --hash=sha256:0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2 \ - --hash=sha256:0ecbc5652bdb6fc9eaf89a7d196e20941adfe812f43bc4ca05d9150496821047 \ - --hash=sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c \ - --hash=sha256:1ba34f04897fcdaa73f74145c25f3ec146fbd56593853e88adc2e811303c5f42 \ - --hash=sha256:241449bf940a5d27309bd317e6f9a2af6932113818bb2b8f5c59ddc7ef16da18 \ - --hash=sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51 \ - --hash=sha256:3dc4fd8058cea1644971207d530e1a03a184a805ffc8ebdddf0599d78a331b81 \ - --hash=sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856 \ - --hash=sha256:4a20ce1e5cb4284a86692fdcba7cb8754185c6b2e5c56fcef3751cf451d3cdc2 \ - --hash=sha256:4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de \ - --hash=sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7 \ - --hash=sha256:58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd \ - --hash=sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2 \ - --hash=sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be \ - --hash=sha256:78198641e5be9521beea5aa782bb551a58068d10e6eb04c9c680c1b69f2e7d45 \ - --hash=sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0 \ - --hash=sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e \ - --hash=sha256:7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c \ - --hash=sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5 \ - --hash=sha256:7b75de3c8b3be1cdb1052747c929440c3eea46c1bc2cb8a6e3a48388e9b7b452 \ - --hash=sha256:7c6d0330c472d96f6a6afe24d80dfdf15176c33096f0a4397ae4c60f3dd3be48 \ - --hash=sha256:828d49b0ff5a0e3975865571c5d91dbbdd0d38d8289b249a163e9425413a5e05 \ - --hash=sha256:84f964e537f916e2cc85199e5a88742e964939b575ac8598b3f9d6cc416cdaf1 \ - --hash=sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93 \ - --hash=sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04 \ - --hash=sha256:8c71ba2cd31fc93748c38e1b613200ff1c2665cbfd5341fe3a61cfde35a1430e \ - --hash=sha256:92e665960f25fcdc73725b9cec7a3824f279ba97a98653afe9ffac2e43668f67 \ - --hash=sha256:94e5e9f108ee10471288214d3d233fbfbb492840a8457eb85178d643ddeb32c7 \ - --hash=sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107 \ - --hash=sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079 \ - --hash=sha256:a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134 \ - --hash=sha256:a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227 \ - --hash=sha256:a9f7355e6fab51f6c369b86fb7571cffa05edee2c2121e0380a37fb9ac1cd5c1 \ - --hash=sha256:ab50ee449bf968271e820086f10a33d101dd060370abc10bcd22279be2656539 \ - --hash=sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e \ - --hash=sha256:b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d \ - --hash=sha256:c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c \ - --hash=sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd \ - --hash=sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020 \ - --hash=sha256:cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd \ - --hash=sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94 \ - --hash=sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a \ - --hash=sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408 \ - --hash=sha256:d8947001be83df1394050758ce0e745dd74fb134eef0a4b5124208dfc3a68c37 \ - --hash=sha256:deb9fde5c60e437ee4821bc9bc39ff31b42135c27e1dc61ef0a629389c1de62e \ - --hash=sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454 \ - --hash=sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c \ - --hash=sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc \ - --hash=sha256:edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37 \ - --hash=sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767 \ - --hash=sha256:eef4c2f3423810b3070ab391f85436d2f8bbfcb286ac15cbc73190b3563b1f1a \ - --hash=sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5 \ - --hash=sha256:f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc \ - --hash=sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67 \ - --hash=sha256:f785f6161f202ab04d8ca194158968798e480ca058943907972da5f12e2881e8 \ - --hash=sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480 \ - --hash=sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb \ - --hash=sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b -exceptiongroup==1.2.2 \ - --hash=sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b \ - --hash=sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc -face==26.0.1 \ - --hash=sha256:8183d94bc248baaea855a9f8445f97a22a9988908e60abddccc6e251da77c4c6 \ - --hash=sha256:ab0a83c37c9789dce658a67a9a80eafaa113c9ec37c5a9d950ff5480542a062d -glom==25.12.0 \ - --hash=sha256:1ae7da88be3693df40ad27bdf57a765a55c075c86c971bcddd67927403eb0069 \ - --hash=sha256:b9f21e77f71a6576a43864e85066b8cc3f0f778d0d50961563f8981377a6dcb1 -googleapis-common-protos==1.76.0 \ - --hash=sha256:59e13344a9f3651aa201a03e2bee815a3bf43a4baf1f9ff8a9b62a840ce25fac \ - --hash=sha256:71db0e44a1d4c2dd024ebad44ca67129cc1e2892e4861df466764749d5ef8642 -h11==0.16.0 \ - --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ - --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 -httpcore==1.0.9 \ - --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ - --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 -httpx==0.28.1 \ - --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ - --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad -httpx-sse==0.4.3 \ - --hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \ - --hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d -idna==3.20 \ - --hash=sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44 \ - --hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c -importlib-metadata==8.7.1 \ - --hash=sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb \ - --hash=sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151 -jsonschema==4.25.1 \ - --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \ - --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85 -jsonschema-specifications==2025.9.1 \ - --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ - --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d -markdown-it-py==4.2.0 \ - --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ - --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a -mcp==1.29.0 \ - --hash=sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36 \ - --hash=sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7 -mdurl==0.1.2 \ - --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ - --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba -opentelemetry-api==1.37.0 \ - --hash=sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7 \ - --hash=sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47 -opentelemetry-exporter-otlp-proto-common==1.37.0 \ - --hash=sha256:53038428449c559b0c564b8d718df3314da387109c4d36bd1b94c9a641b0292e \ - --hash=sha256:c87a1bdd9f41fdc408d9cc9367bb53f8d2602829659f2b90be9f9d79d0bfe62c -opentelemetry-exporter-otlp-proto-http==1.37.0 \ - --hash=sha256:54c42b39945a6cc9d9a2a33decb876eabb9547e0dcb49df090122773447f1aef \ - --hash=sha256:e52e8600f1720d6de298419a802108a8f5afa63c96809ff83becb03f874e44ac -opentelemetry-instrumentation==0.58b0 \ - --hash=sha256:50f97ac03100676c9f7fc28197f8240c7290ca1baa12da8bfbb9a1de4f34cc45 \ - --hash=sha256:df640f3ac715a3e05af145c18f527f4422c6ab6c467e40bd24d2ad75a00cb705 -opentelemetry-instrumentation-requests==0.58b0 \ - --hash=sha256:672a0be0bb5b52bea0c11820b35e27edcf4cd22d34abe4afc59a92a80519f8a8 \ - --hash=sha256:ae9495e6ff64e27bdb839fce91dbb4be56e325139828e8005f875baf41951a2e -opentelemetry-instrumentation-threading==0.58b0 \ - --hash=sha256:eacc072881006aceb5b9b6831bcdce718c67ef6f31ac0b32bd6a23a94d979b4a \ - --hash=sha256:f68c61f77841f9ff6270176f4d496c10addbceacd782af434d705f83e4504862 -opentelemetry-proto==1.37.0 \ - --hash=sha256:30f5c494faf66f77faeaefa35ed4443c5edb3b0aa46dad073ed7210e1a789538 \ - --hash=sha256:8ed8c066ae8828bbf0c39229979bdf583a126981142378a9cbe9d6fd5701c6e2 -opentelemetry-sdk==1.37.0 \ - --hash=sha256:8f3c3c22063e52475c5dbced7209495c2c16723d016d39287dfc215d1771257c \ - --hash=sha256:cc8e089c10953ded765b5ab5669b198bbe0af1b3f89f1007d19acd32dc46dda5 -opentelemetry-semantic-conventions==0.58b0 \ - --hash=sha256:5564905ab1458b96684db1340232729fce3b5375a06e140e8904c78e4f815b28 \ - --hash=sha256:6bd46f51264279c433755767bb44ad00f1c9e2367e1b42af563372c5a6fa0c25 -opentelemetry-util-http==0.58b0 \ - --hash=sha256:6c6b86762ed43025fbd593dc5f700ba0aa3e09711aedc36fd48a13b23d8cb1e7 \ - --hash=sha256:de0154896c3472c6599311c83e0ecee856c4da1b17808d39fdc5cce5312e4d89 -packaging==26.3 \ - --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ - --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c -peewee==3.19.0 \ - --hash=sha256:de220b94766e6008c466e00ce4ba5299b9a832117d9eb36d45d0062f3cfd7417 \ - --hash=sha256:f88292a6f0d7b906cb26bca9c8599b8f4d8920ebd36124400d0cbaaaf915511f -protobuf==6.33.6 \ - --hash=sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326 \ - --hash=sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901 \ - --hash=sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3 \ - --hash=sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a \ - --hash=sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135 \ - --hash=sha256:bd56799fb262994b2c2faa1799693c95cc2e22c62f56fb43af311cae45d26f0e \ - --hash=sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3 \ - --hash=sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2 \ - --hash=sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593 \ - --hash=sha256:f443a394af5ed23672bc6c486be138628fbe5c651ccbc536873d7da23d1868cf -pycparser==3.12 \ - --hash=sha256:a076f3f489b39b43ed13563a1dafe42447445bec2e294154ec5c57fcdda4c85e \ - --hash=sha256:f03387877eab82ce01bbdfd0ba7252ee55a90b1c708d5129e2ce915475cf1db6 -pydantic==2.14.3 \ - --hash=sha256:1054f237f4d4733e5a30e1e2333bd33a2004ab9ab1510bd053a9a31bb48e232d \ - --hash=sha256:3f7b347782ab7e4cf4c798496ae3a82787d9f21ec12676a9c564355eeef8daa8 -pydantic-core==2.50.4 \ - --hash=sha256:0001d6920e3a5d92758cda460d6dc9f918a78ad6d5d092f1c8b5395e6b775356 \ - --hash=sha256:0574866e05f6adeecc520d87e87dd06210c0a51706341f6ef8421572f5a64444 \ - --hash=sha256:09450778e3ab58d3f565899fcfbf6c82926e31af29e28e799d1d50941316e0cb \ - --hash=sha256:0af56dbf8822ba9f4efcbbfdc15478f8b6fb08f1609860cb50aa2cc8476b3c34 \ - --hash=sha256:0b059a7968b1911be5a2d9a8234827ab0bef9d1e465d4b42da525894143550e8 \ - --hash=sha256:0ec20aca2b067369913716847f7dfe5f4dddf16a46965b7a70bbad5defaaba86 \ - --hash=sha256:0f87ae9db5ba8d8de40dcb93982cf8dd544e6552a80d3345af51941cc2146b80 \ - --hash=sha256:1239daaf063e62213ca1f34d96663900bd7510c42a786e843fd328af70e46445 \ - --hash=sha256:13b6dd62b5279f5b9d72f3bf918a35443ee8bd549241d9dc52d79a2c3b6d7016 \ - --hash=sha256:15bece43a02ad543c621c8305e67dedb5f27d378c5ba741e5a2c36523f8ef37f \ - --hash=sha256:16902a147b3da4840612164a5e67947104d63e978f7a57c395877a5e26690b9c \ - --hash=sha256:1afefe44ec042fbabfb4483ea258c04cf75ff4ff328d754f09b321fdaa232aca \ - --hash=sha256:1de7be9a66fc8536770481ab64a05aae5067c725f2b29a09543af00e6a417b97 \ - --hash=sha256:1e20cbde2b6deb33a51d08cd168454dd9044ec7d4474056f4d98d63f51578783 \ - --hash=sha256:2039e323acee9abfec1fb5f661776e8965fe5928f3b7f8431900a774cba5c317 \ - --hash=sha256:219114f8dcdcbe4720d82ef016b1287ce5a0a67d7c69ca61524eb72e82aac344 \ - --hash=sha256:22e874666bdbd96d6bf629d32cd8ab376fedf1fd62585b52a42b9fa9fa6c20b6 \ - --hash=sha256:23a9ba564b2db709ff16eca990992ba95a19069972b34bff87c81d4b473cd755 \ - --hash=sha256:25e7e1080f9f7ce7cd9d114b31470ce912771bd02f38c824721d3c35d312abff \ - --hash=sha256:26cb012c4b8ca9f847bd0ac15978bd3d0f55b050bfbe6c47355edfc7e945029b \ - --hash=sha256:29358a8ca74e7d81158d14e2b10c904bd18773293ae3788716c0626fe0342347 \ - --hash=sha256:2980a701be88f9128a7470189fa3bcda5387f76c298a6f8e000fc29eb2a6f4e7 \ - --hash=sha256:2a60ec2a57ac3ef3767e526e9bf706ec75aa3d79c4ea134d9d4fcf7c62a297a7 \ - --hash=sha256:2f42584e5785385f681163a42506cab0d51295e17599a648396c52a7046c1372 \ - --hash=sha256:2f5cc5bcb94063fbbdfd285a2b302da34af07491337c10bb35de1d5220fa1583 \ - --hash=sha256:2f6767122b37c780bb756a720a4eef2bc6a050a0a1143428834b3cbc89448bd2 \ - --hash=sha256:300753f71c73302e218124baf995f00058e085cd01869bd9610bcbd0bd4500c1 \ - --hash=sha256:30d4dc35bc0eea52202a90b3820445ee0072c820241bfa710a623a078c9b267b \ - --hash=sha256:32a991aebd165f227b7c7c60bb9d66c2c7de9c24308552da264c9938fde0f8d6 \ - --hash=sha256:32d6dda86383645ec428ea64dfc277036f5cbc60a72238eab28b91a3fdb9a113 \ - --hash=sha256:33b21f69540a12c2053cd1d95766d8bd9667a3897f7c712a642de8aa37eb0865 \ - --hash=sha256:39f489ce31272073e6e146f6781690f9c1ee8c126836e6403361ef91154d89fe \ - --hash=sha256:3a48a5254d15a0ad050318bfc7a9566d4ec740118187e8f303e47a6840287eb7 \ - --hash=sha256:3b8ee2589a082739c522c1fe4be227412fcf108b903abb6672ae2f5570558882 \ - --hash=sha256:3de6c2a4df2af0ac9ab4dc1488746fe8b1a90e27955d870d9edda3187d20092b \ - --hash=sha256:3e0b395648c6cb871bceb2906907f255dc876ec425f2d5b024e129985cea8519 \ - --hash=sha256:3e547ceec1da7382f01f9e13963023e53367a71802c88778b21b9645a077cbdf \ - --hash=sha256:41b2cd8b29dbb5a4c9828a70f50d1abfc92011115c813213d3a91205c6f731b3 \ - --hash=sha256:41f4f8ae714e4fea1d635880f8a1af715ecf080eed4d13c5b04a0f3210f9c61a \ - --hash=sha256:474f7e598c7902165662a5d5160d3fce31037890a2975a9871ebf7c05862300b \ - --hash=sha256:477af785d1846e4e96bf56983b92ea7553aaeccdeb8acdaa55c7cee9eda0ae32 \ - --hash=sha256:49eb1241a969a5994db6880c18c012b6cbe2340343d8f529721571806583e500 \ - --hash=sha256:4c29b8f2d6417069bb1f257e6b9113f5d64dc2f554cc200ea90b4fe2c5cdb733 \ - --hash=sha256:4eb15be588407148ef5937dc8d9256a71f6b115e6cddadb7aa20b6ee384680dc \ - --hash=sha256:4eda7e3fdef382b7d4492745988a5baaf6953923a337f8b9dde5f76fa8afde20 \ - --hash=sha256:523466eba8ee10f7480f3c2057563145978011c583130d2cc01fb1e7af808e25 \ - --hash=sha256:57fa0a9490785139f1a4e4f43891830d55dfe4408845c0c9cb5eff4aeaf75f69 \ - --hash=sha256:5b26e5f1793b96cdc60242bf38da86ff24a27a708aa396214cac9b534bf571c7 \ - --hash=sha256:5bc1811ef2b4d4ba63e8e452f6e332b6374bd1d2cbbfdf66826c8cb1f71dd10d \ - --hash=sha256:5c074454dc6d4f6f4ba6561a56bf33a14db7651d882a5a5321ba3ae8868e082d \ - --hash=sha256:5c1013647be741647895c102831dcf79c34209662dbb683df9efb864417ed7d5 \ - --hash=sha256:601fce6cea9ddeaa85329e12e7f351def38d233075f42a3012b9c48071b02941 \ - --hash=sha256:6118e5a5550e36582e19bac1a403711a748f9f18251b054c326d3db02aaa7a18 \ - --hash=sha256:629eae995185540406e3635df5d5cf1d44f19ea240840b835d3f44541ff694d8 \ - --hash=sha256:6348ad12fa741bcc9c2f173e99fbf27cbc038826d8c31a4a7d836eca27878ba2 \ - --hash=sha256:65376dbe1ab4dbc768ca2b72b7c13993562c035f5925fd3bc7ff9c8c585c515d \ - --hash=sha256:6619d49a89a15cd04334f76fec530c155923bfc60da74ff5d815a52629e1aa6c \ - --hash=sha256:664e74848c2f5fc27db8505fe24e1ecd3ac669412913bf73d176916f86cc08e2 \ - --hash=sha256:67f7cc397e1d5556a801d1cba74de0d27d827eb242e6c036c0ca273c745414f1 \ - --hash=sha256:696613a0c0e7b2b35356e389ba4195ce20cfe7a2fc2e992c87d974d973eabec7 \ - --hash=sha256:6c5687b59b6ba0f663c4f2f15470f68507d6b45ccd3a19ee6d224f95cbee70d8 \ - --hash=sha256:6c7d7b0b596319e9ce79ef96ce57a14054a2eac629c0588a0539187a218be799 \ - --hash=sha256:70dbc9f83c41f37a4faa62dae5203853847223a891da446f48ef2efcfd5df806 \ - --hash=sha256:71395a43da5584bb70556f380387e15f845c0dcf76373c1afbfa0025737576ac \ - --hash=sha256:71ade6d2e445839350d589ff99bbddfde16f3f32db28da5c894a09f99c8b2a03 \ - --hash=sha256:71e03d2ea1a221d020cfc5b6cc8bbbedef23a06704c70fcf4d0afb42965b22d1 \ - --hash=sha256:7210398a1607ad607dc73196e575b3148b00a333cbb602ddbe1b60ba54554f43 \ - --hash=sha256:7472c39480ccc11dda69a196d1b7c7a6b48eb6594b77f05d945599d3e47f731c \ - --hash=sha256:7646353a5a7c553c04167aca628e2550b538f492765b03ccf34321a560acc43c \ - --hash=sha256:76e0065998159244ff4c097a6c05ff08019c6fbb6e597a85846e73044f0215b3 \ - --hash=sha256:789a6453e5ffd18a7ab6b8c3a0dd8403053829197ec258ecb90c604c21ffcc99 \ - --hash=sha256:789f88dea57841cdebef392b5d6e90444d767117e9ff2e0d9172df5c9de6690c \ - --hash=sha256:7ac2769a7ddd661e07e26c12220adc31b17014c48234b2a5a88993b8d4109e09 \ - --hash=sha256:817953730e4a73a09008b010c4a42396cefe799616a22df2ac21d3927b3080cf \ - --hash=sha256:83d2beac85b757bfc404c5c0612df51c4ef33bbb7be65cf4a96b4da7cbc4ff4f \ - --hash=sha256:86c7c4615634b5c57a79770f5b738354e45ae219a89949aeae84f19169a05283 \ - --hash=sha256:8923fcd49f875e8a0448d7223d1fdf1a9973ae2d1f20d4158753ceb8909facd2 \ - --hash=sha256:8cceae48ae7f94990c2f4e36291e9ec9868735e8b0f19e4489a17b0422338466 \ - --hash=sha256:92f8f63df729061bb14558ed740e7a222f6e2aa6c2e6a5bcfb4e706676562049 \ - --hash=sha256:930ad463d3fb7a340d323bcc496ec32dae469ec9974c29ff569f19806b6b03b4 \ - --hash=sha256:94733cb447c37b0ce6f71b39ca1288d5f966a084023a5617b4cc35a852586859 \ - --hash=sha256:9be1cfe715ecaa060ffd5d599ef410b94ef2697a0eed4cee562f788a2aa2175b \ - --hash=sha256:a0c5210d038418a115e7cf8706bfa611bd14add2f5787274dba45aadca3155e0 \ - --hash=sha256:a2f8716344bbbd10443391be0b1f2003b34949e3c4e6883005c2bc498ab6b64b \ - --hash=sha256:a4bb174bd65f795c39ad6642d9d5bdc43351b306b7d0a654ab0d0abb38be245f \ - --hash=sha256:a70d5effebd81ebd8e15a8aabdc7980832367b2b00c36543bac378977004cc80 \ - --hash=sha256:a9ee990bf2cfbd81574c5d4f9f6c9d480a14b5b43d2c4f2ca13f4c6f48089394 \ - --hash=sha256:ab2822bcdcefb54d1e8ca29f5e3ec3afe160fb794bb3a3a73b09d11ed2671104 \ - --hash=sha256:ad92d255a165cde5f9882ff69699eaf5495e0a1f32e97566bbbb80f11e46781f \ - --hash=sha256:aead3b74b68396bf0feab5d57e050b730ded7df55853c1e048a87e00aaf033c6 \ - --hash=sha256:aeb2e123cbf978a905e9f55155c1b8316fbbfa7cf64d04521fe174bab302d100 \ - --hash=sha256:aec3c2d741a8e3ee993de7f95f37afb51e31b169c95dbd29da4cf6688f340914 \ - --hash=sha256:b0cdb655fbd1562c2dfc9986f9103a106f7058cd0915afc6e656d4b1d73f3387 \ - --hash=sha256:b26f39a5be3cd91a2035818be3d0ffdf5282c3e1f5f53d3b80b7024021acee52 \ - --hash=sha256:b2f2d13239487cecaae98c9f4d5904ad0737643cdf8873c88ec6a78b12214f20 \ - --hash=sha256:b300a30304c4c6aeab03ed06809819d9eceacdd58441d3c7dffe41edfd006091 \ - --hash=sha256:b42f64825f5d750228f08318a9bf4d723b7ecb4c472aedcb3013c3ec2dc1f949 \ - --hash=sha256:b67d38e8bb0ade7e6e84575e4bb153fab35b122458077273aeb08fdf12be9d2c \ - --hash=sha256:b867f9fd183d76977f810c626d539265183c25b2284762893a31e8d6cacc81c0 \ - --hash=sha256:b99bed995ed7c41515c11fe2beb86f8a6cc298e5537cae2f736de2f982f74ef0 \ - --hash=sha256:bd46bcc28ed85d3cf71581b42d90b4ebd1f2062f81879519fed209f8a4d1cfc6 \ - --hash=sha256:bee28aab5a580eec673f7b68dfa8dece3dd37bd207ef8d22fafe23a8e9837296 \ - --hash=sha256:c04e9d920e2674f54651fd260a15c5d2843526c73d5242ee4311325ddc9573b3 \ - --hash=sha256:c4c5a1770ecfe8bdb6c6f09f8d81ea4de04f7efb012fac1efa4ad315dccc1fd1 \ - --hash=sha256:c733087ecf7dde06b1d071679bfd5f3ba03266ba77814c1c7253bc48c9adedd9 \ - --hash=sha256:c813ada7a651193acda85b6bc1cf12abd4b37cd25c7c6a75d7183512cc374969 \ - --hash=sha256:c81d98e27d9d9261909dcccab1ba5d6ea1c98b0bb9470e368604614fd1d36ddd \ - --hash=sha256:c81e35862aced259177a537ce1a6264c3a97bae1ced184ca9dc10ea754563688 \ - --hash=sha256:c856e439a9d54d514e81a7131229ba151cdf81df7c408e0f6c89340e5003dfe2 \ - --hash=sha256:c9e70012926e97069cdd09221861aff412aa94e4cac2e019055cdb79485b150c \ - --hash=sha256:ca83a32b2b41653d89f09a3d22b770bca96da72273bef8f7a68136bdd1e8b823 \ - --hash=sha256:cbf0777d54672990b10cb8ca9b0523499e182d7e2ea39f86b138519a902aa601 \ - --hash=sha256:d49127f137f871cde3c5650af33bbbdcee02679dae76e020902fa106f522b8ae \ - --hash=sha256:d690121e80ab16e623415351ab24713534d4e500b3cadff216409cb6826063d4 \ - --hash=sha256:d6a166978f22f10cb91413f70519861605219346decf65bc79008f9257c94afe \ - --hash=sha256:d6e3485a5e5c01fb5339c1ee3d4b9e21e2135a2a9d44caafb71acd994487ee7c \ - --hash=sha256:d7d406709ae924cf0b08b8282ca2f0e1c90526911d93bc040557cbc83b74c55f \ - --hash=sha256:d8201c9f9035520cdcc7dcad383b039ec8123969b87e01cf27c3dd883c02313b \ - --hash=sha256:da779ae50b46f3f7d1e1b5f362fdc0ad376a49fd55f6f3122dc2b6149936cdf7 \ - --hash=sha256:dcf2c7eaaa4cafe8e49b017e5bb49e76e084ab6f0cd712ee4019c812b5cf68b8 \ - --hash=sha256:dd8bed00731a21e3106e5dd6bbffb6c775a2c0bbc7c0c9face534d830057bdc9 \ - --hash=sha256:df1c174796cd63eb6b139f7daf783affe1f3ed659605850e843b24764c21bfc5 \ - --hash=sha256:dfcf6e51000b34f7026322cd9ea9bc340d745637e27e0bbc22f504b4f6b967ee \ - --hash=sha256:e498900bfbf7ddddd862f810440c09e9b81674739e8a11d619b40906dccd4a9a \ - --hash=sha256:e512c75ec5cf4475c1aa50ad975ecdf253f4963b06e152e1a11edd6de4010753 \ - --hash=sha256:e5bd109d095aaae989a0463d5ef0fcc43e9ddd5de125e656d3c119a79e466380 \ - --hash=sha256:e6b4b08d09054310a4dc389fa61ee22a7a5a292cb23ab8a985136ca8828bf924 \ - --hash=sha256:e6da138a1ecc1341c8b788a5ee822670b03788e1f5decf7af9d461de6e508b8e \ - --hash=sha256:ea2f61b0bc12f3592288503961b764579dea0658852d96a0b3d823614d808821 \ - --hash=sha256:ed9c0b6f236648dbf30d57e9f57e1f74878caafa04833a19f529b5ba5164b7a8 \ - --hash=sha256:edbca8789fd10f0de534b660a6b4c71f1fd18039679c918dad392ab044bb37e4 \ - --hash=sha256:edc17549ab592e33cfcaf285f047bb75fcd98f608aa9309af1187fd736ce9641 \ - --hash=sha256:ef8a9ddb2e2a4eae4abb72dc0710eca6c2726779b0664bd10c3ce9fcfaed8c4e \ - --hash=sha256:f08ee8248aad2d054e5bc4c73554d4505464698f6d754f16198dbc5764e9821d \ - --hash=sha256:f1d62fb172db35c6478c9323da34ff37d1f3d37d7d44337a9d94ba8c583e5730 \ - --hash=sha256:f8fd23653a252cefabea11cd041efd7b80bdf403e43a36dea53f9cb0aaa21f0f \ - --hash=sha256:faa4e96385c820ce9d7f7a623486e23d4134ae3c9ba2071e44e0ab5a5a5002c4 \ - --hash=sha256:fb2bb5b557080da398b866408117d98fe9731a12334afd0cdff221b458cdc5dc \ - --hash=sha256:fc04affef30be98c1f36f0c422a4061ef618fdc1dde79f5ad5458dd496fecffa \ - --hash=sha256:ffb616d8d55965839670301dc3bae3abacff560a7e137a89056058a8a9da4d96 -pydantic-settings==2.16.2 \ - --hash=sha256:3dc08ffabed2bbd5dc2dd55ead7b78b4e4c5cd776aecd04310db5f1224caaf46 \ - --hash=sha256:8d523e5c129b097686bde019bd6109bff53f4c8c3f63f1ddf9933ffa42e5ab4f -pygments==2.21.0 \ - --hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \ - --hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c -pyjwt==2.15.1 \ - --hash=sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193 \ - --hash=sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8 -python-dotenv==1.2.4 \ - --hash=sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc \ - --hash=sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0 -python-multipart==0.0.33 \ - --hash=sha256:8f16c07b36add9da6b2ee87d3efcaf0a45a6de9c3d0a5dd94e9ccf7675a7bc45 \ - --hash=sha256:b8112424a6abd162ec08d390565dd113ad699f2fe6fef3c987b8bbcbfe9e390d -referencing==0.37.0 \ - --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ - --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 -requests==2.34.2 \ - --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ - --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed -rich==15.0.0 \ - --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ - --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 -rpds-py==2026.9.1 \ - --hash=sha256:00ba2d8c7dd4ee537978ddf4b3fbd712bef2d8751603f7f3146b3f4287768e25 \ - --hash=sha256:01445c8d194aa032a08e944f16567672da1c62dbdbefd8b6d0693032e290cf68 \ - --hash=sha256:028ad274ea951dac64491b5d1e65712a4aeabfdbdb9fccf797b57bd899b0c495 \ - --hash=sha256:0483515261947e4e8b8e1375bf7463e7eb6ccfb3d86e7b554d90cd5285f20f32 \ - --hash=sha256:068c37bba854ec2fe42f7365c640af11dd9895890ccbf2df5070d0c059bd7f96 \ - --hash=sha256:074a4d198bc34d9a8ea425114fc3ded6d11ec01f6a314a8db67454a5152d8834 \ - --hash=sha256:07deecbfce94c78473018bc7d10b337cc651d12df87a1eb2cb3e4024bc9c33d0 \ - --hash=sha256:08dae4a4095150a7c4545a1fb40b98e1ab1744fbc2770d92c977b9dadaa49ab6 \ - --hash=sha256:0da298fb372dc192610a4b9ecbc68a0cd8b675bbbd1fc519d01b41cfd658333e \ - --hash=sha256:0f045bb053c9057720d72c56dffe30dffdc05997b2897a827b9325f0ab6623fa \ - --hash=sha256:10e208f2425d973938afcd56e28a7c4be32e27b6a60b5d381f49fb9d8acf9759 \ - --hash=sha256:136a1c3fe4402b7008bc81cb62ee538481795b61a7e83df88dff3b3f02b726ff \ - --hash=sha256:159a7aab5c5e8b112c8830f54717ce56da1252ebdbb526f5be2df2309280b9e7 \ - --hash=sha256:172e47169583f46ce118cbec68e6795d0da0f4606b488b6434f8276bca0a058c \ - --hash=sha256:1c2d1f6da5128eabf34e963d7163a818846075a52568250d006c4c953b40f903 \ - --hash=sha256:1d55198263bb51f557550c6ed2e6d1cb6a6fed6eb5c9120b741c5926bef8a45d \ - --hash=sha256:1d77b649e6f7cdf12ca5c2a98dad0ad37f9ea9b6f960408a92f0cb12bb3d04d9 \ - --hash=sha256:1e8d4d79d828299bf44a55db22a9388ab967b49d17132c88eab0f4360b48da8e \ - --hash=sha256:22ffd29a63d71fb1b81552c21f2c2b734949b7ac751a9be70675a939a900839b \ - --hash=sha256:2693b2728bbcc48d09a981a356954b0c47c53ff25b545856f28a889ea619f69a \ - --hash=sha256:270bdcdaac5d5b6f73c5e22e7e135c7f2a50e789f71d9e241d5be8d90026e19a \ - --hash=sha256:2711d29b653b3bce48a63d18b9c6b53274669e6d6c4094dddeb4d9a0e45128b2 \ - --hash=sha256:2c16ab111bc27c646ba8aa005d0527754edc538ebb636f0b1bf8e244b48d1945 \ - --hash=sha256:306ee1850d8105b5baf977e78d45fcadd12c1a54678d614c9baf217708446e91 \ - --hash=sha256:3231c4c0e521dafa5be0c9f114ee2c2ad46650836f2d72caa86801950c3e7044 \ - --hash=sha256:3890a6aa36e6baa53d5258a2a25d3ef8b37ad165a6ab27a892d7c3e3a432cd69 \ - --hash=sha256:3a72c11530d71abfb66c8d7696a2f86c43e63fca8b948f1a784ac490f4ec688e \ - --hash=sha256:3b5a6f40f0a1486b4b36c888123afc67acdbd9f33235927acf5ff295429a0ba3 \ - --hash=sha256:3c91c210ae7645626c608400e3519b4a642f837cce09ca830db3beb2e9f274d4 \ - --hash=sha256:3cd182d7291d29b92c521a0069d9c01ba6193628a9a105531d11b40a6d731a33 \ - --hash=sha256:3e524c7874ac72884d28e16dd5b8d839fd09e0fe76b020d3fbca23212a7b8c52 \ - --hash=sha256:3e93b2cd69a9830be33e03945cd7cda940a0a8bfcfbff41d6144f0cb0d3d8bd9 \ - --hash=sha256:3edae8c5ddfdb6985d49ae9d150516e5076888879022f91a26c2de9276ce0bdb \ - --hash=sha256:3f0e9ac28fc067d4d34b88ae43c48e9489455c97fee9633d851f7eeed5a05d35 \ - --hash=sha256:42e75466f83cd43f6026c81eab74246efb2bdadafb307b85700632d06c68f299 \ - --hash=sha256:44b32a7c4f0da3d28af31c259e38ddcff096f855e205ed0671d02fcf44f1ea1c \ - --hash=sha256:457866b85daf5034296666168b84a69e0b2e89dc4f1af102b46f6448a60b9063 \ - --hash=sha256:45bc6bccf78b20fd834237d18db64965d7ee68ba7f60440a26c7ab71e7b8d51a \ - --hash=sha256:46d80bc76b51a6c24f9944368c28d38b8bcbcea1da4f2f8d3ebc31a67e8c6ec6 \ - --hash=sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12 \ - --hash=sha256:492e5e428cbe126221611f47e068f01660352feec4ad18bc0f5ea9b2ae88fb14 \ - --hash=sha256:4b26b03d9d2658ee2fa234f8f4f19f38a09773fe5261028025032e26d4d35af0 \ - --hash=sha256:4c0d2cb595a420b34d5086db0add011e26e2c09d6a024afbac4228bf8f863a30 \ - --hash=sha256:4cfaf02209061880210819934de2f4f6aa83dc04dafe6770276acc240a56da31 \ - --hash=sha256:501909f2e4a1e2dee528ef766fe3c469060ebc17e54a8383d404ba07a81a6f02 \ - --hash=sha256:50906f5aea24b5a865cbd0a589698288631d9f3a54c3a937c83aefa95a0d14af \ - --hash=sha256:54ac2158a6f96cfbabff0b2eedaf94b90c5ec7ca8317fcadc61e1c2b2e0ff6ef \ - --hash=sha256:56c6952a9b15047466d0c2347c446a761d4527f89976156341e68f0ce5cc08b0 \ - --hash=sha256:56cd8b3f77d7b6812f533b662186a1f28316931166ddc00fb893b1b0db7e9888 \ - --hash=sha256:57492a550a1d88d29d003247e5f78dd8cf04a701fac0e4c8db8745a6d2504e0a \ - --hash=sha256:5943980471829f6de242a20b109de3111ba6b77e3af0ffc587028ac854b05e6c \ - --hash=sha256:5c6ee90dee3e85e055ddfd502d611643d9b0fd94c818220bda84ec3dacd9b27b \ - --hash=sha256:5c90e7fa02e8f5de0d10c17595c568ada48c5302e749462c0ea1a4c362111a86 \ - --hash=sha256:5ce8943f79c2210f7abcc28e86367b03b28d95027fd01c46d2472373ae70c86f \ - --hash=sha256:617f59cde379b4f648a09797b7f683d04b90a46344cddab85639da5aff0f5531 \ - --hash=sha256:6307a0da524939decb8ca4a3933b8ab62525794411d6984fca6726e732804af6 \ - --hash=sha256:684fd492fff4fead00587544e059be2bbcb6f93454f21fa2a91b66fc7508be82 \ - --hash=sha256:6b5b393eda5ea42cca1c1a6665f2a4882b4fd5d1777e41ce0545a107fb008c9d \ - --hash=sha256:6b723eb406dec5bc9ec516c73ab9c3239a3284e017f7eb89ee2b3258bd504fb7 \ - --hash=sha256:6b9bf3135b4ad5981df9a73d71a35272d650a2985ae9c2746357b24d59de2448 \ - --hash=sha256:6beb738155fe8ab8091afdfa5a3226b21c2b1593f1e50ebb90eb25b44dbc0391 \ - --hash=sha256:6c0dbbcc19735fe5f8b0a54c07659d154a9e69f47e15d0a6ab7299215daf62cb \ - --hash=sha256:6cdc537c8633d7fd92a82e2e0d2ab74320a3f63d5e59fb9cf08711e08fe151c4 \ - --hash=sha256:6eae33003518fd4cb4f83a218d5371469dd3001aa3b87128c005b07762f7fe5e \ - --hash=sha256:740d0a99cf9de0b17a3943388e9294a59becf75e7c43421f387bd3c7a9901f7c \ - --hash=sha256:75c38c50ab9aca840225d9a9a3810bf11d04bd5c1f186cabbb8aee56db3e9b15 \ - --hash=sha256:761fdae6728ceb99ab182fad2f0cc1e262f610834dc891aea1d1a2a2e634776f \ - --hash=sha256:7664419f27db41d4f1c43a78dccda7dd6e8ef2428df3ee01d0c2a07a6b071297 \ - --hash=sha256:76d3af9732d2dab69f28179b40ba2d87e2f1d5824b4a694780aa787d685e8f36 \ - --hash=sha256:78326f4cb4427a56ba4996c0762b63be45f06b85f086526420d2b3a66e40f84d \ - --hash=sha256:7868b85224291c6cb6759f9b5adb9745f486d226f62b16a614dd5a2a5ab2b35b \ - --hash=sha256:815d26356930846a40c7bc1366e7b1b0320ab8a063e66c11298a208bed0fd237 \ - --hash=sha256:8171b44a054e5c67fd748ada04187f1250bf35b95f85e52ab64bcf3331a923bb \ - --hash=sha256:821b2755db9194409254012f429c56643416fb96ef9be090be82ec8826b7f477 \ - --hash=sha256:837c6b305e26fe0f75b15c92cf3b2ba29e0ae19dc40b1c557b026cb426347d0c \ - --hash=sha256:839dde845559254f34885267c6878f60d61d5205180226d976fe488d45fa128e \ - --hash=sha256:84a6ecc0c940169190d2c23bd969debd48c94dbc855acd60188a68d71d421608 \ - --hash=sha256:8601470267d938bcb7f3ab1a336100af51a4fd5b6ed030ef52461bb3ef5e7e07 \ - --hash=sha256:88b5268892fde430d5531f95bc560b6efbbd67c929662c586afd729a96e7461c \ - --hash=sha256:8aa5dda18d39b6143eb24809d158f9252c88f402749b6f1b62a506cc7d96cc35 \ - --hash=sha256:926bdd3e3b5998ddf70cc64bc8cf57209571f9044542913afb673799fec77dd0 \ - --hash=sha256:96beca19ec79de272e8668585380ff9092c47077c1d7a1e098e00bbd921f4785 \ - --hash=sha256:9a0460d43603d1fd9ef59c30278531e15d78581721ddb538fa560aa7817ea4ad \ - --hash=sha256:a03d57b86d2a51d0a66c92177e2be154ad015f357791d306e714569999cdb4cc \ - --hash=sha256:a36b70596407634ca82d4b989a3729074a008537a0522e4c8046a67c729103e9 \ - --hash=sha256:a3a52a3ba86436ab3aef510fbe21512abc2ddd1993005dfe50514bd2284ef025 \ - --hash=sha256:a3dbc5ed9514908d5046107d7b1346bde71eea61de6e0e4919c19354f97e769f \ - --hash=sha256:a431156bb41865fc14cd5d79bb9d7bbed83110b0159e34e62ae30951f96c0009 \ - --hash=sha256:a575404ebc9cf2e91edd32eaf570ec1430eb900d4f56724ba7dd4bc1fc9c176d \ - --hash=sha256:a5cf77eb04f20b720be95265a3e00eb2a14814074255cc27069c551b2db53118 \ - --hash=sha256:a8763f20692da7df39b0afdd1ba3042b004c50a45994f76c2d9a25641f7673db \ - --hash=sha256:ab4b2fda7c2b542f7f9d886cc6a838c5079d2b76f72e6081411faba11adde2c9 \ - --hash=sha256:addeda51556dac7c1a2f14cda62db8b621cd12afba3091d03a96c72932387eab \ - --hash=sha256:b242c27c8f836305a4a72df9cdd564386ac57b807bd252a063223331c9316b37 \ - --hash=sha256:b4f062343e7ad3fa94f2c66e5ae667dee47ee74dd41a9057c4fbe163236a123d \ - --hash=sha256:b5b8b0753718d258fd454283fbd57e14545d3b40583fa672e27cb4f987626bcc \ - --hash=sha256:be3e47e2d91aa3942ff9bf4077a505226005abfc39b6f7554a91c1b9393986b9 \ - --hash=sha256:befc2d6a953e563f8a7bfd87a42c22ebf8a3e980dcb7b6a4d17b70b0e914e8a3 \ - --hash=sha256:bf35d0568abda97233239ce32896d3ad53fccc537832c104e30c94aa5fb93569 \ - --hash=sha256:c933c6678c6f116ff8af47a4c6db0868b8ace74af0343016c0ef00f00272ea69 \ - --hash=sha256:c9d1aca01f49170fdcf5c92761b1fafe97f554b721ca4570c5949fff778f0d4b \ - --hash=sha256:cdeaa99ce822dca76cfb1b993e9120c5ea212f2eb66d48950ad63c349668a018 \ - --hash=sha256:ce4d4f52e2a4324396caddbd45a97d8d7be5f42edd25d2355282a9c34f9b2f7f \ - --hash=sha256:d1028417bb44037eb3069c1009bd7b7277212876cda22fbe565b0bca9fab6d2c \ - --hash=sha256:d151e148117294133bf8af7eeace085e7e87432db15ab6adf640330298a47f6f \ - --hash=sha256:d7841166b7fa64c9c56404617ae4341448847482d45933b13135d26c130519e5 \ - --hash=sha256:d7fca4eb6df565e2a928f1c7dad92d27db8f9df0f449e76423ed5d7e713ed445 \ - --hash=sha256:d95a354e02393eada6d7351184671aced9d4cce109dabf927cb7aa99624352a1 \ - --hash=sha256:d9edf30457d74eebfd76b045535e36f1cd89062566a128a0db2145ca042d787e \ - --hash=sha256:dbc2673f9223d420c91145599b3ba45a8a50c207d1976908e5fb5ddb0c9b9429 \ - --hash=sha256:e01b3c878c8641913e688edd1b3f08658c6783d29cf6b826bd3c0d1ae7a1ffaa \ - --hash=sha256:e21c1429e205828ea886a2293a4a2c8e01f4c25d9893ca330e97a6cf73f52e7b \ - --hash=sha256:e43d4a1f673e8a1cbd8533e809e02b4bf9d4f2280269bb640436556312121250 \ - --hash=sha256:e6d198bad4e49dd6732fbd636e2fc5c082f45c8cad0b4acb756b00c82c76072e \ - --hash=sha256:e6ea1cda8d8c688278430e4268a42f5e5da3bdd74578dfadc0820c3f1766ce83 \ - --hash=sha256:ea394a937f17a54c51239348bdbe2e3518124c8d4a8951ba04a311d3095bd18f \ - --hash=sha256:eac2f5dbafd585dfe31f86a23ebf0d3ba480a9d49ebc87947267b5608d4ea0cd \ - --hash=sha256:eb61be926bb81567c1f48bdc8aa22b9855048dc2efd53871f9f7e6e9a5632346 \ - --hash=sha256:eba5d173f7d5708b22a93815017a4611873ed54db9f268077c0dd1ed99cfc858 \ - --hash=sha256:ec450527cbf485e13c8d3602a54f428ab0432fdade0ede75efd74b735421c871 \ - --hash=sha256:eef6a03b0b6d08d0835ccfa8ec8d1bc70525e3801387567137b50c557695e6da \ - --hash=sha256:ef0d8c843e2827d6c120ab4687e9423fb1d893db1df27b7c1506615bcb9734a0 \ - --hash=sha256:ef6b65b03247c54692ad4fd9ee97cb772781927db72e3cb05e70b3db6d1ff14f \ - --hash=sha256:f3d6ed6a98cfd19155996605474982cc470d7601746a6439078f1a5a3fa8b050 \ - --hash=sha256:fce4b85234a0cbad67bf8e6e1201ee815d172c9aebad75f25645bc4d834f8e31 \ - --hash=sha256:fda1d96e542c37b6c804547dbf489c129fe7c97183a76a5ec275909ba1a063df \ - --hash=sha256:fdcd198979b4ecffcc1beba366a7fbcf4eb41243691a82fe52ceb0b902f09c12 \ - --hash=sha256:fe5ad0664ec772b02c45859041aa17655709cced7a31005817fbbbd988c25567 -ruamel-yaml==0.19.1 \ - --hash=sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93 \ - --hash=sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993 -ruamel-yaml-clib==0.2.15 \ - --hash=sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490 \ - --hash=sha256:04d21dc9c57d9608225da28285900762befbb0165ae48482c15d8d4989d4af14 \ - --hash=sha256:05c70f7f86be6f7bee53794d80050a28ae7e13e4a0087c1839dcdefd68eb36b6 \ - --hash=sha256:0ba6604bbc3dfcef844631932d06a1a4dcac3fee904efccf582261948431628a \ - --hash=sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9 \ - --hash=sha256:1b45498cc81a4724a2d42273d6cfc243c0547ad7c6b87b4f774cb7bcc131c98d \ - --hash=sha256:1bb7b728fd9f405aa00b4a0b17ba3f3b810d0ccc5f77f7373162e9b5f0ff75d5 \ - --hash=sha256:1f66f600833af58bea694d5892453f2270695b92200280ee8c625ec5a477eed3 \ - --hash=sha256:27dc656e84396e6d687f97c6e65fb284d100483628f02d95464fd731743a4afe \ - --hash=sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c \ - --hash=sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc \ - --hash=sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf \ - --hash=sha256:3cb75a3c14f1d6c3c2a94631e362802f70e83e20d1f2b2ef3026c05b415c4900 \ - --hash=sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a \ - --hash=sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa \ - --hash=sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6 \ - --hash=sha256:468858e5cbde0198337e6a2a78eda8c3fb148bdf4c6498eaf4bc9ba3f8e780bd \ - --hash=sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25 \ - --hash=sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600 \ - --hash=sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf \ - --hash=sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642 \ - --hash=sha256:4be366220090d7c3424ac2b71c90d1044ea34fca8c0b88f250064fd06087e614 \ - --hash=sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf \ - --hash=sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000 \ - --hash=sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb \ - --hash=sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690 \ - --hash=sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e \ - --hash=sha256:56ea19c157ed8c74b6be51b5fa1c3aff6e289a041575f0556f66e5fb848bb137 \ - --hash=sha256:5d3c9210219cbc0f22706f19b154c9a798ff65a6beeafbf77fc9c057ec806f7d \ - --hash=sha256:5fea0932358e18293407feb921d4f4457db837b67ec1837f87074667449f9401 \ - --hash=sha256:617d35dc765715fa86f8c3ccdae1e4229055832c452d4ec20856136acc75053f \ - --hash=sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2 \ - --hash=sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471 \ - --hash=sha256:6f1d38cbe622039d111b69e9ca945e7e3efebb30ba998867908773183357f3ed \ - --hash=sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524 \ - --hash=sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60 \ - --hash=sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef \ - --hash=sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043 \ - --hash=sha256:88eea8baf72f0ccf232c22124d122a7f26e8a24110a0273d9bcddcb0f7e1fa03 \ - --hash=sha256:923816815974425fbb1f1bf57e85eca6e14d8adc313c66db21c094927ad01815 \ - --hash=sha256:9b6f7d74d094d1f3a4e157278da97752f16ee230080ae331fcc219056ca54f77 \ - --hash=sha256:a8220fd4c6f98485e97aea65e1df76d4fed1678ede1fe1d0eed2957230d287c4 \ - --hash=sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d \ - --hash=sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467 \ - --hash=sha256:badd1d7283f3e5894779a6ea8944cc765138b96804496c91812b2829f70e18a7 \ - --hash=sha256:bdc06ad71173b915167702f55d0f3f027fc61abd975bd308a0968c02db4a4c3e \ - --hash=sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec \ - --hash=sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4 \ - --hash=sha256:c583229f336682b7212a43d2fa32c30e643d3076178fb9f7a6a14dde85a2d8bd \ - --hash=sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff \ - --hash=sha256:d290eda8f6ada19e1771b54e5706b8f9807e6bb08e873900d5ba114ced13e02c \ - --hash=sha256:da3d6adadcf55a93c214d23941aef4abfd45652110aed6580e814152f385b862 \ - --hash=sha256:dcc7f3162d3711fd5d52e2267e44636e3e566d1e5675a5f0b30e98f2c4af7974 \ - --hash=sha256:def5663361f6771b18646620fca12968aae730132e104688766cf8a3b1d65922 \ - --hash=sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a \ - --hash=sha256:e9fde97ecb7bb9c41261c2ce0da10323e9227555c674989f8d9eb7572fc2098d \ - --hash=sha256:ef71831bd61fbdb7aa0399d5c4da06bea37107ab5c79ff884cc07f2450910262 \ - --hash=sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144 \ - --hash=sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1 \ - --hash=sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51 \ - --hash=sha256:fe239bdfdae2302e93bd6e8264bd9b71290218fff7084a9db250b55caaccf43f -semantic-version==2.10.0 \ - --hash=sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c \ - --hash=sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177 -semgrep==1.184.0 \ - --hash=sha256:0ce3064175a44236dd9fd35603627e3f56139ac61e13ca967bae2b76b2cec9bf \ - --hash=sha256:0fc8a58e9a62de59ce644dde792dfc36764245d0139ab85632f66a8e0846a8c5 \ - --hash=sha256:3c86b2961b4663d1338fb965de038c7869571661971b78e18e9346ab57e1e20a \ - --hash=sha256:42a59e95433fd05a0908f8083e25656c90994c65549e483e0de8a1a0004a7adf \ - --hash=sha256:cf82d77eb911f921902f3e9d098fb97604c5419c0a87cf7de7ab2944b05e2ceb \ - --hash=sha256:d847abb8fb6fdde77d9e30a6bc869dbfcb68eff4142dcd77897c52635a7600d9 \ - --hash=sha256:f794d7c6d33126b6881f80d069583ef35b52bd796d882a5fa249ce1124aba3f3 -sse-starlette==3.7.0 \ - --hash=sha256:326416da2ab52e9a41b3f409d3ff897eda45aa68c9f3b7b17d0913aab8d219c3 \ - --hash=sha256:f1d3136caa12d129284fca3d2ded79bc9af943320b6472712955fea52a6d3eea -starlette==1.10.0 \ - --hash=sha256:478c0ab3b4ced6e82719849c118fa406f0278a3499c1a898cd15cd31bdc38ce4 \ - --hash=sha256:b8399098d4c1e3522734f7235dc79b1fc967f96cf139c81e0fa30479e0685f9b -structlog==26.1.0 \ - --hash=sha256:e081a26d6c373e6d201eca24eede26d8ffab07f88f477822e679183428d3d91e \ - --hash=sha256:f63a716cbd1b1291cf7661de7794b455acfa4c43c5bcf1630e6ad5ddc1adb3b7 -tomli==2.4.1 \ - --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ - --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ - --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ - --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ - --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ - --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ - --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ - --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ - --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ - --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ - --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ - --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ - --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ - --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ - --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ - --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ - --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ - --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ - --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ - --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ - --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ - --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ - --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ - --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ - --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ - --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ - --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ - --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ - --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ - --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ - --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ - --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ - --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ - --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ - --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ - --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ - --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ - --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ - --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ - --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ - --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ - --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ - --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ - --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ - --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ - --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ - --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 -typing-extensions==4.16.0 \ - --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ - --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 -typing-inspection==0.4.5 \ - --hash=sha256:7f16c411e7930dc79596d42ab44672a3fe5f60eb34b781a1647e7872363120c7 \ - --hash=sha256:bc210535442b39e1bf2805e0c7a17dfd6a4ace760c84c7f7d16245d23a6b1c39 -urllib3==2.8.0 \ - --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 \ - --hash=sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63 -uvicorn==0.54.0 \ - --hash=sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf \ - --hash=sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620 -wcmatch==8.5.2 \ - --hash=sha256:17d3ad3758f9d0b5b4dedc770b65420d4dac62e680229c287bf24c9db856a478 \ - --hash=sha256:a70222b86dea82fb382dd87b73278c10756c138bd6f8f714e2183128887b9eb2 -wrapt==1.17.3 \ - --hash=sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56 \ - --hash=sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828 \ - --hash=sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f \ - --hash=sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396 \ - --hash=sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77 \ - --hash=sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d \ - --hash=sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139 \ - --hash=sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7 \ - --hash=sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb \ - --hash=sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f \ - --hash=sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f \ - --hash=sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067 \ - --hash=sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f \ - --hash=sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7 \ - --hash=sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b \ - --hash=sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc \ - --hash=sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05 \ - --hash=sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd \ - --hash=sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7 \ - --hash=sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9 \ - --hash=sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81 \ - --hash=sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977 \ - --hash=sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa \ - --hash=sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b \ - --hash=sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe \ - --hash=sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58 \ - --hash=sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8 \ - --hash=sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77 \ - --hash=sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85 \ - --hash=sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c \ - --hash=sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df \ - --hash=sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454 \ - --hash=sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a \ - --hash=sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e \ - --hash=sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c \ - --hash=sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6 \ - --hash=sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5 \ - --hash=sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9 \ - --hash=sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd \ - --hash=sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277 \ - --hash=sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225 \ - --hash=sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22 \ - --hash=sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116 \ - --hash=sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16 \ - --hash=sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc \ - --hash=sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00 \ - --hash=sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2 \ - --hash=sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a \ - --hash=sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804 \ - --hash=sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04 \ - --hash=sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1 \ - --hash=sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba \ - --hash=sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390 \ - --hash=sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0 \ - --hash=sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d \ - --hash=sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22 \ - --hash=sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0 \ - --hash=sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2 \ - --hash=sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18 \ - --hash=sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6 \ - --hash=sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311 \ - --hash=sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89 \ - --hash=sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f \ - --hash=sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39 \ - --hash=sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4 \ - --hash=sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5 \ - --hash=sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa \ - --hash=sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a \ - --hash=sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050 \ - --hash=sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6 \ - --hash=sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235 \ - --hash=sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056 \ - --hash=sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2 \ - --hash=sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418 \ - --hash=sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c \ - --hash=sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a \ - --hash=sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6 \ - --hash=sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0 \ - --hash=sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775 \ - --hash=sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10 \ - --hash=sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c -zipp==4.1.2 \ - --hash=sha256:b30cb6cd253122169c47de245ca24a86bac2a5d3802ba15bf7ba6414eb9e144d \ - --hash=sha256:cf474b90b07ea932637e17c6b9479de2b2882900b0d311bdaf4f5c000a15a97f +semgrep==1.184.0 From be08f424560fb5e4751cff0938a5289b4068d1a4 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 11 Nov 2026 22:31:20 +0100 Subject: [PATCH 03/21] Fix parse(container, function) for plain functions as callback (#6073) Signed-off-by: Niels Lohmann --- .../nlohmann/detail/input/input_adapters.hpp | 20 +++++++--- single_include/nlohmann/json.hpp | 20 +++++++--- tests/src/unit-class_parser.cpp | 37 +++++++++++++++++++ 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/include/nlohmann/detail/input/input_adapters.hpp b/include/nlohmann/detail/input/input_adapters.hpp index 96d4691ef..b4c998259 100644 --- a/include/nlohmann/detail/input/input_adapters.hpp +++ b/include/nlohmann/detail/input/input_adapters.hpp @@ -1312,15 +1312,23 @@ struct can_compare_ne_either_order : std::integral_constant < bool, can_compare_ne_impl::value || can_compare_ne_reversed::value > {}; -// std::nullptr_t is excluded explicitly: a literal `nullptr` passed as a -// trailing default argument (e.g. parse(s, nullptr, ...)) must never be -// mistaken for a sentinel, and some compilers (e.g. GCC 4.8) unreliably -// SFINAE the `operator!=` detection above for std::nullptr_t against -// container/string types, which would otherwise make such calls ambiguous -// with the compatible-input overload. +// Two kinds of SentinelType are excluded explicitly: +// +// - std::nullptr_t: a literal `nullptr` passed as a trailing default argument +// (e.g. parse(s, nullptr, ...)) must never be mistaken for a sentinel, and +// some compilers (e.g. GCC 4.8) unreliably SFINAE the `operator!=` detection +// above for std::nullptr_t against container/string types, which would +// otherwise make such calls ambiguous with the compatible-input overload. +// - function pointers and functions: a parser callback passed as plain function +// (e.g. parse(s, cb)) is never a sentinel. The callback's parameter types +// make basic_json an associated class, so ADL finds the friend +// operator!=(const_reference, ScalarType) of basic_json, which makes +// `s != cb` well-formed (both convert to basic_json) and would select the +// iterator/sentinel overload. template struct can_compare_ne : std::integral_constant < bool, !std::is_same::value && + !std::is_function::type>::value && can_compare_ne_either_order::value > {}; template diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 0db1793ac..0adc91c08 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -10421,15 +10421,23 @@ struct can_compare_ne_either_order : std::integral_constant < bool, can_compare_ne_impl::value || can_compare_ne_reversed::value > {}; -// std::nullptr_t is excluded explicitly: a literal `nullptr` passed as a -// trailing default argument (e.g. parse(s, nullptr, ...)) must never be -// mistaken for a sentinel, and some compilers (e.g. GCC 4.8) unreliably -// SFINAE the `operator!=` detection above for std::nullptr_t against -// container/string types, which would otherwise make such calls ambiguous -// with the compatible-input overload. +// Two kinds of SentinelType are excluded explicitly: +// +// - std::nullptr_t: a literal `nullptr` passed as a trailing default argument +// (e.g. parse(s, nullptr, ...)) must never be mistaken for a sentinel, and +// some compilers (e.g. GCC 4.8) unreliably SFINAE the `operator!=` detection +// above for std::nullptr_t against container/string types, which would +// otherwise make such calls ambiguous with the compatible-input overload. +// - function pointers and functions: a parser callback passed as plain function +// (e.g. parse(s, cb)) is never a sentinel. The callback's parameter types +// make basic_json an associated class, so ADL finds the friend +// operator!=(const_reference, ScalarType) of basic_json, which makes +// `s != cb` well-formed (both convert to basic_json) and would select the +// iterator/sentinel overload. template struct can_compare_ne : std::integral_constant < bool, !std::is_same::value && + !std::is_function::type>::value && can_compare_ne_either_order::value > {}; template diff --git a/tests/src/unit-class_parser.cpp b/tests/src/unit-class_parser.cpp index 987e25858..412d0dcb3 100644 --- a/tests/src/unit-class_parser.cpp +++ b/tests/src/unit-class_parser.cpp @@ -3232,3 +3232,40 @@ TEST_CASE("object keys are delivered decoded and validated") CHECK(keys_of_format(std::string("\x87\xC3\x28\x91"), json::input_format_t::bon8).empty()); } } + +namespace +{ +// a plain function (not a lambda or std::function) used as parser callback +bool keep_everything_callback(int /*depth*/, json::parse_event_t /*event*/, json& /*parsed*/) +{ + return true; +} +} // namespace + +TEST_CASE("a plain function is accepted as parser callback for containers") +{ + // ADL finds basic_json's friend operator!= for "container != function + // pointer", which must not make the iterator/sentinel overloads viable + + const json expected = {1, 2}; + const std::string text = "[1,2]"; + const std::vector bytes(text.begin(), text.end()); + const std::vector chars(text.begin(), text.end()); + + CHECK(json::parse(text, keep_everything_callback) == expected); + CHECK(json::parse(text, &keep_everything_callback) == expected); + CHECK(json::parse(std::string("[1,2]"), keep_everything_callback) == expected); + CHECK(json::parse(bytes, keep_everything_callback) == expected); + CHECK(json::parse(bytes, &keep_everything_callback) == expected); + CHECK(json::parse(chars, keep_everything_callback) == expected); + CHECK(json::parse(text.begin(), text.end(), keep_everything_callback) == expected); + + // the sentinel overloads (shared constraint of parse, accept, sax_parse and + // the from_* functions) are not viable for a container and a function + // pointer, but still are for iterator pairs + using callback_pointer = decltype(&keep_everything_callback); + CHECK_FALSE((nlohmann::detail::can_compare_ne::value)); + CHECK_FALSE((nlohmann::detail::can_compare_ne, callback_pointer>::value)); + CHECK((nlohmann::detail::can_compare_ne::value)); + CHECK((nlohmann::detail::can_compare_ne::value)); +} From 0d38e99d834ec2eaa0827a0c2f9d13a2f0ceb3b4 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 11 Nov 2026 22:31:24 +0100 Subject: [PATCH 04/21] Resume patch operations from the common prefix in constant time per step (#6077) Signed-off-by: Niels Lohmann --- include/nlohmann/detail/json_pointer.hpp | 43 ++- single_include/nlohmann/json.hpp | 43 ++- tests/src/unit-json_patch.cpp | 419 +++++++++++++++++++++++ 3 files changed, 495 insertions(+), 10 deletions(-) diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index ec30e17c5..2ab3f7ddb 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -1091,6 +1091,32 @@ class json_pointer } }; + /*! + @brief the number of the deepest step of the previous path that is a prefix of the new one + + Comparing every one of many steps with the beginning of the path would take + quadratic time. Instead, the length of the common beginning of the two paths + is computed once: a step whose prefix is not longer is a prefix of the new + path, too. For few steps, which is the usual case, @ref resolve_plain + compares them, which is cheaper. + */ + template + JSON_HEDLEY_NEVER_INLINE static std::size_t deepest_shared_step(const string_t& path, const std::size_t length, const string_t& previous, const Steps& steps) noexcept + { + const std::size_t same = common_prefix_length(path.data(), previous.data(), (std::min)(length, previous.size())); + std::size_t depth = steps.size(); + for (; depth > 1; --depth) + { + // a step ends at a token boundary of the previous path + const std::size_t prefix = steps[depth - 1].first; + if (prefix <= same && (prefix == length || path[prefix] == '/')) + { + break; + } + } + return depth; + } + /*! @brief resolve a plain JSON pointer without building its reference tokens @@ -1119,13 +1145,20 @@ class json_pointer if (cache.path != nullptr && !steps.empty() && steps[0].second == root) { const string_t& previous = *cache.path; - for (depth = steps.size(); depth > 1; --depth) + if (JSON_HEDLEY_UNLIKELY(steps.size() > 16)) { - // a step ends at a token boundary of the previous path - const std::size_t prefix = steps[depth - 1].first; - if (prefix <= length && (prefix == length || path[prefix] == '/') && std::memcmp(path.data(), previous.data(), prefix) == 0) + depth = deepest_shared_step(path, length, previous, steps); + } + else + { + for (depth = steps.size(); depth > 1; --depth) { - break; + // a step ends at a token boundary of the previous path + const std::size_t prefix = steps[depth - 1].first; + if (prefix <= length && (prefix == length || path[prefix] == '/') && std::memcmp(path.data(), previous.data(), prefix) == 0) + { + break; + } } } steps.resize(depth); diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 0adc91c08..6fa5bbbb6 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -27274,6 +27274,32 @@ class json_pointer } }; + /*! + @brief the number of the deepest step of the previous path that is a prefix of the new one + + Comparing every one of many steps with the beginning of the path would take + quadratic time. Instead, the length of the common beginning of the two paths + is computed once: a step whose prefix is not longer is a prefix of the new + path, too. For few steps, which is the usual case, @ref resolve_plain + compares them, which is cheaper. + */ + template + JSON_HEDLEY_NEVER_INLINE static std::size_t deepest_shared_step(const string_t& path, const std::size_t length, const string_t& previous, const Steps& steps) noexcept + { + const std::size_t same = common_prefix_length(path.data(), previous.data(), (std::min)(length, previous.size())); + std::size_t depth = steps.size(); + for (; depth > 1; --depth) + { + // a step ends at a token boundary of the previous path + const std::size_t prefix = steps[depth - 1].first; + if (prefix <= same && (prefix == length || path[prefix] == '/')) + { + break; + } + } + return depth; + } + /*! @brief resolve a plain JSON pointer without building its reference tokens @@ -27302,13 +27328,20 @@ class json_pointer if (cache.path != nullptr && !steps.empty() && steps[0].second == root) { const string_t& previous = *cache.path; - for (depth = steps.size(); depth > 1; --depth) + if (JSON_HEDLEY_UNLIKELY(steps.size() > 16)) { - // a step ends at a token boundary of the previous path - const std::size_t prefix = steps[depth - 1].first; - if (prefix <= length && (prefix == length || path[prefix] == '/') && std::memcmp(path.data(), previous.data(), prefix) == 0) + depth = deepest_shared_step(path, length, previous, steps); + } + else + { + for (depth = steps.size(); depth > 1; --depth) { - break; + // a step ends at a token boundary of the previous path + const std::size_t prefix = steps[depth - 1].first; + if (prefix <= length && (prefix == length || path[prefix] == '/') && std::memcmp(path.data(), previous.data(), prefix) == 0) + { + break; + } } } steps.resize(depth); diff --git a/tests/src/unit-json_patch.cpp b/tests/src/unit-json_patch.cpp index 37c6e29d9..fead8a6d6 100644 --- a/tests/src/unit-json_patch.cpp +++ b/tests/src/unit-json_patch.cpp @@ -14,9 +14,12 @@ using nlohmann::json; using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) #endif +#include #include +#include #include #include +#include #include #include #include @@ -2551,3 +2554,419 @@ TEST_CASE("JSON patch - diff of leaf values below the bound of the recursive des CHECK(object_patch.size() == 2); CHECK(object_source.patch(object_patch) == object_target); } + +namespace +{ +// all pointers to values below `value` (not to `value` itself) +void collect_pointers(const json& value, const std::string& path, std::vector& pointers) +{ + if (value.is_object()) + { + for (const auto& element : value.items()) + { + std::string child = path; + child += '/'; + for (const char c : element.key()) + { + if (c == '~') + { + child += "~0"; + } + else if (c == '/') + { + child += "~1"; + } + else + { + child += c; + } + } + pointers.push_back(child); + collect_pointers(element.value(), child, pointers); + } + } + else if (value.is_array()) + { + for (std::size_t i = 0; i < value.size(); ++i) + { + std::string child = path; + child += '/'; + child += std::to_string(i); + pointers.push_back(child); + collect_pointers(value[i], child, pointers); + } + } +} + +// a random document of at most `depth` levels, with the names as keys +json random_document(std::mt19937& gen, const std::vector& names, const int depth) +{ + const int kind = std::uniform_int_distribution(0, 9)(gen); + if (depth == 0 || kind < 2) + { + return std::uniform_int_distribution(0, 3)(gen); + } + const int size = std::uniform_int_distribution(1, 3)(gen); + json result = kind < 6 ? json::array() : json::object(); + for (int i = 0; i < size; ++i) + { + if (kind < 6) + { + result.push_back(random_document(gen, names, depth - 1)); + } + else + { + result[names[std::uniform_int_distribution(0, names.size() - 1)(gen)]] = random_document(gen, names, depth - 1); + } + } + return result; +} + +// the strings, one after the other +std::string concat(std::string first, const std::string& second, const std::string& third = std::string()) +{ + first += second; + first += third; + return first; +} + +// the best of five times in seconds that std::clock() measures to apply the patch +double patch_cpu_time(json& doc, const json& patch) +{ + double best = 1e30; + for (int i = 0; i < 5; ++i) + { + const std::clock_t start = std::clock(); + doc.patch_inplace(patch); + best = (std::min)(best, static_cast(std::clock() - start) / static_cast(CLOCKS_PER_SEC)); + } + return best; +} +} // namespace + +TEST_CASE("JSON patch - consecutive operations with paths that share a prefix") +{ + SECTION("tokens that start the same way") + { + const json doc = {{"a", {{"bc", {{"x", 1}}}, {"b", {{"x", 2}}}, {"b2", 3}}}}; + + // the second path shares "/a/b" with the first, but not the token "bc" + const json patch = json::parse(R"([ + {"op": "test", "path": "/a/bc/x", "value": 1}, + {"op": "test", "path": "/a/b/x", "value": 2}, + {"op": "test", "path": "/a/bc/x", "value": 1}, + {"op": "test", "path": "/a/b2", "value": 3}, + {"op": "test", "path": "/a/b/x", "value": 2} + ])"); + CHECK(doc.patch(patch) == doc); + + json inplace = doc; + inplace.patch_inplace(patch); + CHECK(inplace == doc); + + // a path that is a prefix of the previous one, and the other way round + const json prefixes = json::parse(R"([ + {"op": "test", "path": "/a/bc/x", "value": 1}, + {"op": "test", "path": "/a/bc", "value": {"x": 1}}, + {"op": "test", "path": "/a/bc/x", "value": 1}, + {"op": "test", "path": "/a", "value": {"bc": {"x": 1}, "b": {"x": 2}, "b2": 3}}, + {"op": "test", "path": "/a/b/x", "value": 2}, + {"op": "test", "path": "", "value": {"a": {"bc": {"x": 1}, "b": {"x": 2}, "b2": 3}}} + ])"); + CHECK(doc.patch(prefixes) == doc); + } + + SECTION("operations above a path that was resolved before") + { + const json doc = json::parse(R"({"a": [{"b": [1, 2, 3]}, {"b": [4, 5, 6]}], "c": {"d": {"e": 7}}})"); + + // an element inserted before the elements of an array moves them + CHECK(doc.patch(json::parse(R"([ + {"op": "test", "path": "/a/0/b/2", "value": 3}, + {"op": "add", "path": "/a/0", "value": {"b": [9]}}, + {"op": "test", "path": "/a/1/b/2", "value": 3}, + {"op": "test", "path": "/a/0/b/0", "value": 9}, + {"op": "test", "path": "/a/2/b/0", "value": 4} + ])")) == json::parse(R"({"a": [{"b": [9]}, {"b": [1, 2, 3]}, {"b": [4, 5, 6]}], "c": {"d": {"e": 7}}})")); + + // an element removed from the front + CHECK(doc.patch(json::parse(R"([ + {"op": "test", "path": "/a/1/b/2", "value": 6}, + {"op": "remove", "path": "/a/0"}, + {"op": "test", "path": "/a/0/b/2", "value": 6}, + {"op": "replace", "path": "/a/0/b/2", "value": 60}, + {"op": "test", "path": "/a/0/b/2", "value": 60} + ])")) == json::parse(R"({"a": [{"b": [4, 5, 60]}], "c": {"d": {"e": 7}}})")); + + // a value replaced above the path that was resolved before + CHECK(doc.patch(json::parse(R"([ + {"op": "test", "path": "/c/d/e", "value": 7}, + {"op": "replace", "path": "/c/d", "value": {"e": 8, "f": {"g": 9}}}, + {"op": "test", "path": "/c/d/e", "value": 8}, + {"op": "test", "path": "/c/d/f/g", "value": 9}, + {"op": "remove", "path": "/c/d/f"}, + {"op": "add", "path": "/c/d/f", "value": [1]}, + {"op": "test", "path": "/c/d/f/0", "value": 1} + ])")) == json::parse(R"({"a": [{"b": [1, 2, 3]}, {"b": [4, 5, 6]}], "c": {"d": {"e": 8, "f": [1]}}})")); + +#if !defined(JSON_NOEXCEPTION) + // a value removed above the path that was resolved before is gone + const json removed = json::parse(R"([ + {"op": "test", "path": "/c/d/e", "value": 7}, + {"op": "remove", "path": "/c/d"}, + {"op": "test", "path": "/c/d/e", "value": 7} + ])"); + CHECK_THROWS_AS(doc.patch(removed), json::other_error&); + json inplace = doc; + CHECK_THROWS_AS(inplace.patch_inplace(removed), json::other_error&); + + const json emptied = json::parse(R"([ + {"op": "test", "path": "/a/1/b/2", "value": 6}, + {"op": "remove", "path": "/a/1"}, + {"op": "test", "path": "/a/1/b/2", "value": 6} + ])"); + CHECK_THROWS_AS(doc.patch(emptied), json::other_error&); +#endif + } + + SECTION("paths with more than 16 tokens") + { + // 40 levels, with an array at the bottom and a second branch at level 20 + std::string prefix20; + std::string prefix40; + json doc = {{"leaf", {1, 2, 3}}}; + for (int i = 40; i > 0; --i) + { + if (i == 20) + { + doc["other"] = {{"x", 5}}; + } + doc = {{"n", doc}}; + } + for (int i = 1; i <= 40; ++i) + { + prefix40 += "/n"; + if (i == 20) + { + prefix20 = prefix40; + } + } + + const auto op = [](const char* name, const std::string & path, const json & value) + { + return json({{"op", name}, {"path", path}, {"value", value}}); + }; + const json patch = + { + op("test", concat(prefix40, "/leaf/2"), 3), + op("test", concat(prefix20, "/other/x"), 5), + op("test", concat(prefix40, "/leaf/0"), 1), + op("add", concat(prefix40, "/leaf/0"), 0), + op("test", concat(prefix40, "/leaf/3"), 3), + op("replace", concat(prefix20, "/other"), {{"x", 6}}), + op("test", concat(prefix20, "/other/x"), 6), + op("test", concat(prefix40, "/leaf/1"), 1), + {{"op", "remove"}, {"path", concat(prefix20, "/other")}}, + op("test", concat(prefix40, "/leaf/1"), 1) + }; + json expected = doc; + for (const auto& operation : patch) + { + expected = expected.patch(json::array({operation})); + } + CHECK(doc.patch(patch) == expected); + json inplace = doc; + inplace.patch_inplace(patch); + CHECK(inplace == expected); + CHECK(expected[json::json_pointer(concat(prefix40, "/leaf"))] == json({0, 1, 2, 3})); + CHECK(!expected[json::json_pointer(prefix20)].contains("other")); + +#if !defined(JSON_NOEXCEPTION) + // the value that was resolved before is gone with the member that held it + const json gone = + { + op("test", concat(prefix40, "/leaf/2"), 3), + {{"op", "remove"}, {"path", prefix20}}, + op("test", concat(prefix40, "/leaf/2"), 3) + }; + CHECK_THROWS_AS(doc.patch(gone), json::other_error&); +#endif + } + + SECTION("random sequences of operations give the same result as one operation at a time") + { + std::mt19937 gen(20261110); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible + const std::vector names = {"a", "b", "ab", "abc", "a/b", "a~b", "1", "10", "token_longer_than_eight_bytes"}; + const auto pick = [&gen](const std::size_t n) + { + return std::uniform_int_distribution(0, n - 1)(gen); + }; + + for (int round = 0; round < 300; ++round) + { + const json start = random_document(gen, names, 7); + json current = start; + json patch = json::array(); + std::string previous; + + for (int step = 0; step < 25; ++step) + { + std::vector pointers; + collect_pointers(current, "", pointers); + if (pointers.empty()) + { + break; + } + + // most of the time, stay below a part of the previous path + std::vector candidates; + if (!previous.empty() && pick(10) < 8) + { + std::string prefix = previous; + const std::size_t cut = prefix.find('/', 1 + pick(prefix.size())); + prefix = prefix.substr(0, cut == std::string::npos ? prefix.size() : cut); + for (const auto& p : pointers) + { + if (p.compare(0, prefix.size(), prefix) == 0) + { + candidates.push_back(p); + } + } + } + if (candidates.empty()) + { + candidates = pointers; + } + const std::string path = candidates[pick(candidates.size())]; + const json::json_pointer pointer(path); + + json operation; + switch (pick(5)) + { + case 0: + operation = {{"op", "test"}, {"path", path}, {"value", current[pointer]}}; + break; + case 1: + operation = {{"op", "replace"}, {"path", path}, {"value", static_cast(pick(100))}}; + break; + case 2: + operation = {{"op", "remove"}, {"path", path}}; + break; + default: + { + // add below an array or object, which may be the root + const json::json_pointer parent_pointer = pick(4) == 0 ? json::json_pointer() : pointer; + const std::string parent_path = parent_pointer.to_string(); + const json& parent = current[parent_pointer]; + std::string token = "-"; + if (parent.is_array()) + { + token = pick(3) == 0 ? "-" : std::to_string(pick(parent.size() + 1)); + } + else if (parent.is_object()) + { + token = pick(2) == 0 ? "new" : "ab"; + } + else + { + // a primitive parent: replace it instead + operation = {{"op", "replace"}, {"path", parent_path}, {"value", json::array({1, 2})}}; + break; + } + operation = {{"op", "add"}, {"path", concat(parent_path, "/", token)}, {"value", json::object({{"k", {1, 2}}})}}; + break; + } + } + if (operation["path"].get().empty()) + { + continue; + } + + // one operation at a time: the cache is empty at the start of each + current = current.patch(json::array({operation})); + patch.push_back(operation); + previous = operation["path"].get(); + } + + CHECK(start.patch(patch) == current); + json inplace = start; + inplace.patch_inplace(patch); + CHECK(inplace == current); + + // the same for a type that keeps the order of the members, which makes + // objects that were built in a different order unequal: leave out the + // tests of structured values + json without_tests = json::array(); + for (const auto& operation : patch) + { + if (operation["op"] != "test" || !operation["value"].is_structured()) + { + without_tests.push_back(operation); + } + } + const nlohmann::ordered_json ordered_patch = without_tests; + nlohmann::ordered_json ordered_inplace = start; + ordered_inplace.patch_inplace(ordered_patch); + CHECK(json(ordered_inplace) == current); + } + } +} + +TEST_CASE("JSON patch - time is linear for long paths that share a prefix") +{ + // The time of the patch is compared with the time of a patch that is + // linear for sure, because a bound in seconds would depend on the machine: + // both test the same two values at the end of paths with 2n tokens. The + // second path of the first patch shares n tokens with the first one and + // continues for n tokens: the ratio is 1 to 2, and about 60 when every + // step is compared with the start of the path. The time that std::clock() + // measures is mostly the time of the processor, which a busy machine does + // not inflate (on MSVC, it is the time that has passed). + constexpr std::size_t n = 25000; + + const auto nest = [](const std::size_t depth, const std::string & inner) + { + std::string text; + for (std::size_t i = 0; i < depth; ++i) + { + text += "{\"a\":"; + } + text += inner; + text.append(depth, '}'); + return text; + }; + std::string as; + for (std::size_t i = 0; i < n; ++i) + { + as += "/a"; + } + const std::string deep = nest(n, "1"); + std::string members = "{\"x\":"; + members += deep; + members += ",\"y\":"; + members += deep; + members += "}"; + json doc = json::parse(nest(n, members)); + + const auto test_op = [](const std::string & path) + { + return json({{"op", "test"}, {"path", path}, {"value", 1}}); + }; + const json shared = json::array({test_op(concat(as, "/x", as)), test_op(concat(as, "/y", as))}); + const json same = json::array({test_op(concat(as, "/x", as)), test_op(concat(as, "/x", as))}); + + // the operations do not change the document, which is too deep to copy; + // a busy machine may spoil one comparison, hardly two + bool linear = false; + double t_shared = 0; + double t_same = 0; + for (int attempt = 0; attempt < 2 && !linear; ++attempt) + { + t_shared = patch_cpu_time(doc, shared); + t_same = (std::max)(patch_cpu_time(doc, same), 1e-3); + linear = t_shared < 10 * t_same; + } + CAPTURE(t_shared) + CAPTURE(t_same) + CHECK(linear); +} From 0660d78b9c2470991061305cd8dd6ed1fc5b66b6 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 11 Nov 2026 22:31:28 +0100 Subject: [PATCH 05/21] Correct the UTF-8 claims for binary input in the security guide (#6080) Signed-off-by: Niels Lohmann --- .../docs/api/macros/json_strict_binary_utf8.md | 2 +- .../docs/features/parsing/untrusted_input.md | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/mkdocs/docs/api/macros/json_strict_binary_utf8.md b/docs/mkdocs/docs/api/macros/json_strict_binary_utf8.md index 8421801aa..0f1bad4f7 100644 --- a/docs/mkdocs/docs/api/macros/json_strict_binary_utf8.md +++ b/docs/mkdocs/docs/api/macros/json_strict_binary_utf8.md @@ -93,7 +93,7 @@ The default value is `0` (disabled, the behavior of version 3.12.0 and earlier i ## See also - [Binary formats](../../features/binary_formats/index.md) - overview of the supported binary formats -- [`from_cbor`](../basic_json/from_cbor.md) - also affected when parsing CBOR strings +- [`from_cbor`](../basic_json/from_cbor.md) - not affected by the macro; pass `error_handler` to validate CBOR strings - [`to_cbor`](../basic_json/to_cbor.md) - create a CBOR serialization of a JSON value - [`to_ubjson`](../basic_json/to_ubjson.md) - create a UBJSON serialization of a JSON value - [`to_bjdata`](../basic_json/to_bjdata.md) - create a BJData serialization of a JSON value diff --git a/docs/mkdocs/docs/features/parsing/untrusted_input.md b/docs/mkdocs/docs/features/parsing/untrusted_input.md index 1fe7b64aa..6492728d0 100644 --- a/docs/mkdocs/docs/features/parsing/untrusted_input.md +++ b/docs/mkdocs/docs/features/parsing/untrusted_input.md @@ -79,15 +79,19 @@ For the binary formats, an announced size is never trusted outright: ## Strings -Invalid UTF-8 is rejected while parsing, not just while serializing: +Invalid UTF-8 is rejected while parsing JSON text, not just while serializing: - In JSON text, an ill-formed UTF-8 byte in a string is a [`parse_error.101`](../../home/exceptions.md#jsonexceptionparse_error101) ("invalid string: ill-formed UTF-8 byte"). - In UTF-16 or UTF-32 input, an ill-formed sequence is a [`parse_error.101`](../../home/exceptions.md#jsonexceptionparse_error101) as well: a lone surrogate, a high surrogate that is not followed by its low surrogate, and a code point above `U+10FFFF` are all rejected. -- In a binary format, a string that is not valid UTF-8 is a - [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113). + +In a binary format, a string that is not valid UTF-8 is accepted unchanged by default (none of the formats except BON8 +requires a decoder to reject it). Pass [`error_handler_t::strict`](../../api/basic_json/error_handler_t.md) to the +`from_*` function to get a [`parse_error.113`](../../home/exceptions.md#jsonexceptionparse_error113) instead. BON8 +always validates ([`parse_error.112`](../../home/exceptions.md#jsonexceptionparse_error112) for an ill-formed +sequence). A `#!cpp '\0'` (NUL) byte *inside* a quoted JSON string is always rejected (it must be escaped as `\u0000`). A NUL byte *outside* of a string is different: by default it is silently treated as the end of the input, so trailing bytes after @@ -98,8 +102,8 @@ that happens to embed a NUL is a way to make part of it disappear without a pars unexpected byte instead. Parsing is not the only place invalid UTF-8 matters: a string that reached a `#!cpp json` value some other way (for -example, constructed by application code, or, before JSON_STRICT_NUL_HANDLING existed, read from a binary format that -does not validate strings) still has to round-trip back to JSON text. By default, +example, constructed by application code, or read from a binary format with the default `error_handler_t::keep`) +still has to round-trip back to JSON text. By default, [`dump()`](../../api/basic_json/dump.md) throws [`type_error.316`](../../home/exceptions.md#jsonexceptiontype_error316) if the string is not valid UTF-8; passing [`error_handler_t::replace`](../../api/basic_json/error_handler_t.md) or `error_handler_t::ignore` avoids the exception @@ -152,6 +156,8 @@ indices (or the pointer as a whole) before using them, or use [`at`](../../api/b format lengths. - Decide up front how you want a string with invalid UTF-8 — from any source, not only parsing — to be serialized (`strict`, `replace`, or `ignore`), rather than discovering it from an uncaught `type_error.316`. +- For untrusted input in a binary format, pass `error_handler_t::strict` to the `from_*` function if the strings must be + valid UTF-8; by default they are not checked (BON8 always checks). - If a stray NUL byte silently truncating trailing input is a problem for your input format, define `JSON_STRICT_NUL_HANDLING`. - Decide whether duplicate object keys should be an error for your application, and add a callback if so. From 1dae5a66b1f8ee780f16ecad92a4e8cbe0fc8f1b Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 11 Nov 2026 22:32:05 +0100 Subject: [PATCH 06/21] Add JSON_RECURSION_DEPTH_LIMIT to set how deep value walks recurse (#6081) Signed-off-by: Niels Lohmann --- .github/workflows/ubuntu.yml | 2 +- cmake/ci.cmake | 21 + docs/mkdocs/docs/api/macros/index.md | 2 + .../docs/api/macros/json_no_thread_local.md | 1 + .../api/macros/json_recursion_depth_limit.md | 307 +++++++++ docs/mkdocs/docs/community/assurance_case.md | 7 +- .../examples/json_recursion_depth_limit.cpp | 29 + .../json_recursion_depth_limit.output | 4 + docs/mkdocs/docs/features/macros.md | 10 + .../docs/features/parsing/untrusted_input.md | 30 +- docs/mkdocs/mkdocs.yml | 1 + include/nlohmann/detail/macro_scope.hpp | 6 + .../nlohmann/detail/recursion_depth_limit.hpp | 81 ++- include/nlohmann/json.hpp | 37 +- single_include/nlohmann/json.hpp | 124 +++- tests/CMakeLists.txt | 14 + tests/src/unit-allocator.cpp | 26 +- tests/src/unit-comparison.cpp | 20 +- tests/src/unit-custom-array-type.cpp | 8 +- tests/src/unit-custom-base-class.cpp | 4 +- tests/src/unit-diagnostic-positions.cpp | 24 +- tests/src/unit-diagnostics.cpp | 4 +- tests/src/unit-json_patch.cpp | 7 +- tests/src/unit-large_json_binary_writers.cpp | 29 +- tests/src/unit-large_json_conversions.cpp | 4 +- tests/src/unit-merge_diff.cpp | 5 +- tests/src/unit-merge_patch.cpp | 4 +- tests/src/unit-modifiers.cpp | 4 +- tests/src/unit-recursion_depth_limit.cpp | 614 ++++++++++++++++++ tests/src/unit-regression1.cpp | 6 +- 30 files changed, 1340 insertions(+), 95 deletions(-) create mode 100644 docs/mkdocs/docs/api/macros/json_recursion_depth_limit.md create mode 100644 docs/mkdocs/docs/examples/json_recursion_depth_limit.cpp create mode 100644 docs/mkdocs/docs/examples/json_recursion_depth_limit.output create mode 100644 tests/src/unit-recursion_depth_limit.cpp diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index e0cd83af9..0f6956254 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -153,7 +153,7 @@ jobs: container: ubuntu:24.04 strategy: matrix: - target: [ci_cmake_flags, ci_test_diagnostics, ci_test_diagnostic_positions, ci_test_noexceptions, ci_test_noimplicitconversions, ci_test_legacycomparison, ci_test_noglobaludls, ci_test_disableenumserialization, ci_test_disabletuplereferenceconversion, ci_test_skiplibraryversioncheck, ci_test_simdutf, ci_test_strict_nul_handling, ci_test_delete_deprecated_functions, ci_test_no_thread_local] + target: [ci_cmake_flags, ci_test_diagnostics, ci_test_diagnostic_positions, ci_test_noexceptions, ci_test_noimplicitconversions, ci_test_legacycomparison, ci_test_noglobaludls, ci_test_disableenumserialization, ci_test_disabletuplereferenceconversion, ci_test_skiplibraryversioncheck, ci_test_simdutf, ci_test_strict_nul_handling, ci_test_delete_deprecated_functions, ci_test_no_thread_local, ci_test_recursion_depth_limit_0] steps: - name: Install build-essential run: apt-get update ; apt-get install -y build-essential unzip wget git diff --git a/cmake/ci.cmake b/cmake/ci.cmake index b41ab8b62..7cc5adebe 100644 --- a/cmake/ci.cmake +++ b/cmake/ci.cmake @@ -342,6 +342,27 @@ add_custom_target(ci_test_no_thread_local COMMENT "Compile and test without thread-local storage" ) +############################################################################### +# Change the number of levels the walks over a value recurse into. +############################################################################### + +# JSON_RECURSION_DEPTH_LIMIT is 128 by default, which is what the whole test +# suite otherwise runs with. 0 sends every walk to its explicit stack at once, +# so this target runs the whole suite through the walks that the default only +# uses for values nested more deeply than 128 levels: results must not depend on +# the limit. The largest limit, 254, is covered by a build of +# tests/src/unit-recursion_depth_limit.cpp (see tests/CMakeLists.txt). +add_custom_target(ci_test_recursion_depth_limit_0 + COMMAND ${CMAKE_COMMAND} + -DCMAKE_BUILD_TYPE=Debug -GNinja + -DJSON_BuildTests=ON -DJSON_FastTests=ON + -DCMAKE_CXX_FLAGS=-DJSON_RECURSION_DEPTH_LIMIT=0 + -S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_recursion_depth_limit_0 + COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_recursion_depth_limit_0 + COMMAND cd ${PROJECT_BINARY_DIR}/build_recursion_depth_limit_0 && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure + COMMENT "Compile and test with JSON_RECURSION_DEPTH_LIMIT=0" +) + ############################################################################### # Coverage. ############################################################################### diff --git a/docs/mkdocs/docs/api/macros/index.md b/docs/mkdocs/docs/api/macros/index.md index 2ddf6f245..0392ed448 100644 --- a/docs/mkdocs/docs/api/macros/index.md +++ b/docs/mkdocs/docs/api/macros/index.md @@ -40,6 +40,8 @@ header. See also the [macro overview page](../../features/macros.md). - [**JSON_NO_IO**](json_no_io.md) - switch off functions relying on certain C++ I/O headers - [**JSON_NO_SIMD**](json_no_simd.md) - use only portable code instead of vector instructions (SSE2, SSSE3, NEON) - [**JSON_NO_THREAD_LOCAL**](json_no_thread_local.md) - switch off the use of `thread_local` storage +- [**JSON_RECURSION_DEPTH_LIMIT**](json_recursion_depth_limit.md) - set how many nesting levels the operations on a value + recurse into on the call stack (default 128) - [**JSON_SKIP_UNSUPPORTED_COMPILER_CHECK**](json_skip_unsupported_compiler_check.md) - do not warn about unsupported compilers - [**JSON_USE_GLOBAL_UDLS**](json_use_global_udls.md) - place user-defined string literals (UDLs) into the global namespace - [**JSON_USE_SIMDUTF**](json_use_simdutf.md) - use the simdutf library to accelerate UTF-8 validation diff --git a/docs/mkdocs/docs/api/macros/json_no_thread_local.md b/docs/mkdocs/docs/api/macros/json_no_thread_local.md index 400d8fab7..c842ef047 100644 --- a/docs/mkdocs/docs/api/macros/json_no_thread_local.md +++ b/docs/mkdocs/docs/api/macros/json_no_thread_local.md @@ -45,6 +45,7 @@ Copying and comparing fall back to working without the call stack there, as they ## See also +- [`JSON_RECURSION_DEPTH_LIMIT`](json_recursion_depth_limit.md) - set how many nesting levels the operations recurse into - [FAQ: Thread safety](../../home/faq.md#thread-safety) - thread-safety guarantees of the library - [Supported Macros](../../features/macros.md) - guide to all supported macros diff --git a/docs/mkdocs/docs/api/macros/json_recursion_depth_limit.md b/docs/mkdocs/docs/api/macros/json_recursion_depth_limit.md new file mode 100644 index 000000000..0d7dc66e3 --- /dev/null +++ b/docs/mkdocs/docs/api/macros/json_recursion_depth_limit.md @@ -0,0 +1,307 @@ +# JSON_RECURSION_DEPTH_LIMIT + +```cpp +#define JSON_RECURSION_DEPTH_LIMIT /* value */ +``` + +This macro sets how many nesting levels the operations that walk a value recurse into on the call stack. Beyond that, +they continue with an explicit stack on the heap. The value is an integer constant from `0` to `254`; the default is +`128`. + +The operations are copying a value, comparing two values, serializing with [`dump`](../basic_json/dump.md) or +[`operator<<`](../operator_ltlt.md), hashing with `std::hash`, [`merge_patch`](../basic_json/merge_patch.md), +[`update`](../basic_json/update.md) with `merge_objects`, [`diff`](../basic_json/diff.md), +[`merge_diff`](../basic_json/merge_diff.md), converting a value to another specialization of `basic_json`, destroying a +value, and the binary writers [`to_cbor`](../basic_json/to_cbor.md), +[`to_msgpack`](../basic_json/to_msgpack.md), [`to_ubjson`](../basic_json/to_ubjson.md), +[`to_bjdata`](../basic_json/to_bjdata.md), and [`to_bon8`](../basic_json/to_bon8.md). They share this one limit; none +has +a limit of its own. Recursing is the fastest way to walk a value, but a value nested deeply enough would exhaust the +call +stack, so the library recurses only as far as the limit and finishes the levels below with an explicit stack. This is +what makes all of them safe for untrusted input +(see [Nesting depth](../../features/parsing/untrusted_input.md#nesting-depth)). Parsing, the binary readers, +[`flatten`](../basic_json/flatten.md), [`unflatten`](../basic_json/unflatten.md), and +[`to_bson`](../basic_json/to_bson.md) always use an explicit stack and do not depend on the macro. The lockstep +comparison of two [`json_view`](../basic_json_view/index.md) documents has a fixed bound of its own, 48 levels, +and does not depend on it either. + +The call stack that these operations need grows with the limit: roughly *a few kilobytes plus a cost per level times the +limit*, where the cost per level is up to about 2.3 KiB in an unoptimized and up to about 0.5 KiB in an optimized build +(see [Stack needed](#stack-needed)). The default of `128` levels is tuned for threads with ordinary stacks of a few +hundred kilobytes or more. Set a lower limit if your threads have small stacks (RTOS tasks, fibers, threads of the musl +C +library, which has a default stack of 128 KiB), or a higher limit to trade stack for speed on deeply nested values. + +The macro changes only how much stack is used and how fast the operations are. It never changes a result: every +operation returns the same value for every setting of the limit, for values nested less deeply than the limit as well as +for values nested more deeply. + +## Default definition + +The default value is `128`. + +```cpp +#define JSON_RECURSION_DEPTH_LIMIT 128 +``` + +When the macro is not defined, the library defines it to its default value. + +## Notes + +!!! note "Allowed values" + + The value must be an integer constant expression from `0` to `254`; any other value (a negative number, a number + above `254`, a floating-point number such as `254.0`, or `true` or `false`) is rejected at compile time with an + error message that names the macro. A character of any character type (`'a'`, `L'a'`, `u8'a'`, ...) is rejected, + too, although it is an integer. + + - `0` is allowed, and means that every operation uses its explicit stack from the first level on and no operation + recurses over the nesting of a value at all. That needs a constant amount of call stack (at most 16 KiB in the + measurements below, for a value nested 20,000 levels deep) and is the setting for tiny stacks, at the price of + speed (see [Speed](#speed)). + - `254` is the largest allowed value. Copying a value and comparing two values (with `<=>`, or with the operators on + objects and arrays of a custom type) cannot pass the depth down as an argument, because the signatures of the + copy constructor and the operators are fixed. They count the levels they have descended into in a variable of type + `#!cpp std::uint8_t` that exists once per thread (see [`JSON_NO_THREAD_LOCAL`](json_no_thread_local.md)). Only + the levels that are descended into are counted, so the count never exceeds the limit, not even when user code (a + copy constructor of a custom base class, say) copies a value while a copy is going on. A byte holds `255` at + most, and the limit stays below that, so that the count cannot wrap around. (More levels would hardly be + useful: `254` levels need up to 576 KiB of stack in an unoptimized build already, twice as much as the + default.) + +### Stack needed + +The tables show the smallest stack that an operation needs, in KiB, at several limits, on a value that is nested +more deeply than the limit (limit + 10 levels). Each cell shows the result for Clang (Apple clang 21) and for +GCC 16, separated by a slash; both measured on macOS with arm64, compiling in C++20 mode. The operations run in a +thread whose stack has the size given, and are not given any stack that they do not need: the value is built, and +the result destroyed, outside of that thread. Stacks can only be set in steps of 16 KiB (the page size), so `16` +means "16 KiB or less". A row stands for all operations listed; the table shows the largest value among them. + +Unoptimized (`-O0`): + +| Operation | limit 16 | limit 32 | limit 64 | limit 128 | limit 254 | +|---|---:|---:|---:|---:|---:| +| `dump()` and `dump(indent)`, `operator<<` | 48 / 32 | 80 / 64 | 144 / 128 | 288 / 256 | 576 / 512 | +| copy construction and assignment | 32 / 16 | 48 / 32 | 112 / 80 | 224 / 144 | 432 / 304 | +| `==` | 16 / 16 | 16 / 16 | 16 / 16 | 48 / 32 | 96 / 48 | +| `<=>` (C++20) | 16 / 16 | 16 / 16 | 32 / 32 | 64 / 64 | 144 / 128 | +| `std::hash` | 16 / 16 | 16 / 16 | 32 / 32 | 80 / 64 | 160 / 128 | +| `merge_patch`, `update(j, true)`, `patch` | 32 / 16 | 48 / 32 | 112 / 80 | 224 / 144 | 432 / 304 | +| `diff` | 16 / 16 | 16 / 16 | 32 / 32 | 64 / 48 | 144 / 96 | +| `merge_diff` | 16 / 16 | 48 / 32 | 80 / 48 | 176 / 112 | 352 / 224 | +| `to_cbor`, `to_msgpack` | 16 / 16 | 32 / 16 | 64 / 32 | 112 / 80 | 224 / 160 | +| `to_ubjson`, `to_bjdata` | 16 / 16 | 16 / 16 | 48 / 32 | 96 / 64 | 176 / 112 | +| `to_bon8` | 16 / 16 | 16 / 16 | 16 / 16 | 32 / 16 | 64 / 48 | +| conversion to another `basic_json` type | 32 / 16 | 64 / 32 | 112 / 80 | 224 / 144 | 448 / 304 | +| destruction | 16 / 16 | 16 / 16 | 16 / 16 | 16 / 16 | 32 / 32 | + +Optimized (`-O2`): + +| Operation | limit 16 | limit 32 | limit 64 | limit 128 | limit 254 | +|---|---:|---:|---:|---:|---:| +| `dump()` and `dump(indent)`, `operator<<` | 16 / 16 | 16 / 16 | 16 / 16 | 16 / 16 | 32 / 32 | +| copy construction and assignment | 16 / 16 | 16 / 16 | 16 / 16 | 32 / 32 | 64 / 48 | +| `==` | 16 / 16 | 16 / 16 | 16 / 16 | 16 / 16 | 16 / 16 | +| `<=>` (C++20) | 16 / 16 | 16 / 16 | 16 / 16 | 16 / 16 | 48 / 16 | +| `std::hash` | 16 / 16 | 16 / 16 | 16 / 16 | 32 / 48 | 80 / 80 | +| `merge_patch`, `update(j, true)`, `patch` | 16 / 16 | 16 / 16 | 16 / 16 | 32 / 32 | 64 / 48 | +| `diff` | 16 / 16 | 16 / 16 | 16 / 32 | 48 / 48 | 96 / 112 | +| `merge_diff` | 16 / 16 | 16 / 16 | 16 / 16 | 48 / 32 | 96 / 64 | +| `to_cbor`, `to_msgpack` | 16 / 16 | 16 / 16 | 16 / 16 | 16 / 16 | 16 / 16 | +| `to_ubjson`, `to_bjdata` | 16 / 16 | 16 / 16 | 16 / 16 | 32 / 32 | 64 / 64 | +| `to_bon8` | 16 / 16 | 16 / 16 | 16 / 16 | 16 / 32 | 32 / 64 | +| conversion to another `basic_json` type | 16 / 16 | 16 / 16 | 16 / 16 | 32 / 48 | 64 / 96 | +| destruction | 16 / 16 | 16 / 16 | 16 / 16 | 16 / 16 | 16 / 16 | + +With a limit of `0`, every operation in the tables ran in a 16 KiB stack at `-O0` (Clang and GCC), on a value nested +20,000 levels deep. The operations that do not depend on the macro (`flatten`, `unflatten`, `to_bson`) needed +16 KiB or less at every limit. + +The rule of thumb is + +> stack needed ≈ a few KiB + cost per level × limit + +with a cost per level of up to 2.3 KiB (`dump` at `-O0` with Clang; 2.1 KiB with GCC), 1.7 KiB for copying and merging +(1.3 KiB with GCC), and 0.1 to 0.5 KiB for most other operations at `-O0`; and 0.5 KiB or less at `-O2`. The numbers +come from the difference between the limits `128` and `254` in the tables, so they are rough: other compilers, +other targets (the frames of x86-64 code are about as large), other standard libraries, `JSON_DIAGNOSTICS`, and +sanitizers change them. Measure with your own tool chain (see the example below) rather than relying on the tables. +The stack that your own code and the operating system need comes on top of these numbers. + +Which limit to choose, for the stack that you can spare for the library (the largest limit of those measured +for which the slowest operation needs at most that much): + +| Stack for the library | optimized (`-O2`) | unoptimized (`-O0`) | +|---|---:|---:| +| 32 KiB | 64 | 0 | +| 64 KiB | 128 | 16 | +| 128 KiB (the default stack of threads on musl) | 254 | 32 | +| 256 KiB | 254 | 64 | +| 512 KiB | 254 | 128 | +| 1 MiB or more | 254 | 254 | + +### Speed + +Operations are faster the more of a value they handle with plain recursion. The levels below the limit are handled with +an explicit stack, which has to be kept on the heap and costs more per level. The macro therefore trades speed against +stack. + +The table shows the time of copying a value (including destroying the copy), of `==` on two equal values, of `dump`, and +of `std::hash` for several limits, relative to the default of `128` (`1.00` means the same time; more is slower). It was +measured with Clang 21 at `-O2` in C++17 mode on macOS with arm64, as the best of seven runs of each setting (two +rounds, best of both). The documents are ordinary JSON files that are nested 7 (`canada.json`), 8 +(`citm_catalog.json`), 10 (`twitter.json`), and 2 levels deep (`jeopardy.json`); the other values are one array or object +with one member per level, nested 1,000 and 100,000 levels deep. The absolute time at the limit `128` is given in the +third column. Differences of up to about 0.1 are noise: a different limit changes the layout of the compiled code, and +timings of the same binary vary by about as much. The slow columns (limits 0 and 1) vary more: repeating `std::hash` on +`canada.json` gave 1.27 to 1.30 for limit 0 and 1.4 to 1.9 for limit 1, so read the cells of those two columns as +accurate to about 0.3. + +| Value | Operation | limit 128 (absolute) | limit 0 | limit 1 | limit 16 | limit 254 | +|---|---|---:|---:|---:|---:|---:| +| `canada.json` (2.1 MB) | copy | 3.5 ms | 1.80 | 1.80 | 0.99 | 1.00 | +| `canada.json` (2.1 MB) | `==` | 350.5 µs | 2.36 | 2.38 | 1.00 | 0.99 | +| `canada.json` (2.1 MB) | `dump` | 2.4 ms | 1.11 | 1.11 | 1.00 | 1.00 | +| `canada.json` (2.1 MB) | `std::hash` | 920.2 µs | 1.33 | 1.59 | 1.00 | 1.00 | +| `citm_catalog.json` (1.7 MB) | copy | 1.8 ms | 1.38 | 1.36 | 1.00 | 0.99 | +| `citm_catalog.json` (1.7 MB) | `==` | 275.5 µs | 1.31 | 1.31 | 0.99 | 1.00 | +| `citm_catalog.json` (1.7 MB) | `dump` | 314.6 µs | 1.24 | 1.23 | 1.00 | 1.00 | +| `citm_catalog.json` (1.7 MB) | `std::hash` | 309.1 µs | 1.77 | 1.42 | 1.06 | 1.05 | +| `twitter.json` (0.6 MB) | copy | 865.7 µs | 1.45 | 1.47 | 1.01 | 1.00 | +| `twitter.json` (0.6 MB) | `==` | 135.8 µs | 1.26 | 1.23 | 1.00 | 1.00 | +| `twitter.json` (0.6 MB) | `dump` | 224.6 µs | 1.12 | 1.12 | 1.01 | 1.00 | +| `twitter.json` (0.6 MB) | `std::hash` | 132.9 µs | 1.28 | 1.27 | 1.03 | 1.04 | +| `jeopardy.json` (53 MB) | copy | 111.1 ms | 1.22 | 1.25 | 1.00 | 1.00 | +| `jeopardy.json` (53 MB) | `==` | 23.5 ms | 1.29 | 1.53 | 1.07 | 1.00 | +| `jeopardy.json` (53 MB) | `dump` | 33.2 ms | 1.14 | 1.34 | 1.03 | 1.01 | +| `jeopardy.json` (53 MB) | `std::hash` | 19.9 ms | 1.17 | 1.52 | 1.05 | 1.00 | +| arrays, 1,000 levels | copy | 69.9 µs | 0.95 | 0.98 | 0.92 | 0.99 | +| arrays, 1,000 levels | `==` | 10.4 µs | 1.02 | 1.01 | 0.96 | 1.00 | +| arrays, 1,000 levels | `dump` | 9.6 µs | 0.97 | 0.96 | 0.97 | 1.06 | +| arrays, 1,000 levels | `std::hash` | 16.9 µs | 1.00 | 1.01 | 1.01 | 0.98 | +| objects, 1,000 levels | copy | 77.1 µs | 0.98 | 0.97 | 0.97 | 1.03 | +| objects, 1,000 levels | `==` | 12.0 µs | 0.96 | 0.95 | 0.95 | 1.07 | +| objects, 1,000 levels | `dump` | 12.6 µs | 0.97 | 0.95 | 0.98 | 1.01 | +| objects, 1,000 levels | `std::hash` | 17.3 µs | 0.99 | 1.01 | 0.98 | 1.00 | +| arrays, 100,000 levels | copy | 6.4 ms | 1.00 | 1.00 | 1.00 | 1.00 | +| arrays, 100,000 levels | `==` | 1.1 ms | 1.01 | 1.03 | 1.00 | 1.06 | +| arrays, 100,000 levels | `dump` | 917.9 µs | 1.01 | 1.01 | 1.02 | 1.01 | +| arrays, 100,000 levels | `std::hash` | 1.7 ms | 1.04 | 1.05 | 0.99 | 0.99 | +| objects, 100,000 levels | copy | 7.5 ms | 0.99 | 1.02 | 1.01 | 0.99 | +| objects, 100,000 levels | `==` | 1.3 ms | 1.02 | 1.09 | 1.08 | 0.96 | +| objects, 100,000 levels | `dump` | 1.2 ms | 1.06 | 0.99 | 1.01 | 0.99 | +| objects, 100,000 levels | `std::hash` | 1.8 ms | 1.00 | 1.00 | 1.00 | 1.00 | + +The results are as follows. + +- A limit of `16` or more does not change the speed of ordinary documents (differences are within noise), because they + are not nested more deeply than that anyway. A document nested more deeply than the limit is walked partly with the + explicit stack. +- A limit of `0` or `1` makes the operations on ordinary documents 1.1 to 2.4 times as slow, because even their first + levels are handled with the explicit stack. The slowdown is largest for `==` on a document of many small containers + (`canada.json`: 2.4 times) and for copying (up to 1.8 times). +- For values that are nested far more deeply than any limit (1,000 and 100,000 levels deep), the limit makes no + measurable difference: almost all of the levels are handled with the explicit stack for every setting. + Raising the limit gains nothing for them, and lowering it costs nothing. + +!!! note "Other macros" + + - With [`JSON_NO_THREAD_LOCAL`](json_no_thread_local.md) defined (or for Clang targeting MinGW, where the library + defines it by itself), copying and comparing never descend, so for them the limit does not matter. The other + operations still recurse as many levels as the limit says. The range of the limit is the same. + - The macro is not part of the [ABI tag](../../features/namespace.md), as `JSON_NO_THREAD_LOCAL` is not, and it is + not undefined at the end of `json.hpp`. + +!!! warning "Use the same value in every translation unit" + + All translation units of a program that include the library must define the same value. Otherwise, the program + violates the One Definition Rule: the function `nlohmann::detail::recursion_depth_limit()` and all the functions + that use it are defined differently in different translation units. Which limit a call uses then depends on which + functions the compiler inlined (an inlined copy uses the value of its own translation unit) and which out-of-line + copy of each remaining function the linker kept, which is not specified. A function compiled with one value can + call a function compiled with another, so a single operation can mix both limits. + + Mixing values cannot corrupt the byte per thread that counts the levels of copying and comparing: each limit + is at most `254`, and all of them stop counting at their own limit, so the count never exceeds `254`, whichever + translation unit's definition runs. The counter exists once per thread and `basic_json` specialization, and is + shared by all translation units. Nor can mixing values change any result, as no result depends on the limit. What + changes is how much stack is used and how fast the operations are. Nevertheless, avoid it: the program has undefined + behavior as far as the C++ standard is concerned. + +## Examples + +??? example "Example: lower the limit" + + The example nests an array 5,000 levels deep. With the limit set to 16, the library recurses over the first 16 + levels + and over the rest uses an explicit stack. The results are those of the default limit. + + ```cpp + --8<-- "examples/json_recursion_depth_limit.cpp" + ``` + + Output: + + ``` + --8<-- "examples/json_recursion_depth_limit.output" + ``` + +??? example "Example: find out how much stack an operation needs" + + The program runs `dump()` on a value nested `depth` levels deep in a thread with a stack of `kib` KiB. Compile it + with the options and the limit that you use, e.g. `-DJSON_RECURSION_DEPTH_LIMIT=64`, run it with `depth` set to the + limit plus 10 and decreasing values of `kib` (the stack size must be a multiple of the page size, often 4 or 16 + KiB), + and note the smallest stack with which it still finishes. That is how the tables above were measured. + + ```cpp + #include + #include + #include + #include + #include + #include + + static nlohmann::json value; + + static void* run(void*) + { + std::cout << "dump() ok, " << value.dump().size() << " bytes\n"; + return nullptr; + } + + int main(int argc, char** argv) + { + const int depth = std::atoi(argv[1]); + const std::size_t kib = static_cast(std::atoi(argv[2])); + + // built on the main thread, without recursion + value = 1; + for (int i = 0; i < depth; ++i) + { + nlohmann::json outer = nlohmann::json::array(); + outer.push_back(std::move(value)); + value = std::move(outer); + } + + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr, kib * 1024); + pthread_t thread; + pthread_create(&thread, &attr, run, nullptr); + pthread_join(thread, nullptr); + } + ``` + +## See also + +- [Nesting depth](../../features/parsing/untrusted_input.md#nesting-depth) - how the library handles deeply nested + values +- [`JSON_NO_THREAD_LOCAL`](json_no_thread_local.md) - switch off the use of `thread_local` storage +- [Supported Macros](../../features/macros.md) - guide to all supported macros + +## Version history + +- Added in version 3.13.0. diff --git a/docs/mkdocs/docs/community/assurance_case.md b/docs/mkdocs/docs/community/assurance_case.md index 02dc6d7d7..54c9a344b 100644 --- a/docs/mkdocs/docs/community/assurance_case.md +++ b/docs/mkdocs/docs/community/assurance_case.md @@ -53,12 +53,15 @@ flowchart LR only as bytes are actually read, arrays reserve at most a fixed number of elements up front, and sizes that no container can hold are rejected. - **Memory is owned by values.** Each `basic_json` value owns its content, and there is no manual memory management in - user code. The destructor does not recurse, so destroying a deeply nested value does not exhaust the stack. + user code. The destructor recurses only over a bounded number of nesting levels and then continues without the call + stack, so destroying a deeply nested value does not exhaust the stack. - **Bounded recursion.** The JSON parser and the binary readers keep their state in explicit stacks instead of recursing per nesting level. Operations that walk a value, such as [`dump`](../api/basic_json/dump.md), copying, comparison, hashing, [`merge_patch`](../api/basic_json/merge_patch.md), [`diff`](../api/basic_json/diff.md), [`flatten`](../api/basic_json/flatten.md), and the binary writers, recurse only up to a fixed depth and continue with - an explicit stack below it. Applications that process untrusted input can limit its nesting depth with a + an explicit stack below it. The depth is 128 levels by default and can be changed with + [`JSON_RECURSION_DEPTH_LIMIT`](../api/macros/json_recursion_depth_limit.md), for example to run on a small stack. + Applications that process untrusted input can limit its nesting depth with a [parser callback](../features/parsing/parser_callbacks.md). - **Invariants are checked.** The class invariant (for instance, that the pointer for the stored type is never null) is checked with runtime assertions throughout the test suite. diff --git a/docs/mkdocs/docs/examples/json_recursion_depth_limit.cpp b/docs/mkdocs/docs/examples/json_recursion_depth_limit.cpp new file mode 100644 index 000000000..7a2bfdfc3 --- /dev/null +++ b/docs/mkdocs/docs/examples/json_recursion_depth_limit.cpp @@ -0,0 +1,29 @@ +#include +#include +#include + +// walk over at most 16 nesting levels on the call stack, and over the rest with an explicit stack +#define JSON_RECURSION_DEPTH_LIMIT 16 +#include + +using json = nlohmann::json; + +int main() +{ + // an array nested 5,000 levels deep, built without recursion + json j = 1; + for (int i = 0; i < 5000; ++i) + { + json outer = json::array(); + outer.push_back(std::move(j)); + j = std::move(outer); + } + + // the results are those of any other limit + const json copy = j; + std::cout << std::boolalpha; + std::cout << "limit: " << JSON_RECURSION_DEPTH_LIMIT << '\n'; + std::cout << "copy equals original: " << (copy == j) << '\n'; + std::cout << "length of dump(): " << j.dump().size() << '\n'; + std::cout << "same hash: " << (std::hash {}(copy) == std::hash {}(j)) << '\n'; +} diff --git a/docs/mkdocs/docs/examples/json_recursion_depth_limit.output b/docs/mkdocs/docs/examples/json_recursion_depth_limit.output new file mode 100644 index 000000000..efb8175db --- /dev/null +++ b/docs/mkdocs/docs/examples/json_recursion_depth_limit.output @@ -0,0 +1,4 @@ +limit: 16 +copy equals original: true +length of dump(): 10001 +same hash: true diff --git a/docs/mkdocs/docs/features/macros.md b/docs/mkdocs/docs/features/macros.md index 33b2273b9..ace29de02 100644 --- a/docs/mkdocs/docs/features/macros.md +++ b/docs/mkdocs/docs/features/macros.md @@ -198,6 +198,16 @@ this is planned to become the default in version 4.0.0. See [full documentation of `JSON_PRECISE_STREAM_POSITION`](../api/macros/json_precise_stream_position.md). +## `JSON_RECURSION_DEPTH_LIMIT` + +Sets the number of nesting levels that the operations walking a value (copying, comparing, serializing, hashing, +merging, diffing, converting, destroying, and the binary writers) recurse into on the call stack before they continue +with an explicit stack on the heap. The default is `128`; allowed are the integers from `0` to `254`. Lower it for +threads with small stacks, raise it to trade stack for speed. Results are the same for every setting; the value must be +the same in all translation units of a program. + +See [full documentation of `JSON_RECURSION_DEPTH_LIMIT`](../api/macros/json_recursion_depth_limit.md). + ## `JSON_SKIP_LIBRARY_VERSION_CHECK` When defined, the library will not create a compiler warning when a different version of the library was already diff --git a/docs/mkdocs/docs/features/parsing/untrusted_input.md b/docs/mkdocs/docs/features/parsing/untrusted_input.md index 6492728d0..0353bdfbd 100644 --- a/docs/mkdocs/docs/features/parsing/untrusted_input.md +++ b/docs/mkdocs/docs/features/parsing/untrusted_input.md @@ -54,10 +54,32 @@ Once a value has been parsed, operations that walk it recursively — serializin [`dump`](../../api/basic_json/dump.md), hashing it, copying it, comparing two values with `#!cpp ==`, `#!cpp <`, or (in C++20) `#!cpp <=>`, merging with [`update`](../../api/basic_json/update.md), applying a [`merge_patch`](../../api/basic_json/merge_patch.md), creating a patch with [`diff`](../../api/basic_json/diff.md) or -[`merge_diff`](../../api/basic_json/merge_diff.md), [`flatten`](../../api/basic_json/flatten.md), and the binary -writers (`to_cbor`, `to_msgpack`, ...) — descend at most 128 levels on the call stack and continue -below that with an explicit stack instead, so none of them can exhaust the stack either, however deeply the value is -nested. Destroying a value (its destructor) never recurses at all, regardless of nesting depth, for the same reason. +[`merge_diff`](../../api/basic_json/merge_diff.md), converting it to another `basic_json` type, destroying it, and the +binary writers (`to_cbor`, `to_msgpack`, ...) — descend at most +[`JSON_RECURSION_DEPTH_LIMIT`](../../api/macros/json_recursion_depth_limit.md) levels on the call stack, 128 by default, +and continue below that with an explicit stack on the heap. So the call stack does not grow with the nesting depth of the +value, however deeply it is nested. ([`flatten`](../../api/basic_json/flatten.md), +[`unflatten`](../../api/basic_json/unflatten.md), and [`to_bson`](../../api/basic_json/to_bson.md) never use the call +stack for the nesting.) + +The call stack still needs room for the levels that are recursed into, and the default limit assumes a stack of ordinary +size. The stack that an operation needs is roughly proportional to the limit; with the default of 128 levels, the most +demanding operation (`dump` in an unoptimized build) needed up to 288 KiB, and an optimized build needed 48 KiB or less, +in measurements on macOS with arm64. The most demanding operation needed this much stack at the limits `0`, `32`, +`128`, and `254`: + +| Stack needed, in KiB | limit 0 | limit 32 | limit 128 (default) | limit 254 | +|---|---:|---:|---:|---:| +| unoptimized build (`-O0`) | 16 or less | 80 | 288 | 576 | +| optimized build (`-O2`) | 16 or less | 16 or less | 48 | 112 | + +A thread with a smaller stack (a task of a real-time operating system, a fiber, or a thread of the musl C library, whose +default stack is 128 KiB) can still overflow its stack on deeply nested input, which is why the limit can be lowered: +define `JSON_RECURSION_DEPTH_LIMIT` to a smaller value, down to `0`, before including the library. Such a limit changes +only the stack that is needed and the speed (values nested more deeply than the limit are walked more slowly), never a +result. See the [documentation of the macro](../../api/macros/json_recursion_depth_limit.md) for the complete tables, +the speed, and the rules for choosing a value. Destroying a value (its destructor) recurses over the same number of +levels and then continues without the call stack, and needs 32 KiB or less at every limit. ## Input size diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index 4c587ad9d..44e06c4ff 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -454,6 +454,7 @@ nav: - 'JSON_NO_SIMD': api/macros/json_no_simd.md - 'JSON_NO_THREAD_LOCAL': api/macros/json_no_thread_local.md - 'JSON_PRECISE_STREAM_POSITION': api/macros/json_precise_stream_position.md + - 'JSON_RECURSION_DEPTH_LIMIT': api/macros/json_recursion_depth_limit.md - 'JSON_SKIP_LIBRARY_VERSION_CHECK': api/macros/json_skip_library_version_check.md - 'JSON_SKIP_UNSUPPORTED_COMPILER_CHECK': api/macros/json_skip_unsupported_compiler_check.md - 'JSON_STRICT_BINARY_UTF8': api/macros/json_strict_binary_utf8.md diff --git a/include/nlohmann/detail/macro_scope.hpp b/include/nlohmann/detail/macro_scope.hpp index 208c1c3ce..76d191128 100644 --- a/include/nlohmann/detail/macro_scope.hpp +++ b/include/nlohmann/detail/macro_scope.hpp @@ -224,6 +224,12 @@ #define JSON_NO_THREAD_LOCAL 1 #endif +// JSON_RECURSION_DEPTH_LIMIT (the number of nesting levels the walks over a +// value recurse into before they continue with an explicit stack; 128 by +// default, 0 to 254 allowed) is defined and checked where it is used, in +// detail/recursion_depth_limit.hpp. Like JSON_NO_THREAD_LOCAL it is neither part +// of the ABI tag nor undefined at the end of json.hpp. + // allow disabling exceptions #if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) #define JSON_THROW(exception) throw exception diff --git a/include/nlohmann/detail/recursion_depth_limit.hpp b/include/nlohmann/detail/recursion_depth_limit.hpp index 6553a4d05..dced5d73c 100644 --- a/include/nlohmann/detail/recursion_depth_limit.hpp +++ b/include/nlohmann/detail/recursion_depth_limit.hpp @@ -9,9 +9,22 @@ #pragma once #include // size_t +#include // false_type, is_integral, remove_cv, true_type #include +// JSON_RECURSION_DEPTH_LIMIT: the number of nesting levels the operations that +// walk a value recurse into on the call stack before they continue with an +// explicit stack (see detail::recursion_depth_limit() below). +// +// May be defined by the user before the first include of the library; it has +// to be the same in every translation unit of a program (it is not part of the +// ABI tag, see https://json.nlohmann.me/api/macros/json_recursion_depth_limit/). +// Allowed: an integer constant expression from 0 to 254. The default is 128. +#ifndef JSON_RECURSION_DEPTH_LIMIT + #define JSON_RECURSION_DEPTH_LIMIT 128 +#endif + NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail { @@ -20,21 +33,71 @@ namespace detail @brief the number of nesting levels an operation recurses into Operations that walk a value (copying, comparing, serializing, hashing, merging, -...) recurse once -per nesting level, which is fastest, but a value nested deeply enough would -exhaust the call stack. So they recurse only this many levels deep and finish -whatever lies below with an explicit stack. All of them share this limit. +diffing, converting, destroying, and writing the binary formats CBOR, +MessagePack, UBJSON, BJData, and BON8) recurse once per nesting level, which is +fastest, but a value nested deeply enough would exhaust the call stack. So they +recurse only this many levels deep and finish whatever lies below with an +explicit stack that lives on the heap. All of them share this limit and none has +a constant of its own. It is the value of the macro JSON_RECURSION_DEPTH_LIMIT, +128 by default. (Parsing, the binary readers, flatten(), unflatten(), and +to_bson() always use an explicit stack and do not read it, and the lockstep +comparison of json_view documents has a fixed bound of its own, 48 levels.) -Most of them pass the depth down as an argument. The copy constructor and the -comparison operators cannot, as their signatures are fixed, so they count it -in basic_json::nesting_depth() instead, a byte per thread; the limit must -therefore stay below 255. +Most of the operations pass the depth down as an argument. The copy constructor +and the comparison operators cannot, as their signatures are fixed, so they +count it in basic_json::nesting_depth() instead, a byte per thread. Only the +levels that are descended into are counted, so the count never exceeds the limit. +The limit must fit in the byte with some headroom, so that the count cannot wrap +around: it must be below 255, and 254 is the largest value allowed. (More levels +would hardly be useful: 254 levels need up to 576 KiB of stack in an unoptimized +build already, twice the stack of the default.) +The smallest value is 0: every operation then uses its explicit stack from the +first level on and no operation recurses over the nesting of a value at all. +That is slower, but needs a constant amount of call stack. It works because +every use of the limit compares a depth with it (`depth >= limit`, or `<` for +the opposite), which holds for 0 as for any other value; nothing computes +`limit - 1` or `limit + 1`, so nothing wraps around. Each operation's explicit +stack handles any nesting level, including the first, so none of them relies on +having recursed first. + +The call stack that an operation needs is roughly a few KiB plus a cost per level +times the limit. At the default of 128 levels, an unoptimized build needs up to +about 290 KiB (`dump()` with Clang at -O0: about 2.3 KiB per level) and an +optimized build up to about 48 KiB; with 254 levels it is up to 576 KiB and +112 KiB, and with 0 it is 16 KiB or less. Lower the limit for threads with small +stacks (RTOS tasks, fibers, musl threads), raise it to trade stack for speed on +deeply nested values. All of this, with the measurements, is in the +documentation of the macro. + +@sa https://json.nlohmann.me/api/macros/json_recursion_depth_limit/ @sa https://github.com/nlohmann/json/issues/5387 */ +/// the type of the value of JSON_RECURSION_DEPTH_LIMIT is bool or a character type +template struct recursion_depth_limit_is_char_or_bool : std::false_type {}; +template<> struct recursion_depth_limit_is_char_or_bool : std::true_type {}; +template<> struct recursion_depth_limit_is_char_or_bool : std::true_type {}; +template<> struct recursion_depth_limit_is_char_or_bool : std::true_type {}; +template<> struct recursion_depth_limit_is_char_or_bool : std::true_type {}; +template<> struct recursion_depth_limit_is_char_or_bool : std::true_type {}; +template<> struct recursion_depth_limit_is_char_or_bool : std::true_type {}; +template<> struct recursion_depth_limit_is_char_or_bool : std::true_type {}; +#if defined(__cpp_char8_t) +template<> struct recursion_depth_limit_is_char_or_bool : std::true_type {}; +#endif + constexpr std::size_t recursion_depth_limit() noexcept { - return 128; + // an integer: not a floating-point number, whose range check would pass for 1.5 or 254.0 + // (bool and the character types are integer types, too, but true, false or 'a' are + // no way to say a limit; JSON_UBJSON_VALUELESS_ELEMENTS_PER_BYTE refuses them as well) + static_assert(std::is_integral::value + && !recursion_depth_limit_is_char_or_bool::type>::value, + "JSON_RECURSION_DEPTH_LIMIT must be an integer constant (not bool or a character type)"); + static_assert(JSON_RECURSION_DEPTH_LIMIT >= 0 && JSON_RECURSION_DEPTH_LIMIT <= 254, + "JSON_RECURSION_DEPTH_LIMIT must be between 0 and 254: the nesting depth is counted in a byte per thread " + "and may exceed the limit by one (see https://json.nlohmann.me/api/macros/json_recursion_depth_limit/)"); + return static_cast(JSON_RECURSION_DEPTH_LIMIT); } } // namespace detail diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index bd06811d9..ea5977460 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -1256,8 +1256,10 @@ public: } #ifndef JSON_NO_THREAD_LOCAL - // nesting_depth() is a byte and may exceed the limit by one level - static_assert(detail::recursion_depth_limit() < 255, "the nesting depth count must fit in a byte"); + // nesting_depth() is a byte that never exceeds the limit (JSON_RECURSION_DEPTH_LIMIT), + // which is at most 254: the largest value stays one below the byte's maximum, so + // that the count cannot wrap around to 0 and let a descent go on without a bound. + // recursion_depth_limit() checks this range with a static_assert. /*! @brief how many levels the operation going on in this thread has descended into @@ -1268,8 +1270,15 @@ public: user code nests them anyway, sharing the count only ends a descent sooner than it had to, which costs a little speed and is never wrong. - A byte is enough: the count never exceeds the limit by more than the single - level that notices the limit has been reached. + A byte is enough: only the levels that are descended into are counted + (@ref nesting_depth_guard), so the count never exceeds the limit, and the + limit is at most 254. + + The count is per thread and not per value or per library configuration: all + translation units of a program share it, even if they were compiled with + different values of JSON_RECURSION_DEPTH_LIMIT (which is not supported, see + the documentation of the macro); each of them then compares the shared count + with its own limit. */ static std::uint8_t& nesting_depth() noexcept { @@ -1327,14 +1336,25 @@ public: #endif { #ifndef JSON_NO_THREAD_LOCAL - ++nesting_depth(); + // Only a level that is entered is counted, so that the count never + // exceeds the limit. A level that is not entered is finished without + // descending, and whatever runs while it is (a copy made by a custom + // base class, say) sees the same count as the level itself did and + // is not entered either. + if (m_okay) + { + ++nesting_depth(); + } #endif } ~nesting_depth_guard() { #ifndef JSON_NO_THREAD_LOCAL - --nesting_depth(); + if (m_okay) + { + --nesting_depth(); + } #endif } @@ -5246,7 +5266,10 @@ public: */ void update_members(const iterator& first, const iterator& last, const bool merge_objects, const std::size_t depth) { - if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) + // without merge_objects the loop below never descends, so there is no + // call stack to save (and the explicit stack always merges); the + // distinction matters for a limit of 0, which reaches this at depth 0 + if (JSON_HEDLEY_UNLIKELY(merge_objects && depth >= detail::recursion_depth_limit())) { update_members_iteratively(first, last); return; diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 6fa5bbbb6..112441bf1 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -2597,6 +2597,12 @@ JSON_HEDLEY_DIAGNOSTIC_POP #define JSON_NO_THREAD_LOCAL 1 #endif +// JSON_RECURSION_DEPTH_LIMIT (the number of nesting levels the walks over a +// value recurse into before they continue with an explicit stack; 128 by +// default, 0 to 254 allowed) is defined and checked where it is used, in +// detail/recursion_depth_limit.hpp. Like JSON_NO_THREAD_LOCAL it is neither part +// of the ABI tag nor undefined at the end of json.hpp. + // allow disabling exceptions #if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) #define JSON_THROW(exception) throw exception @@ -8600,10 +8606,23 @@ NLOHMANN_JSON_NAMESPACE_END #include // size_t +#include // false_type, is_integral, remove_cv, true_type // #include +// JSON_RECURSION_DEPTH_LIMIT: the number of nesting levels the operations that +// walk a value recurse into on the call stack before they continue with an +// explicit stack (see detail::recursion_depth_limit() below). +// +// May be defined by the user before the first include of the library; it has +// to be the same in every translation unit of a program (it is not part of the +// ABI tag, see https://json.nlohmann.me/api/macros/json_recursion_depth_limit/). +// Allowed: an integer constant expression from 0 to 254. The default is 128. +#ifndef JSON_RECURSION_DEPTH_LIMIT + #define JSON_RECURSION_DEPTH_LIMIT 128 +#endif + NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail { @@ -8612,21 +8631,71 @@ namespace detail @brief the number of nesting levels an operation recurses into Operations that walk a value (copying, comparing, serializing, hashing, merging, -...) recurse once -per nesting level, which is fastest, but a value nested deeply enough would -exhaust the call stack. So they recurse only this many levels deep and finish -whatever lies below with an explicit stack. All of them share this limit. +diffing, converting, destroying, and writing the binary formats CBOR, +MessagePack, UBJSON, BJData, and BON8) recurse once per nesting level, which is +fastest, but a value nested deeply enough would exhaust the call stack. So they +recurse only this many levels deep and finish whatever lies below with an +explicit stack that lives on the heap. All of them share this limit and none has +a constant of its own. It is the value of the macro JSON_RECURSION_DEPTH_LIMIT, +128 by default. (Parsing, the binary readers, flatten(), unflatten(), and +to_bson() always use an explicit stack and do not read it, and the lockstep +comparison of json_view documents has a fixed bound of its own, 48 levels.) -Most of them pass the depth down as an argument. The copy constructor and the -comparison operators cannot, as their signatures are fixed, so they count it -in basic_json::nesting_depth() instead, a byte per thread; the limit must -therefore stay below 255. +Most of the operations pass the depth down as an argument. The copy constructor +and the comparison operators cannot, as their signatures are fixed, so they +count it in basic_json::nesting_depth() instead, a byte per thread. Only the +levels that are descended into are counted, so the count never exceeds the limit. +The limit must fit in the byte with some headroom, so that the count cannot wrap +around: it must be below 255, and 254 is the largest value allowed. (More levels +would hardly be useful: 254 levels need up to 576 KiB of stack in an unoptimized +build already, twice the stack of the default.) +The smallest value is 0: every operation then uses its explicit stack from the +first level on and no operation recurses over the nesting of a value at all. +That is slower, but needs a constant amount of call stack. It works because +every use of the limit compares a depth with it (`depth >= limit`, or `<` for +the opposite), which holds for 0 as for any other value; nothing computes +`limit - 1` or `limit + 1`, so nothing wraps around. Each operation's explicit +stack handles any nesting level, including the first, so none of them relies on +having recursed first. + +The call stack that an operation needs is roughly a few KiB plus a cost per level +times the limit. At the default of 128 levels, an unoptimized build needs up to +about 290 KiB (`dump()` with Clang at -O0: about 2.3 KiB per level) and an +optimized build up to about 48 KiB; with 254 levels it is up to 576 KiB and +112 KiB, and with 0 it is 16 KiB or less. Lower the limit for threads with small +stacks (RTOS tasks, fibers, musl threads), raise it to trade stack for speed on +deeply nested values. All of this, with the measurements, is in the +documentation of the macro. + +@sa https://json.nlohmann.me/api/macros/json_recursion_depth_limit/ @sa https://github.com/nlohmann/json/issues/5387 */ +/// the type of the value of JSON_RECURSION_DEPTH_LIMIT is bool or a character type +template struct recursion_depth_limit_is_char_or_bool : std::false_type {}; +template<> struct recursion_depth_limit_is_char_or_bool : std::true_type {}; +template<> struct recursion_depth_limit_is_char_or_bool : std::true_type {}; +template<> struct recursion_depth_limit_is_char_or_bool : std::true_type {}; +template<> struct recursion_depth_limit_is_char_or_bool : std::true_type {}; +template<> struct recursion_depth_limit_is_char_or_bool : std::true_type {}; +template<> struct recursion_depth_limit_is_char_or_bool : std::true_type {}; +template<> struct recursion_depth_limit_is_char_or_bool : std::true_type {}; +#if defined(__cpp_char8_t) +template<> struct recursion_depth_limit_is_char_or_bool : std::true_type {}; +#endif + constexpr std::size_t recursion_depth_limit() noexcept { - return 128; + // an integer: not a floating-point number, whose range check would pass for 1.5 or 254.0 + // (bool and the character types are integer types, too, but true, false or 'a' are + // no way to say a limit; JSON_UBJSON_VALUELESS_ELEMENTS_PER_BYTE refuses them as well) + static_assert(std::is_integral::value + && !recursion_depth_limit_is_char_or_bool::type>::value, + "JSON_RECURSION_DEPTH_LIMIT must be an integer constant (not bool or a character type)"); + static_assert(JSON_RECURSION_DEPTH_LIMIT >= 0 && JSON_RECURSION_DEPTH_LIMIT <= 254, + "JSON_RECURSION_DEPTH_LIMIT must be between 0 and 254: the nesting depth is counted in a byte per thread " + "and may exceed the limit by one (see https://json.nlohmann.me/api/macros/json_recursion_depth_limit/)"); + return static_cast(JSON_RECURSION_DEPTH_LIMIT); } } // namespace detail @@ -40447,8 +40516,10 @@ public: } #ifndef JSON_NO_THREAD_LOCAL - // nesting_depth() is a byte and may exceed the limit by one level - static_assert(detail::recursion_depth_limit() < 255, "the nesting depth count must fit in a byte"); + // nesting_depth() is a byte that never exceeds the limit (JSON_RECURSION_DEPTH_LIMIT), + // which is at most 254: the largest value stays one below the byte's maximum, so + // that the count cannot wrap around to 0 and let a descent go on without a bound. + // recursion_depth_limit() checks this range with a static_assert. /*! @brief how many levels the operation going on in this thread has descended into @@ -40459,8 +40530,15 @@ public: user code nests them anyway, sharing the count only ends a descent sooner than it had to, which costs a little speed and is never wrong. - A byte is enough: the count never exceeds the limit by more than the single - level that notices the limit has been reached. + A byte is enough: only the levels that are descended into are counted + (@ref nesting_depth_guard), so the count never exceeds the limit, and the + limit is at most 254. + + The count is per thread and not per value or per library configuration: all + translation units of a program share it, even if they were compiled with + different values of JSON_RECURSION_DEPTH_LIMIT (which is not supported, see + the documentation of the macro); each of them then compares the shared count + with its own limit. */ static std::uint8_t& nesting_depth() noexcept { @@ -40518,14 +40596,25 @@ public: #endif { #ifndef JSON_NO_THREAD_LOCAL - ++nesting_depth(); + // Only a level that is entered is counted, so that the count never + // exceeds the limit. A level that is not entered is finished without + // descending, and whatever runs while it is (a copy made by a custom + // base class, say) sees the same count as the level itself did and + // is not entered either. + if (m_okay) + { + ++nesting_depth(); + } #endif } ~nesting_depth_guard() { #ifndef JSON_NO_THREAD_LOCAL - --nesting_depth(); + if (m_okay) + { + --nesting_depth(); + } #endif } @@ -44437,7 +44526,10 @@ public: */ void update_members(const iterator& first, const iterator& last, const bool merge_objects, const std::size_t depth) { - if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) + // without merge_objects the loop below never descends, so there is no + // call stack to save (and the explicit stack always merges); the + // distinction matters for a limit of 0, which reaches this at depth 0 + if (JSON_HEDLEY_UNLIKELY(merge_objects && depth >= detail::recursion_depth_limit())) { update_members_iteratively(first, last); return; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f4e574fc1..e7d90cecd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -355,6 +355,20 @@ json_test_add_test_for(src/unit-comparison.cpp MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} ) +# test the walks over deeply nested values with the extreme settings of +# JSON_RECURSION_DEPTH_LIMIT: 0 (every walk uses its explicit stack from the +# first level on), 1, and 254 (the largest limit that fits the byte that counts +# the nesting of a copy) +foreach(recursion_depth_limit 0 1 254) + json_test_set_test_options(test-recursion_depth_limit_${recursion_depth_limit} + COMPILE_DEFINITIONS JSON_RECURSION_DEPTH_LIMIT=${recursion_depth_limit} + ) + json_test_add_test_for(src/unit-recursion_depth_limit.cpp + NAME test-recursion_depth_limit_${recursion_depth_limit} + MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} + ) +endforeach() + # test the parser again with JSON_DIAGNOSTIC_POSITIONS enabled json_test_set_test_options(test-class_parser_diagnostic_positions COMPILE_DEFINITIONS JSON_DIAGNOSTIC_POSITIONS=1 diff --git a/tests/src/unit-allocator.cpp b/tests/src/unit-allocator.cpp index 9862e8c5e..73693347f 100644 --- a/tests/src/unit-allocator.cpp +++ b/tests/src/unit-allocator.cpp @@ -236,7 +236,8 @@ TEST_CASE("controlled bad_alloc") next_construct_fails = false; - // deeper than the 128 levels the copy constructor descends into + // deeper than the levels the copy constructor descends into + // (recursion_depth_limit(): 128 by default, 254 at most) const std::size_t depth = 300; my_json j = 1; @@ -300,8 +301,9 @@ struct nth_alloc_fails_allocator : std::allocator }; }; -// builds a value nested more than 128 levels deep - the bound the copy -// constructor descends into before it continues without the call stack - and +// builds a value nested more than recursion_depth_limit() (128 by default) +// levels deep - the bound the copy constructor descends into before it +// continues without the call stack - and // checks that a copy survives any single allocation of it failing: every // attempt either throws std::bad_alloc, without crashing or leaving the // source altered, or completes the copy @@ -312,9 +314,9 @@ void check_deep_copy_survives_failing_allocation(bool nest_objects) fail_at_alloc_call = -1; - // [[[ ... [1] ... ]]], or the same nesting with objects, 130 levels deep + // [[[ ... [1] ... ]]], or the same nesting with objects, two levels deeper than the limit BasicJsonType src = 1; - for (std::size_t i = 0; i < 130; ++i) + for (std::size_t i = 0; i < nlohmann::detail::recursion_depth_limit() + 2; ++i) { if (nest_objects) { @@ -440,8 +442,9 @@ TEST_CASE("deep copy uses the provided allocator") { using counting_json = nlohmann::json::with_allocator_t; - // deeper than the 128 levels the copy constructor descends into, so the - // innermost objects are copied by the iterative deep copy + // deeper than the levels the copy constructor descends into (128 by + // default, 254 at most), so the innermost objects are copied by the + // iterative deep copy counting_json j = 1; for (std::size_t i = 0; i < 300; ++i) { @@ -499,11 +502,12 @@ TEST_CASE("converting a deeply nested value from another specialization fails cl #if !(defined(_MSC_VER) && _MSC_VER < 1910 && defined(_ITERATOR_DEBUG_LEVEL) && _ITERATOR_DEBUG_LEVEL > 0) using countdown_json = nlohmann::json::with_allocator_t; - // deeper than the 128 levels the converting constructor descends into, so - // that failures land on both sides of the bound - or, built with - // JSON_NO_THREAD_LOCAL, all in the iterative conversion + // deeper than the levels the converting constructor descends into + // (recursion_depth_limit(), 128 by default), so that failures land on both + // sides of the bound - or, built with JSON_NO_THREAD_LOCAL, all in the + // iterative conversion json j = {1, "two", {{"three", 3}}}; - for (std::size_t i = 0; i < 150; ++i) + for (std::size_t i = 0; i < nlohmann::detail::recursion_depth_limit() + 22; ++i) { j = json{{"a", json::array({j, "sibling"})}}; } diff --git a/tests/src/unit-comparison.cpp b/tests/src/unit-comparison.cpp index 4ac43320b..bec6b136e 100644 --- a/tests/src/unit-comparison.cpp +++ b/tests/src/unit-comparison.cpp @@ -62,6 +62,15 @@ template<> struct StringMaker namespace { +// nesting depths on either side of detail::recursion_depth_limit(): none, the +// last one within the limit, the first one past it, and well past it (0, 127, +// 128, and 200 by default) +std::vector depths_around_recursion_limit() +{ + const std::size_t limit = nlohmann::detail::recursion_depth_limit(); + return {0, limit > 0 ? limit - 1 : 0, limit, limit + 72}; +} + // helper function to check std::less // see https://en.cppreference.com/w/cpp/utility/functional/less template > @@ -968,7 +977,7 @@ TEST_CASE("copying an object preserves its comparator's state") object["a"] = 3; REQUIRE(object.dump() == R"({"B":2,"a":3,"b":1})"); - for (const std::size_t depth : std::vector {0, 127, 128, 200}) + for (const std::size_t depth : depths_around_recursion_limit()) { CAPTURE(depth) @@ -1011,7 +1020,7 @@ TEST_CASE("equality of an object whose comparator treats different keys as equiv // sanity check: the object type's own comparison already disagrees CHECK_FALSE(a.get_ref() == b.get_ref()); - for (const std::size_t depth : std::vector {0, 127, 128, 200}) + for (const std::size_t depth : depths_around_recursion_limit()) { CAPTURE(depth) @@ -1135,9 +1144,10 @@ TEST_CASE("operator<=> of binary values with a different subtype does not depend return result; }; - // 127 levels stay within nesting_depth_limit() (128); 128 and 200 do not, - // and must still agree with the levels that do - for (const std::size_t depth : std::vector {0, 127, 128, 200}) + // one level less than the recursion depth limit (127 by default) stays within it; + // the limit itself and more (128 and 200) do not, and must still agree with the + // levels that do + for (const std::size_t depth : depths_around_recursion_limit()) { CAPTURE(depth) const json x = deep(a, depth); diff --git a/tests/src/unit-custom-array-type.cpp b/tests/src/unit-custom-array-type.cpp index 65285eb54..e5e3307b9 100644 --- a/tests/src/unit-custom-array-type.cpp +++ b/tests/src/unit-custom-array-type.cpp @@ -11,6 +11,7 @@ #include using nlohmann::json; +#include #include #include #include @@ -217,16 +218,17 @@ TEST_CASE("array type with std::deque nested beyond the recursion limit") // recursion_depth_limit() with an explicit stack of frames; with the // iterators of std::deque, these frames are copied and moved through // their own special members + const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 72; // 200 by default deque_json j = 1; - for (int i = 0; i < 200; ++i) + for (std::size_t i = 0; i < depth; ++i) { deque_json outer = deque_json::array(); outer.push_back(std::move(j)); j = std::move(outer); } - // 200 arrays of one element each (0x81) around the integer 1 - std::vector expected(200, 0x81); + // `depth` arrays of one element each (0x81) around the integer 1 + std::vector expected(depth, 0x81); expected.push_back(0x01); CHECK(deque_json::to_cbor(j) == expected); } diff --git a/tests/src/unit-custom-base-class.cpp b/tests/src/unit-custom-base-class.cpp index 71698d041..c3033afe9 100644 --- a/tests/src/unit-custom-base-class.cpp +++ b/tests/src/unit-custom-base-class.cpp @@ -475,10 +475,10 @@ TEST_CASE("Regression test for issue #5674 - copy construction must not require SECTION("nested deeper than the copy constructor's descent bound") { - // beyond nesting_depth_limit() (128) levels, the copy constructor + // beyond recursion_depth_limit() (128 by default) levels, the copy constructor // copies without the call stack (copy_iteratively / copy_array_level), // which used to assign the base class of every element it created - const std::size_t depth = 300; + const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 172; const json_with_const_base j = make_nested_array(depth); const json_with_const_base copy = j; // NOLINT(performance-unnecessary-copy-initialization) diff --git a/tests/src/unit-diagnostic-positions.cpp b/tests/src/unit-diagnostic-positions.cpp index 85284d9d4..8722a8787 100644 --- a/tests/src/unit-diagnostic-positions.cpp +++ b/tests/src/unit-diagnostic-positions.cpp @@ -128,16 +128,19 @@ TEST_CASE("Better diagnostics with positions") check_copy(depth, true); }; + // around detail::recursion_depth_limit() (127, 128, and 129 by default) + const std::size_t limit = nlohmann::detail::recursion_depth_limit(); + check_arrays(1); - check_arrays(127); - check_arrays(128); - check_arrays(129); + check_arrays(limit > 0 ? limit - 1 : 0); + check_arrays(limit); + check_arrays(limit + 1); check_arrays(300); check_objects(1); - check_objects(127); - check_objects(128); - check_objects(129); + check_objects(limit > 0 ? limit - 1 : 0); + check_objects(limit); + check_objects(limit + 1); check_objects(300); } @@ -186,10 +189,13 @@ TEST_CASE("Better diagnostics with positions") } }; + // around detail::recursion_depth_limit() (127, 128, and 129 by default) + const std::size_t limit = nlohmann::detail::recursion_depth_limit(); + check_conversion(1); - check_conversion(127); - check_conversion(128); - check_conversion(129); + check_conversion(limit > 0 ? limit - 1 : 0); + check_conversion(limit); + check_conversion(limit + 1); check_conversion(300); } diff --git a/tests/src/unit-diagnostics.cpp b/tests/src/unit-diagnostics.cpp index 2987d348c..801b041cd 100644 --- a/tests/src/unit-diagnostics.cpp +++ b/tests/src/unit-diagnostics.cpp @@ -515,9 +515,11 @@ TEST_CASE("Regression tests for extended diagnostics") // update(j, true) around its descent bound, where the nested vectors // grow while the objects are merged without recursing + const std::size_t limit = nlohmann::detail::recursion_depth_limit(); + // (the limit can be 0, where "one level less" is no level) for (const std::size_t depth : { - nlohmann::detail::recursion_depth_limit() - 1, nlohmann::detail::recursion_depth_limit(), nlohmann::detail::recursion_depth_limit() + 2 + limit > 0 ? limit - 1 : 0, limit, limit + 2 }) { ordered_json j = {{"z", {{"x", 1}}}}; diff --git a/tests/src/unit-json_patch.cpp b/tests/src/unit-json_patch.cpp index fead8a6d6..6b38cbcd6 100644 --- a/tests/src/unit-json_patch.cpp +++ b/tests/src/unit-json_patch.cpp @@ -1978,14 +1978,15 @@ TEST_CASE("JSON patch: diff of deeply nested values") SECTION("the diff reproduces the target at every depth") { // depths on either side of the nesting depth up to which diff() - // recurses (detail::recursion_depth_limit(), 128); not every depth up + // recurses (detail::recursion_depth_limit(), 128 by default); not every depth up // to 300, as the test would then time out under Valgrind std::vector depths; for (std::size_t depth = 0; depth <= 16; ++depth) { depths.push_back(depth); } - for (std::size_t depth = 120; depth <= 136; ++depth) + const std::size_t limit = nlohmann::detail::recursion_depth_limit(); + for (std::size_t depth = limit > 8 ? limit - 8 : 0; depth <= limit + 8; ++depth) { depths.push_back(depth); } @@ -2527,7 +2528,7 @@ TEST_CASE("JSON patch - diff of leaf values below the bound of the recursive des // the values at the innermost level are diffed without recursion: equal // leaves yield nothing, leaves of different types and different leaves // are replaced - const std::size_t depth = 200; + const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 72; // 200 by default json source = {1, "text", 3, {{"x", 1}, {"y", 2}}, true, nullptr}; json target = {1, 2, 4, {{"x", 1}, {"y", 3}}, true, nullptr}; diff --git a/tests/src/unit-large_json_binary_writers.cpp b/tests/src/unit-large_json_binary_writers.cpp index 339c62135..8c8755e81 100644 --- a/tests/src/unit-large_json_binary_writers.cpp +++ b/tests/src/unit-large_json_binary_writers.cpp @@ -50,13 +50,18 @@ json nested_object(const std::size_t depth, json leaf) TEST_CASE("issue #5392 - binary writers on deeply nested values") { - // 200 is past the point where the writers stop recursing, and still - // shallow enough that from_* and operator== (which still recurse) are fine. - const json deep_array = nested_array(200, json(0)); - const json deep_object = nested_object(200, json("x")); - const json empty_array = nested_array(200, json::array()); - const json empty_object = nested_object(200, json::object()); - const json mixed = nested_object(80, nested_array(80, json(true))); + // The limit (128 by default) of the levels that the writers recurse into. + const std::size_t limit = nlohmann::detail::recursion_depth_limit(); + + // 72 levels more than the limit (200 by default) is past the point where + // the writers stop recursing, and still shallow enough that from_* (which + // does not recurse) and operator== (which does, up to the limit) are quick. + const json deep_array = nested_array(limit + 72, json(0)); + const json deep_object = nested_object(limit + 72, json("x")); + const json empty_array = nested_array(limit + 72, json::array()); + const json empty_object = nested_object(limit + 72, json::object()); + // 2 * (limit / 2 + 16) levels: 160 by default, and more than the limit for every limit + const json mixed = nested_object((limit / 2) + 16, nested_array((limit / 2) + 16, json(true))); SECTION("roundtrip past the recursion bound") { @@ -95,7 +100,7 @@ TEST_CASE("issue #5392 - binary writers on deeply nested values") SECTION("the two ways of writing a value meet at the bound") { - for (std::size_t depth = 120; depth <= 140; ++depth) + for (std::size_t depth = limit > 8 ? limit - 8 : 0; depth <= limit + 12; ++depth) { CAPTURE(depth) @@ -118,8 +123,8 @@ TEST_CASE("issue #5392 - binary writers on deeply nested values") const json ndarray = json({{"_ArrayType_", "uint8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}}); const json invalid = json({{"_ArrayType_", "nope"}, {"_ArraySize_", {1}}, {"_ArrayData_", {1}}}); - const json deep_ndarray = nested_array(140, ndarray); - const json deep_invalid = nested_array(140, invalid); + const json deep_ndarray = nested_array(limit + 12, ndarray); + const json deep_invalid = nested_array(limit + 12, invalid); CHECK(json::from_bjdata(json::to_bjdata(deep_ndarray)) == deep_ndarray); CHECK(json::from_bjdata(json::to_bjdata(deep_invalid)) == deep_invalid); @@ -131,10 +136,10 @@ TEST_CASE("issue #5392 - binary writers on deeply nested values") // nested one-element arrays around the recursion bound: the exact // bytes a writer produces do not depend on whether it stayed on the // call stack or moved to the heap one partway through + // (the limit can be 0, where "one level less" is no level) for (const std::size_t depth : { - nlohmann::detail::recursion_depth_limit() - 1, nlohmann::detail::recursion_depth_limit(), - nlohmann::detail::recursion_depth_limit() + 1, nlohmann::detail::recursion_depth_limit() + 2 + limit > 0 ? limit - 1 : 0, limit, limit + 1, limit + 2 }) { CAPTURE(depth) diff --git a/tests/src/unit-large_json_conversions.cpp b/tests/src/unit-large_json_conversions.cpp index 1fd30aa89..ba584df62 100644 --- a/tests/src/unit-large_json_conversions.cpp +++ b/tests/src/unit-large_json_conversions.cpp @@ -115,7 +115,7 @@ TEST_CASE("issue #5650 - stack overflow converting between specializations") // members that the two object types enumerate in different orders. const auto bury = [](nlohmann::ordered_json value) { - for (std::size_t i = 0; i < 200; ++i) + for (std::size_t i = 0; i < nlohmann::detail::recursion_depth_limit() + 72; ++i) { value = nlohmann::ordered_json::array({std::move(value)}); } @@ -124,7 +124,7 @@ TEST_CASE("issue #5650 - stack overflow converting between specializations") const auto dig = [](const json & value) { const json* current = &value; - for (std::size_t i = 0; i < 200; ++i) + for (std::size_t i = 0; i < nlohmann::detail::recursion_depth_limit() + 72; ++i) { current = ¤t->at(0); } diff --git a/tests/src/unit-merge_diff.cpp b/tests/src/unit-merge_diff.cpp index 77fa58768..6b3e67278 100644 --- a/tests/src/unit-merge_diff.cpp +++ b/tests/src/unit-merge_diff.cpp @@ -930,14 +930,15 @@ TEST_CASE("JSON Merge Patch diff of deeply nested values") SECTION("the patch is the same past the descent bound") { // depths on either side of the nesting depth up to which merge_diff() - // recurses (detail::recursion_depth_limit(), 128); not every depth up + // recurses (detail::recursion_depth_limit(), 128 by default); not every depth up // to 300, to keep the test fast under Valgrind std::vector depths; for (std::size_t depth = 0; depth <= 16; ++depth) { depths.push_back(depth); } - for (std::size_t depth = 120; depth <= 136; ++depth) + const std::size_t limit = nlohmann::detail::recursion_depth_limit(); + for (std::size_t depth = limit > 8 ? limit - 8 : 0; depth <= limit + 8; ++depth) { depths.push_back(depth); } diff --git a/tests/src/unit-merge_patch.cpp b/tests/src/unit-merge_patch.cpp index 3f5d79b3c..bb48cf070 100644 --- a/tests/src/unit-merge_patch.cpp +++ b/tests/src/unit-merge_patch.cpp @@ -302,7 +302,7 @@ TEST_CASE("JSON Merge Patch on deeply nested values") SECTION("patching past the descent bound gives the same result") { // every depth on either side of where the iterative version takes - // over (detail::recursion_depth_limit(), 128) + // over (detail::recursion_depth_limit(), 128 by default) for (std::size_t depth = 0; depth <= 300; ++depth) { CAPTURE(depth) @@ -396,7 +396,7 @@ TEST_CASE("merge_patch() with an argument that aliases *this (#5641)") SECTION("a patch nested past the iterative descent bound aliases *this") { // every depth on either side of where the iterative version takes - // over (detail::recursion_depth_limit(), 128); patching *this with + // over (detail::recursion_depth_limit(), 128 by default); patching *this with // itself is idempotent, aliased or not for (const std::size_t depth : { diff --git a/tests/src/unit-modifiers.cpp b/tests/src/unit-modifiers.cpp index 3377eace0..0963b9881 100644 --- a/tests/src/unit-modifiers.cpp +++ b/tests/src/unit-modifiers.cpp @@ -1124,7 +1124,7 @@ TEST_CASE("update() on deeply nested values") SECTION("merging past the descent bound gives the same result") { // every depth on either side of where the iterative version takes - // over (detail::recursion_depth_limit(), 128) + // over (detail::recursion_depth_limit(), 128 by default) for (std::size_t depth = 0; depth <= 300; ++depth) { CAPTURE(depth) @@ -1215,7 +1215,7 @@ TEST_CASE("update() with an argument that aliases *this (#5641)") SECTION("nested past the iterative descent bound aliases *this") { // every depth on either side of where the iterative version takes - // over (detail::recursion_depth_limit(), 128); merging *this into + // over (detail::recursion_depth_limit(), 128 by default); merging *this into // itself is idempotent, aliased or not for (const std::size_t depth : { diff --git a/tests/src/unit-recursion_depth_limit.cpp b/tests/src/unit-recursion_depth_limit.cpp new file mode 100644 index 000000000..cf62d1218 --- /dev/null +++ b/tests/src/unit-recursion_depth_limit.cpp @@ -0,0 +1,614 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// JSON_RECURSION_DEPTH_LIMIT changes only how much call stack the walks over a +// value use, never what they return. This file checks both: the macro is +// honoured (the number of levels the containers handle themselves is the limit) +// and every walk returns what it returns with the default limit. The expected +// results are built here, without the library, so that the same file passes for +// every limit; tests/CMakeLists.txt builds it with limits 0, 1, and 254 besides +// the default. Values are nested up to 300 levels (and past every limit, so that +// the explicit stack of every walk is exercised, even for the limit of 254) and +// are built and taken apart without recursion. Keep the depths moderate: walks +// like diff and dump with indentation are quadratic in the depth. + +namespace +{ +constexpr std::size_t limit() +{ + return nlohmann::detail::recursion_depth_limit(); +} + +// a value nested as @a kinds says, outermost level first: 'a' is an array of +// one element, 'o' an object with the single key "a"; @a leaf is innermost +json build(const std::string& kinds, json leaf) +{ + json j = std::move(leaf); + for (std::size_t i = kinds.size(); i-- > 0;) + { + if (kinds[i] == 'a') + { + json a = json::array(); + a.push_back(std::move(j)); + j = std::move(a); + } + else + { + json o = json::object(); + o["a"] = std::move(j); + j = std::move(o); + } + } + return j; +} + +std::string repeat(const std::string& s, std::size_t n) +{ + std::string result; + for (std::size_t i = 0; i < n; ++i) + { + result += s; + } + return result; +} + +// the nesting of the given shape and depth: arrays, objects, or both alternating +std::string make_kinds(char shape, std::size_t depth) +{ + std::string kinds; + for (std::size_t i = 0; i < depth; ++i) + { + if (shape == 'm') + { + kinds += i % 2 == 0 ? 'a' : 'o'; + } + else + { + kinds += shape; + } + } + return kinds; +} + +std::string expected_compact(const std::string& kinds, const std::string& leaf) +{ + std::string s; + for (const char kind : kinds) + { + s += kind == 'a' ? "[" : "{\"a\":"; + } + s += leaf; + for (std::size_t i = kinds.size(); i-- > 0;) + { + s += kinds[i] == 'a' ? "]" : "}"; + } + return s; +} + +// dump(1) +std::string expected_indented(const std::string& kinds, const std::string& leaf) +{ + std::string s; + for (std::size_t i = 0; i < kinds.size(); ++i) + { + s += kinds[i] == 'a' ? "[\n" : "{\n"; + s += std::string(i + 1, ' '); + if (kinds[i] == 'o') + { + s += "\"a\": "; + } + } + s += leaf; + for (std::size_t i = kinds.size(); i-- > 0;) + { + s += "\n" + std::string(i, ' ') + (kinds[i] == 'a' ? "]" : "}"); + } + return s; +} + +// the JSON pointer to the innermost value +std::string pointer(const std::string& kinds) +{ + std::string s; + for (const char kind : kinds) + { + s += kind == 'a' ? "/0" : "/a"; + } + return s; +} + +// the binary formats: the bytes that open and close a level, and the leaf 1 +std::vector expected_cbor(const std::string& kinds) +{ + std::vector bytes; + for (const char kind : kinds) + { + if (kind == 'a') + { + bytes.push_back(0x81); + } + else + { + bytes.push_back(0xA1); + bytes.push_back(0x61); + bytes.push_back('a'); + } + } + bytes.push_back(0x01); + return bytes; +} + +std::vector expected_msgpack(const std::string& kinds) +{ + std::vector bytes; + for (const char kind : kinds) + { + if (kind == 'a') + { + bytes.push_back(0x91); + } + else + { + bytes.push_back(0x81); + bytes.push_back(0xA1); + bytes.push_back('a'); + } + } + bytes.push_back(0x01); + return bytes; +} + +std::vector expected_ubjson(const std::string& kinds) +{ + std::vector bytes; + for (const char kind : kinds) + { + if (kind == 'a') + { + bytes.push_back('['); + } + else + { + bytes.push_back('{'); + bytes.push_back('i'); + bytes.push_back(1); + bytes.push_back('a'); + } + } + bytes.push_back('i'); + bytes.push_back(1); + for (std::size_t i = kinds.size(); i-- > 0;) + { + bytes.push_back(kinds[i] == 'a' ? ']' : '}'); + } + return bytes; +} + +// the depths worth trying for the limit: either side of it, twice it (if that +// is not too deep to be quick), and a value nested well past all limits +std::vector depths() +{ + std::vector result; + result.push_back(1); + result.push_back(2); + result.push_back(limit() > 0 ? limit() - 1 : 3); + result.push_back(limit()); + result.push_back(limit() + 1); + if ((2 * limit()) + 5 <= 300) + { + result.push_back((2 * limit()) + 5); + } + result.push_back(300); + std::sort(result.begin(), result.end()); + result.erase(std::unique(result.begin(), result.end()), result.end()); + result.erase(std::remove(result.begin(), result.end(), static_cast(0)), result.end()); + return result; +} + +// the call for every shape and depth +void for_each_value(const std::function& check) +{ + const std::string shapes = "aom"; + for (const char shape : shapes) + { + for (const std::size_t depth : depths()) + { + CAPTURE(shape) + CAPTURE(depth) + check(make_kinds(shape, depth)); + } + } +} + +// An array type that counts how many of its copies are being made at once. A +// copy of a value copies one level at a time through the array type until the +// limit has been reached, so the peak is the number of levels that were copied +// with the call stack. +std::size_t copies_going_on = 0; +std::size_t copies_peak = 0; +std::size_t equal_going_on = 0; +std::size_t equal_peak = 0; + +template> +class counting_vector : public std::vector +{ + public: + counting_vector() = default; + + template + counting_vector(InputIt first, InputIt last) : std::vector(first, last) {} + + counting_vector(const counting_vector& other) : std::vector() + { + ++copies_going_on; + copies_peak = (std::max)(copies_peak, copies_going_on); + this->std::vector::operator=(static_cast&>(other)); + --copies_going_on; + } + + counting_vector(counting_vector&&) = default; + counting_vector& operator=(const counting_vector&) = default; + counting_vector& operator=(counting_vector&&) = default; + ~counting_vector() = default; + + friend bool operator==(const counting_vector& lhs, const counting_vector& rhs) + { + ++equal_going_on; + equal_peak = (std::max)(equal_peak, equal_going_on); + const bool result = static_cast&>(lhs) == static_cast&>(rhs); + --equal_going_on; + return result; + } +}; + +using counting_json = json::with_array_t; + +// A base class whose copy constructor copies a whole value of the same +// specialization when it copies a marked element, as user code may do from a +// hook. That copy runs while the copy that it is part of is going on. +struct copy_hook // NOLINT(cppcoreguidelines-special-member-functions,hicpp-special-member-functions) +{ + int mark = 0; + + copy_hook() = default; + copy_hook(const copy_hook& other); + copy_hook(copy_hook&&) = default; + copy_hook& operator=(const copy_hook&) = default; + copy_hook& operator=(copy_hook&&) = default; +}; + +using hook_json = nlohmann::basic_json, copy_hook>; + +const hook_json* hook_copy_source = nullptr; +int hook_copies_going_on = 0; + +copy_hook::copy_hook(const copy_hook& other) : mark(other.mark) +{ + // the value that is copied is marked, too: its copy copies it again, once + if (other.mark == 1 && hook_copy_source != nullptr && hook_copies_going_on < 2) + { + ++hook_copies_going_on; + const hook_json nested = *hook_copy_source; // NOLINT(performance-unnecessary-copy-initialization) + static_cast(nested); + --hook_copies_going_on; + } +} + +// an array nested @a depth levels around the integer 1, which is marked if @a marked +hook_json hook_nested(std::size_t depth, bool marked) +{ + hook_json j = 1; + if (marked) + { + static_cast(j).mark = 1; + } + for (std::size_t i = 0; i < depth; ++i) + { + hook_json a = hook_json::array(); + a.push_back(std::move(j)); + j = std::move(a); + } + return j; +} +} // namespace + +// the macro is the limit, and is in the range the library supports +static_assert(nlohmann::detail::recursion_depth_limit() == static_cast(JSON_RECURSION_DEPTH_LIMIT), + "the library does not use JSON_RECURSION_DEPTH_LIMIT"); +static_assert(nlohmann::detail::recursion_depth_limit() <= 254, "the nesting depth must fit in a byte"); + +// the macro must not be a character or bool: every character type is refused +static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool::value, "bool is refused"); +static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool::value, "char is refused"); +static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool::value, "signed char is refused"); +static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool::value, "unsigned char is refused"); +static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool::value, "wchar_t is refused"); +static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool::value, "char16_t is refused"); +static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool::value, "char32_t is refused"); +#if defined(__cpp_char8_t) + static_assert(nlohmann::detail::recursion_depth_limit_is_char_or_bool::value, "char8_t is refused"); +#endif +static_assert(!nlohmann::detail::recursion_depth_limit_is_char_or_bool::value, "int is accepted"); +static_assert(!nlohmann::detail::recursion_depth_limit_is_char_or_bool::value, "unsigned long long is accepted"); + +TEST_CASE("JSON_RECURSION_DEPTH_LIMIT is honoured") +{ + SECTION("copying and comparing stop descending at the limit") + { + // copy and compare a value nested deeper than the limit, with the array type that counts + counting_json inner = 1; + for (std::size_t i = 0; i < (2 * limit()) + 5; ++i) + { + counting_json a = counting_json::array(); + a.push_back(std::move(inner)); + inner = std::move(a); + } + + copies_going_on = copies_peak = 0; + const counting_json copy = inner; + CHECK(copies_going_on == 0); + + equal_going_on = equal_peak = 0; + CHECK((inner == copy)); + CHECK(equal_going_on == 0); + +#ifdef JSON_NO_THREAD_LOCAL + // without the counters of the levels, no level is descended into + CHECK(copies_peak == 0); + CHECK(equal_peak == 0); +#else + // copying descends into as many levels as the limit says, and nothing further + CHECK(copies_peak == limit()); + CHECK(equal_peak == limit()); +#endif + + // a value that is not nested deeper than the limit is not cut short + const std::size_t shallow = limit() > 0 ? limit() : 1; + counting_json small = 1; + for (std::size_t i = 0; i < shallow; ++i) + { + counting_json a = counting_json::array(); + a.push_back(std::move(small)); + small = std::move(a); + } + copies_going_on = copies_peak = 0; + const counting_json small_copy = small; + CHECK(copies_going_on == 0); +#ifndef JSON_NO_THREAD_LOCAL + CHECK(copies_peak == limit()); +#endif + CHECK(small_copy.dump() == expected_compact(make_kinds('a', shallow), "1")); + } +} + +TEST_CASE("a copy made while a copy is going on is bounded, too") +{ + // The innermost element of the value is marked, and copying it (which happens + // after limit() levels have been entered, when the copy continues without the + // call stack) copies the value again, and that copy once more. Those copies + // must not be given limit() levels of their own: the count of the levels entered + // never exceeds the limit, not even by one, which for the largest limit would + // wrap the byte that holds it around to 0. + const hook_json marked = hook_nested((2 * limit()) + 5, true); + hook_copy_source = &marked; + + copies_going_on = copies_peak = 0; + const hook_json copy = marked; // NOLINT(performance-unnecessary-copy-initialization) + hook_copy_source = nullptr; + CHECK(copies_going_on == 0); + CHECK(static_cast(copy).mark == 0); // (the top level is not marked) + +#ifdef JSON_NO_THREAD_LOCAL + CHECK(copies_peak == 0); +#else + CHECK(copies_peak == limit()); +#endif +} + +TEST_CASE("walks over values nested around the recursion depth limit") +{ + SECTION("copying and comparing") + { + for_each_value([](const std::string & kinds) + { + const json a = build(kinds, 1); + const json b = a; // NOLINT(performance-unnecessary-copy-initialization) + CHECK((a == b)); + CHECK_FALSE((a != b)); + CHECK_FALSE((a < b)); + CHECK((a <= b)); + CHECK((a >= b)); + CHECK_FALSE((a > b)); + + json assigned; + assigned = a; + CHECK((assigned == a)); + + // differing only in the innermost value + const json c = build(kinds, 2); + CHECK((a != c)); + CHECK((a < c)); + CHECK((c > a)); + CHECK_FALSE((c < a)); + CHECK_FALSE((a == c)); + + // a same-shape array or object that is one level shallower + const json shallower = build(kinds.substr(1), 1); + CHECK((shallower != a)); + const json outer = build(kinds.substr(0, 1), 1); + CHECK((outer != a) == (kinds.size() > 1)); + + const json moved = std::move(assigned); + CHECK((moved == a)); + }); + } + + SECTION("serializing") + { + for_each_value([](const std::string & kinds) + { + const json a = build(kinds, 1); + const std::string compact = expected_compact(kinds, "1"); + const std::string indented = expected_indented(kinds, "1"); + + CHECK(a.dump() == compact); + CHECK(a.dump(1) == indented); + + std::ostringstream compact_stream; + compact_stream << a; + CHECK(compact_stream.str() == compact); + std::ostringstream indented_stream; + indented_stream << std::setw(1) << a; + CHECK(indented_stream.str() == indented); + + CHECK((json::parse(compact) == a)); + CHECK((json::parse(indented) == a)); + }); + } + + SECTION("hashing") + { + for_each_value([](const std::string & kinds) + { + const json a = build(kinds, 1); + const std::hash hash; + CHECK(hash(a) == hash(json(a))); + CHECK(hash(a) == hash(json::parse(expected_compact(kinds, "1")))); + CHECK(hash(a) != hash(build(kinds, 2))); + CHECK(hash(a) != hash(build(kinds.substr(1), 1))); + }); + } + + SECTION("diff and patch") + { + for_each_value([](const std::string & kinds) + { + const json a = build(kinds, 1); + const json b = build(kinds, 2); + + const json difference = json::diff(a, b); + CHECK(difference.dump() == "[{\"op\":\"replace\",\"path\":\"" + pointer(kinds) + "\",\"value\":2}]"); + CHECK((a.patch(difference) == b)); + CHECK((json::diff(a, a) == json::array())); + }); + } + + SECTION("flatten and unflatten") + { + for_each_value([](const std::string & kinds) + { + const json a = build(kinds, 1); + const json flat = a.flatten(); + CHECK(flat.dump() == "{\"" + pointer(kinds) + "\":1}"); + CHECK((flat.unflatten() == a)); + }); + } + + SECTION("merge_patch and update") + { + for (const std::size_t depth : depths()) + { + CAPTURE(depth) + const std::string kinds = make_kinds('o', depth); + const std::string all_but_leaf = repeat("{\"a\":", depth - 1); + + // the patch removes the innermost member, or replaces it + json target = build(kinds, 1); + target.merge_patch(build(kinds, json(nullptr))); + CHECK(target.dump() == all_but_leaf + "{}" + repeat("}", depth - 1)); + + target = build(kinds, 1); + target.merge_patch(build(kinds, 2)); + CHECK(target.dump() == expected_compact(kinds, "2")); + + // merging the objects of two values nested alike, and replacing the first level + const json base = build(kinds.substr(1), json::parse("{\"x\":1}")); + const json other = build(kinds.substr(1), json::parse("{\"y\":2}")); + json merged = json{{"a", base}}; + merged.update(json{{"a", other}}, true); + CHECK(merged.dump() == expected_compact(kinds, "{\"x\":1,\"y\":2}")); + + json replaced = json{{"a", base}}; + replaced.update(json{{"a", other}}); + CHECK(replaced.dump() == expected_compact(kinds, "{\"y\":2}")); + + // update from a range + json from_range = json{{"a", base}}; + const json source = json{{"a", other}}; + from_range.update(source.begin(), source.end(), true); + CHECK((from_range == merged)); + } + } + + SECTION("binary formats") + { + for_each_value([](const std::string & kinds) + { + const json a = build(kinds, 1); + + const std::vector cbor = json::to_cbor(a); + CHECK(cbor == expected_cbor(kinds)); + CHECK((json::from_cbor(cbor) == a)); + + const std::vector msgpack = json::to_msgpack(a); + CHECK(msgpack == expected_msgpack(kinds)); + CHECK((json::from_msgpack(msgpack) == a)); + + const std::vector ubjson = json::to_ubjson(a); + CHECK(ubjson == expected_ubjson(kinds)); + CHECK((json::from_ubjson(ubjson) == a)); + + CHECK((json::from_bjdata(json::to_bjdata(a)) == a)); + CHECK((json::from_bon8(json::to_bon8(a)) == a)); + + // BSON documents are objects + const json document = json{{"a", a}}; + CHECK((json::from_bson(json::to_bson(document)) == document)); + }); + } + + SECTION("conversion to another specialization") + { + for_each_value([](const std::string & kinds) + { + const json a = build(kinds, 1); + const nlohmann::ordered_json ordered = a; + CHECK(ordered.dump() == expected_compact(kinds, "1")); + const json back = ordered; + CHECK((back == a)); + }); + } + + SECTION("destroying") + { + for_each_value([](const std::string & kinds) + { + json a = build(kinds, 1); + a = nullptr; + CHECK(a.is_null()); + }); + } +} diff --git a/tests/src/unit-regression1.cpp b/tests/src/unit-regression1.cpp index 367f52dfa..fcb533c5d 100644 --- a/tests/src/unit-regression1.cpp +++ b/tests/src/unit-regression1.cpp @@ -1554,8 +1554,10 @@ TEST_CASE("regression tests 1") SECTION("a missing member of an object") { my_json j = make(); -#ifdef JSON_NO_THREAD_LOCAL - // (set by the user, or by the library for clang with MinGW) +#if defined(JSON_NO_THREAD_LOCAL) || JSON_RECURSION_DEPTH_LIMIT < 2 + // (set by the user, or by the library for clang with MinGW; or fewer than two + // levels are copied by the object type itself, as for a recursion depth limit + // of 0 or 1: the objects below the outermost one are the second level) // the objects below the outermost one are not copied by their // object_t here, but member by member, which keeps their // timestamps correct: "c" is not mistaken for "b" and is inserted From b5967d0ff9e816631824edf7d49f4e7096be651c Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 11 Nov 2026 22:32:29 +0100 Subject: [PATCH 07/21] Export the editing API of json_view from the C++20 module (#6090) Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/features/modules.md | 7 +++++++ src/modules/json.cppm | 6 ++++++ tests/module_cpp20/main.cpp | 19 ++++++++++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/mkdocs/docs/features/modules.md b/docs/mkdocs/docs/features/modules.md index 780809f08..9b3b7acd0 100644 --- a/docs/mkdocs/docs/features/modules.md +++ b/docs/mkdocs/docs/features/modules.md @@ -36,15 +36,21 @@ Only the following symbols are exported from `nlohmann.json`: - `nlohmann::basic_json` - `nlohmann::basic_json_document` - `nlohmann::basic_json_view` +- `nlohmann::is_json_view` - `nlohmann::json` - `nlohmann::json_document` +- `nlohmann::json_editable_document` +- `nlohmann::json_editable_view` - `nlohmann::json_pointer` - `nlohmann::json_view` - `nlohmann::ordered_json` - `nlohmann::ordered_json_document` +- `nlohmann::ordered_json_editable_document` +- `nlohmann::ordered_json_editable_view` - `nlohmann::ordered_json_view` - `nlohmann::ordered_map` - `nlohmann::to_string` +- `nlohmann::view_mode` - `nlohmann::literals::json_literals::operator""_json` - `nlohmann::literals::json_literals::operator""_json_pointer` @@ -56,6 +62,7 @@ Additionally, the following `nlohmann::detail` symbols are exported, solely to w and should not be used directly: - `nlohmann::detail::json_sax_dom_callback_parser` +- `nlohmann::detail::json_sax_dom_parser` - `nlohmann::detail::unknown_size` ## Known issues diff --git a/src/modules/json.cppm b/src/modules/json.cppm index 3917e0722..1108113bf 100644 --- a/src/modules/json.cppm +++ b/src/modules/json.cppm @@ -36,15 +36,21 @@ using NLOHMANN_JSON_NAMESPACE::adl_serializer; using NLOHMANN_JSON_NAMESPACE::basic_json; using NLOHMANN_JSON_NAMESPACE::basic_json_document; using NLOHMANN_JSON_NAMESPACE::basic_json_view; +using NLOHMANN_JSON_NAMESPACE::is_json_view; using NLOHMANN_JSON_NAMESPACE::json; using NLOHMANN_JSON_NAMESPACE::json_document; +using NLOHMANN_JSON_NAMESPACE::json_editable_document; +using NLOHMANN_JSON_NAMESPACE::json_editable_view; using NLOHMANN_JSON_NAMESPACE::json_pointer; using NLOHMANN_JSON_NAMESPACE::json_view; using NLOHMANN_JSON_NAMESPACE::ordered_json; using NLOHMANN_JSON_NAMESPACE::ordered_json_document; +using NLOHMANN_JSON_NAMESPACE::ordered_json_editable_document; +using NLOHMANN_JSON_NAMESPACE::ordered_json_editable_view; using NLOHMANN_JSON_NAMESPACE::ordered_json_view; using NLOHMANN_JSON_NAMESPACE::ordered_map; using NLOHMANN_JSON_NAMESPACE::to_string; +using NLOHMANN_JSON_NAMESPACE::view_mode; inline namespace literals { diff --git a/tests/module_cpp20/main.cpp b/tests/module_cpp20/main.cpp index 4e492cef9..a475c8bff 100644 --- a/tests/module_cpp20/main.cpp +++ b/tests/module_cpp20/main.cpp @@ -56,9 +56,26 @@ int main() const nlohmann::json_document doc = nlohmann::json_document::parse(R"({"a": 1, "list": [1, 2, 3]})"); const std::size_t doc_size = doc.root().size(); + // editable documents and views, view_mode, is_json_view and the ordered variants + nlohmann::json_editable_document edoc = nlohmann::json_editable_document::parse(R"({"a": 1, "list": [1, 2, 3]})"); + edoc.set(edoc.root(), "b", 2); + edoc.push_back(edoc.root()["list"], 4); + const nlohmann::json_editable_view eroot = edoc.root(); + const std::size_t edit_size = eroot.size(); + const std::size_t list_size = eroot["list"].size(); + nlohmann::ordered_json_editable_document oedoc = nlohmann::ordered_json_editable_document::parse(R"({"z": 1, "y": 2})"); + oedoc.erase(oedoc.root(), "z"); + const nlohmann::ordered_json_editable_view oeroot = oedoc.root(); + nlohmann::basic_json_document mdoc = nlohmann::basic_json_document::parse("[]"); + mdoc.push_back(mdoc.root(), 1); + static_assert(nlohmann::is_json_view::value, "json_view is a view"); + static_assert(nlohmann::is_json_view::value, "json_editable_view is a view"); + static_assert(!nlohmann::is_json_view::value, "json is not a view"); + // use every result so the references cannot be optimized away return (a == 1 && last == 3 && b == 2 && lit.size() == 3 && m.size() == 1 && !dumped.empty() && !os.str().empty() - && doc_size == 2) + && doc_size == 2 + && edit_size == 3 && list_size == 4 && oeroot.size() == 1 && mdoc.root().size() == 1) ? 0 : 1; } From abab6f2f766e072f5435827eff57dfee1f497ed1 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 11 Nov 2026 22:32:50 +0100 Subject: [PATCH 08/21] docs: qualify push_back({value}) and += {value} brace-init behavior (#6091) Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/operator+=.md | 10 ++++++ docs/mkdocs/docs/api/basic_json/push_back.md | 10 ++++++ .../macros/json_brace_init_copy_semantics.md | 10 ++++-- tests/src/unit-brace-init-copy-semantics.cpp | 31 +++++++++++++++++++ 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/operator+=.md b/docs/mkdocs/docs/api/basic_json/operator+=.md index 73dc0e260..4baa2d11a 100644 --- a/docs/mkdocs/docs/api/basic_json/operator+=.md +++ b/docs/mkdocs/docs/api/basic_json/operator+=.md @@ -27,6 +27,16 @@ reference operator+=(initializer_list_t init); `init` is converted into an object element and added using `operator+=(const typename object_t::value_type&)`. Otherwise, `init` is converted to a JSON value and added using `operator+=(basic_json&&)`. + !!! note "Single braced JSON value" + + For a call such as `#!cpp arr += {obj}`, where the initializer list contains a single JSON value `obj`, + compilers may disagree unless + [`JSON_BRACE_INIT_COPY_SEMANTICS`](../macros/json_brace_init_copy_semantics.md) is defined to `1`: GCC, for + example, selects this overload and appends a single-element array `[obj]`, whereas Clang selects the + `basic_json` overload and appends a copy of `obj`. With the macro, GCC and Clang both append a copy of `obj`. + To get a portable result without the macro, write `#!cpp arr += obj` to append a copy, or + `#!cpp arr += json::array({obj})` to append a single-element array. + ## Iterator invalidation For all cases where an element is added to an **array**, a reallocation can happen, in which case all iterators (including diff --git a/docs/mkdocs/docs/api/basic_json/push_back.md b/docs/mkdocs/docs/api/basic_json/push_back.md index 4bc885277..16e23b344 100644 --- a/docs/mkdocs/docs/api/basic_json/push_back.md +++ b/docs/mkdocs/docs/api/basic_json/push_back.md @@ -27,6 +27,16 @@ void push_back(initializer_list_t init); `init` is converted into an object element and added using `push_back(const typename object_t::value_type&)`. Otherwise, `init` is converted to a JSON value and added using `push_back(basic_json&&)`. + !!! note "Single braced JSON value" + + For a call such as `#!cpp arr.push_back({obj})`, where the initializer list contains a single JSON value `obj`, + compilers may disagree unless + [`JSON_BRACE_INIT_COPY_SEMANTICS`](../macros/json_brace_init_copy_semantics.md) is defined to `1`: GCC, for + example, selects this overload and appends a single-element array `[obj]`, whereas Clang selects the + `basic_json` overload and appends a copy of `obj`. With the macro, GCC and Clang both append a copy of `obj`. + To get a portable result without the macro, write `#!cpp arr.push_back(obj)` to append a copy, or + `#!cpp arr.push_back(json::array({obj}))` to append a single-element array. + ## Iterator invalidation For all cases where an element is added to an **array**, a reallocation can happen, in which case all iterators (including diff --git a/docs/mkdocs/docs/api/macros/json_brace_init_copy_semantics.md b/docs/mkdocs/docs/api/macros/json_brace_init_copy_semantics.md index 796b20c6b..daa24ec87 100644 --- a/docs/mkdocs/docs/api/macros/json_brace_init_copy_semantics.md +++ b/docs/mkdocs/docs/api/macros/json_brace_init_copy_semantics.md @@ -27,9 +27,13 @@ The default value is `0` (disabled — existing behavior is preserved). json j{obj}; ``` - creates a single-element **array** `[{"key":"value"}]` instead of a copy of `obj`. This behavior is - compiler-dependent for older compilers (GCC wrapped, Clang did not), but starting from Clang 20, both compilers - behave the same way. + creates a single-element **array** `[{"key":"value"}]` instead of a copy of `obj`. For this constructor, the + behavior was compiler-dependent for older compilers (GCC wrapped, Clang did not), but starting from Clang 20, both + compilers behave the same way. + + The calls `#!cpp arr.push_back({obj})` and `#!cpp arr += {obj}` are still compiler-dependent without the macro, + even with Clang 20 and later: GCC converts `{obj}` to a single-element array and appends that array, whereas Clang + appends a copy of `obj`. With the macro defined to `1`, both compilers append a copy of `obj`. Enabling this macro opts into copy/move semantics for this case (see [#5074](https://github.com/nlohmann/json/issues/5074)). diff --git a/tests/src/unit-brace-init-copy-semantics.cpp b/tests/src/unit-brace-init-copy-semantics.cpp index e3406758b..7faf58e5f 100644 --- a/tests/src/unit-brace-init-copy-semantics.cpp +++ b/tests/src/unit-brace-init-copy-semantics.cpp @@ -94,6 +94,37 @@ TEST_CASE("JSON_BRACE_INIT_COPY_SEMANTICS") CHECK(j3 == pair_shaped); } + SECTION("push_back and operator+= with a single braced JSON value") + { + // with the macro, the result does not depend on the compiler; + // without it, GCC wraps a JSON value and Clang does not (a literal + // such as {1} is wrapped by both) + json const obj = {{"key", "value"}}; + json const arr = {1, 2}; + + json a1 = json::array(); + a1.push_back({obj}); + CHECK(a1 == json::array({obj})); + + json a2 = json::array(); + a2 += {obj}; + CHECK(a2 == json::array({obj})); + + json b1 = json::array(); + b1.push_back({arr}); + CHECK(b1 == json::array({arr})); + + json b2 = json::array(); + b2 += {arr}; + CHECK(b2 == json::array({arr})); + + // a literal is copied as well + json c = json::array(); + c.push_back({1}); + c += {2}; + CHECK(c == json::array({1, 2})); + } + SECTION("what the macro does not change") { // lists with more than one element are unaffected From a7b377e9ecb6d4503788b8163fb95a813bd843e8 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 11 Nov 2026 22:33:11 +0100 Subject: [PATCH 09/21] docs: state what the converting constructor does on a string/key mismatch (#6092) Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/basic_json.md | 16 +++++++++--- docs/mkdocs/docs/api/basic_json/object_t.md | 9 ++++--- docs/mkdocs/docs/api/basic_json/string_t.md | 8 +++--- .../features/types/template_parameters.md | 25 +++++++++++-------- 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/basic_json.md b/docs/mkdocs/docs/api/basic_json/basic_json.md index dadb2d887..f5c861712 100644 --- a/docs/mkdocs/docs/api/basic_json/basic_json.md +++ b/docs/mkdocs/docs/api/basic_json/basic_json.md @@ -93,10 +93,18 @@ basic_json(basic_json&& other) noexcept; The constructor tries to convert the internal `m_value` of the parameter. Each member value (object, array, string, etc.) is serialized via the corresponding `to_json()` overload. For objects and strings, the conversion requires that the *target* `basic_json` type's `object_t::key_type` (or `string_t`) be directly constructible from the - *source* type's corresponding member type via `is_constructible`. If this requirement is not met, the conversion - does not fail to compile; instead, it silently falls back to the array-conversion path, which represents objects - as arrays of `[key, value]` pairs and strings as arrays of character codes. This is a known limitation tracked in - [issue #3425](https://github.com/nlohmann/json/issues/3425). + *source* type's corresponding member type via `is_constructible`. If this requirement is not met, the result + depends on the *source* string type: + + - If it is a range of characters (it has `begin()` and `end()`, as `std::string` does), the conversion falls back to + the array-conversion path, which represents objects as arrays of `[key, value]` pairs and strings as arrays of + character codes. This result has a different type than the source value, so the constructor's + [`JSON_ASSERT`](../macros/json_assert.md) fails; with its default definition, `assert`, the program aborts unless + `NDEBUG` is defined, in which case the array is returned. + - Otherwise, the conversion does not compile, even though `#!cpp std::is_constructible` reports `#!cpp true`; for + example, when converting a `basic_json` whose `string_t` is the `alt_string` of the unit tests to `json`. + + This is a known limitation tracked in [issue #3425](https://github.com/nlohmann/json/issues/3425). 5. Creates a JSON value of type array or object from the passed initializer list `init`. In case `type_deduction` is `#!cpp true` (default), the type of the JSON value to be created is deducted from the initializer list `init` diff --git a/docs/mkdocs/docs/api/basic_json/object_t.md b/docs/mkdocs/docs/api/basic_json/object_t.md index 835d7b185..f75bbeec1 100644 --- a/docs/mkdocs/docs/api/basic_json/object_t.md +++ b/docs/mkdocs/docs/api/basic_json/object_t.md @@ -102,9 +102,12 @@ objects. When converting an object from one `basic_json` specialization to another via the [converting constructor](basic_json.md) (overload 4), the target `object_t`'s `key_type` must be -directly constructible from the source `basic_json`'s `string_t` type (or more generally, from the -source object's key type). If this requirement is not met, the conversion does not fail; instead, -the object is silently converted as an array of key-value pairs, which is incorrect. See +directly constructible from the source object's key type (with the default `std::map`, the source `basic_json`'s +`string_t`). If this requirement is not met and the source string type is a range of characters, the +object is converted as an array of key-value pairs, which is incorrect: the converting constructor's +[`JSON_ASSERT`](../macros/json_assert.md) fails; with its default definition, `assert`, the program aborts unless +`NDEBUG` is defined, in which case the array is returned. If the source string type is not a range of characters, the +conversion does not compile at all. See [`basic_json`](basic_json.md) and [issue #3425](https://github.com/nlohmann/json/issues/3425) for details and an example. ## Examples diff --git a/docs/mkdocs/docs/api/basic_json/string_t.md b/docs/mkdocs/docs/api/basic_json/string_t.md index 1e3a4b2b5..27f0a3aa2 100644 --- a/docs/mkdocs/docs/api/basic_json/string_t.md +++ b/docs/mkdocs/docs/api/basic_json/string_t.md @@ -60,9 +60,11 @@ String values are stored as pointers in a `basic_json` type. That is, for any ac When converting a string value from one `basic_json` specialization to another via the [converting constructor](basic_json.md) (overload 4), the target `string_t` must be directly constructible from the source `basic_json`'s `string_t` type. If this requirement is not met, the -conversion does not fail; instead, the string is silently converted as an array of character codes, -which is incorrect. See [issue #3425](https://github.com/nlohmann/json/issues/3425) for details -and an example. +conversion depends on the source string type. If it is a range of characters, the string is converted as an array of +character codes, which is incorrect: the converting constructor's [`JSON_ASSERT`](../macros/json_assert.md) fails; with +its default definition, `assert`, the program aborts unless `NDEBUG` is defined, in which case the array is returned. +If the source string type is not a range of characters, the conversion does not compile at all. See +[issue #3425](https://github.com/nlohmann/json/issues/3425) for details and an example. ## Examples diff --git a/docs/mkdocs/docs/features/types/template_parameters.md b/docs/mkdocs/docs/features/types/template_parameters.md index 6ae6820f5..9c3479ab0 100644 --- a/docs/mkdocs/docs/features/types/template_parameters.md +++ b/docs/mkdocs/docs/features/types/template_parameters.md @@ -28,15 +28,16 @@ Requirements are split into two groups: [`BinaryType`](#binarytype)'s `value_type`, and [`NumberUnsignedType`](#numberintegertype-and-numberunsignedtype) being at least as wide as [`NumberIntegerType`](#numberintegertype-and-numberunsignedtype). The rest are not diagnosed with dedicated error messages, and violating most of them results in a compiler error somewhere inside - the library. Four violations are not caught at compile time at all: + the library. Four violations are not (always) caught at compile time: - A [`StringType`](#stringtype) whose `data()` is not null-terminated compiles and silently misparses numbers stored as a `#!cpp long double` that is not IEEE 754 binary64 (e.g., the 80-bit x87 format), because the lexer hands the buffer to `#!cpp std::strtold`. - A stateful [`AllocatorType`](#allocatortype) compiles and silently ignores its state: allocation, deallocation, and [`get_allocator()`](../../api/basic_json/get_allocator.md) each use a different default-constructed instance. - - The two [cross-specialization conversions](#cross-specialization-conversions) below. These abort on an assertion - in a normal build, and only fail silently under `#!cpp NDEBUG`. + - The two [cross-specialization conversions](#cross-specialization-conversions) below, when the source string type + is a range of characters. They compile and fail the `#!cpp JSON_ASSERT`: with its default definition, the program + aborts unless `#!cpp NDEBUG` is defined, in which case the wrong result is returned silently. ## Overview @@ -765,14 +766,18 @@ See [`json_base_class_t`](../../api/basic_json/json_base_class_t.md) for an exam ## Cross-specialization conversions Converting a value from one `basic_json` specialization into another (see the -[converting constructor](../../api/basic_json/basic_json.md)) imposes two additional requirements that are not -diagnosed at compile time. With assertions enabled they abort on the `#!cpp JSON_ASSERT` at the end of the converting -constructor; under `#!cpp NDEBUG` they fail **silently** at runtime: +[converting constructor](../../api/basic_json/basic_json.md)) imposes two additional requirements: -- The target `string_t` must be directly constructible from the source `string_t`. Otherwise the string is converted to - an array of character codes. -- The target `object_t::key_type` must be directly constructible from the source object's key type. Otherwise the - object is converted to an array of key/value pairs. +- The target `string_t` must be directly constructible from the source `string_t`. +- The target `object_t::key_type` must be directly constructible from the source object's key type. + +If a requirement is not met, the result depends on the source string type. If it is a range of characters (it has +`begin()` and `end()`, as `std::string` does), the conversion compiles and then fails the `#!cpp JSON_ASSERT` at the +end of the converting constructor: with its default definition, `assert`, the program aborts unless `#!cpp NDEBUG` is +defined, in which case it **silently** returns an array (of character codes for a string, of key/value pairs for an +object). Otherwise, the conversion does not compile, even though +`#!cpp std::is_constructible` reports `#!cpp true`; for example, when converting a `basic_json` whose `string_t` is the +`alt_string` of the unit tests to `json`. See [issue #3425](https://github.com/nlohmann/json/issues/3425), [`string_t`](../../api/basic_json/string_t.md), and [`object_t`](../../api/basic_json/object_t.md). From 94331d1050a1294cd4ed8b1fec157f250282640d Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 11 Nov 2026 22:36:17 +0100 Subject: [PATCH 10/21] Document that read() must not be given a view of the document itself (#6086) * Document that read() must not be given a view of the document itself basic_json_document::read() releases the previous text, the decoded strings and the edits before it parses its input. An input that points into them (source(), get_string(), number_token(), or a string of an edited document) is read after its memory was cleared or freed. A guard that copies such input costs about 1 ns per read() (5 % of a read of a 7-byte input), which this API exists to avoid, so state the precondition in read.md, lifetime.md and the pages of the views that hand out such strings instead, and show how to copy the string first. Signed-off-by: Niels Lohmann * Assert in debug builds that read() is not given its own text read() releases the document's text, decoded strings, owned image and edits before it parses, so an input that points into them is read after release. Without NDEBUG, read() now asserts that a borrowed input, a copied contiguous input and (before C++17) a string_ref input are not held by the document: its owned text, its decoded strings, an owned image, and the current buffer of edited strings. Release builds keep the documented precondition without a check, which would cost 2-4.5 % per read() of a small message. JSON_ASSERT is undefined at the end of json.hpp, so the view uses assert() through a new internal NLOHMANN_VIEW_ASSERT. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- .../docs/api/basic_json_document/index.md | 5 +- .../docs/api/basic_json_document/read.md | 28 ++++++++ .../docs/api/basic_json_document/source.md | 3 + .../docs/api/basic_json_view/get_string.md | 4 ++ .../docs/api/basic_json_view/number_token.md | 11 ++- .../docs/features/json_view/lifetime.md | 7 ++ .../nlohmann/detail/view/document_data.hpp | 18 +++++ include/nlohmann/detail/view/macro_scope.hpp | 5 ++ .../nlohmann/detail/view/macro_unscope.hpp | 1 + include/nlohmann/json_view.hpp | 23 +++++++ single_include/nlohmann/json_view.hpp | 47 +++++++++++++ tests/src/unit-json_view_builder.cpp | 68 +++++++++++++++++++ 12 files changed, 215 insertions(+), 5 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json_document/index.md b/docs/mkdocs/docs/api/basic_json_document/index.md index 9b1cb35d7..3d7f41aad 100644 --- a/docs/mkdocs/docs/api/basic_json_document/index.md +++ b/docs/mkdocs/docs/api/basic_json_document/index.md @@ -111,8 +111,9 @@ points apply to every edit: the end, and, with [`number_format::source`](../basic_json_view/number_format.md), keeps the spelling of every number that was not itself edited — see [Editing a document](../../features/json_view/editing.md) for why this matters. -- [`read()`](read.md) discards all edits, [`shrink_to_fit()`](shrink_to_fit.md) does not move the node index once - there are edits, and [`memory_usage()`](memory_usage.md) includes the memory edits use. +- [`read()`](read.md) discards all edits (so its `input` must not be a string obtained from this document), + [`shrink_to_fit()`](shrink_to_fit.md) does not move the node index once there are edits, and + [`memory_usage()`](memory_usage.md) includes the memory edits use. [`source_offset()`](../basic_json_view/source_offset.md) of a value introduced by an edit is `#!cpp static_cast(-1)`, the same value it reports for a decoded string. diff --git a/docs/mkdocs/docs/api/basic_json_document/read.md b/docs/mkdocs/docs/api/basic_json_document/read.md index 1552c1007..de3246f6d 100644 --- a/docs/mkdocs/docs/api/basic_json_document/read.md +++ b/docs/mkdocs/docs/api/basic_json_document/read.md @@ -46,6 +46,34 @@ Linear in the length of the input. Every view taken from `#!cpp *this` before the call — including the previous [`root()`](root.md) — is invalidated, whether or not the new parse succeeds; take fresh views from [`root()`](root.md) afterward. +!!! warning "`input` must not point into the document itself" + + `read()` releases the document's previous text, its decoded strings, and (for an editable document) its edits + *before* it parses `input`. An `input` that is, or points into, any of them violates a precondition: it may seem to + work, parse empty or garbled text, be reported by a sanitizer, or read freed memory (undefined behavior). + + This applies to every `string_view` obtained from the document whose characters live in memory that `read()` + releases: + + - [`source()`](source.md) of a document that owns its text; + - [`get_string()`](../basic_json_view/get_string.md) (also as `get()` or a `string_view` member of a + decoded struct) of a string with escapes, of a string of a document that owns its text, and of an edited string; + - [`number_token()`](../basic_json_view/number_token.md) of a number of a document that owns its text, and of a + number written by an edit; + - the key of an object member (`key()`) if it has escapes, was written by an edit, or the document owns its text. + + It does not apply to a text that the caller owns and the document borrows, nor to the strings, keys, and tokens that + are read from such a text unchanged, nor to the decoded strings of a borrowed [image](load.md), which live in the + caller's image: `read()` never touches them. To parse JSON that a document holds in a string of its own (for + example, JSON embedded in a string value), copy the string first, for example with + `#!cpp std::string inner(doc.root()["payload"].get_string()); doc.read(inner);`. + + A debug build (one without `NDEBUG`) checks this with `assert()`: an `input` in the document's text, decoded + strings, or image, or in the most recent buffer of strings written by edits, ends the program. (Strings written + by earlier edits are not covered.) A release build does not check, to keep the cost of reusing a document at a + minimum. The check uses `assert()` directly, not [`JSON_ASSERT`](../macros/json_assert.md), which `json.hpp` + undefines at its end. + `input` is borrowed or owned by the same rules as [`parse()`](parse.md#notes); a document can borrow on one call and own on the next, since ownership is decided freshly each time. An input that is copied (see the table in [`parse()`](parse.md#notes)) is written into the document's previous owned copy, reusing its memory; a moved-in diff --git a/docs/mkdocs/docs/api/basic_json_document/source.md b/docs/mkdocs/docs/api/basic_json_document/source.md index a0c865195..860315944 100644 --- a/docs/mkdocs/docs/api/basic_json_document/source.md +++ b/docs/mkdocs/docs/api/basic_json_document/source.md @@ -24,6 +24,9 @@ Constant. For a borrowed document, `source()` points directly into the caller's buffer, so it is only valid while that buffer is; see [`owns_source`](owns_source.md). +The text of a document that owns it must not be passed to [`read()`](read.md) of the same document: `read()` releases +an owned text before it parses its input. Copy it first. (A borrowed text belongs to the caller and is not affected.) + ## Examples ??? example diff --git a/docs/mkdocs/docs/api/basic_json_view/get_string.md b/docs/mkdocs/docs/api/basic_json_view/get_string.md index 542ce4688..d17d80855 100644 --- a/docs/mkdocs/docs/api/basic_json_view/get_string.md +++ b/docs/mkdocs/docs/api/basic_json_view/get_string.md @@ -46,6 +46,10 @@ The returned [`string_view_t`](index.md#member-types) is valid exactly as long a source text, which for a borrowed document must remain valid **and unmodified** for as long as the document exists, since floating-point values and unescaped strings are read from the source lazily. +A string with escapes, a string of a document that owns its text, and an edited string must not be passed to +[`read()`](../basic_json_document/read.md) of the document it came from: `read()` releases them before it parses its +input. Copy the string first. (A string read unchanged from a borrowed text is not affected.) + ## Examples ??? example diff --git a/docs/mkdocs/docs/api/basic_json_view/number_token.md b/docs/mkdocs/docs/api/basic_json_view/number_token.md index 8ee8ce348..4811b65f7 100644 --- a/docs/mkdocs/docs/api/basic_json_view/number_token.md +++ b/docs/mkdocs/docs/api/basic_json_view/number_token.md @@ -38,9 +38,14 @@ by the grammar; trailing zeros in the fraction; the case and sign of the exponen useful precisely where that form must survive — a price or an identifier that must be reproduced exactly, or a number too large for any of `BasicJsonType`'s number types to hold without loss. -The returned [`string_view_t`](index.md#member-types) always points into the document's -[`source()`](../basic_json_document/source.md) text — numbers are never decoded into the document's separate string -buffer — and is valid exactly as long as that text is, see the [validity rules](index.md) of `basic_json_view`. +The returned [`string_view_t`](index.md#member-types) points into the document's +[`source()`](../basic_json_document/source.md) text or, for a number written by an +[edit](../basic_json_document/index.md#edits), into storage owned by the document (the token of an edited `NaN` is +`nan`). It is valid exactly as long as that text or storage is, see the [validity rules](index.md) of +`basic_json_view`. Numbers are never decoded into the document's separate string buffer. + +For the same reason, the token must not be passed to [`read()`](../basic_json_document/read.md) of the document it came +from if that document owns its text or the number was written by an edit: `read()` releases both before it parses. ## Examples diff --git a/docs/mkdocs/docs/features/json_view/lifetime.md b/docs/mkdocs/docs/features/json_view/lifetime.md index 2ecc8b711..4bb01a743 100644 --- a/docs/mkdocs/docs/features/json_view/lifetime.md +++ b/docs/mkdocs/docs/features/json_view/lifetime.md @@ -50,6 +50,13 @@ The same holds for what a view hands out without copying: a string from [`number_token()`](../../api/basic_json_view/number_token.md), and [`string_view` members](reading.md#string_view-members) of decoded structs are valid only as long as the document and, if it borrows, its source text. +This includes [`read()`](../../api/basic_json_document/read.md): it releases the document's text, decoded strings, and +edits before it parses its input, so the input must not be a `string_view` obtained from the document itself: its +[`source()`](../../api/basic_json_document/source.md), a string from `get_string()`, a token from `number_token()`, or a +key from an iterator (those of a borrowed text that the caller owns, read unchanged, are not affected). To re-parse a +string that the document holds, for example JSON embedded in a string value, copy the string into a `#!cpp std::string` +first and pass the copy; see [`read()`](../../api/basic_json_document/read.md#notes) for the exact list. + ??? example "Example: borrowed and owned documents, and when views become invalid" ```cpp diff --git a/include/nlohmann/detail/view/document_data.hpp b/include/nlohmann/detail/view/document_data.hpp index 2c5f97767..c1e179e93 100644 --- a/include/nlohmann/detail/view/document_data.hpp +++ b/include/nlohmann/detail/view/document_data.hpp @@ -12,6 +12,7 @@ #include // size_t #include // uint8_t, uint32_t #include // memcmp, memcpy +#include // less #include // numeric_limits #include // map #include // unique_ptr @@ -149,6 +150,23 @@ struct document_data node_capacity = inline_capacity; } + /// whether the n bytes at p overlap memory that the next parse releases or + /// reuses: the owned text, the decoded strings, an owned image, and the + /// current buffer of strings written by edits (earlier edit buffers are not + /// covered, as their sizes are not kept); for the debug check in read() + bool holds(const char* p, std::size_t n) const noexcept + { + const auto overlaps = [p, n](const char* b, std::size_t m) noexcept + { + // (std::less: a total order also for pointers into different objects) + return n != 0 && m != 0 && std::less()(p, b + m) && std::less()(b, p + n); + }; + return overlaps(owned.data(), owned.capacity()) + || overlaps(arena.data(), arena.capacity()) + || overlaps(reinterpret_cast(owned_image.data()), owned_image.capacity()) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + || (edits && !edits->texts.empty() && overlaps(edits->texts.back().get(), edits->text_cap)); + } + /// the largest node count whose size in bytes fits a std::size_t static constexpr std::size_t max_nodes() noexcept { diff --git a/include/nlohmann/detail/view/macro_scope.hpp b/include/nlohmann/detail/view/macro_scope.hpp index f19492648..7340181d7 100644 --- a/include/nlohmann/detail/view/macro_scope.hpp +++ b/include/nlohmann/detail/view/macro_scope.hpp @@ -71,6 +71,11 @@ #define NLOHMANN_VIEW_THROW JSON_THROW_USER #endif +// debug checks of preconditions (assert(), off with NDEBUG): JSON_ASSERT is +// one of the macros that json.hpp undefines at its end +#include // assert +#define NLOHMANN_VIEW_ASSERT(x) assert(x) + // the parser stores a node's first word at once where the layout of `node` is // known to be little-endian (MSVC targets are); elsewhere field by field #if (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) || defined(_MSC_VER) diff --git a/include/nlohmann/detail/view/macro_unscope.hpp b/include/nlohmann/detail/view/macro_unscope.hpp index 0c4fab4d2..e0fb6554b 100644 --- a/include/nlohmann/detail/view/macro_unscope.hpp +++ b/include/nlohmann/detail/view/macro_unscope.hpp @@ -18,6 +18,7 @@ #undef NLOHMANN_VIEW_NOCLONE #undef NLOHMANN_VIEW_NODISCARD #undef NLOHMANN_VIEW_THROW +#undef NLOHMANN_VIEW_ASSERT #undef NLOHMANN_VIEW_LITTLE_ENDIAN #undef NLOHMANN_VIEW_REPEAT16 #undef NLOHMANN_VIEW_REPEAT_SECOND_8 diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 64aa8f723..b922d82dd 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -1531,6 +1531,10 @@ class basic_json_document /// sentinel: src[size] is readable and 0 (std::string, C strings) void build(const char* src, std::size_t size, on_error allow_exceptions, bool comments, bool trailing_commas, bool owned, bool sentinel) { + // precondition of read(): a borrowed input is not held by this document, + // which releases what it holds below (checked in debug builds only, as + // it would cost every read() of a small message a few percent) + NLOHMANN_VIEW_ASSERT(owned || !m_data || !m_data->holds(src, size)); ensure_data(src, size); document_data& d = *m_data; if (!owned) @@ -1620,6 +1624,10 @@ class basic_json_document template void read_kind(const T& s, on_error ae, bool c, bool tc, std::integral_constant /*unused*/) { + // (the precondition of read() as for a borrowed input, although only an + // input in the owned text breaks a copy: the same call borrows, for + // example a std::string_view in C++17, and must not depend on that) + NLOHMANN_VIEW_ASSERT(!m_data || !m_data->holds(reinterpret_cast(s.data()), static_cast(s.size()))); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) build_copy(s, ae, c, tc); } @@ -1638,9 +1646,24 @@ class basic_json_document template void read_kind(T&& input, on_error ae, bool c, bool tc, std::integral_constant /*unused*/) { + NLOHMANN_VIEW_ASSERT(!held(input)); // (see the copy_range overload) build_copy(std::forward(input), ae, c, tc); } + /// whether an input read through the input adapter is held by this document: + /// string_view_t before C++17 (a string handed out by a view), for the + /// debug check of read(); other inputs of this kind do not point into it + bool held(const detail::view::string_ref& s) const noexcept + { + return m_data && m_data->holds(s.data(), s.size()); + } + + template + bool held(const T& /*input*/) const noexcept + { + return false; + } + template void read_range(IteratorType first, IteratorType last, on_error ae, bool c, bool tc) { diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index f69d1ae43..b2523284d 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -106,6 +106,7 @@ #include // size_t #include // uint8_t, uint32_t #include // memcmp, memcpy +#include // less #include // numeric_limits #include // map #include // unique_ptr @@ -188,6 +189,11 @@ #define NLOHMANN_VIEW_THROW JSON_THROW_USER #endif +// debug checks of preconditions (assert(), off with NDEBUG): JSON_ASSERT is +// one of the macros that json.hpp undefines at its end +#include // assert +#define NLOHMANN_VIEW_ASSERT(x) assert(x) + // the parser stores a node's first word at once where the layout of `node` is // known to be little-endian (MSVC targets are); elsewhere field by field #if (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) || defined(_MSC_VER) @@ -488,6 +494,23 @@ struct document_data node_capacity = inline_capacity; } + /// whether the n bytes at p overlap memory that the next parse releases or + /// reuses: the owned text, the decoded strings, an owned image, and the + /// current buffer of strings written by edits (earlier edit buffers are not + /// covered, as their sizes are not kept); for the debug check in read() + bool holds(const char* p, std::size_t n) const noexcept + { + const auto overlaps = [p, n](const char* b, std::size_t m) noexcept + { + // (std::less: a total order also for pointers into different objects) + return n != 0 && m != 0 && std::less()(p, b + m) && std::less()(b, p + n); + }; + return overlaps(owned.data(), owned.capacity()) + || overlaps(arena.data(), arena.capacity()) + || overlaps(reinterpret_cast(owned_image.data()), owned_image.capacity()) // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + || (edits && !edits->texts.empty() && overlaps(edits->texts.back().get(), edits->text_cap)); + } + /// the largest node count whose size in bytes fits a std::size_t static constexpr std::size_t max_nodes() noexcept { @@ -11630,6 +11653,10 @@ class basic_json_document /// sentinel: src[size] is readable and 0 (std::string, C strings) void build(const char* src, std::size_t size, on_error allow_exceptions, bool comments, bool trailing_commas, bool owned, bool sentinel) { + // precondition of read(): a borrowed input is not held by this document, + // which releases what it holds below (checked in debug builds only, as + // it would cost every read() of a small message a few percent) + NLOHMANN_VIEW_ASSERT(owned || !m_data || !m_data->holds(src, size)); ensure_data(src, size); document_data& d = *m_data; if (!owned) @@ -11719,6 +11746,10 @@ class basic_json_document template void read_kind(const T& s, on_error ae, bool c, bool tc, std::integral_constant /*unused*/) { + // (the precondition of read() as for a borrowed input, although only an + // input in the owned text breaks a copy: the same call borrows, for + // example a std::string_view in C++17, and must not depend on that) + NLOHMANN_VIEW_ASSERT(!m_data || !m_data->holds(reinterpret_cast(s.data()), static_cast(s.size()))); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) build_copy(s, ae, c, tc); } @@ -11737,9 +11768,24 @@ class basic_json_document template void read_kind(T&& input, on_error ae, bool c, bool tc, std::integral_constant /*unused*/) { + NLOHMANN_VIEW_ASSERT(!held(input)); // (see the copy_range overload) build_copy(std::forward(input), ae, c, tc); } + /// whether an input read through the input adapter is held by this document: + /// string_view_t before C++17 (a string handed out by a view), for the + /// debug check of read(); other inputs of this kind do not point into it + bool held(const detail::view::string_ref& s) const noexcept + { + return m_data && m_data->holds(s.data(), s.size()); + } + + template + bool held(const T& /*input*/) const noexcept + { + return false; + } + template void read_range(IteratorType first, IteratorType last, on_error ae, bool c, bool tc) { @@ -11895,6 +11941,7 @@ class tuple_element> // NOLINT(cert #undef NLOHMANN_VIEW_NOCLONE #undef NLOHMANN_VIEW_NODISCARD #undef NLOHMANN_VIEW_THROW +#undef NLOHMANN_VIEW_ASSERT #undef NLOHMANN_VIEW_LITTLE_ENDIAN #undef NLOHMANN_VIEW_REPEAT16 #undef NLOHMANN_VIEW_REPEAT_SECOND_8 diff --git a/tests/src/unit-json_view_builder.cpp b/tests/src/unit-json_view_builder.cpp index 988c25771..ea3c12de7 100644 --- a/tests/src/unit-json_view_builder.cpp +++ b/tests/src/unit-json_view_builder.cpp @@ -906,3 +906,71 @@ TEST_CASE("json_view node array size limit") d->reserve(cap); CHECK(d->nodes == nodes); } + +TEST_CASE("json_view document_data::holds() (the debug check of read())") +{ + std::unique_ptr d(document_data::create(0)); + const std::string other = "[1,2,3]"; + // an empty document holds nothing + CHECK(!d->holds(other.data(), other.size())); + + SECTION("owned text") + { + d->owned = "{\"a\":\"some text that does not fit a short string\"}"; + const char* const b = d->owned.data(); + CHECK(d->holds(b, 1)); + CHECK(d->holds(b + 5, 3)); + CHECK(d->holds(b + d->owned.size() - 1, 1)); + // also after clear(), which keeps the buffer that the next parse writes + d->owned.clear(); + CHECK(d->holds(b, 4)); + CHECK(!d->holds(other.data(), other.size())); + CHECK(!d->holds(b, 0)); + } + + SECTION("decoded strings") + { + d->arena.assign(32, 'x'); + CHECK(d->holds(d->arena.data() + 31, 1)); + CHECK(d->holds(d->arena.data() + 31, 8)); // (overlapping the end) + CHECK(!d->holds(other.data(), other.size())); + } + + SECTION("owned image") + { + d->owned_image.assign(16, 0); + CHECK(d->holds(reinterpret_cast(d->owned_image.data()) + 8, 2)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } + + SECTION("current edit buffer") + { + d->edits.reset(new document_data::edit_state()); // NOLINT(cppcoreguidelines-owning-memory) + CHECK(!d->holds(other.data(), other.size())); + d->edits->texts.emplace_back(new char[16]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + d->edits->text_cap = 16; + CHECK(d->holds(d->edits->texts.back().get() + 3, 3)); + CHECK(!d->holds(other.data(), other.size())); + } +} + +TEST_CASE("json_view read() of inputs the check must accept") +{ + // in a debug build each read() below runs the check; none of the inputs + // is held by the document + const std::string text = R"({"a":"x\ny","b":[1,2]})"; + nlohmann::json_document d; + d.read(text); // borrowed + d.read(text); // the same borrowed text again + CHECK(d.root()["b"][1].get() == 2); + const std::string copy(d.root()["a"].get_string()); // a decoded string, copied first + d.read(std::string(text)); // owned + d.read(R"(["x"])"); + d.read(text); + CHECK(copy == "x\ny"); + nlohmann::json_editable_document e; + e.read(text); + e.set(e.root(), "a", "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]"); + const std::string edited(e.root()["a"].get_string()); // an edited string, copied first + e.read(edited); + CHECK(e.root().size() == 16); +} From 53d097f98076370b2369ab32b6a1ff8d36097da7 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 11 Nov 2026 22:36:17 +0100 Subject: [PATCH 11/21] docs: correct the json_view dump() guarantee for loaded images (#6087) dump() said a view never throws type_error.316 because the text is always validated. A document loaded with image_check::bounds or none can hold invalid UTF-8: dump() writes it unchanged, and dump(ensure_ascii) throws type_error.316. (The matching correction of number_token() is part of the read() documentation change below which this commit is stacked.) Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json_view/dump.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json_view/dump.md b/docs/mkdocs/docs/api/basic_json_view/dump.md index 2fd64a2ce..112716852 100644 --- a/docs/mkdocs/docs/api/basic_json_view/dump.md +++ b/docs/mkdocs/docs/api/basic_json_view/dump.md @@ -48,8 +48,11 @@ Strong exception safety: if an exception is thrown, there are no changes to the May throw `#!cpp std::bad_alloc` if allocating the output string fails. Unlike [`BasicJsonType::dump`](../basic_json/dump.md), there is no `error_handler` parameter and no -[`type_error.316`](../../home/exceptions.md#jsonexceptiontype_error316): the view only ever holds text the parser -already validated as UTF-8, so there is nothing to replace or ignore. +[`type_error.316`](../../home/exceptions.md#jsonexceptiontype_error316): the parser validated the text the view +holds as UTF-8, so there is nothing to replace or ignore. Only a document loaded with +[`image_check::bounds` or `image_check::none`](../basic_json_document/load.md#image_check) can still throw it: these do +not check the strings, so `dump()` writes such a string as it is, which can be invalid UTF-8 or invalid JSON, and +`dump()` with `ensure_ascii` set to `#!cpp true` throws `type_error.316` for invalid UTF-8. ## Complexity From 9d35fe6d2189ce42a47cfb1e43778c84bf71b2a9 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 11 Nov 2026 22:36:54 +0100 Subject: [PATCH 12/21] Reject integer tokens in float nodes of fully checked images (#6084) image_check::full accepted a float node whose token is an integer literal that fits 64 bits (e.g. 10446744073709551616 changed from 18446744073709551616). The parser stores an integer node for such a token, so the loaded document differed from what parsing the same text gives. check_float_ranges now requires an integer token of a float node not to fit uint64_t (or int64_t when negative). Signed-off-by: Niels Lohmann --- .../docs/api/basic_json_document/load.md | 11 ++--- include/nlohmann/detail/view/image.hpp | 25 ++++++++++- single_include/nlohmann/json_view.hpp | 25 ++++++++++- tests/src/unit-json_view_image.cpp | 42 +++++++++++++++++++ 4 files changed, 96 insertions(+), 7 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json_document/load.md b/docs/mkdocs/docs/api/basic_json_document/load.md index 46a71cb06..2782653d7 100644 --- a/docs/mkdocs/docs/api/basic_json_document/load.md +++ b/docs/mkdocs/docs/api/basic_json_document/load.md @@ -113,11 +113,12 @@ How thoroughly `load()` validates `image` before trusting it. `full` (the default) : Checks everything the parser itself guarantees: structure and bounds; that every string is valid UTF-8 (and, for a string still in the source text, that it contains no quote, backslash, or control character); and that every number - token is well-formed and matches the value stored for it. Strings and float tokens may share a range only if the - ranges are identical (as nodes that share a value do), and never overlap otherwise. Reading and serializing a - checked image is safe and always produces valid JSON, exactly as for a parsed document. This is the right choice for - an image from anything you do not fully control — a file, a cache shared with other processes, a peer on the - network. + token is well-formed and matches the value stored for it, and has the type the parser gives it (a float node only + for a token with a fraction or an exponent, or an integer that does not fit 64 bits). Strings and float tokens may + share a range only if the ranges are identical (as nodes that share a value do), and never overlap otherwise. + Reading and serializing a checked image is safe and always produces valid JSON, exactly as for a parsed document. + This is the right choice for an image from anything you do not fully control — a file, a cache shared with other + processes, a peer on the network. `bounds` : Checks structure and bounds only: that every offset and count in the node index stays inside the image. So diff --git a/include/nlohmann/detail/view/image.hpp b/include/nlohmann/detail/view/image.hpp index e6680730f..5459f435f 100644 --- a/include/nlohmann/detail/view/image.hpp +++ b/include/nlohmann/detail/view/image.hpp @@ -416,6 +416,28 @@ inline bool float_token_finite(const unsigned char* s, std::size_t len, const nu return true; } +/// whether an integer token (well-formed, no fraction or exponent) fits 64 +/// bits: the parser stores a number_integer or number_unsigned node for it, +/// and a number_float node only for a token that does not fit +inline bool integer_token_fits(const number_token& t) +{ + if (t.int_digits > 20) + { + return false; + } + if (t.int_digits == 20) + { + // (20 digits are compared with 2^64 - 1; a negative one is below -2^63) + // (only a leading 1 needs the comparison: the first digit of 20 is not 0) + return !t.negative && *t.int_start == '1' && std::memcmp(t.int_start, "18446744073709551615", 20) <= 0; + } + if (t.negative && t.int_digits == 19) + { + return *t.int_start != '9' || std::memcmp(t.int_start, "9223372036854775808", 19) <= 0; + } + return true; +} + /// whether an integer node matches its token the way the parser records it /// (after the bounds check; the token has at most 256 characters) inline bool check_integer(const node& n, const unsigned char* text) @@ -535,7 +557,8 @@ inline bool check_float_ranges(std::vector& ranges, const unsigned if (!plain_float_token(s, r.len, r.extra)) { number_token t{}; - if (!scan_number_token(s, r.len, t) || !float_token_finite(s, r.len, t)) + // (an integer token is a float node only if it does not fit 64 bits) + if (!scan_number_token(s, r.len, t) || (!t.is_float && integer_token_fits(t)) || !float_token_finite(s, r.len, t)) { return false; } diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index b2523284d..f2a39d268 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -7439,6 +7439,28 @@ inline bool float_token_finite(const unsigned char* s, std::size_t len, const nu return true; } +/// whether an integer token (well-formed, no fraction or exponent) fits 64 +/// bits: the parser stores a number_integer or number_unsigned node for it, +/// and a number_float node only for a token that does not fit +inline bool integer_token_fits(const number_token& t) +{ + if (t.int_digits > 20) + { + return false; + } + if (t.int_digits == 20) + { + // (20 digits are compared with 2^64 - 1; a negative one is below -2^63) + // (only a leading 1 needs the comparison: the first digit of 20 is not 0) + return !t.negative && *t.int_start == '1' && std::memcmp(t.int_start, "18446744073709551615", 20) <= 0; + } + if (t.negative && t.int_digits == 19) + { + return *t.int_start != '9' || std::memcmp(t.int_start, "9223372036854775808", 19) <= 0; + } + return true; +} + /// whether an integer node matches its token the way the parser records it /// (after the bounds check; the token has at most 256 characters) inline bool check_integer(const node& n, const unsigned char* text) @@ -7558,7 +7580,8 @@ inline bool check_float_ranges(std::vector& ranges, const unsigned if (!plain_float_token(s, r.len, r.extra)) { number_token t{}; - if (!scan_number_token(s, r.len, t) || !float_token_finite(s, r.len, t)) + // (an integer token is a float node only if it does not fit 64 bits) + if (!scan_number_token(s, r.len, t) || (!t.is_float && integer_token_fits(t)) || !float_token_finite(s, r.len, t)) { return false; } diff --git a/tests/src/unit-json_view_image.cpp b/tests/src/unit-json_view_image.cpp index a4e282639..a339385b5 100644 --- a/tests/src/unit-json_view_image.cpp +++ b/tests/src/unit-json_view_image.cpp @@ -759,6 +759,48 @@ TEST_CASE("json_view images: errors") } } + SECTION("float tokens: an integer token is a float node only if it does not fit 64 bits") + { + for (const char* const token : + { + "18446744073709551616", "-9223372036854775809", "100000000000000000000", "-18446744073709551616", "123456789012345678901234567890" + }) + { + CAPTURE(token) + const std::string text = std::string("[") + token + "]"; + const json_document d = json_document::parse(text); + CHECK(d.root()[0].is_number_float()); + const std::vector image = d.save(); + const std::size_t at = text_at(image) + 1; // the first byte of the token + CHECK(loaded_dump(image) == json::parse(text).dump()); + for (std::size_t pos = token[0] == '-' ? 1 : 0; token[pos] != '\0'; ++pos) // (not the sign: that changes the digits) + { + for (char digit = '0'; digit <= '9'; ++digit) + { + std::vector changed = image; + changed[at + pos] = static_cast(digit); + std::string t = token; + t[pos] = digit; + CAPTURE(pos) + CAPTURE(digit) + CAPTURE(t) + // the parser stores an integer for the changed token if it is one (and fits) + const bool float_node = json::accept(t) && json::parse(t).is_number_float(); + const std::string result = load_result(changed, image_check::full); + if (float_node) + { + CHECK(result.empty()); + CHECK(loaded_dump(changed) == json::parse("[" + t + "]").dump()); + } + else + { + CHECK(result == check_failed); + } + } + } + } + } + SECTION("invalid UTF-8 of a decoded string: the compact dump writes it, the others throw") { const json_document d = json_document::parse(R"({"k\n":"\u00e9x"})"); From 02d57db31ebb10a9ea96ca06d61b1fc07eae00b2 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 11 Nov 2026 22:36:54 +0100 Subject: [PATCH 13/21] Do not compare an edited NaN equal to itself in json_view (#6085) Two views compared equal when the tokens of two floats were equal, and the same node was equal to itself. An edit writes a NaN as the token "nan", so a view of it (or of an array holding it) was equal to itself and to another NaN, while json(NaN) == json(NaN) and view == json are false. The token shortcut now excludes the edited "nan" token (found by the missing digit layout), dispatched on the document mode by a tag: a read-only document cannot hold a NaN, and its walk is unchanged (instruction count and cycles on canada, citm and twitter within noise). For editable documents the check costs about 2 % instructions on canada (cycles within noise). The same-node shortcut is kept for read-only documents and for editable ones that were never edited; a document with edits is walked, so comparing it with itself is linear instead of constant. Signed-off-by: Niels Lohmann --- .../docs/api/basic_json_view/operator_eq.md | 4 +- include/nlohmann/detail/view/compare.hpp | 27 +++++++++++-- single_include/nlohmann/json_view.hpp | 27 +++++++++++-- tests/src/unit-json_view_edit_values.cpp | 38 +++++++++++++++++++ 4 files changed, 87 insertions(+), 9 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json_view/operator_eq.md b/docs/mkdocs/docs/api/basic_json_view/operator_eq.md index b385aa0a9..2da60db9e 100644 --- a/docs/mkdocs/docs/api/basic_json_view/operator_eq.md +++ b/docs/mkdocs/docs/api/basic_json_view/operator_eq.md @@ -57,7 +57,9 @@ compared. Two arrays of different [`size()`](size.md) are rejected without visit Only a single number, boolean, or `#!cpp null` value is ever materialized into a `BasicJsonType`, to reuse its `operator==` — for numbers, so that values written differently in the source text but equal in value (e.g. an integer and a floating-point literal) still compare equal, following the same rules `BasicJsonType` does for special -values such as `#!cpp NaN`. Constructing one of these scalars never allocates. Strings are compared directly, without +values such as `#!cpp NaN`: a `#!cpp NaN` that an [edit](../basic_json_document/set.md) wrote is not equal to itself, so +a view of it is not equal to itself either, nor is a view of an array or object that contains one, as for +`BasicJsonType`. Constructing one of these scalars never allocates. Strings are compared directly, without allocating, either from the source text on both sides or, for overload 2, against `BasicJsonType`'s own string. Arrays and objects are never materialized at all; only their elements or members are visited, one pair at a time. diff --git a/include/nlohmann/detail/view/compare.hpp b/include/nlohmann/detail/view/compare.hpp index 2d2c238b9..ab0e5bbef 100644 --- a/include/nlohmann/detail/view/compare.hpp +++ b/include/nlohmann/detail/view/compare.hpp @@ -79,6 +79,14 @@ NLOHMANN_VIEW_ALWAYS_INLINE bool bytes_equal(const char* x, const char* y, std:: return true; } +/// whether a float node holds the text nan, which an edit writes for a NaN (and records without a digit layout, so +/// that a parsed float is told apart by one comparison of what the walk has loaded anyway): equal tokens are an +/// equal value, except for this one +NLOHMANN_VIEW_ALWAYS_INLINE bool float_token_is_nan(const node& n, const char* s) noexcept +{ + return NLOHMANN_VIEW_UNLIKELY(n.extra == 0xFFFFu) && n.len == 3 && s[0] == 'n'; +} + /// whether the n1 bytes at x are before the n2 bytes at y (memcmp() order, then the shorter first); /// the first difference is found eight bytes at a time NLOHMANN_VIEW_ALWAYS_INLINE bool bytes_less(const char* x, std::size_t n1, const char* y, std::size_t n2) noexcept @@ -688,7 +696,7 @@ class lockstep case value_t::string: return a->len == b->len && bytes_equal(m_a.str(*a), m_b.str(*b), a->len); case value_t::number_float: // (the token, and the digit layout that the value is read with: they only differ in a damaged image) - return a->extra == b->extra && a->len == b->len && bytes_equal(m_a.str(*a), m_b.str(*b), a->len); + return same_float(a, b, std::integral_constant {}); case value_t::number_integer: case value_t::number_unsigned: return integer_bits(*a) == integer_bits(*b); @@ -783,6 +791,17 @@ class lockstep return true; } + /// (a read-only document holds no NaN) + bool same_float(const node* a, const node* b, std::false_type /*editable*/) const noexcept + { + return a->extra == b->extra && a->len == b->len && bytes_equal(m_a.str(*a), m_b.str(*b), a->len); + } + + bool same_float(const node* a, const node* b, std::true_type /*editable*/) const noexcept + { + return a->extra == b->extra && a->len == b->len && bytes_equal(m_a.str(*a), m_b.str(*b), a->len) && !float_token_is_nan(*a, m_a.str(*a)); + } + bool elements(const node* a, const node* b, unsigned budget, lockstep_path::entry& stop) noexcept { const node* pa = nav_a::first(m_a, a); @@ -839,9 +858,9 @@ bool certainly_equal(const view_side& a, const view_side::nav::editable() || a.document().edits == nullptr)) { - return true; // (the same node of the same document: documents have distinct nodes) + return true; // (the same node of the same document: documents have distinct nodes; a document with edits is walked, as an edit can have written a NaN, which is not equal to itself) } path.resume = nullptr; if (path.next < path.length && path.nodes[path.next].a == na && path.nodes[path.next].b == nb) @@ -886,7 +905,7 @@ void resume_array(const lockstep_path& /*path*/, const A& /*a*/, const B& /*b*/, template bool floats_equal(const view_side& a, const view_side& b) { - return (a.value_node()->extra == b.value_node()->extra && a.float_token() == b.float_token()) || std::equal_to()(a.floating(), b.floating()); + return (a.value_node()->extra == b.value_node()->extra && a.float_token() == b.float_token() && !(view_side::nav::editable() && float_token_is_nan(*a.value_node(), a.float_token().data()))) || std::equal_to()(a.floating(), b.floating()); } template diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index f2a39d268..af959c06b 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -3262,6 +3262,14 @@ NLOHMANN_VIEW_ALWAYS_INLINE bool bytes_equal(const char* x, const char* y, std:: return true; } +/// whether a float node holds the text nan, which an edit writes for a NaN (and records without a digit layout, so +/// that a parsed float is told apart by one comparison of what the walk has loaded anyway): equal tokens are an +/// equal value, except for this one +NLOHMANN_VIEW_ALWAYS_INLINE bool float_token_is_nan(const node& n, const char* s) noexcept +{ + return NLOHMANN_VIEW_UNLIKELY(n.extra == 0xFFFFu) && n.len == 3 && s[0] == 'n'; +} + /// whether the n1 bytes at x are before the n2 bytes at y (memcmp() order, then the shorter first); /// the first difference is found eight bytes at a time NLOHMANN_VIEW_ALWAYS_INLINE bool bytes_less(const char* x, std::size_t n1, const char* y, std::size_t n2) noexcept @@ -3871,7 +3879,7 @@ class lockstep case value_t::string: return a->len == b->len && bytes_equal(m_a.str(*a), m_b.str(*b), a->len); case value_t::number_float: // (the token, and the digit layout that the value is read with: they only differ in a damaged image) - return a->extra == b->extra && a->len == b->len && bytes_equal(m_a.str(*a), m_b.str(*b), a->len); + return same_float(a, b, std::integral_constant {}); case value_t::number_integer: case value_t::number_unsigned: return integer_bits(*a) == integer_bits(*b); @@ -3966,6 +3974,17 @@ class lockstep return true; } + /// (a read-only document holds no NaN) + bool same_float(const node* a, const node* b, std::false_type /*editable*/) const noexcept + { + return a->extra == b->extra && a->len == b->len && bytes_equal(m_a.str(*a), m_b.str(*b), a->len); + } + + bool same_float(const node* a, const node* b, std::true_type /*editable*/) const noexcept + { + return a->extra == b->extra && a->len == b->len && bytes_equal(m_a.str(*a), m_b.str(*b), a->len) && !float_token_is_nan(*a, m_a.str(*a)); + } + bool elements(const node* a, const node* b, unsigned budget, lockstep_path::entry& stop) noexcept { const node* pa = nav_a::first(m_a, a); @@ -4022,9 +4041,9 @@ bool certainly_equal(const view_side& a, const view_side::nav::editable() || a.document().edits == nullptr)) { - return true; // (the same node of the same document: documents have distinct nodes) + return true; // (the same node of the same document: documents have distinct nodes; a document with edits is walked, as an edit can have written a NaN, which is not equal to itself) } path.resume = nullptr; if (path.next < path.length && path.nodes[path.next].a == na && path.nodes[path.next].b == nb) @@ -4069,7 +4088,7 @@ void resume_array(const lockstep_path& /*path*/, const A& /*a*/, const B& /*b*/, template bool floats_equal(const view_side& a, const view_side& b) { - return (a.value_node()->extra == b.value_node()->extra && a.float_token() == b.float_token()) || std::equal_to()(a.floating(), b.floating()); + return (a.value_node()->extra == b.value_node()->extra && a.float_token() == b.float_token() && !(view_side::nav::editable() && float_token_is_nan(*a.value_node(), a.float_token().data()))) || std::equal_to()(a.floating(), b.floating()); } template diff --git a/tests/src/unit-json_view_edit_values.cpp b/tests/src/unit-json_view_edit_values.cpp index 881d957f8..034b94e16 100644 --- a/tests/src/unit-json_view_edit_values.cpp +++ b/tests/src/unit-json_view_edit_values.cpp @@ -206,6 +206,44 @@ TEST_CASE("json_view edits: views and values") CHECK(d.root().materialize().dump() == json::parse(R"([1.5, 100.0, 0.1, null, null, 18446744073709551615, -9223372036854775808])").dump()); } + SECTION("a NaN written by an edit is not equal to itself") + { + // as for basic_json: NaN != NaN, so a view of it, of the array that holds it, and the same view twice agree + json_editable_document d = json_editable_document::parse("[0, 0, 1.5]"); + d.set(d.root(), 0, std::numeric_limits::quiet_NaN()); + d.set(d.root(), 1, std::numeric_limits::quiet_NaN()); + json_editable_document other = json_editable_document::parse("[0, 0, 1.5]"); + other.set(other.root(), 0, std::numeric_limits::quiet_NaN()); + other.set(other.root(), 1, std::numeric_limits::quiet_NaN()); + const json value = d.root().materialize(); + + CHECK_FALSE(value == value); + CHECK_FALSE(d.root()[0] == d.root()[0]); + CHECK_FALSE(d.root()[0] == d.root()[1]); + CHECK_FALSE(d.root() == d.root()); + CHECK_FALSE(d.root() == other.root()); + CHECK_FALSE(d.root()[0] == other.root()[0]); + CHECK(d.root() != d.root()); + CHECK(d.root()[0] != d.root()[0]); + CHECK_FALSE(d.root() == value); + CHECK_FALSE(d.root()[0] == value[0]); + // the other elements are equal as before + CHECK(d.root()[2] == d.root()[2]); + CHECK(d.root()[2] == other.root()[2]); + // finite and infinite floats written by an edit stay equal to themselves + d.set(d.root(), 0, 0.1); + d.set(d.root(), 1, std::numeric_limits::infinity()); + other.set(other.root(), 0, 0.1); + other.set(other.root(), 1, std::numeric_limits::infinity()); + CHECK(d.root() == d.root()); + CHECK(d.root() == other.root()); + CHECK(d.root()[1] == other.root()[1]); + // a document that was never edited + const json_editable_document untouched = json_editable_document::parse("[0.5, [1, 2]]"); + CHECK(untouched.root() == untouched.root()); + CHECK(untouched.root()[1] == untouched.root()[1]); + } + SECTION("numbers of other float types") { // doubles have their own path to the output; other float types are From e0b76bac31e6cac3649a3ddb82aaddb0bbeaede2 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 11 Nov 2026 22:37:46 +0100 Subject: [PATCH 14/21] Guard the image part of the alt_string test against big-endian targets (#6074) The test for json_view documents over an alternative string type called alt_editable_document::load() and save(). Both exist only on little-endian targets, so the test suite did not compile on big-endian targets such as s390x. Guard the two image lines with the condition the other image tests use (unit-json_view_image.cpp, unit-json_view_positions.cpp). Signed-off-by: Niels Lohmann --- tests/src/unit-alt-string.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/src/unit-alt-string.cpp b/tests/src/unit-alt-string.cpp index 4a0260a10..79e59b660 100644 --- a/tests/src/unit-alt-string.cpp +++ b/tests/src/unit-alt-string.cpp @@ -531,8 +531,11 @@ TEST_CASE("alternative string type with json_view") CHECK(d.root().materialize() == expected); CHECK(alt_json::parse(d.root().dump()) == expected); + // save() and load() do not exist on big-endian targets +#if !(defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) const alt_editable_document loaded = alt_editable_document::load(d.save()); CHECK(loaded.root().materialize() == expected); +#endif } } From 03ad4a3e7fff0ac491ee25913a4f68489bec8963 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Nov 2026 22:37:57 +0100 Subject: [PATCH 15/21] build(deps): bump the codeql-action group with 3 updates (#6065) Bumps the codeql-action group with 3 updates: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [github/codeql-action/upload-sarif](https://github.com/github/codeql-action). Updates `github/codeql-action/init` from 4.39.2 to 4.39.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/1fc58d04a4d4374f921bdac573c3811c240f9c26...33d7b85da1a04171e211befe2c739303b9a4c066) Updates `github/codeql-action/analyze` from 4.39.2 to 4.39.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/1fc58d04a4d4374f921bdac573c3811c240f9c26...33d7b85da1a04171e211befe2c739303b9a4c066) Updates `github/codeql-action/upload-sarif` from 4.39.2 to 4.39.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/1fc58d04a4d4374f921bdac573c3811c240f9c26...33d7b85da1a04171e211befe2c739303b9a4c066) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.39.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: codeql-action - dependency-name: github/codeql-action/analyze dependency-version: 4.39.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: codeql-action - dependency-name: github/codeql-action/upload-sarif dependency-version: 4.39.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: codeql-action ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 4 ++-- .github/workflows/flawfinder.yml | 2 +- .github/workflows/scorecards.yml | 2 +- .github/workflows/semgrep.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5dfa80842..aa982f139 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@1fc58d04a4d4374f921bdac573c3811c240f9c26 # v4.39.2 + uses: github/codeql-action/init@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3 with: languages: c-cpp # Analyze the sources without compiling them. Autobuild compiled the @@ -47,4 +47,4 @@ jobs: build-mode: none - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@1fc58d04a4d4374f921bdac573c3811c240f9c26 # v4.39.2 + uses: github/codeql-action/analyze@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3 diff --git a/.github/workflows/flawfinder.yml b/.github/workflows/flawfinder.yml index 24edc15f0..608523b7e 100644 --- a/.github/workflows/flawfinder.yml +++ b/.github/workflows/flawfinder.yml @@ -48,6 +48,6 @@ jobs: output: 'flawfinder_results.sarif' - name: Upload analysis results to GitHub Security tab - uses: github/codeql-action/upload-sarif@1fc58d04a4d4374f921bdac573c3811c240f9c26 # v4.39.2 + uses: github/codeql-action/upload-sarif@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3 with: sarif_file: ${{github.workspace}}/flawfinder_results.sarif diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 2a79b7574..b2e08c488 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -80,6 +80,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@1fc58d04a4d4374f921bdac573c3811c240f9c26 # v4.39.2 + uses: github/codeql-action/upload-sarif@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3 with: sarif_file: results.sarif diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 30f8b931a..ca73edb55 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -69,7 +69,7 @@ jobs: # Upload SARIF file generated in previous step - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@1fc58d04a4d4374f921bdac573c3811c240f9c26 # v4.39.2 + uses: github/codeql-action/upload-sarif@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3 with: sarif_file: semgrep.sarif if: always() From 07aa3d1d691f15613647ebdf6d95afe8d8f7b34f Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Thu, 12 Nov 2026 08:40:05 +0100 Subject: [PATCH 16/21] docs: fix NaN ordering, FILE* locking and binary integer-range statements (#6088) * docs: state that NaN ordering inside containers depends on the standard When the element-wise comparison of arrays or objects reaches a NaN and a number (or another NaN) before any other difference, the result depends on the language standard. Before C++20 the pair counts as equivalent and the following elements (or the sizes) decide, so [1] <= [NaN] is true. Since C++20 the comparison is unordered and <, <=, >, >= all yield false. Document this on operator<, <=, >, >= and <=>. Signed-off-by: Niels Lohmann * docs: fix the FILE* notes of parse() and the locking promise The performance note of parse() said a FILE* is read one character at a time. With glibc and the Apple C library the characters in the FILE's buffer are scanned in bulk; only elsewhere is it one character at a time. The file is locked once only where the C library has unlocked stdio functions (not on musl, MinGW, newlib, with JSON_DISABLE_UNLOCKED_FILE_IO, or under MSan). Qualify the promise on parse.md, sax_parse.md, the parsing page and the performance page. Signed-off-by: Niels Lohmann * docs: fix integer ranges in the MessagePack and BJData tables number_integer_t is std::int64_t, so number_integer cannot hold values above 9223372036854775807; those are number_unsigned. The MessagePack row for uint 64 listed the range up to 18446744073709551615, and the BJData table had a uint64 row for number_integer that can never be selected. The CBOR table was already corrected. Signed-off-by: Niels Lohmann * docs: name the macros that turn off FILE* locking and bulk scans parse() and sax_parse() now say that the FILE* is locked unless JSON_DISABLE_UNLOCKED_FILE_IO is defined to 1, and the input-source table and the performance page name JSON_DISABLE_STREAM_BULK_SCAN and JSON_DISABLE_UNLOCKED_FILE_IO with what each turns off. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/operator_ge.md | 8 ++++++++ docs/mkdocs/docs/api/basic_json/operator_gt.md | 8 ++++++++ docs/mkdocs/docs/api/basic_json/operator_le.md | 8 ++++++++ docs/mkdocs/docs/api/basic_json/operator_lt.md | 8 ++++++++ .../mkdocs/docs/api/basic_json/operator_spaceship.md | 4 ++++ docs/mkdocs/docs/api/basic_json/parse.md | 12 +++++++----- docs/mkdocs/docs/api/basic_json/sax_parse.md | 7 ++++--- docs/mkdocs/docs/features/binary_formats/bjdata.md | 1 - .../docs/features/binary_formats/messagepack.md | 2 +- docs/mkdocs/docs/features/parsing/index.md | 8 +++++++- docs/mkdocs/docs/features/performance.md | 10 +++++++--- 11 files changed, 62 insertions(+), 14 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/operator_ge.md b/docs/mkdocs/docs/api/basic_json/operator_ge.md index 54383c1e8..91334bc2c 100644 --- a/docs/mkdocs/docs/api/basic_json/operator_ge.md +++ b/docs/mkdocs/docs/api/basic_json/operator_ge.md @@ -60,6 +60,14 @@ Linear. 2. Comparing a `NaN` with another `NaN`. 3. Comparing a `NaN` and any other number. + For arrays and objects, the elements are compared in order. If the comparison reaches a `NaN` and a number (or + another `NaN`) before any other difference, the result depends on the language standard. Before C++20, the pair + counts as equivalent and the following elements (or the sizes) decide: for instance, `[NaN] < [1, NaN]` and + `[1] <= [NaN]` are `#!cpp true`. Since C++20, the whole comparison is unordered, and `<`, `<=`, `>`, and `>=` all + yield `#!cpp false`. Otherwise (for example `[1, NaN] < [2, NaN]`, or objects whose keys differ), the result is the + same for all language standards. The same program can therefore order such values differently when it is compiled + with a different language standard. + !!! note "Operator overload resolution" Since C++20 overload resolution will consider the _rewritten candidate_ generated from diff --git a/docs/mkdocs/docs/api/basic_json/operator_gt.md b/docs/mkdocs/docs/api/basic_json/operator_gt.md index dea5ef76c..d105fd9ab 100644 --- a/docs/mkdocs/docs/api/basic_json/operator_gt.md +++ b/docs/mkdocs/docs/api/basic_json/operator_gt.md @@ -60,6 +60,14 @@ Linear. 2. Comparing a `NaN` with another `NaN`. 3. Comparing a `NaN` and any other number. + For arrays and objects, the elements are compared in order. If the comparison reaches a `NaN` and a number (or + another `NaN`) before any other difference, the result depends on the language standard. Before C++20, the pair + counts as equivalent and the following elements (or the sizes) decide: for instance, `[NaN] < [1, NaN]` and + `[1] <= [NaN]` are `#!cpp true`. Since C++20, the whole comparison is unordered, and `<`, `<=`, `>`, and `>=` all + yield `#!cpp false`. Otherwise (for example `[1, NaN] < [2, NaN]`, or objects whose keys differ), the result is the + same for all language standards. The same program can therefore order such values differently when it is compiled + with a different language standard. + !!! note "Operator overload resolution" Since C++20 overload resolution will consider the _rewritten candidate_ generated from diff --git a/docs/mkdocs/docs/api/basic_json/operator_le.md b/docs/mkdocs/docs/api/basic_json/operator_le.md index 8818b221a..a9b2cd836 100644 --- a/docs/mkdocs/docs/api/basic_json/operator_le.md +++ b/docs/mkdocs/docs/api/basic_json/operator_le.md @@ -61,6 +61,14 @@ Linear. 2. Comparing a `NaN` with another `NaN`. 3. Comparing a `NaN` and any other number. + For arrays and objects, the elements are compared in order. If the comparison reaches a `NaN` and a number (or + another `NaN`) before any other difference, the result depends on the language standard. Before C++20, the pair + counts as equivalent and the following elements (or the sizes) decide: for instance, `[NaN] < [1, NaN]` and + `[1] <= [NaN]` are `#!cpp true`. Since C++20, the whole comparison is unordered, and `<`, `<=`, `>`, and `>=` all + yield `#!cpp false`. Otherwise (for example `[1, NaN] < [2, NaN]`, or objects whose keys differ), the result is the + same for all language standards. The same program can therefore order such values differently when it is compiled + with a different language standard. + !!! note "Operator overload resolution" Since C++20 overload resolution will consider the _rewritten candidate_ generated from diff --git a/docs/mkdocs/docs/api/basic_json/operator_lt.md b/docs/mkdocs/docs/api/basic_json/operator_lt.md index e18d591ca..f3ebd4d64 100644 --- a/docs/mkdocs/docs/api/basic_json/operator_lt.md +++ b/docs/mkdocs/docs/api/basic_json/operator_lt.md @@ -71,6 +71,14 @@ Linear. 2. Comparing a `NaN` with another `NaN`. 3. Comparing a `NaN` and any other number. + For arrays and objects, the elements are compared in order. If the comparison reaches a `NaN` and a number (or + another `NaN`) before any other difference, the result depends on the language standard. Before C++20, the pair + counts as equivalent and the following elements (or the sizes) decide: for instance, `[NaN] < [1, NaN]` and + `[1] <= [NaN]` are `#!cpp true`. Since C++20, the whole comparison is unordered, and `<`, `<=`, `>`, and `>=` all + yield `#!cpp false`. Otherwise (for example `[1, NaN] < [2, NaN]`, or objects whose keys differ), the result is the + same for all language standards. The same program can therefore order such values differently when it is compiled + with a different language standard. + !!! note "Operator overload resolution" Since C++20 overload resolution will consider the _rewritten candidate_ generated from diff --git a/docs/mkdocs/docs/api/basic_json/operator_spaceship.md b/docs/mkdocs/docs/api/basic_json/operator_spaceship.md index 7aed091ef..b85bda52f 100644 --- a/docs/mkdocs/docs/api/basic_json/operator_spaceship.md +++ b/docs/mkdocs/docs/api/basic_json/operator_spaceship.md @@ -55,6 +55,10 @@ Linear. 1. Comparing a `NaN` with itself. 2. Comparing a `NaN` with another `NaN`. 3. Comparing a `NaN` and any other number. + - For arrays and objects, the elements are compared in order. If the comparison reaches a `NaN` and a number (or + another `NaN`) before any other difference, the result is `std::partial_ordering::unordered`. (Without C++20, the + comparison operators `<`, `<=`, `>`, and `>=` use the comparison of the containers, which treats such a pair as + equivalent; see [**operator<**](operator_lt.md).) ## Examples diff --git a/docs/mkdocs/docs/api/basic_json/parse.md b/docs/mkdocs/docs/api/basic_json/parse.md index 0ebcdb496..dca49ea23 100644 --- a/docs/mkdocs/docs/api/basic_json/parse.md +++ b/docs/mkdocs/docs/api/basic_json/parse.md @@ -130,11 +130,13 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by Contiguous inputs (strings, string views, vectors, arrays, pointer ranges) are the fastest to parse. A `#!cpp std::istream` comes close: the parser scans the characters its stream buffer holds in bulk, without reading - ahead, so the stream is left exactly where it would be after reading one character at a time. A `#!cpp FILE*` is - read one character at a time, but the file is locked only once, so other threads that use the same `FILE` object - wait until parsing is done. See [input sources and performance](../../features/parsing/index.md#input-sources-and-performance), - [`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md), and - [`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md). + ahead, so the stream is left exactly where it would be after reading one character at a time. With glibc + and the Apple C library, the characters the `#!cpp FILE` holds in its buffer are scanned in bulk like a stream's; + elsewhere a `#!cpp FILE*` is read one character at a time. Where the C library has unlocked stdio functions, the + file is locked only once, so other threads that use the same `FILE` object wait until parsing is done, unless + [`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) is defined to `1`. See + [input sources and performance](../../features/parsing/index.md#input-sources-and-performance) and + [`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md). ## Examples diff --git a/docs/mkdocs/docs/api/basic_json/sax_parse.md b/docs/mkdocs/docs/api/basic_json/sax_parse.md index af2480e53..134f0b3c6 100644 --- a/docs/mkdocs/docs/api/basic_json/sax_parse.md +++ b/docs/mkdocs/docs/api/basic_json/sax_parse.md @@ -124,9 +124,10 @@ consumer `sax` has a super-linear complexity. A UTF-8 byte order mark is silently ignored. -When reading from a `#!c FILE*`, the file is locked while it is parsed. The lock is recursive, so the SAX handler may -use the same file with stdio functions, but it must not wait for another thread that uses the same `FILE` object; see -[`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md). +When reading from a `#!c FILE*` where the C library has unlocked stdio functions, the file is locked while it is parsed, +unless [`JSON_DISABLE_UNLOCKED_FILE_IO`](../macros/json_disable_unlocked_file_io.md) is defined to `1`. +The lock is recursive, so the SAX handler may use the same file with stdio functions, but it must not wait for another +thread that uses the same `FILE` object. ## Examples diff --git a/docs/mkdocs/docs/features/binary_formats/bjdata.md b/docs/mkdocs/docs/features/binary_formats/bjdata.md index c458af6b3..769da056b 100644 --- a/docs/mkdocs/docs/features/binary_formats/bjdata.md +++ b/docs/mkdocs/docs/features/binary_formats/bjdata.md @@ -88,7 +88,6 @@ The library uses the following mapping from JSON values types to BJData types ac | number_integer | 65536..2147483647 | int32 | `l` | | number_integer | 2147483648..4294967295 | uint32 | `m` | | number_integer | 4294967296..9223372036854775807 | int64 | `L` | -| number_integer | 9223372036854775808..18446744073709551615 | uint64 | `M` | | number_unsigned | 0..127 | int8 | `i` | | number_unsigned | 128..255 | uint8 | `U` | | number_unsigned | 256..32767 | int16 | `I` | diff --git a/docs/mkdocs/docs/features/binary_formats/messagepack.md b/docs/mkdocs/docs/features/binary_formats/messagepack.md index 17c05625d..00d6ee39e 100644 --- a/docs/mkdocs/docs/features/binary_formats/messagepack.md +++ b/docs/mkdocs/docs/features/binary_formats/messagepack.md @@ -61,7 +61,7 @@ specification: | number_integer | 128..255 | uint 8 | 0xCC | | number_integer | 256..65535 | uint 16 | 0xCD | | number_integer | 65536..4294967295 | uint 32 | 0xCE | -| number_integer | 4294967296..18446744073709551615 | uint 64 | 0xCF | +| number_integer | 4294967296..9223372036854775807 | uint 64 | 0xCF | | number_unsigned | 0..127 | positive fixint | 0x00..0x7F | | number_unsigned | 128..255 | uint 8 | 0xCC | | number_unsigned | 256..65535 | uint 16 | 0xCD | diff --git a/docs/mkdocs/docs/features/parsing/index.md b/docs/mkdocs/docs/features/parsing/index.md index 38e523f88..32f4549e8 100644 --- a/docs/mkdocs/docs/features/parsing/index.md +++ b/docs/mkdocs/docs/features/parsing/index.md @@ -77,9 +77,15 @@ them faster than others: |--------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------| | strings, string views, string literals, contiguous containers, pointer or iterator ranges over contiguous memory | the whole input is scanned in bulk: runs of string characters, numbers, and indentation are recognized several bytes at a time | | `#!cpp std::istream` (`#!cpp std::ifstream`, `#!cpp std::istringstream`, ...) | the characters the stream buffer holds are scanned in bulk like a string; the stream buffer is asked for more when they are used up | -| `#!cpp FILE*` | with glibc and the Apple C library, the characters the `FILE` holds in its buffer are scanned in bulk like a stream's; elsewhere one character at a time; the file is locked once for the whole parse | +| `#!cpp FILE*` | with glibc and the Apple C library, the characters the `FILE` holds in its buffer are scanned in bulk like a stream's; elsewhere one character at a time; where the C library has unlocked stdio functions, the file is locked once for the whole parse | | other iterator ranges (e.g., `#!cpp std::list`, `#!cpp std::istreambuf_iterator`), wide strings | one character at a time | +Defining [`JSON_DISABLE_STREAM_BULK_SCAN`](../../api/macros/json_disable_stream_bulk_scan.md) to `1` turns off the bulk +scan of a stream buffer, so a `#!cpp std::istream` is read one character at a time. Defining +[`JSON_DISABLE_UNLOCKED_FILE_IO`](../../api/macros/json_disable_unlocked_file_io.md) to `1` makes a `#!cpp FILE*` read +every character with `std::fgetc`, without locking the file for the whole parse and without the bulk scan of its buffer +(which `JSON_DISABLE_STREAM_BULK_SCAN` does not affect). Neither changes any result. + Input in one block of memory is read even faster with the default options (no [parser callback](../../api/basic_json/parser_callback_t.md), no comments, no trailing commas, and for `sax_parse()` `strict = true`): `parse()`, `accept()`, and `sax_parse()` then read the grammar and the tokens in one loop over the diff --git a/docs/mkdocs/docs/features/performance.md b/docs/mkdocs/docs/features/performance.md index b901e7ff3..53cd4623d 100644 --- a/docs/mkdocs/docs/features/performance.md +++ b/docs/mkdocs/docs/features/performance.md @@ -27,9 +27,13 @@ A `#!cpp std::istream` (including `#!cpp std::ifstream`) gets the same bulk scan characters its stream buffer already holds (the *get area*) and asks the stream buffer for more only when they are used up. Nothing is read ahead, so the stream is left exactly where it would be after reading one character at a time, and [`operator>>`](../api/operator_gtgt.md) can still read several values from one stream. A stream buffer without a get -area is read one character at a time. A `#!cpp FILE*` is locked once per parse instead of once per character, and with -glibc and the Apple C library, the characters in its buffer are scanned in bulk as a stream's get area is. See -[input sources and performance](parsing/index.md#input-sources-and-performance) for the full table. Parsing from a +area is read one character at a time. Where the C library has unlocked stdio functions, a `#!cpp FILE*` is locked once +per parse instead of once per character, and with glibc and the Apple C library, the characters in its buffer are +scanned in bulk as a stream's get area is. +[`JSON_DISABLE_STREAM_BULK_SCAN`](../api/macros/json_disable_stream_bulk_scan.md) turns off the bulk scan of streams, and +[`JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md) both the single lock and the bulk scan +of a `#!cpp FILE*`. See [input sources and performance](parsing/index.md#input-sources-and-performance) for the full +table. Parsing from a stream takes about 1.15 to 1.45 times as long as parsing an equivalent `#!cpp std::string`, which `parse()` reads with a parser of its own (see there). From 17e3de0fcceddcf6171bf148077ba0bd631408ea Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Thu, 12 Nov 2026 08:40:06 +0100 Subject: [PATCH 17/21] docs: add version-history notes for fixed start_pos and error positions (#6089) Two bugs of 3.12.0 are fixed on develop without a note on the pages. With JSON_DIAGNOSTIC_POSITIONS, start_pos() of a string that contains escape sequences was too large; the position is now that of the opening quote. A parse error for an unexpected number that is followed by a line feed reported column 0 (for "1 2" and a line feed, line 1, column 0); for "1", line feed, "2", line feed it now reports line 2, column 1. Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/parse.md | 4 ++++ docs/mkdocs/docs/api/basic_json/start_pos.md | 4 ++++ docs/mkdocs/docs/api/macros/json_diagnostic_positions.md | 2 ++ 3 files changed, 10 insertions(+) diff --git a/docs/mkdocs/docs/api/basic_json/parse.md b/docs/mkdocs/docs/api/basic_json/parse.md index dca49ea23..13b1fa9b7 100644 --- a/docs/mkdocs/docs/api/basic_json/parse.md +++ b/docs/mkdocs/docs/api/basic_json/parse.md @@ -313,6 +313,10 @@ outside of a string, invalid) byte; see the [FAQ entry](../../home/faq.md#nul-by it as end of input; planned to become the default in version 4.0.0. - Extended empty-input detection to also cover an `std::istream` without a stream buffer, and fixed a crash (`std::terminate`) when parsing from an `std::istream` with `eofbit` in its exception mask, in version 3.13.0. +- Fixed the position that a [`parse_error.101`](../../home/exceptions.md#jsonexceptionparse_error101) reports for an + unexpected number that is followed by a line feed in version 3.13.0: for the input `1`, line feed, `2`, line feed, + version 3.12.0 reported "line 2, column 0" and version 3.13.0 reports "line 2, column 1"; for `1 2` and a line feed, + the report was "line 1, column 0" and is now "line 1, column 3". - Parsing from an `std::istream` scans the characters in its stream buffer in bulk in version 3.13.0 ([`JSON_DISABLE_STREAM_BULK_SCAN`](../macros/json_disable_stream_bulk_scan.md) restores reading one character at a time). diff --git a/docs/mkdocs/docs/api/basic_json/start_pos.md b/docs/mkdocs/docs/api/basic_json/start_pos.md index fb43fb52b..68fabbec8 100644 --- a/docs/mkdocs/docs/api/basic_json/start_pos.md +++ b/docs/mkdocs/docs/api/basic_json/start_pos.md @@ -67,3 +67,7 @@ Constant. ## Version history - Added in version 3.12.0. +- Fixed in version 3.13.0: in version 3.12.0, the start position of a string that contains escape sequences (for + example `#!cpp "\n"` or `#!cpp "\u00e4"`) was too large, by the number of bytes the escape sequences add to the + source text. The position is now always that of the opening `"`. The "(bytes a-b)" range in the messages of + [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) was wrong in the same way. diff --git a/docs/mkdocs/docs/api/macros/json_diagnostic_positions.md b/docs/mkdocs/docs/api/macros/json_diagnostic_positions.md index 340498acb..4358a373a 100644 --- a/docs/mkdocs/docs/api/macros/json_diagnostic_positions.md +++ b/docs/mkdocs/docs/api/macros/json_diagnostic_positions.md @@ -125,3 +125,5 @@ When the macro is not defined, the library will define it to its default value. ## Version history - Added in version 3.12.0. +- Fixed in version 3.13.0: in version 3.12.0, [`start_pos()`](../basic_json/start_pos.md) of a string that contains + escape sequences was too large; it is now the position of the opening `"`. From cdfb130fa6582e8dbfae5391201c1c747290b071 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Thu, 12 Nov 2026 08:40:18 +0100 Subject: [PATCH 18/21] Read a std::u8string from a JSON string and store it as one in every configuration (#6075) * Read a std::u8string from a JSON string and store it as one in every configuration In C++20, json(std::u8string) was stored as a JSON string only when std::filesystem::path support was available (the to_json overload sat inside the JSON_HAS_FILESYSTEM guard), and there was no matching from_json, so get() threw type_error.302 ("type must be array, but is string"). Without std::filesystem, the same expression still produced an array of code units (the 3.12.0 behavior). Move the to_json overload out of the filesystem guard so that a std::u8string is a string in every configuration, build the value directly from the code units (this also works for a custom string_t such as the tests' alt_string), and add a from_json overload that reads a JSON string. The array of numbers written by 3.12.0 is still accepted; other types throw type_error.302 ("type must be string or array"). std::u8string_view and char8_t arrays are unchanged. The tests now run in C++20/23 (the file mentions JSON_HAS_CPP_20/23), also without std::filesystem (test-conversions2_no_filesystem), and with ordered_json and a custom string type. The new behavior is documented. Signed-off-by: Niels Lohmann * Fill the std::u8string with resize and indexing instead of push_back The push_back loop is not vectorized: reading a 1 kB string took about 2.2 us against 0.39 us with resize and direct element access (clang -O2). Signed-off-by: Niels Lohmann * Write the std::u8string from_json template header in the compact form Signed-off-by: Niels Lohmann * Ignore libstdc++ char to char8_t sign change in the clang sanitizer job Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- cmake/clang_sanitizer_ignorelist.txt | 5 + docs/mkdocs/docs/api/basic_json/basic_json.md | 6 +- docs/mkdocs/docs/api/basic_json/get.md | 6 + .../features/types/template_parameters.md | 2 +- docs/mkdocs/docs/home/releases/unreleased.md | 10 ++ .../nlohmann/detail/conversions/from_json.hpp | 31 +++++ .../nlohmann/detail/conversions/to_json.hpp | 10 +- single_include/nlohmann/json.hpp | 41 ++++++- tests/CMakeLists.txt | 10 ++ tests/src/unit-alt-string.cpp | 16 +++ tests/src/unit-conversions2.cpp | 108 +++++++++++++++--- 11 files changed, 222 insertions(+), 23 deletions(-) diff --git a/cmake/clang_sanitizer_ignorelist.txt b/cmake/clang_sanitizer_ignorelist.txt index ec54ac571..66f62b43e 100644 --- a/cmake/clang_sanitizer_ignorelist.txt +++ b/cmake/clang_sanitizer_ignorelist.txt @@ -9,6 +9,11 @@ # __n2)`, which stores `__n1 - __n2` in a signed difference_type and clamps it to int. Any comparison with a shorter # view wraps that subtraction on purpose, and -fsanitize=integer reports it as implicit-integer-sign-change. json_view # compares keys as string views, so the same check is excluded for as well. +# std::filesystem::path::u8string() in libstdc++ 14 builds a std::u8string from the native char sequence, so +# basic_string's range copy (_S_copy_chars) converts every char >= 0x80 to char8_t, which -fsanitize=integer reports as +# implicit-integer-sign-change (test-conversions2_cpp20/cpp23, non-ASCII path). The conversion is intended and happens +# inside the standard library, not in this library. [implicit-integer-sign-change] src:*/include/c++/*/format src:*/include/c++/*/string_view +src:*/include/c++/*/bits/basic_string.h diff --git a/docs/mkdocs/docs/api/basic_json/basic_json.md b/docs/mkdocs/docs/api/basic_json/basic_json.md index f5c861712..c029f519c 100644 --- a/docs/mkdocs/docs/api/basic_json/basic_json.md +++ b/docs/mkdocs/docs/api/basic_json/basic_json.md @@ -67,7 +67,11 @@ basic_json(basic_json&& other) noexcept; - **objects**: [`object_t`](object_t.md) and all kinds of compatible associative containers such as `std::map`, `std::unordered_map`, `std::multimap`, and `std::unordered_multimap` with a `key_type` compatible to `string_t` and a `value_type` from which a `basic_json` value can be constructed. - - **strings**: `string_t`, string literals, and all compatible string containers can be used. + - **strings**: `string_t`, string literals, and all compatible string containers can be used. Since version 3.13.0, + this includes `#!cpp std::u8string` (C++20): its code units are stored as a JSON string and + [`get()`](get.md) reads it back (in version 3.12.0, a `#!cpp std::u8string` was stored as an array + of numbers; `get()` still accepts such an array). `#!cpp std::u8string_view` and `#!cpp char8_t` + arrays are not strings; they are stored as arrays of code units. !!! note "Rvalue containers are moved from" diff --git a/docs/mkdocs/docs/api/basic_json/get.md b/docs/mkdocs/docs/api/basic_json/get.md index 06e34218a..a590ae03a 100644 --- a/docs/mkdocs/docs/api/basic_json/get.md +++ b/docs/mkdocs/docs/api/basic_json/get.md @@ -130,6 +130,12 @@ overload (3). `#!cpp json(true).get()` throw. This is the behavior of all 3.x versions; [`basic_json_view::get()`](../basic_json_view/get.md) behaves the same. +!!! note "`std::u8string` conversions" + + Since version 3.13.0, `#!cpp get()` (C++20) reads a JSON string; it also accepts an array of numbers, + the form in which version 3.12.0 stored a `#!cpp std::u8string`. Any other JSON type throws + [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) ("type must be string or array"). + !!! note "`std::optional` conversions" Prior to version 3.13.0, `#!cpp get>()` (and other conversions to `std::optional`) failed to diff --git a/docs/mkdocs/docs/features/types/template_parameters.md b/docs/mkdocs/docs/features/types/template_parameters.md index 9c3479ab0..6a09fbeef 100644 --- a/docs/mkdocs/docs/features/types/template_parameters.md +++ b/docs/mkdocs/docs/features/types/template_parameters.md @@ -426,7 +426,7 @@ using array_t = ArrayType>; | Type | Reason | |----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------| | `#!cpp std::wstring`, `#!cpp std::u16string`, `#!cpp std::u32string` | the character type is not one byte wide | -| `#!cpp std::u8string` | one byte wide, but `#!cpp char8_t` is not `#!cpp char`-compatible | +| `#!cpp std::u8string` | one byte wide, but `#!cpp char8_t` is not `#!cpp char`-compatible (as a *value*, it converts to and from a JSON string) | | `absl::Cord` | no `value_type`, and the storage is not contiguous | | `QString` | no `append(const char*, size_type)`; its `QChar` is also two bytes wide, though that is never diagnosed | diff --git a/docs/mkdocs/docs/home/releases/unreleased.md b/docs/mkdocs/docs/home/releases/unreleased.md index b27da9748..01b334247 100644 --- a/docs/mkdocs/docs/home/releases/unreleased.md +++ b/docs/mkdocs/docs/home/releases/unreleased.md @@ -8,6 +8,16 @@ These changes are part of the `develop` branch, but not of a release yet. exception is thrown during the conversion, the source may be partially moved-from, so the strong exception guarantee no longer holds for the source of an rvalue conversion (the JSON value itself is unaffected). Lvalues and `const` objects are still copied. See [`basic_json`](../../api/basic_json/basic_json.md). +- In C++20 (library support for `char8_t`, `__cpp_lib_char8_t`), a `#!cpp std::u8string` is a JSON **string**: + `json j = std::u8string(u8"abc");` stores `"abc"`, and `#!cpp j.get()` reads it back. Version 3.12.0 + stored a `#!cpp std::u8string` as an array of code units (`[97,98,99]`); on the 3.13.0 development branch it was + already stored as a string if `std::filesystem` is available, but could not be read back. Now the result no longer + depends on whether `std::filesystem::path` is supported, and `#!cpp get()` accepts both a JSON string + and the array of numbers written by 3.12.0. **Consequence:** JSON documents produced from a `#!cpp std::u8string` by + 3.12.0 (arrays) are still read, but code that checks `is_array()` on such a value, or that reads it as + `#!cpp std::vector` / `#!cpp std::vector`, now sees a string; to keep the array form, convert + explicitly, e.g., `#!cpp json(std::vector(s.begin(), s.end()))`. `#!cpp std::u8string_view` and + `#!cpp char8_t` arrays are unchanged (arrays of code units). See [`basic_json`](../../api/basic_json/basic_json.md). ## API changes diff --git a/include/nlohmann/detail/conversions/from_json.hpp b/include/nlohmann/detail/conversions/from_json.hpp index 09f0713ea..18c0775ec 100644 --- a/include/nlohmann/detail/conversions/from_json.hpp +++ b/include/nlohmann/detail/conversions/from_json.hpp @@ -785,6 +785,37 @@ void from_json(const BasicJsonType& j, std::unordered_map::string_t::value_type, char>::value, int> = 0> +inline void from_json(const BasicJsonType& j, std::basic_string& s) +{ + if (j.is_string()) + { + const auto& str = string_value(j); + const char* const data = str.data(); + const std::size_t size = str.size(); + std::basic_string ret(s.get_allocator()); + ret.resize(size); + for (std::size_t i = 0; i < size; ++i) + { + ret[i] = static_cast(static_cast(data[i])); + } + s = std::move(ret); + } + else if (j.is_array()) + { + from_json_array_impl(j, s, priority_tag<3> {}); + } + else + { + throw_type_must_be("string or array", j); + } +} +#endif + #if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM // Workaround for MSVC 19.51 (and possibly later): in large cpp files, the compiler may fail to resolve with generic has_from_json (issue #4996) diff --git a/include/nlohmann/detail/conversions/to_json.hpp b/include/nlohmann/detail/conversions/to_json.hpp index 05ae82370..3d95370d6 100644 --- a/include/nlohmann/detail/conversions/to_json.hpp +++ b/include/nlohmann/detail/conversions/to_json.hpp @@ -625,16 +625,20 @@ inline void to_json(BasicJsonType& j, const T& t) to_json_tuple_impl(j, t, make_index_sequence::value> {}); } -#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM +// a std::u8string is a JSON string (it was an array of code units before 3.13.0); +// defined outside the std::filesystem guard so that the result does not depend +// on whether std::filesystem::path is supported #if defined(__cpp_lib_char8_t) template inline void to_json(BasicJsonType& j, const std::basic_string& s) { - using OtherAllocator = typename std::allocator_traits::template rebind_alloc; - j = std::basic_string, OtherAllocator>(s.begin(), s.end(), s.get_allocator()); + using string_t = typename BasicJsonType::string_t; + // char may alias any object; the code units are copied unchanged + j = string_t(reinterpret_cast(s.data()), s.size()); } #endif +#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM // Workaround for MSVC 19.51 (and possibly later): in large cpp files, the compiler may fail to resolve with generic has_to_json (issue #4996) template struct has_to_json : std::true_type {}; diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 112441bf1..102eb94ad 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -6934,6 +6934,37 @@ void from_json(const BasicJsonType& j, std::unordered_map::string_t::value_type, char>::value, int> = 0> +inline void from_json(const BasicJsonType& j, std::basic_string& s) +{ + if (j.is_string()) + { + const auto& str = string_value(j); + const char* const data = str.data(); + const std::size_t size = str.size(); + std::basic_string ret(s.get_allocator()); + ret.resize(size); + for (std::size_t i = 0; i < size; ++i) + { + ret[i] = static_cast(static_cast(data[i])); + } + s = std::move(ret); + } + else if (j.is_array()) + { + from_json_array_impl(j, s, priority_tag<3> {}); + } + else + { + throw_type_must_be("string or array", j); + } +} +#endif + #if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM // Workaround for MSVC 19.51 (and possibly later): in large cpp files, the compiler may fail to resolve with generic has_from_json (issue #4996) @@ -8335,16 +8366,20 @@ inline void to_json(BasicJsonType& j, const T& t) to_json_tuple_impl(j, t, make_index_sequence::value> {}); } -#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM +// a std::u8string is a JSON string (it was an array of code units before 3.13.0); +// defined outside the std::filesystem guard so that the result does not depend +// on whether std::filesystem::path is supported #if defined(__cpp_lib_char8_t) template inline void to_json(BasicJsonType& j, const std::basic_string& s) { - using OtherAllocator = typename std::allocator_traits::template rebind_alloc; - j = std::basic_string, OtherAllocator>(s.begin(), s.end(), s.get_allocator()); + using string_t = typename BasicJsonType::string_t; + // char may alias any object; the code units are copied unchanged + j = string_t(reinterpret_cast(s.data()), s.size()); } #endif +#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM // Workaround for MSVC 19.51 (and possibly later): in large cpp files, the compiler may fail to resolve with generic has_to_json (issue #4996) template struct has_to_json : std::true_type {}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e7d90cecd..026d766b4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -378,6 +378,16 @@ json_test_add_test_for(src/unit-class_parser.cpp MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} ) +# test the conversions again without std::filesystem::path support: a +# std::u8string must be a JSON string in this configuration, too +json_test_set_test_options(test-conversions2_no_filesystem + COMPILE_DEFINITIONS JSON_HAS_FILESYSTEM=0 JSON_HAS_EXPERIMENTAL_FILESYSTEM=0 +) +json_test_add_test_for(src/unit-conversions2.cpp + NAME test-conversions2_no_filesystem + MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} +) + # test diagnostic positions again without regular diagnostics (JSON pointer paths) json_test_set_test_options(test-diagnostic-positions_only COMPILE_DEFINITIONS JSON_DIAGNOSTICS=0 diff --git a/tests/src/unit-alt-string.cpp b/tests/src/unit-alt-string.cpp index 79e59b660..add4e199c 100644 --- a/tests/src/unit-alt-string.cpp +++ b/tests/src/unit-alt-string.cpp @@ -539,4 +539,20 @@ TEST_CASE("alternative string type with json_view") } } +// a std::u8string is a JSON string, whatever the string type of the JSON value +// JSON_HAS_CPP_20 JSON_HAS_CPP_23 (do not remove; see note at top of file) +#if defined(__cpp_lib_char8_t) +TEST_CASE("alternative string type and std::u8string") +{ + const std::u8string s = u8"abc"; + const alt_json j = s; + CHECK(j.is_string()); + CHECK(j.get() == alt_string("abc")); + CHECK(j.get() == s); + + // the array of code units written by versions before 3.13.0 + CHECK(alt_json::array({0x61, 0x62, 0x63}).get() == s); +} +#endif + DOCTEST_CLANG_SUPPRESS_WARNING_POP diff --git a/tests/src/unit-conversions2.cpp b/tests/src/unit-conversions2.cpp index c543e4ff8..8cb44a231 100644 --- a/tests/src/unit-conversions2.cpp +++ b/tests/src/unit-conversions2.cpp @@ -27,10 +27,12 @@ using nlohmann::json; #include #include #include +#include #include #include #include #include +#include // NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair DOCTEST_CLANG_SUPPRESS_WARNING_PUSH @@ -707,38 +709,114 @@ TEST_CASE("std::filesystem::path") } #endif -// the ADL to_json overload for std::u8string only exists under the same guard -// as std::filesystem::path support (it is otherwise only reached indirectly, -// via std::filesystem::path::u8string()) -- mirror both #if conditions from -// include/nlohmann/detail/conversions/to_json.hpp exactly -#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM +// a std::u8string is a JSON string since 3.13.0 (it was an array of code units +// before); independent of whether std::filesystem::path is supported, see the +// test-conversions2_no_filesystem target +// JSON_HAS_CPP_20 JSON_HAS_CPP_23 (do not remove; see note at top of file) #if defined(__cpp_lib_char8_t) TEST_CASE("std::u8string") { + // use \u universal-character-names (rather than raw \x byte escapes + // or literal non-ASCII source bytes) to compose the multi-byte UTF-8 + // encoding -- MSVC treats \x escapes used that way inside a u8 + // literal as a nonstandard extension (warning C5321), which some of + // our CI configs promote to an error; \u is portable and produces + // the exact same encoded bytes without depending on the source + // file's encoding + const std::u8string utf8 = u8"P\u011B\u0161ina"; + SECTION("ascii") { const std::u8string s = u8"Path"; json const j = s; + CHECK(j.is_string()); CHECK(j.template get() == "Path"); } SECTION("utf-8") { - // use \u universal-character-names (rather than raw \x byte escapes - // or literal non-ASCII source bytes) to compose the multi-byte UTF-8 - // encoding -- MSVC treats \x escapes used that way inside a u8 - // literal as a nonstandard extension (warning C5321), which some of - // our CI configs promote to an error; \u is portable and produces - // the exact same encoded bytes without depending on the source - // file's encoding - const std::u8string s = u8"P\u011B\u0161ina"; - json const j = s; + json const j = utf8; + CHECK(j.is_string()); CHECK(j.template get() == "P\xc4\x9b\xc5\xa1ina"); } -} + + SECTION("round trip") + { + json const j = utf8; + CHECK(j.template get() == utf8); + + std::u8string back; + j.get_to(back); + CHECK(back == utf8); + + // from a string + json const j_string = "P\xc4\x9b\xc5\xa1ina"; + CHECK(j_string.template get() == utf8); + + // empty, and with an embedded NUL + CHECK(json(std::u8string()).template get().empty()); + const std::u8string with_nul(u8"a\0b", 3); + CHECK(json(with_nul).template get() == with_nul); + + // the lvalue and rvalue paths, and in containers + std::u8string lvalue = utf8; + CHECK(json(lvalue).template get() == utf8); + CHECK(json(std::move(lvalue)).template get() == utf8); + const std::vector vec = {utf8, u8"x"}; + json const j_vec = vec; + CHECK(j_vec.is_array()); + CHECK(j_vec[0].is_string()); + CHECK(j_vec.template get>() == vec); + const std::map map = {{"k", utf8}}; + CHECK(json(map).template get>() == map); + } + + SECTION("array of code units (written by versions before 3.13.0)") + { + json const j = json::array({0x50, 0xC4, 0x9B, 0xC5, 0xA1, 0x69, 0x6E, 0x61}); + CHECK(j.template get() == utf8); + CHECK(json::array().template get().empty()); + CHECK(json::parse("[97, 98]").template get() == u8"ab"); + + std::u8string s = u8"overwritten"; + j.get_to(s); + CHECK(s == utf8); + } + + SECTION("other types") + { +#if !defined(JSON_NOEXCEPTION) + CHECK_THROWS_WITH_AS(json(1).template get(), "[json.exception.type_error.302] type must be string or array, but is number", json::type_error); + CHECK_THROWS_WITH_AS(json(nullptr).template get(), "[json.exception.type_error.302] type must be string or array, but is null", json::type_error); + CHECK_THROWS_AS(json::object().template get(), json::type_error); + // an element that is not a number + CHECK_THROWS_AS(json::array({"a"}).template get(), json::type_error); #endif + } + + SECTION("ordered_json") + { + nlohmann::ordered_json const j = utf8; + CHECK(j.is_string()); + CHECK(j.template get() == "P\xc4\x9b\xc5\xa1ina"); + CHECK(j.template get() == utf8); + CHECK(nlohmann::ordered_json::array({0x61, 0x62}).template get() == u8"ab"); + } + + SECTION("std::u8string_view and char8_t arrays are unchanged (arrays of code units)") + { + const std::u8string_view view = utf8; + json const j_view = view; + CHECK(j_view.is_array()); + CHECK(j_view.size() == 8); + + const char8_t arr[] = u8"ab"; + json const j_arr = arr; + CHECK(j_arr == json::array({0x61, 0x62, 0})); + } +} #endif #if !defined(JSON_NOEXCEPTION) From 2755952d36c61845c6db599dbc1ea181b090b3a2 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 13 Nov 2026 07:26:20 +0100 Subject: [PATCH 19/21] Fix develop CI: GCC 4.8, Clang 3.4 and clang-tidy errors (#6100) * Fix develop CI: GCC 4.8, Clang 3.4 and clang-tidy errors - unit-json_patch.cpp: move multi-line raw strings out of CHECK(), which GCC 4.8 cannot parse inside a macro argument - unit-recursion_depth_limit.cpp: value-initialize the const std::hash (Clang 3.4 rejects default initialization of a const object) - json_view: silence modernize-use-transparent-functors (std::less<> is C++14), bugprone-suspicious-stringview-data-usage (the length comes from the node) and the deliberate use-after-clear() / owning-memory hits in unit-json_view_builder.cpp; use a raw string literal there Signed-off-by: Niels Lohmann * Fix clang-tidy errors in the std::u8string tests Test the lvalue path with the const original instead of a mutable copy and the rvalue path with a temporary (misc-const-correctness), and mark the intentional char8_t array with NOLINT (avoid-c-arrays). Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- include/nlohmann/detail/view/compare.hpp | 2 ++ include/nlohmann/detail/view/document_data.hpp | 3 ++- single_include/nlohmann/json_view.hpp | 5 ++++- tests/src/unit-conversions2.cpp | 7 +++---- tests/src/unit-json_patch.cpp | 15 +++++++++------ tests/src/unit-json_view_builder.cpp | 7 ++++--- tests/src/unit-recursion_depth_limit.cpp | 2 +- 7 files changed, 25 insertions(+), 16 deletions(-) diff --git a/include/nlohmann/detail/view/compare.hpp b/include/nlohmann/detail/view/compare.hpp index ab0e5bbef..1927e9de0 100644 --- a/include/nlohmann/detail/view/compare.hpp +++ b/include/nlohmann/detail/view/compare.hpp @@ -905,6 +905,8 @@ void resume_array(const lockstep_path& /*path*/, const A& /*a*/, const B& /*b*/, template bool floats_equal(const view_side& a, const view_side& b) { + // float_token_is_nan() reads at most the node length of the token, which is not null terminated + // NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage) return (a.value_node()->extra == b.value_node()->extra && a.float_token() == b.float_token() && !(view_side::nav::editable() && float_token_is_nan(*a.value_node(), a.float_token().data()))) || std::equal_to()(a.floating(), b.floating()); } diff --git a/include/nlohmann/detail/view/document_data.hpp b/include/nlohmann/detail/view/document_data.hpp index c1e179e93..aea6c6972 100644 --- a/include/nlohmann/detail/view/document_data.hpp +++ b/include/nlohmann/detail/view/document_data.hpp @@ -158,7 +158,8 @@ struct document_data { const auto overlaps = [p, n](const char* b, std::size_t m) noexcept { - // (std::less: a total order also for pointers into different objects) + // (std::less: a total order also for pointers into different objects; std::less<> needs C++14) + // NOLINTNEXTLINE(modernize-use-transparent-functors) return n != 0 && m != 0 && std::less()(p, b + m) && std::less()(b, p + n); }; return overlaps(owned.data(), owned.capacity()) diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index af959c06b..233a55487 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -502,7 +502,8 @@ struct document_data { const auto overlaps = [p, n](const char* b, std::size_t m) noexcept { - // (std::less: a total order also for pointers into different objects) + // (std::less: a total order also for pointers into different objects; std::less<> needs C++14) + // NOLINTNEXTLINE(modernize-use-transparent-functors) return n != 0 && m != 0 && std::less()(p, b + m) && std::less()(b, p + n); }; return overlaps(owned.data(), owned.capacity()) @@ -4088,6 +4089,8 @@ void resume_array(const lockstep_path& /*path*/, const A& /*a*/, const B& /*b*/, template bool floats_equal(const view_side& a, const view_side& b) { + // float_token_is_nan() reads at most the node length of the token, which is not null terminated + // NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage) return (a.value_node()->extra == b.value_node()->extra && a.float_token() == b.float_token() && !(view_side::nav::editable() && float_token_is_nan(*a.value_node(), a.float_token().data()))) || std::equal_to()(a.floating(), b.floating()); } diff --git a/tests/src/unit-conversions2.cpp b/tests/src/unit-conversions2.cpp index 8cb44a231..afba3e1e9 100644 --- a/tests/src/unit-conversions2.cpp +++ b/tests/src/unit-conversions2.cpp @@ -761,9 +761,8 @@ TEST_CASE("std::u8string") CHECK(json(with_nul).template get() == with_nul); // the lvalue and rvalue paths, and in containers - std::u8string lvalue = utf8; - CHECK(json(lvalue).template get() == utf8); - CHECK(json(std::move(lvalue)).template get() == utf8); + CHECK(json(utf8).template get() == utf8); + CHECK(json(std::u8string(utf8)).template get() == utf8); const std::vector vec = {utf8, u8"x"}; json const j_vec = vec; CHECK(j_vec.is_array()); @@ -812,7 +811,7 @@ TEST_CASE("std::u8string") CHECK(j_view.is_array()); CHECK(j_view.size() == 8); - const char8_t arr[] = u8"ab"; + const char8_t arr[] = u8"ab"; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) json const j_arr = arr; CHECK(j_arr == json::array({0x61, 0x62, 0})); } diff --git a/tests/src/unit-json_patch.cpp b/tests/src/unit-json_patch.cpp index 6b38cbcd6..7ae82c1b7 100644 --- a/tests/src/unit-json_patch.cpp +++ b/tests/src/unit-json_patch.cpp @@ -2682,25 +2682,27 @@ TEST_CASE("JSON patch - consecutive operations with paths that share a prefix") const json doc = json::parse(R"({"a": [{"b": [1, 2, 3]}, {"b": [4, 5, 6]}], "c": {"d": {"e": 7}}})"); // an element inserted before the elements of an array moves them - CHECK(doc.patch(json::parse(R"([ + const json inserted = json::parse(R"([ {"op": "test", "path": "/a/0/b/2", "value": 3}, {"op": "add", "path": "/a/0", "value": {"b": [9]}}, {"op": "test", "path": "/a/1/b/2", "value": 3}, {"op": "test", "path": "/a/0/b/0", "value": 9}, {"op": "test", "path": "/a/2/b/0", "value": 4} - ])")) == json::parse(R"({"a": [{"b": [9]}, {"b": [1, 2, 3]}, {"b": [4, 5, 6]}], "c": {"d": {"e": 7}}})")); + ])"); + CHECK(doc.patch(inserted) == json::parse(R"({"a": [{"b": [9]}, {"b": [1, 2, 3]}, {"b": [4, 5, 6]}], "c": {"d": {"e": 7}}})")); // an element removed from the front - CHECK(doc.patch(json::parse(R"([ + const json removed_front = json::parse(R"([ {"op": "test", "path": "/a/1/b/2", "value": 6}, {"op": "remove", "path": "/a/0"}, {"op": "test", "path": "/a/0/b/2", "value": 6}, {"op": "replace", "path": "/a/0/b/2", "value": 60}, {"op": "test", "path": "/a/0/b/2", "value": 60} - ])")) == json::parse(R"({"a": [{"b": [4, 5, 60]}], "c": {"d": {"e": 7}}})")); + ])"); + CHECK(doc.patch(removed_front) == json::parse(R"({"a": [{"b": [4, 5, 60]}], "c": {"d": {"e": 7}}})")); // a value replaced above the path that was resolved before - CHECK(doc.patch(json::parse(R"([ + const json replaced_above = json::parse(R"([ {"op": "test", "path": "/c/d/e", "value": 7}, {"op": "replace", "path": "/c/d", "value": {"e": 8, "f": {"g": 9}}}, {"op": "test", "path": "/c/d/e", "value": 8}, @@ -2708,7 +2710,8 @@ TEST_CASE("JSON patch - consecutive operations with paths that share a prefix") {"op": "remove", "path": "/c/d/f"}, {"op": "add", "path": "/c/d/f", "value": [1]}, {"op": "test", "path": "/c/d/f/0", "value": 1} - ])")) == json::parse(R"({"a": [{"b": [1, 2, 3]}, {"b": [4, 5, 6]}], "c": {"d": {"e": 8, "f": [1]}}})")); + ])"); + CHECK(doc.patch(replaced_above) == json::parse(R"({"a": [{"b": [1, 2, 3]}, {"b": [4, 5, 6]}], "c": {"d": {"e": 8, "f": [1]}}})")); #if !defined(JSON_NOEXCEPTION) // a value removed above the path that was resolved before is gone diff --git a/tests/src/unit-json_view_builder.cpp b/tests/src/unit-json_view_builder.cpp index ea3c12de7..fed2b7a19 100644 --- a/tests/src/unit-json_view_builder.cpp +++ b/tests/src/unit-json_view_builder.cpp @@ -916,14 +916,15 @@ TEST_CASE("json_view document_data::holds() (the debug check of read())") SECTION("owned text") { - d->owned = "{\"a\":\"some text that does not fit a short string\"}"; + d->owned = R"({"a":"some text that does not fit a short string"})"; const char* const b = d->owned.data(); CHECK(d->holds(b, 1)); CHECK(d->holds(b + 5, 3)); CHECK(d->holds(b + d->owned.size() - 1, 1)); // also after clear(), which keeps the buffer that the next parse writes d->owned.clear(); - CHECK(d->holds(b, 4)); + // (b is only compared with the buffer, never read) + CHECK(d->holds(b, 4)); // NOLINT(clang-analyzer-cplusplus.InnerPointer) CHECK(!d->holds(other.data(), other.size())); CHECK(!d->holds(b, 0)); } @@ -946,7 +947,7 @@ TEST_CASE("json_view document_data::holds() (the debug check of read())") { d->edits.reset(new document_data::edit_state()); // NOLINT(cppcoreguidelines-owning-memory) CHECK(!d->holds(other.data(), other.size())); - d->edits->texts.emplace_back(new char[16]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + d->edits->texts.emplace_back(new char[16]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,cppcoreguidelines-owning-memory) d->edits->text_cap = 16; CHECK(d->holds(d->edits->texts.back().get() + 3, 3)); CHECK(!d->holds(other.data(), other.size())); diff --git a/tests/src/unit-recursion_depth_limit.cpp b/tests/src/unit-recursion_depth_limit.cpp index cf62d1218..2869a7241 100644 --- a/tests/src/unit-recursion_depth_limit.cpp +++ b/tests/src/unit-recursion_depth_limit.cpp @@ -494,7 +494,7 @@ TEST_CASE("walks over values nested around the recursion depth limit") for_each_value([](const std::string & kinds) { const json a = build(kinds, 1); - const std::hash hash; + const std::hash hash{}; CHECK(hash(a) == hash(json(a))); CHECK(hash(a) == hash(json::parse(expected_compact(kinds, "1")))); CHECK(hash(a) != hash(build(kinds, 2))); From 641e312f96bc5500f1d08462facf14bda2a19fe1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Nov 2026 07:26:32 +0100 Subject: [PATCH 20/21] build(deps): bump watchdog from 7.0.0 to 7.1.0 in /tools/serve_header (#6101) Bumps [watchdog](https://github.com/gorakhargosh/watchdog) from 7.0.0 to 7.1.0. - [Release notes](https://github.com/gorakhargosh/watchdog/releases) - [Changelog](https://github.com/gorakhargosh/watchdog/blob/master/changelog.rst) - [Commits](https://github.com/gorakhargosh/watchdog/compare/v7.0.0...v7.1.0) --- updated-dependencies: - dependency-name: watchdog dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tools/serve_header/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/serve_header/requirements.txt b/tools/serve_header/requirements.txt index a80bc3798..77ae05c94 100644 --- a/tools/serve_header/requirements.txt +++ b/tools/serve_header/requirements.txt @@ -1,2 +1,2 @@ PyYAML==6.0.3 -watchdog==7.0.0 +watchdog==7.1.0 From 5bad6c518bf6663b69f505511a19c98c91f77c3a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Nov 2026 07:26:42 +0100 Subject: [PATCH 21/21] build(deps): bump jsdom in /docs/mkdocs/scripts/mermaid (#6102) Bumps [jsdom](https://github.com/jsdom/jsdom) from 30.1.3 to 31.0.0. - [Release notes](https://github.com/jsdom/jsdom/releases) - [Commits](https://github.com/jsdom/jsdom/compare/v30.1.3...v31.0.0) --- updated-dependencies: - dependency-name: jsdom dependency-version: 31.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/mkdocs/scripts/mermaid/package-lock.json | 338 +++++------------- docs/mkdocs/scripts/mermaid/package.json | 2 +- 2 files changed, 93 insertions(+), 247 deletions(-) diff --git a/docs/mkdocs/scripts/mermaid/package-lock.json b/docs/mkdocs/scripts/mermaid/package-lock.json index 5e9ff461b..863f047b1 100644 --- a/docs/mkdocs/scripts/mermaid/package-lock.json +++ b/docs/mkdocs/scripts/mermaid/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "check-mermaid", "dependencies": { - "jsdom": "30.1.3", + "jsdom": "31.0.0", "mermaid": "12.3.0" } }, @@ -23,55 +23,12 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@asamuzakjp/css-color": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.1.3.tgz", - "integrity": "sha512-1t1U8Cm3RBl9vh3RAZhjkPaduzLSvQJRToRrxpGR/eDRW99mOREleuu+mWfiFa795bUzjwht7NgphCKTs2ianQ==", - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^3.4.2", - "@csstools/css-color-parser": "^4.2.5", - "@csstools/css-parser-algorithms": "^4.0.2", - "@csstools/css-tokenizer": "^4.0.2", - "lru-cache": "^11.5.3" - }, - "engines": { - "node": "^22.22.2 || ^24.15.0 || >=26.0.0" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-10.0.3.tgz", - "integrity": "sha512-3+777rEPsOOjoy5/sRmIzHnoRDaLpm3XWY2dmWn/N7f/XwDG0bcpnFaKL3X9FYJUbNwLmpcJC1bUYexJRxsFlw==", - "license": "MIT", - "dependencies": { - "bidi-js": "^1.1.0", - "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.5.3" - }, - "engines": { - "node": "^22.22.2 || ^24.15.0 || >=26.0.0" - } - }, "node_modules/@braintree/sanitize-url": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", "license": "MIT" }, - "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "license": "MIT", - "dependencies": { - "css-tree": "^3.0.0" - }, - "bin": { - "specificity": "bin/cli.js" - } - }, "node_modules/@chevrotain/cst-dts-gen": { "version": "13.2.0", "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-13.2.0.tgz", @@ -109,140 +66,6 @@ "integrity": "sha512-SzKYY68rseOrb9KILVzPsl6SjkQtdWw01SRGB5aCVI5kFcHltQsOYV3JB0Ffst72DgkvHCyDQ0NLwxtmCCf97Q==", "license": "Apache-2.0" }, - "node_modules/@csstools/color-helpers": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz", - "integrity": "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@csstools/css-calc": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.3.tgz", - "integrity": "sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.2", - "@csstools/css-tokenizer": "^4.0.2" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.6.tgz", - "integrity": "sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^6.1.2", - "@csstools/css-calc": "^3.4.3" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.2", - "@csstools/css-tokenizer": "^4.0.2" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.2.tgz", - "integrity": "sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.2" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.15.tgz", - "integrity": "sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "peerDependencies": { - "css-tree": "^3.2.1" - }, - "peerDependenciesMeta": { - "css-tree": { - "optional": true - } - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz", - "integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, "node_modules/@exodus/bytes": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz", @@ -565,13 +388,29 @@ "d3-transition": "^3.0.1" } }, - "node_modules/bidi-js": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", - "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "node_modules/bcp-47": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.2.tgz", + "integrity": "sha512-wsv86UuNFuPnVieKLyECTT+CP1WSAHeoIg5P3xu2kA9Z+7fVPoKt7dxzSeEhtJel0AI+NW3fIW3KtTKQKUCngg==", "license": "MIT", "dependencies": { - "require-from-string": "^2.0.2" + "is-alphabetical": "^2.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/chevrotain": { @@ -608,19 +447,6 @@ "layout-base": "^1.0.0" } }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, "node_modules/cytoscape": { "version": "3.34.3", "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.3.tgz", @@ -1170,9 +996,9 @@ "license": "EPL-2.0 OR GPL-3.0-or-later" }, "node_modules/entities": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", - "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-9.0.0.tgz", + "integrity": "sha512-oJFNDJg3kTPwAD1cNn2+WmUq4wFoo09npc3Ydf1D7ED4smRiWrsKnaLxykrTWgyGEcusc87P5dxlAleol9kpkg==", "license": "BSD-2-Clause", "engines": { "node": ">=20.19.0" @@ -1242,6 +1068,40 @@ "node": ">=12" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -1249,40 +1109,38 @@ "license": "MIT" }, "node_modules/jsdom": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.3.tgz", - "integrity": "sha512-hQMUPvRgN7KxsfIytDQji7bimuJeRQa6Qo5+iuiZQk5CafeRxLIA10/cG5YCBFNpD9RO3oqIiN4CO1Y44kxqXg==", + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-31.0.0.tgz", + "integrity": "sha512-xAkhD9z1eyFC5+QUi9K24CaZJgdCQ2GOGLW1GdVDoQvkp/AxEH9TQ0pAplPOlIiV92hfcgnFOADZypbjgbBT6Q==", "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^7.1.3", - "@asamuzakjp/dom-selector": "^10.0.3", - "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.15", "@exodus/bytes": "^1.16.0", - "css-tree": "^3.2.1", + "bcp-47": "^2.1.2", + "bcp-47-match": "^2.0.3", "data-urls": "^8.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^7.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.3", - "parse5": "^8.0.1", + "parse5": "^8.1.0", + "parse5-parser-stream": "^8.1.0", "saxes": "^6.0.0", "tough-cookie": "^6.0.2", - "undici": "^8.12.1", + "undici": "^8.12.3", "w3c-xmlserializer": "^6.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^17.2.0", + "whatwg-url": "^17.3.0", "xml-name-validator": "^5.0.0" }, "engines": { "node": "^22.22.2 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "canvas": "^3.2.3" + "@napi-rs/canvas": "^1.0.13" }, "peerDependenciesMeta": { - "canvas": { + "@napi-rs/canvas": { "optional": true } } @@ -1350,12 +1208,6 @@ "node": ">= 20" } }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "license": "CC0-1.0" - }, "node_modules/mermaid": { "version": "12.3.0", "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-12.3.0.tgz", @@ -1396,12 +1248,24 @@ "license": "MIT" }, "node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.1.0.tgz", + "integrity": "sha512-sq3qBFpee5jeocbFbO48VHlGr8sT7mbXS0vxlKN6sottBtJzrOG3998sr0llSZcQAGgfpdmEYAIZE0P9z0dE8w==", "license": "MIT", "dependencies": { - "entities": "^8.0.0" + "entities": "^9.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-8.1.0.tgz", + "integrity": "sha512-58MPfNTA+DsIzro41rU94xghnu/h2N4yEPdPdGPlZVHM/QvA5jlVGYVo7IzlHbFLHYx3cZUnr9s9HBrA0tEYyw==", + "license": "MIT", + "dependencies": { + "parse5": "^8.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -1438,15 +1302,6 @@ "node": ">=6" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/robust-predicates": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", @@ -1489,15 +1344,6 @@ "node": ">=v12.22.7" } }, - "node_modules/source-map-js": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", - "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/stylis": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", @@ -1565,9 +1411,9 @@ } }, "node_modules/undici": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.2.tgz", - "integrity": "sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==", + "version": "8.12.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.4.tgz", + "integrity": "sha512-8Mx+uNKKER2bA4AdtedkqRbwUCIJFr2nJckXRe5yrDqzePqwLP7VNcctN8/XdmDSbF7mS+jJKIszlL+vbam1VA==", "license": "MIT", "engines": { "node": ">=22.19.0" @@ -1604,9 +1450,9 @@ } }, "node_modules/whatwg-url": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.2.0.tgz", - "integrity": "sha512-IhLIHNcBrzIixC5XXsbr4DxdjajyA5vQ5RwGZMtrUp+GbtE5Nrsge9QKau7jXrC0aTXLYGV0xiW7Tb4YS2aomw==", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.3.0.tgz", + "integrity": "sha512-KwOAJrpf+GTgBvcBQJcP38sMF0DsQKdwtG+TxM2pW44P6E5k0hDbWrDmyuJDIF0Uef0aQq0ZoJs+5oI+k8ddhg==", "license": "MIT", "dependencies": { "@exodus/bytes": "^1.16.0", diff --git a/docs/mkdocs/scripts/mermaid/package.json b/docs/mkdocs/scripts/mermaid/package.json index d06363536..cb4f83118 100644 --- a/docs/mkdocs/scripts/mermaid/package.json +++ b/docs/mkdocs/scripts/mermaid/package.json @@ -4,7 +4,7 @@ "description": "Validate the Mermaid diagrams of the documentation (see check_mermaid.mjs)", "type": "module", "dependencies": { - "jsdom": "30.1.3", + "jsdom": "31.0.0", "mermaid": "12.3.0" } }