From 6c5a726b4d2b3d14c919e4b39eb9716af9c8ecf0 Mon Sep 17 00:00:00 2001 From: John Turpish Date: Thu, 21 Dec 2023 05:47:54 -0500 Subject: [PATCH] Start saving throttles As if they're preferences Persist feedback loop --- .github/tour.sh | 81 + .github/ut.sh | 19 + .github/workflows/library.yml | 23 +- .github/workflows/tour.yml | 72 +- .github/workflows/ut.yml | 23 +- .idea/codeStyles/codeStyleConfig.xml | 2 +- .../120.0.6099.71/url/url_util.cc.patch | 17 - .../chrome/browser/ipfs_extra_parts.cc | 10 + .../chrome/browser/ipfs_extra_parts.h | 10 + .../browser/prefs/browser_prefs.cc.patch | 36 + .../122.0.6170.3/url/url_canon_ipfs.cc | 33 +- .../chrome/browser/BUILD.gn.patch | 16 +- .../chrome/browser/about_flags.cc.patch | 6 +- .../chrome_content_browser_client.cc.patch | 29 +- .../chrome/browser/flag-metadata.json.patch | 4 +- .../chrome/browser/flag_descriptions.cc.patch | 2 +- .../chrome/browser/flag_descriptions.h.patch | 2 +- .../chrome/browser/ipfs_extra_parts.cc | 10 + .../chrome/browser/ipfs_extra_parts.h | 10 + .../browser/prefs/browser_prefs.cc.patch | 36 + .../122.0.6182.0/url/url_canon.h.patch | 4 +- .../chrome/browser/BUILD.gn.patch | 10 +- .../chrome/browser/about_flags.cc.patch | 10 +- ...me_autocomplete_scheme_classifier.cc.patch | 0 .../chrome_content_browser_client.cc.patch | 43 +- .../chrome/browser/flag-metadata.json.patch | 4 +- .../chrome/browser/flag_descriptions.cc.patch | 4 +- .../chrome/browser/flag_descriptions.h.patch | 6 +- .../chrome/browser/ipfs_extra_parts.cc | 10 + .../chrome/browser/ipfs_extra_parts.h | 10 + .../browser/prefs/browser_prefs.cc.patch | 36 + .../common/chrome_content_client.cc.patch | 0 .../components/cbor/reader.cc.patch | 0 .../components/cbor/reader.h.patch | 0 .../components/cbor/reader_unittest.cc.patch | 0 .../components/cbor/values.cc.patch | 0 .../components/cbor/values.h.patch | 0 .../components/cbor/writer.cc.patch | 0 .../components/cbor/writer_unittest.cc.patch | 0 .../clipboard_recent_content_generic.cc.patch | 0 .../net/dns/dns_config_service_linux.cc.patch | 0 .../weborigin/scheme_registry.cc.patch | 0 .../url/BUILD.gn.patch | 6 +- .../url/url_canon.h.patch | 4 +- .../url/url_canon_ipfs.cc | 35 +- .../122.0.6194.0/url/url_util.cc.patch | 22 + cmake/chromium.py | 10 +- cmake/inc_link.py | 4 +- cmake/patch.py | 153 +- cmake/tidy.py | 23 + cmake/verbose.py | 3 +- component/CMakeLists.txt | 1 + component/block_http_request.cc | 6 +- component/chromium_ipfs_context.cc | 29 +- component/chromium_ipfs_context.h | 12 +- component/crypto_api.cc | 14 +- component/crypto_api.h | 15 +- component/inter_request_state.cc | 51 +- component/inter_request_state.h | 15 +- component/interceptor.cc | 3 +- component/interceptor.h | 2 + component/ipfs_url_loader.cc | 17 +- component/patches/122.0.6182.0.patch | 10714 +++++++++++++- ...120.0.6099.71.patch => 122.0.6194.0.patch} | 182 +- ...c59146681c9207a4610d12669dd4a0603af2.patch | 11532 ++++++++++++++++ component/preferences.cc | 89 + component/preferences.h | 36 + component/url_loader_factory.cc | 29 +- component/url_loader_factory.h | 8 +- library/conanfile.py | 7 +- library/include/ipfs_client/context_api.h | 5 + library/include/ipfs_client/gateway_spec.h | 19 + library/include/ipfs_client/gateways.h | 11 +- .../include/ipfs_client/gw/gateway_request.h | 6 +- library/include/ipfs_client/ipld/dag_node.h | 1 - .../ipfs_client/ipld/resolution_state.h | 6 +- library/include/ipfs_client/orchestrator.h | 2 +- library/include/ipfs_client/pb_dag.h | 2 - library/include/ipfs_client/test_context.h | 11 +- library/include/libp2p/common/types.hpp | 39 - library/include/libp2p/crypto/key.h | 100 - .../libp2p/crypto/protobuf/protobuf_key.hpp | 29 - .../include/libp2p/multi/multibase_codec.hpp | 65 - .../multi/multibase_codec/codecs/base16.h | 24 - .../multi/multibase_codec/codecs/base32.hpp | 52 - .../multi/multibase_codec/codecs/base36.hpp | 42 - .../multi/multibase_codec/codecs/base58.hpp | 42 - .../multibase_codec/codecs/base_error.hpp | 24 - .../include/libp2p/multi/multicodec_type.hpp | 78 - library/src/ipfs_client/car.cc | 3 +- library/src/ipfs_client/cid.cc | 2 - library/src/ipfs_client/context_api.cc | 5 + library/src/ipfs_client/gateways.cc | 36 +- library/src/ipfs_client/gateways_unittest.cc | 4 +- .../src/ipfs_client/gw/default_requestor.cc | 17 +- .../gw/default_requestor_unittest.cc | 3 +- .../ipfs_client/gw/gateway_http_requestor.cc | 56 +- .../ipfs_client/gw/gateway_http_requestor.h | 3 - library/src/ipfs_client/gw/gateway_request.cc | 40 +- library/src/ipfs_client/gw/gateway_state.cc | 52 + library/src/ipfs_client/gw/gateway_state.h | 34 + .../ipfs_client/gw/gateway_state_unittest.cc | 20 + .../ipfs_client/gw/multi_gateway_requestor.cc | 158 + .../ipfs_client/gw/multi_gateway_requestor.h | 33 + library/src/ipfs_client/gw/requestor_pool.cc | 73 - .../ipfs_client/gw/requestor_pool_unittest.cc | 46 - .../src/ipfs_client/gw/requestor_unittest.cc | 1 + .../gw/terminating_requestor_unittest.cc | 1 + library/src/ipfs_client/ipld/dag_node.cc | 21 +- .../src/ipfs_client/ipld/directory_shard.cc | 4 +- library/src/ipfs_client/ipld/ipns_name.cc | 39 +- library/src/ipfs_client/ipld/ipns_name.h | 4 +- .../src/ipfs_client/ipld/resolution_state.cc | 21 +- library/src/ipfs_client/ipld/root.cc | 1 + .../src/ipfs_client/ipld/small_directory.cc | 2 + library/src/ipfs_client/ipld/symlink.cc | 1 - .../src/ipfs_client/ipld/symlink_unittest.cc | 11 + .../src/ipfs_client/ipns_record_unittest.cc | 4 + library/src/ipfs_client/orchestrator.cc | 41 +- .../src/ipfs_client/orchestrator_unittest.cc | 3 + library/src/ipfs_client/pb_dag.cc | 1 - library/src/ipfs_client/pb_dag_unittest.cc | 2 +- library/src/ipfs_client/test_context.cc | 96 +- library/src/libp2p/crypto/protobuf_key.hpp | 29 - .../multi/multibase_codec/codecs/base16.cc | 104 - .../multibase_codec/codecs/base16_unittest.cc | 21 - .../multi/multibase_codec/codecs/base32.cc | 200 - .../multi/multibase_codec/codecs/base36.cc | 58 - .../multibase_codec/codecs/base36_unittest.cc | 28 - .../libp2p/multi/muticodec_type_unittest.cc | 42 - library/src/vocab/html_escape_unittest.cc | 16 + ...vPciWi2srZusBPH6nzrmYenpQKaBS1jL6PXTA1yN6w | 2 - ...xynXoBvGDfRbQx1PSLufk3X5juVqm8KwxeMbUG1N6J | 2 - ...W81waexXEK7FxVyfePLH5fvb2b4M6xaRHMR3yM7VeZ | 2 - ...qSSVfKSP6b2gb3HQM4yiZ3netkpGUp8zmto5dTyg8R | 2 - ...oYcjn5pa8qWQfBNH58NomjePyFSZDsFhAV895u5Bdq | 2 - ...8ymecEzgNiNbcmecv9qvDwmiUms2yH3JsjEh4GSoda | 2 - ...ixRgsHY1xgpQ3DxeTRrrF5EsNnuRbCkhP4A1XX7iLr | 2 - ...pid8j9yeQo9ibqcL6meRmS6QaR9b713xzSbUTYD3DW | 2 - ...qzX9DBztn4Y8LNWfiKSJZQdgsjajZxwoGGrwo8FyWT | 2 - ...nFioaJbQLCoz7jW4LKiVPwDHLLPMo7sKYUfbxgBTEJ | 2 - ...ZCYjU3S8D9fnoSKdZa16iayHqBRN2Rasc6kZbp1z4i | 2 - ...LkAPTfKD686jh6eoECRmB2jYDaNAWXYGHzbvfFiUKy | 2 - ...NeD31QypqpeAaRZmtYALTEuRMmVvB2V1TKAdczdMjE | 2 - ...dyMqFsgWdjrU6Q7e9RqCopd8uwdCfJ6Vg4ZGXCEPVG | 2 - ...vc1RaX4f3AMWrMwBJ4fHvBzQLAkpe7jA7scuzAGPgm | 2 - ...yrjYXauvrg5pg4FdBWQmGVQojGHkBLosWqCrEtfL6a | 2 - ...rF1PnZ76U9FFmBBWNdxL3DC8L2hLt89DxoDJiMwzyq | 2 - ...jzkbe5yP8NM1WUvPq5LRToMuvZECSrW7QtVdrTeWZD | 2 - ...5ARTnu85pJ26Mxmhhiddvkkg7cQDZc6wszYMWetkcL | 2 - ...24y7nKXUYFX2NLWUUTw5oK2zCAgwNjyq6nSUzin76J | 2 - ...u7SZtyYU22pANU4WivHyqWShVMPeigJd7aeEgffEvV | 2 - ...gXz9KWc3gioJECyKmC5y89a1AN27ZPzC8MSa9HGb8h | 2 - ...SptPpdiA5yfWwdkFxFWGZN4pHAx2qGE1qzAkL1ZK2P | 2 - ...ixfarfyd4ovykuhm3ghbbpbywknw7wljtvvipcwb7e | Bin 0 -> 2130 bytes ...zjkvpfmrncgoky6klimtd65bniz5ovxe3ahepzjune | Bin 0 -> 262158 bytes test_data/include/mock_api.h | 3 + 157 files changed, 23599 insertions(+), 1944 deletions(-) create mode 100755 .github/tour.sh create mode 100755 .github/ut.sh delete mode 100644 chromium_edits/120.0.6099.71/url/url_util.cc.patch create mode 100644 chromium_edits/122.0.6170.3/chrome/browser/ipfs_extra_parts.cc create mode 100644 chromium_edits/122.0.6170.3/chrome/browser/ipfs_extra_parts.h create mode 100644 chromium_edits/122.0.6170.3/chrome/browser/prefs/browser_prefs.cc.patch create mode 100644 chromium_edits/122.0.6182.0/chrome/browser/ipfs_extra_parts.cc create mode 100644 chromium_edits/122.0.6182.0/chrome/browser/ipfs_extra_parts.h create mode 100644 chromium_edits/122.0.6182.0/chrome/browser/prefs/browser_prefs.cc.patch rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/chrome/browser/BUILD.gn.patch (72%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/chrome/browser/about_flags.cc.patch (88%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.cc.patch (100%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/chrome/browser/chrome_content_browser_client.cc.patch (72%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/chrome/browser/flag-metadata.json.patch (88%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/chrome/browser/flag_descriptions.cc.patch (85%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/chrome/browser/flag_descriptions.h.patch (87%) create mode 100644 chromium_edits/122.0.6194.0/chrome/browser/ipfs_extra_parts.cc create mode 100644 chromium_edits/122.0.6194.0/chrome/browser/ipfs_extra_parts.h create mode 100644 chromium_edits/122.0.6194.0/chrome/browser/prefs/browser_prefs.cc.patch rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/chrome/common/chrome_content_client.cc.patch (100%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/components/cbor/reader.cc.patch (100%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/components/cbor/reader.h.patch (100%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/components/cbor/reader_unittest.cc.patch (100%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/components/cbor/values.cc.patch (100%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/components/cbor/values.h.patch (100%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/components/cbor/writer.cc.patch (100%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/components/cbor/writer_unittest.cc.patch (100%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/components/open_from_clipboard/clipboard_recent_content_generic.cc.patch (100%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/net/dns/dns_config_service_linux.cc.patch (100%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/third_party/blink/renderer/platform/weborigin/scheme_registry.cc.patch (100%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/url/BUILD.gn.patch (87%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/url/url_canon.h.patch (91%) rename chromium_edits/{120.0.6099.71 => 122.0.6194.0}/url/url_canon_ipfs.cc (62%) create mode 100644 chromium_edits/122.0.6194.0/url/url_util.cc.patch create mode 100755 cmake/tidy.py rename component/patches/{120.0.6099.71.patch => 122.0.6194.0.patch} (87%) create mode 100644 component/patches/2f84c59146681c9207a4610d12669dd4a0603af2.patch create mode 100644 component/preferences.cc create mode 100644 component/preferences.h create mode 100644 library/include/ipfs_client/gateway_spec.h delete mode 100644 library/include/libp2p/common/types.hpp delete mode 100644 library/include/libp2p/crypto/key.h delete mode 100644 library/include/libp2p/crypto/protobuf/protobuf_key.hpp delete mode 100644 library/include/libp2p/multi/multibase_codec.hpp delete mode 100644 library/include/libp2p/multi/multibase_codec/codecs/base16.h delete mode 100644 library/include/libp2p/multi/multibase_codec/codecs/base32.hpp delete mode 100644 library/include/libp2p/multi/multibase_codec/codecs/base36.hpp delete mode 100644 library/include/libp2p/multi/multibase_codec/codecs/base58.hpp delete mode 100644 library/include/libp2p/multi/multibase_codec/codecs/base_error.hpp delete mode 100644 library/include/libp2p/multi/multicodec_type.hpp create mode 100644 library/src/ipfs_client/gw/gateway_state.cc create mode 100644 library/src/ipfs_client/gw/gateway_state.h create mode 100644 library/src/ipfs_client/gw/gateway_state_unittest.cc create mode 100644 library/src/ipfs_client/gw/multi_gateway_requestor.cc create mode 100644 library/src/ipfs_client/gw/multi_gateway_requestor.h delete mode 100644 library/src/ipfs_client/gw/requestor_pool.cc delete mode 100644 library/src/ipfs_client/gw/requestor_pool_unittest.cc delete mode 100644 library/src/libp2p/crypto/protobuf_key.hpp delete mode 100644 library/src/libp2p/multi/multibase_codec/codecs/base16.cc delete mode 100644 library/src/libp2p/multi/multibase_codec/codecs/base16_unittest.cc delete mode 100644 library/src/libp2p/multi/multibase_codec/codecs/base32.cc delete mode 100644 library/src/libp2p/multi/multibase_codec/codecs/base36.cc delete mode 100644 library/src/libp2p/multi/multibase_codec/codecs/base36_unittest.cc delete mode 100644 library/src/libp2p/multi/muticodec_type_unittest.cc create mode 100644 library/src/vocab/html_escape_unittest.cc delete mode 100644 test_data/blocks/QmNcvPciWi2srZusBPH6nzrmYenpQKaBS1jL6PXTA1yN6w delete mode 100644 test_data/blocks/QmPLxynXoBvGDfRbQx1PSLufk3X5juVqm8KwxeMbUG1N6J delete mode 100644 test_data/blocks/QmPRW81waexXEK7FxVyfePLH5fvb2b4M6xaRHMR3yM7VeZ delete mode 100644 test_data/blocks/QmPRqSSVfKSP6b2gb3HQM4yiZ3netkpGUp8zmto5dTyg8R delete mode 100644 test_data/blocks/QmPdoYcjn5pa8qWQfBNH58NomjePyFSZDsFhAV895u5Bdq delete mode 100644 test_data/blocks/QmPe8ymecEzgNiNbcmecv9qvDwmiUms2yH3JsjEh4GSoda delete mode 100644 test_data/blocks/QmPeixRgsHY1xgpQ3DxeTRrrF5EsNnuRbCkhP4A1XX7iLr delete mode 100644 test_data/blocks/QmPepid8j9yeQo9ibqcL6meRmS6QaR9b713xzSbUTYD3DW delete mode 100644 test_data/blocks/QmPqqzX9DBztn4Y8LNWfiKSJZQdgsjajZxwoGGrwo8FyWT delete mode 100644 test_data/blocks/QmQ8nFioaJbQLCoz7jW4LKiVPwDHLLPMo7sKYUfbxgBTEJ delete mode 100644 test_data/blocks/QmQLZCYjU3S8D9fnoSKdZa16iayHqBRN2Rasc6kZbp1z4i delete mode 100644 test_data/blocks/QmQTLkAPTfKD686jh6eoECRmB2jYDaNAWXYGHzbvfFiUKy delete mode 100644 test_data/blocks/QmQbNeD31QypqpeAaRZmtYALTEuRMmVvB2V1TKAdczdMjE delete mode 100644 test_data/blocks/QmQbdyMqFsgWdjrU6Q7e9RqCopd8uwdCfJ6Vg4ZGXCEPVG delete mode 100644 test_data/blocks/QmQfvc1RaX4f3AMWrMwBJ4fHvBzQLAkpe7jA7scuzAGPgm delete mode 100644 test_data/blocks/QmQfyrjYXauvrg5pg4FdBWQmGVQojGHkBLosWqCrEtfL6a delete mode 100644 test_data/blocks/QmQhrF1PnZ76U9FFmBBWNdxL3DC8L2hLt89DxoDJiMwzyq delete mode 100644 test_data/blocks/QmQijzkbe5yP8NM1WUvPq5LRToMuvZECSrW7QtVdrTeWZD delete mode 100644 test_data/blocks/QmQj5ARTnu85pJ26Mxmhhiddvkkg7cQDZc6wszYMWetkcL delete mode 100644 test_data/blocks/QmQx24y7nKXUYFX2NLWUUTw5oK2zCAgwNjyq6nSUzin76J delete mode 100644 test_data/blocks/QmR1u7SZtyYU22pANU4WivHyqWShVMPeigJd7aeEgffEvV delete mode 100644 test_data/blocks/QmR3gXz9KWc3gioJECyKmC5y89a1AN27ZPzC8MSa9HGb8h delete mode 100644 test_data/blocks/QmR4SptPpdiA5yfWwdkFxFWGZN4pHAx2qGE1qzAkL1ZK2P create mode 100644 test_data/blocks/bafkreidzbjhm6fubixfarfyd4ovykuhm3ghbbpbywknw7wljtvvipcwb7e create mode 100644 test_data/blocks/bafybeiag67bl2upfzjkvpfmrncgoky6klimtd65bniz5ovxe3ahepzjune diff --git a/.github/tour.sh b/.github/tour.sh new file mode 100755 index 00000000..e516f1ef --- /dev/null +++ b/.github/tour.sh @@ -0,0 +1,81 @@ +#!/bin/bash -ex +echo Clone tester repo. +git clone https://github.com/John-LittleBearLabs/ipfs_client_clitester.git + +echo Install dependencies. +sudo apt-get update +sudo apt-get install --yes cmake ninja-build binutils libc6{,-dev} +pip3 install conan +conan profile detect || echo "Profile detection failed. Perhaps the default profile already existed - perhaps this user has already done some conan-based builds." + +echo Build conan library +conan create --build=missing ipfs_chromium/library/ + +echo Configure clitester +mkdir tester_build +cmake \ + -G Ninja \ + -S ipfs_client_clitester \ + -B tester_build \ + -D CMAKE_BUILD_TYPE=Release + +echo Build clitester +cmake --build tester_build --config Release + +echo Start test server +( timeout 3600 python3 ./ipfs_chromium/test_data/test_server.py 8080 2>&1 | tee server.log & ) & +for t in 1{0..9} +do + if grep -n . server.log + then + break + elif curl -m $t 'http://localhost:8080/ping' + then + sleep 1 + else + sleep ${t} + fi +done + +function url_case() { + echo "url_case(" "${@}" ")" + if timeout 360 ./tester_build/clitester warning "${1}://${2}" + then + echo clitester exited with successful status + else + echo "Directly reported error code from clitester run - usually means timeout killed it. " "${@}" + exit 7 + fi + n=`sed 's,[^A-Za-z0-9\.],_,g' <<< ${2}` + if cat "_${1}_${n}" | md5sum | cut -d ' ' -f 1 > actual + then + ls -lrth _ip?s_* + if [ $# -ge 3 ] + then + echo "${3}" > expected + if diff actual expected + then + echo good + else + echo "Got wrong result: " "${@}" + exit 8 + fi + fi + else + grep -n . server.log || sleep 1 + ls -lrth _ip?s_* + echo "Failure: ${*}" + exit 9 + fi +} + +url_case ipfs bafkqacdjmrsw45djor4q ff483d1ff591898a9942916050d2ca3f 'Identity (inlined) CID' + +url_case ipfs baguqeerah2nswg7r2pvlpbnsz5y4c4pr4wsgbzixdl632w5qxvedqzryf54q 7750fd7b0928f007e1d181763c0dbdb5 'A DAG-JSON document. The block itself md5s to b92348005af4ae4795e6f312844fb359, but the response we are hashing here is an HTML preview page. This does mean this test breaks if you make the preview less ugly.' + +url_case ipns en.wikipedia-on-ipfs.org/I/HFE_Too_Slow_1.JPG.webp 09c09b2654e8529740b5a7625e39e0c8 'An image fetched through DNSLink and HAMT sharded directories.' +url_case ipfs bafybeieb33pqideyl5ncd33kho622thym5rqv6sujrmelcuhkjlf2hdpu4/Big%20Buck%20Bunny.webm 06d51286e56badb4455594ebed6daba2 'A large UnixFS file - several hundred blocks.' +url_case ipns k51qzi5uqu5dijv526o4z2z10ejylnel0bfvrtw53itcmsecffo8yf0zb4g9gi/symlinks/relative_link.txt cfe9b69523140b5b5e63874a8e4997e4 'A relative symlink resolves successfully to the file pointed to.' + +echo Stop test server. +killall python3 2>/dev/null || true diff --git a/.github/ut.sh b/.github/ut.sh new file mode 100755 index 00000000..2b7cdd38 --- /dev/null +++ b/.github/ut.sh @@ -0,0 +1,19 @@ +#!/bin/bash -ex + +echo Install dependencies + sudo apt-get update + sudo apt-get install --yes cmake ninja-build lcov binutils doxygen graphviz libc6{,-dev} valgrind + npm install -g @marp-team/marp-cli + +echo Configure + mkdir build + cmake \ + -G Ninja \ + -S ipfs_chromium \ + -B build \ + -D CMAKE_BUILD_TYPE=Debug + +echo Run Tests + cmake --build build --config Debug --target run_tests +echo Generate Coverage Report + cmake --build build --config Debug --target cov diff --git a/.github/workflows/library.yml b/.github/workflows/library.yml index b28249cd..0605c056 100644 --- a/.github/workflows/library.yml +++ b/.github/workflows/library.yml @@ -4,13 +4,24 @@ on: release: types: [created] jobs: - test: - uses: little-bear-labs/ipfs-chromium/.github/workflows/ut.yml@flows - tour: - uses: little-bear-labs/ipfs-chromium/.github/workflows/tour.yml@flows + prechecks: + runs-on: ubuntu-latest + steps: + - name: Checkout ipfs_chromium + uses: actions/checkout@v4 + with: + path: 'ipfs_chromium' + - name: Run Unit Tests + run: ./ipfs_chromium/.github/ut.sh + - name: Tour de IPFS + run: ./ipfs_chromium/.github/tour.sh + - name: Upload coverage reports to Codecov.com + uses: codecov/codecov-action@v3 + with: + files: build/library/cov.info build: name: ${{ matrix.config.name }} - needs: [test, tour] + needs: [prechecks] runs-on: ${{ matrix.config.os }} strategy: fail-fast: true @@ -18,7 +29,7 @@ jobs: config: - { name: "macOS", - os: macos-12, + os: macos-latest, build_type: "Release", cc: "clang", cxx: "clang++", diff --git a/.github/workflows/tour.yml b/.github/workflows/tour.yml index f712526f..ac17d3e1 100644 --- a/.github/workflows/tour.yml +++ b/.github/workflows/tour.yml @@ -11,74 +11,6 @@ jobs: uses: actions/checkout@v4 with: path: 'ipfs_chromium' - - name: Checkout ipfs_client_clitester - uses: actions/checkout@v4 - with: - path: 'ipfs_client_clitester' - repository: 'John-LittleBearLabs/ipfs_client_clitester' - - name: Install dependencies - shell: bash - run: | - sudo apt-get update - sudo apt-get install --yes cmake ninja-build binutils libc6{,-dev} - pip3 install conan - conan profile detect - - name: Build conan_lib - run: | - conan create ipfs_chromium/library/ - - name: Configure clitester - shell: bash - run: | - mkdir tester_build - set +e - cmake \ - -G Ninja \ - -S ipfs_client_clitester \ - -B tester_build \ - -D CMAKE_BUILD_TYPE=Release - - name: Build clitester - shell: bash - run: cmake --build tester_build --config Release - - name: Runit - shell: bash - run: | - set -ex - ( timeout 3600 python3 ./ipfs_chromium/test_data/test_server.py 8080 2>&1 | tee server.log & ) & - for t in 1{0..9} - do - if grep -n . server.log - then - break - elif curl -m $t 'http://localhost:8080/ping' - then - sleep 1 - else - sleep ${t} - fi - done - function url_case() { - echo "url_case(" "${@}" ")" - timeout 360 ./tester_build/clitester note "${1}://${2}" - n=`sed 's,[^A-Za-z0-9\.],_,g' <<< ${2}` - if cat "_${1}_${n}" | md5sum | cut -d ' ' -f 1 > actual - then - ls -lrth _ip?s_* - if [ $# -ge 3 ] - then - echo "${3}" > expected - diff actual expected - fi - else - grep -n . server.log || sleep 1 - ls -lrth _ip?s_* - exit 9 - fi - } - url_case ipfs bafkqacdjmrsw45djor4q ff483d1ff591898a9942916050d2ca3f + - name: Run committed script + run: ./ipfs_chromium/.github/tour.sh - true the block itself md5s to b92348005af4ae4795e6f312844fb359, but the response is an HTML preview page - url_case ipfs baguqeerah2nswg7r2pvlpbnsz5y4c4pr4wsgbzixdl632w5qxvedqzryf54q 7750fd7b0928f007e1d181763c0dbdb5 - - url_case ipns en.wikipedia-on-ipfs.org/I/HFE_Too_Slow_1.JPG.webp 8238a73ddb12e56f8f3879cc91d2739e - url_case ipfs bafybeieb33pqideyl5ncd33kho622thym5rqv6sujrmelcuhkjlf2hdpu4/Big%20Buck%20Bunny.webm 06d51286e56badb4455594ebed6daba2 - killall python3 2>/dev/null || true diff --git a/.github/workflows/ut.yml b/.github/workflows/ut.yml index 72564395..887091fb 100644 --- a/.github/workflows/ut.yml +++ b/.github/workflows/ut.yml @@ -11,26 +11,11 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install --yes cmake ninja-build lcov binutils doxygen graphviz libc6{,-dev} valgrind - npm install -g @marp-team/marp-cli - - name: Configure - shell: bash - run: | - mkdir build - cmake \ - -G Ninja \ - -S . \ - -B build \ - -D CMAKE_BUILD_TYPE=Debug - - name: Run Tests - shell: bash - run: cmake --build build --config Debug --target run_tests - - name: Generate Coverage Report + with: + path: 'ipfs_chromium' + - name: Run versioned script shell: bash - run: cmake --build build --config Debug --target cov + run: ./ipfs_chromium/.github/ut.sh - name: Upload coverage reports to Codecov.com uses: codecov/codecov-action@v3 with: diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index 79ee123c..a55e7a17 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,5 @@ - \ No newline at end of file diff --git a/chromium_edits/120.0.6099.71/url/url_util.cc.patch b/chromium_edits/120.0.6099.71/url/url_util.cc.patch deleted file mode 100644 index 0332e847..00000000 --- a/chromium_edits/120.0.6099.71/url/url_util.cc.patch +++ /dev/null @@ -1,17 +0,0 @@ -diff --git a/url/url_util.cc b/url/url_util.cc -index 9258cfcfada47..daf10e4c3b741 100644 ---- a/url/url_util.cc -+++ b/url/url_util.cc -@@ -277,6 +277,12 @@ bool DoCanonicalize(const CHAR* spec, - charset_converter, output, - output_parsed); - -+ } else if (DoCompareSchemeComponent(spec, scheme, "ipfs")) { -+ // Switch multibase away from case-sensitive ones before continuing canonicalization. -+ ParseStandardURL(spec, spec_len, &parsed_input); -+ success = CanonicalizeIpfsURL(spec, spec_len, parsed_input, scheme_type, -+ charset_converter, output, output_parsed); -+ - } else if (DoIsStandard(spec, scheme, &scheme_type)) { - // All "normal" URLs. - ParseStandardURL(spec, spec_len, &parsed_input); diff --git a/chromium_edits/122.0.6170.3/chrome/browser/ipfs_extra_parts.cc b/chromium_edits/122.0.6170.3/chrome/browser/ipfs_extra_parts.cc new file mode 100644 index 00000000..90d2596f --- /dev/null +++ b/chromium_edits/122.0.6170.3/chrome/browser/ipfs_extra_parts.cc @@ -0,0 +1,10 @@ +#include "ipfs_extra_parts.h" + +#include "profiles/profile.h" + +#include + +void IpfsExtraParts::PostProfileInit(Profile* profile, bool /* is_initial_profile */ ) { + DCHECK(profile); + ipfs::InterRequestState::CreateForBrowserContext(profile, profile->GetPrefs()); +} diff --git a/chromium_edits/122.0.6170.3/chrome/browser/ipfs_extra_parts.h b/chromium_edits/122.0.6170.3/chrome/browser/ipfs_extra_parts.h new file mode 100644 index 00000000..2059c437 --- /dev/null +++ b/chromium_edits/122.0.6170.3/chrome/browser/ipfs_extra_parts.h @@ -0,0 +1,10 @@ +#ifndef IPFS_EXTRA_PART_H_ +#define IPFS_EXTRA_PART_H_ + +#include + +class IpfsExtraParts : public ChromeBrowserMainExtraParts { + void PostProfileInit(Profile* profile, bool is_initial_profile) override; +}; + +#endif // IPFS_EXTRA_PART_H_ diff --git a/chromium_edits/122.0.6170.3/chrome/browser/prefs/browser_prefs.cc.patch b/chromium_edits/122.0.6170.3/chrome/browser/prefs/browser_prefs.cc.patch new file mode 100644 index 00000000..e806d5f6 --- /dev/null +++ b/chromium_edits/122.0.6170.3/chrome/browser/prefs/browser_prefs.cc.patch @@ -0,0 +1,36 @@ +diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc +index fc9fcf1ff478a..800961b3c8767 100644 +--- a/chrome/browser/prefs/browser_prefs.cc ++++ b/chrome/browser/prefs/browser_prefs.cc +@@ -190,6 +190,7 @@ + #include "printing/buildflags/buildflags.h" + #include "rlz/buildflags/buildflags.h" + #include "third_party/abseil-cpp/absl/types/optional.h" ++#include "third_party/ipfs_client/ipfs_buildflags.h" + + #if BUILDFLAG(ENABLE_BACKGROUND_MODE) + #include "chrome/browser/background/background_mode_manager.h" +@@ -241,6 +242,11 @@ + #include "chrome/browser/pdf/pdf_pref_names.h" + #endif // BUILDFLAG(ENABLE_PDF) + ++#if BUILDFLAG(ENABLE_IPFS) ++#include "components/ipfs/ipfs_features.h" ++#include "components/ipfs/preferences.h" ++#endif ++ + #if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE) + #include "chrome/browser/screen_ai/pref_names.h" + #endif +@@ -1658,6 +1664,11 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry, + IncognitoModePrefs::RegisterProfilePrefs(registry); + invalidation::PerUserTopicSubscriptionManager::RegisterProfilePrefs(registry); + invalidation::InvalidatorRegistrarWithMemory::RegisterProfilePrefs(registry); ++#if BUILDFLAG(ENABLE_IPFS) ++ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { ++ ipfs::RegisterPreferences(registry); ++ } ++#endif + language::LanguagePrefs::RegisterProfilePrefs(registry); + login_detection::prefs::RegisterProfilePrefs(registry); + lookalikes::RegisterProfilePrefs(registry); diff --git a/chromium_edits/122.0.6170.3/url/url_canon_ipfs.cc b/chromium_edits/122.0.6170.3/url/url_canon_ipfs.cc index da3a5f03..9511e3f5 100644 --- a/chromium_edits/122.0.6170.3/url/url_canon_ipfs.cc +++ b/chromium_edits/122.0.6170.3/url/url_canon_ipfs.cc @@ -1,14 +1,10 @@ #include "url_canon_internal.h" -#include +#include #include #include -namespace m = libp2p::multi; -using Cid = m::ContentIdentifier; -using CidCodec = m::ContentIdentifierCodec; - bool url::CanonicalizeIpfsURL(const char* spec, int spec_len, const Parsed& parsed, @@ -22,30 +18,17 @@ bool url::CanonicalizeIpfsURL(const char* spec, if ( parsed.host.len < 1 ) { return false; } - std::string cid_str{ spec + parsed.host.begin, static_cast(parsed.host.len) }; - auto maybe_cid = CidCodec::fromString(cid_str); - if ( !maybe_cid.has_value() ) { - auto e = libp2p::multi::Stringify(maybe_cid.error()); - std::ostringstream err; - err << e << ' ' - << std::string_view{spec,static_cast(spec_len)}; - maybe_cid = ipfs::id_cid::forText( err.str() ); - } - auto cid = maybe_cid.value(); - if ( cid.version == Cid::Version::V0 ) { - //TODO dcheck content_type == DAG_PB && content_address.getType() == sha256 - cid = Cid{ - Cid::Version::V1, - cid.content_type, - cid.content_address - }; + std::string_view cid_str{ spec + parsed.host.begin, static_cast(parsed.host.len) }; + auto cid = ipfs::Cid(cid_str); + if ( !cid.valid() ) { + cid = ipfs::id_cid::forText( std::string{cid_str} + " is not a valid CID." ); } - auto as_str = CidCodec::toString(cid); - if ( !as_str.has_value() ) { + auto as_str = cid.to_string(); + if ( as_str.empty() ) { return false; } std::string stdurl{ spec, static_cast(parsed.host.begin) }; - stdurl.append( as_str.value() ); + stdurl.append( as_str ); stdurl.append( spec + parsed.host.end(), spec_len - parsed.host.end() ); spec = stdurl.data(); spec_len = static_cast(stdurl.size()); diff --git a/chromium_edits/122.0.6182.0/chrome/browser/BUILD.gn.patch b/chromium_edits/122.0.6182.0/chrome/browser/BUILD.gn.patch index 361a5a1c..680271b3 100644 --- a/chromium_edits/122.0.6182.0/chrome/browser/BUILD.gn.patch +++ b/chromium_edits/122.0.6182.0/chrome/browser/BUILD.gn.patch @@ -1,5 +1,5 @@ diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn -index 0fb94c9b9b67e..df25995c80baa 100644 +index a188528a9e262..88df13b162858 100644 --- a/chrome/browser/BUILD.gn +++ b/chrome/browser/BUILD.gn @@ -40,6 +40,7 @@ import("//rlz/buildflags/buildflags.gni") @@ -10,11 +10,23 @@ index 0fb94c9b9b67e..df25995c80baa 100644 import("//third_party/protobuf/proto_library.gni") import("//third_party/webrtc/webrtc.gni") import("//third_party/widevine/cdm/widevine.gni") -@@ -2596,6 +2597,10 @@ static_library("browser") { +@@ -1912,7 +1913,6 @@ static_library("browser") { + "user_education/user_education_service_factory.h", + ] + } +- + configs += [ + "//build/config/compiler:wexit_time_destructors", + "//build/config:precompiled_headers", +@@ -2604,6 +2604,14 @@ static_library("browser") { ] } + if (enable_ipfs) { ++ sources += [ ++ "ipfs_extra_parts.cc", ++ "ipfs_extra_parts.h", ++ ] + deps += [ "//components/ipfs" ] + } + diff --git a/chromium_edits/122.0.6182.0/chrome/browser/about_flags.cc.patch b/chromium_edits/122.0.6182.0/chrome/browser/about_flags.cc.patch index addab35d..a9dd9da9 100644 --- a/chromium_edits/122.0.6182.0/chrome/browser/about_flags.cc.patch +++ b/chromium_edits/122.0.6182.0/chrome/browser/about_flags.cc.patch @@ -1,5 +1,5 @@ diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc -index 52ecc40da0226..42d45b977471e 100644 +index a7907d8b188d8..68a96934ccf48 100644 --- a/chrome/browser/about_flags.cc +++ b/chrome/browser/about_flags.cc @@ -213,6 +213,7 @@ @@ -10,7 +10,7 @@ index 52ecc40da0226..42d45b977471e 100644 #include "ui/accessibility/accessibility_features.h" #include "ui/accessibility/accessibility_switches.h" #include "ui/base/ui_base_features.h" -@@ -313,6 +314,10 @@ +@@ -308,6 +309,10 @@ #include "extensions/common/switches.h" #endif // BUILDFLAG(ENABLE_EXTENSIONS) @@ -21,7 +21,7 @@ index 52ecc40da0226..42d45b977471e 100644 #if BUILDFLAG(ENABLE_PDF) #include "pdf/pdf_features.h" #endif -@@ -9851,6 +9856,14 @@ const FeatureEntry kFeatureEntries[] = { +@@ -9731,6 +9736,14 @@ const FeatureEntry kFeatureEntries[] = { flag_descriptions::kOmitCorsClientCertDescription, kOsAll, FEATURE_VALUE_TYPE(network::features::kOmitCorsClientCert)}, diff --git a/chromium_edits/122.0.6182.0/chrome/browser/chrome_content_browser_client.cc.patch b/chromium_edits/122.0.6182.0/chrome/browser/chrome_content_browser_client.cc.patch index f212961c..157b9909 100644 --- a/chromium_edits/122.0.6182.0/chrome/browser/chrome_content_browser_client.cc.patch +++ b/chromium_edits/122.0.6182.0/chrome/browser/chrome_content_browser_client.cc.patch @@ -1,8 +1,8 @@ diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc -index a9b87ac2fcd74..987686c621664 100644 +index d3d67d83a514e..a5b1ef6339211 100644 --- a/chrome/browser/chrome_content_browser_client.cc +++ b/chrome/browser/chrome_content_browser_client.cc -@@ -377,6 +377,7 @@ +@@ -378,6 +378,7 @@ #include "third_party/blink/public/common/switches.h" #include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom.h" #include "third_party/blink/public/public_buildflags.h" @@ -10,11 +10,12 @@ index a9b87ac2fcd74..987686c621664 100644 #include "third_party/widevine/cdm/buildflags.h" #include "ui/base/clipboard/clipboard_format_type.h" #include "ui/base/l10n/l10n_util.h" -@@ -499,6 +500,12 @@ +@@ -500,6 +501,13 @@ #include "chrome/browser/fuchsia/chrome_browser_main_parts_fuchsia.h" #endif +#if BUILDFLAG(ENABLE_IPFS) ++#include "chrome/browser/ipfs_extra_parts.h" +#include "components/ipfs/interceptor.h" +#include "components/ipfs/ipfs_features.h" +#include "components/ipfs/url_loader_factory.h" @@ -23,7 +24,19 @@ index a9b87ac2fcd74..987686c621664 100644 #if BUILDFLAG(IS_CHROMEOS) #include "base/debug/leak_annotations.h" #include "chrome/browser/apps/app_service/app_install/app_install_navigation_throttle.h" -@@ -6157,12 +6164,23 @@ void ChromeContentBrowserClient:: +@@ -1712,6 +1720,11 @@ ChromeContentBrowserClient::CreateBrowserMainParts(bool is_integration_test) { + main_parts->AddParts( + std::make_unique()); + ++#if BUILDFLAG(ENABLE_IPFS) ++ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { ++ main_parts->AddParts(std::make_unique()); ++ } ++#endif + return main_parts; + } + +@@ -6084,12 +6097,25 @@ void ChromeContentBrowserClient:: const absl::optional& request_initiator_origin, NonNetworkURLLoaderFactoryMap* factories) { #if BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(ENABLE_EXTENSIONS) || \ @@ -38,18 +51,20 @@ index a9b87ac2fcd74..987686c621664 100644 +#if BUILDFLAG(ENABLE_IPFS) + if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { + network::mojom::URLLoaderFactory* default_factory = g_browser_process->system_network_context_manager()->GetURLLoaderFactory(); ++ auto* context = web_contents->GetBrowserContext(); + ipfs::IpfsURLLoaderFactory::Create( + factories, -+ web_contents->GetBrowserContext(), ++ context, + default_factory, -+ GetSystemNetworkContext() ++ GetSystemNetworkContext(), ++ Profile::FromBrowserContext(context)->GetPrefs() + ); + } +#endif // BUILDFLAG(ENABLE_IPFS) #if BUILDFLAG(IS_CHROMEOS_ASH) if (web_contents) { -@@ -6304,6 +6322,11 @@ ChromeContentBrowserClient::WillCreateURLLoaderRequestInterceptors( +@@ -6231,6 +6257,11 @@ ChromeContentBrowserClient::WillCreateURLLoaderRequestInterceptors( scoped_refptr navigation_response_task_runner) { std::vector> interceptors; diff --git a/chromium_edits/122.0.6182.0/chrome/browser/flag-metadata.json.patch b/chromium_edits/122.0.6182.0/chrome/browser/flag-metadata.json.patch index b1fe0af9..1894add3 100644 --- a/chromium_edits/122.0.6182.0/chrome/browser/flag-metadata.json.patch +++ b/chromium_edits/122.0.6182.0/chrome/browser/flag-metadata.json.patch @@ -1,8 +1,8 @@ diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json -index 5e02e86a700cb..1f06bded184bf 100644 +index 0b51e78fcb8b9..9571b2c92c57f 100644 --- a/chrome/browser/flag-metadata.json +++ b/chrome/browser/flag-metadata.json -@@ -2956,6 +2956,11 @@ +@@ -2948,6 +2948,11 @@ "owners": [ "hanxi@chromium.org", "wychen@chromium.org" ], "expiry_milestone": 130 }, diff --git a/chromium_edits/122.0.6182.0/chrome/browser/flag_descriptions.cc.patch b/chromium_edits/122.0.6182.0/chrome/browser/flag_descriptions.cc.patch index 15c80675..7f1bc89b 100644 --- a/chromium_edits/122.0.6182.0/chrome/browser/flag_descriptions.cc.patch +++ b/chromium_edits/122.0.6182.0/chrome/browser/flag_descriptions.cc.patch @@ -1,5 +1,5 @@ diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc -index d6073d3514930..f80f1330f9865 100644 +index b2992e30f9811..f92d8a322b634 100644 --- a/chrome/browser/flag_descriptions.cc +++ b/chrome/browser/flag_descriptions.cc @@ -288,6 +288,11 @@ const char kEnableBenchmarkingDescription[] = diff --git a/chromium_edits/122.0.6182.0/chrome/browser/flag_descriptions.h.patch b/chromium_edits/122.0.6182.0/chrome/browser/flag_descriptions.h.patch index f5e7ba0e..d83ce8f4 100644 --- a/chromium_edits/122.0.6182.0/chrome/browser/flag_descriptions.h.patch +++ b/chromium_edits/122.0.6182.0/chrome/browser/flag_descriptions.h.patch @@ -1,5 +1,5 @@ diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h -index 4df49302f94b5..f7d3b65112d8b 100644 +index ad76d832395a1..438facecff519 100644 --- a/chrome/browser/flag_descriptions.h +++ b/chrome/browser/flag_descriptions.h @@ -23,6 +23,7 @@ diff --git a/chromium_edits/122.0.6182.0/chrome/browser/ipfs_extra_parts.cc b/chromium_edits/122.0.6182.0/chrome/browser/ipfs_extra_parts.cc new file mode 100644 index 00000000..90d2596f --- /dev/null +++ b/chromium_edits/122.0.6182.0/chrome/browser/ipfs_extra_parts.cc @@ -0,0 +1,10 @@ +#include "ipfs_extra_parts.h" + +#include "profiles/profile.h" + +#include + +void IpfsExtraParts::PostProfileInit(Profile* profile, bool /* is_initial_profile */ ) { + DCHECK(profile); + ipfs::InterRequestState::CreateForBrowserContext(profile, profile->GetPrefs()); +} diff --git a/chromium_edits/122.0.6182.0/chrome/browser/ipfs_extra_parts.h b/chromium_edits/122.0.6182.0/chrome/browser/ipfs_extra_parts.h new file mode 100644 index 00000000..2059c437 --- /dev/null +++ b/chromium_edits/122.0.6182.0/chrome/browser/ipfs_extra_parts.h @@ -0,0 +1,10 @@ +#ifndef IPFS_EXTRA_PART_H_ +#define IPFS_EXTRA_PART_H_ + +#include + +class IpfsExtraParts : public ChromeBrowserMainExtraParts { + void PostProfileInit(Profile* profile, bool is_initial_profile) override; +}; + +#endif // IPFS_EXTRA_PART_H_ diff --git a/chromium_edits/122.0.6182.0/chrome/browser/prefs/browser_prefs.cc.patch b/chromium_edits/122.0.6182.0/chrome/browser/prefs/browser_prefs.cc.patch new file mode 100644 index 00000000..e806d5f6 --- /dev/null +++ b/chromium_edits/122.0.6182.0/chrome/browser/prefs/browser_prefs.cc.patch @@ -0,0 +1,36 @@ +diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc +index fc9fcf1ff478a..800961b3c8767 100644 +--- a/chrome/browser/prefs/browser_prefs.cc ++++ b/chrome/browser/prefs/browser_prefs.cc +@@ -190,6 +190,7 @@ + #include "printing/buildflags/buildflags.h" + #include "rlz/buildflags/buildflags.h" + #include "third_party/abseil-cpp/absl/types/optional.h" ++#include "third_party/ipfs_client/ipfs_buildflags.h" + + #if BUILDFLAG(ENABLE_BACKGROUND_MODE) + #include "chrome/browser/background/background_mode_manager.h" +@@ -241,6 +242,11 @@ + #include "chrome/browser/pdf/pdf_pref_names.h" + #endif // BUILDFLAG(ENABLE_PDF) + ++#if BUILDFLAG(ENABLE_IPFS) ++#include "components/ipfs/ipfs_features.h" ++#include "components/ipfs/preferences.h" ++#endif ++ + #if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE) + #include "chrome/browser/screen_ai/pref_names.h" + #endif +@@ -1658,6 +1664,11 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry, + IncognitoModePrefs::RegisterProfilePrefs(registry); + invalidation::PerUserTopicSubscriptionManager::RegisterProfilePrefs(registry); + invalidation::InvalidatorRegistrarWithMemory::RegisterProfilePrefs(registry); ++#if BUILDFLAG(ENABLE_IPFS) ++ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { ++ ipfs::RegisterPreferences(registry); ++ } ++#endif + language::LanguagePrefs::RegisterProfilePrefs(registry); + login_detection::prefs::RegisterProfilePrefs(registry); + lookalikes::RegisterProfilePrefs(registry); diff --git a/chromium_edits/122.0.6182.0/url/url_canon.h.patch b/chromium_edits/122.0.6182.0/url/url_canon.h.patch index 24ae1ba4..0d4fbfdf 100644 --- a/chromium_edits/122.0.6182.0/url/url_canon.h.patch +++ b/chromium_edits/122.0.6182.0/url/url_canon.h.patch @@ -1,8 +1,8 @@ diff --git a/url/url_canon.h b/url/url_canon.h -index d3a7fabf09fa8..06db17242248f 100644 +index 913b3685c6fec..3c3c55e580564 100644 --- a/url/url_canon.h +++ b/url/url_canon.h -@@ -697,6 +697,23 @@ bool CanonicalizeMailtoURL(const char16_t* spec, +@@ -792,6 +792,23 @@ bool CanonicalizeMailtoURL(const char16_t* spec, CanonOutput* output, Parsed* new_parsed); diff --git a/chromium_edits/120.0.6099.71/chrome/browser/BUILD.gn.patch b/chromium_edits/122.0.6194.0/chrome/browser/BUILD.gn.patch similarity index 72% rename from chromium_edits/120.0.6099.71/chrome/browser/BUILD.gn.patch rename to chromium_edits/122.0.6194.0/chrome/browser/BUILD.gn.patch index 6045c760..3f644ee6 100644 --- a/chromium_edits/120.0.6099.71/chrome/browser/BUILD.gn.patch +++ b/chromium_edits/122.0.6194.0/chrome/browser/BUILD.gn.patch @@ -1,8 +1,8 @@ diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn -index 44d3b5e543101..ece46a3db0bea 100644 +index ea574b1863e18..7239008b4e215 100644 --- a/chrome/browser/BUILD.gn +++ b/chrome/browser/BUILD.gn -@@ -40,6 +40,7 @@ import("//rlz/buildflags/buildflags.gni") +@@ -39,6 +39,7 @@ import("//rlz/buildflags/buildflags.gni") import("//sandbox/features.gni") import("//testing/libfuzzer/fuzzer_test.gni") import("//third_party/blink/public/public_features.gni") @@ -10,11 +10,15 @@ index 44d3b5e543101..ece46a3db0bea 100644 import("//third_party/protobuf/proto_library.gni") import("//third_party/webrtc/webrtc.gni") import("//third_party/widevine/cdm/widevine.gni") -@@ -2660,6 +2661,10 @@ static_library("browser") { +@@ -2615,6 +2616,14 @@ static_library("browser") { ] } + if (enable_ipfs) { ++ sources += [ ++ "ipfs_extra_parts.cc", ++ "ipfs_extra_parts.h", ++ ] + deps += [ "//components/ipfs" ] + } + diff --git a/chromium_edits/120.0.6099.71/chrome/browser/about_flags.cc.patch b/chromium_edits/122.0.6194.0/chrome/browser/about_flags.cc.patch similarity index 88% rename from chromium_edits/120.0.6099.71/chrome/browser/about_flags.cc.patch rename to chromium_edits/122.0.6194.0/chrome/browser/about_flags.cc.patch index bec369fa..6e4ac390 100644 --- a/chromium_edits/120.0.6099.71/chrome/browser/about_flags.cc.patch +++ b/chromium_edits/122.0.6194.0/chrome/browser/about_flags.cc.patch @@ -1,16 +1,16 @@ diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc -index 8736a91998f6b..da556109dcbb1 100644 +index b076446400486..2b2f2724d3b75 100644 --- a/chrome/browser/about_flags.cc +++ b/chrome/browser/about_flags.cc -@@ -213,6 +213,7 @@ +@@ -214,6 +214,7 @@ #include "third_party/blink/public/common/features_generated.h" #include "third_party/blink/public/common/forcedark/forcedark_switches.h" #include "third_party/blink/public/common/switches.h" +#include "third_party/ipfs_client/ipfs_buildflags.h" #include "ui/accessibility/accessibility_features.h" #include "ui/accessibility/accessibility_switches.h" - #include "ui/base/ui_base_features.h" -@@ -314,6 +315,10 @@ + #include "ui/base/ozone_buildflags.h" +@@ -310,6 +311,10 @@ #include "extensions/common/switches.h" #endif // BUILDFLAG(ENABLE_EXTENSIONS) @@ -21,7 +21,7 @@ index 8736a91998f6b..da556109dcbb1 100644 #if BUILDFLAG(ENABLE_PDF) #include "pdf/pdf_features.h" #endif -@@ -9922,6 +9927,14 @@ const FeatureEntry kFeatureEntries[] = { +@@ -9459,6 +9464,14 @@ const FeatureEntry kFeatureEntries[] = { flag_descriptions::kOmitCorsClientCertDescription, kOsAll, FEATURE_VALUE_TYPE(network::features::kOmitCorsClientCert)}, diff --git a/chromium_edits/120.0.6099.71/chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.cc.patch b/chromium_edits/122.0.6194.0/chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.cc.patch similarity index 100% rename from chromium_edits/120.0.6099.71/chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.cc.patch rename to chromium_edits/122.0.6194.0/chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.cc.patch diff --git a/chromium_edits/120.0.6099.71/chrome/browser/chrome_content_browser_client.cc.patch b/chromium_edits/122.0.6194.0/chrome/browser/chrome_content_browser_client.cc.patch similarity index 72% rename from chromium_edits/120.0.6099.71/chrome/browser/chrome_content_browser_client.cc.patch rename to chromium_edits/122.0.6194.0/chrome/browser/chrome_content_browser_client.cc.patch index 580e2fee..2f68a7f3 100644 --- a/chromium_edits/120.0.6099.71/chrome/browser/chrome_content_browser_client.cc.patch +++ b/chromium_edits/122.0.6194.0/chrome/browser/chrome_content_browser_client.cc.patch @@ -1,17 +1,8 @@ diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc -index d9c675c8aca73..cb360f4e7ca5b 100644 +index 667370e623970..c09550c753e8e 100644 --- a/chrome/browser/chrome_content_browser_client.cc +++ b/chrome/browser/chrome_content_browser_client.cc -@@ -228,6 +228,8 @@ - #include "components/error_page/common/localized_error.h" - #include "components/error_page/content/browser/net_error_auto_reloader.h" - #include "components/google/core/common/google_switches.h" -+#include "components/ipfs/interceptor.h" -+#include "components/ipfs/url_loader_factory.h" - #include "components/keep_alive_registry/keep_alive_types.h" - #include "components/keep_alive_registry/scoped_keep_alive.h" - #include "components/language/core/browser/pref_names.h" -@@ -364,6 +366,7 @@ +@@ -377,6 +377,7 @@ #include "third_party/blink/public/common/switches.h" #include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom.h" #include "third_party/blink/public/public_buildflags.h" @@ -19,11 +10,12 @@ index d9c675c8aca73..cb360f4e7ca5b 100644 #include "third_party/widevine/cdm/buildflags.h" #include "ui/base/clipboard/clipboard_format_type.h" #include "ui/base/l10n/l10n_util.h" -@@ -487,6 +490,12 @@ +@@ -499,6 +500,13 @@ #include "chrome/browser/fuchsia/chrome_browser_main_parts_fuchsia.h" #endif +#if BUILDFLAG(ENABLE_IPFS) ++#include "chrome/browser/ipfs_extra_parts.h" +#include "components/ipfs/interceptor.h" +#include "components/ipfs/ipfs_features.h" +#include "components/ipfs/url_loader_factory.h" @@ -31,8 +23,20 @@ index d9c675c8aca73..cb360f4e7ca5b 100644 + #if BUILDFLAG(IS_CHROMEOS) #include "base/debug/leak_annotations.h" - #include "chrome/browser/apps/intent_helper/chromeos_disabled_apps_throttle.h" -@@ -6180,12 +6189,23 @@ void ChromeContentBrowserClient:: + #include "chrome/browser/apps/app_service/app_install/app_install_navigation_throttle.h" +@@ -1711,6 +1719,11 @@ ChromeContentBrowserClient::CreateBrowserMainParts(bool is_integration_test) { + main_parts->AddParts( + std::make_unique()); + ++#if BUILDFLAG(ENABLE_IPFS) ++ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { ++ main_parts->AddParts(std::make_unique()); ++ } ++#endif + return main_parts; + } + +@@ -6075,12 +6088,25 @@ void ChromeContentBrowserClient:: const absl::optional& request_initiator_origin, NonNetworkURLLoaderFactoryMap* factories) { #if BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(ENABLE_EXTENSIONS) || \ @@ -42,23 +46,24 @@ index d9c675c8aca73..cb360f4e7ca5b 100644 RenderFrameHost::FromID(render_process_id, render_frame_id); WebContents* web_contents = WebContents::FromRenderFrameHost(frame_host); #endif // BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(ENABLE_EXTENSIONS) || \ -- // !BUILDFLAG(IS_ANDROID) -+ // !BUILDFLAG(IS_ANDROID) || BUILDFLAG(ENABLE_IPFS) + // !BUILDFLAG(IS_ANDROID) +#if BUILDFLAG(ENABLE_IPFS) + if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { + network::mojom::URLLoaderFactory* default_factory = g_browser_process->system_network_context_manager()->GetURLLoaderFactory(); ++ auto* context = web_contents->GetBrowserContext(); + ipfs::IpfsURLLoaderFactory::Create( + factories, -+ web_contents->GetBrowserContext(), ++ context, + default_factory, -+ GetSystemNetworkContext() ++ GetSystemNetworkContext(), ++ Profile::FromBrowserContext(context)->GetPrefs() + ); + } +#endif // BUILDFLAG(ENABLE_IPFS) #if BUILDFLAG(IS_CHROMEOS_ASH) if (web_contents) { -@@ -6327,6 +6347,11 @@ ChromeContentBrowserClient::WillCreateURLLoaderRequestInterceptors( +@@ -6222,6 +6248,11 @@ ChromeContentBrowserClient::WillCreateURLLoaderRequestInterceptors( scoped_refptr navigation_response_task_runner) { std::vector> interceptors; diff --git a/chromium_edits/120.0.6099.71/chrome/browser/flag-metadata.json.patch b/chromium_edits/122.0.6194.0/chrome/browser/flag-metadata.json.patch similarity index 88% rename from chromium_edits/120.0.6099.71/chrome/browser/flag-metadata.json.patch rename to chromium_edits/122.0.6194.0/chrome/browser/flag-metadata.json.patch index f7c408d8..75727bd8 100644 --- a/chromium_edits/120.0.6099.71/chrome/browser/flag-metadata.json.patch +++ b/chromium_edits/122.0.6194.0/chrome/browser/flag-metadata.json.patch @@ -1,8 +1,8 @@ diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json -index 400505f9d64e6..320b730d894c6 100644 +index 7a0c7c06f4e8e..8af9a104e1fe8 100644 --- a/chrome/browser/flag-metadata.json +++ b/chrome/browser/flag-metadata.json -@@ -2868,6 +2868,11 @@ +@@ -2931,6 +2931,11 @@ "owners": [ "hanxi@chromium.org", "wychen@chromium.org" ], "expiry_milestone": 130 }, diff --git a/chromium_edits/120.0.6099.71/chrome/browser/flag_descriptions.cc.patch b/chromium_edits/122.0.6194.0/chrome/browser/flag_descriptions.cc.patch similarity index 85% rename from chromium_edits/120.0.6099.71/chrome/browser/flag_descriptions.cc.patch rename to chromium_edits/122.0.6194.0/chrome/browser/flag_descriptions.cc.patch index d021535f..a687852a 100644 --- a/chromium_edits/120.0.6099.71/chrome/browser/flag_descriptions.cc.patch +++ b/chromium_edits/122.0.6194.0/chrome/browser/flag_descriptions.cc.patch @@ -1,8 +1,8 @@ diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc -index f92c5df0fc600..e131baa70cce2 100644 +index b4f75320e4669..fc2ef00f6eb5c 100644 --- a/chrome/browser/flag_descriptions.cc +++ b/chrome/browser/flag_descriptions.cc -@@ -248,6 +248,11 @@ const char kEnableBenchmarkingDescription[] = +@@ -289,6 +289,11 @@ const char kEnableBenchmarkingDescription[] = "after 3 restarts. On the third restart, the flag will appear to be off " "but the effect is still active."; diff --git a/chromium_edits/120.0.6099.71/chrome/browser/flag_descriptions.h.patch b/chromium_edits/122.0.6194.0/chrome/browser/flag_descriptions.h.patch similarity index 87% rename from chromium_edits/120.0.6099.71/chrome/browser/flag_descriptions.h.patch rename to chromium_edits/122.0.6194.0/chrome/browser/flag_descriptions.h.patch index e7d95550..40c0f537 100644 --- a/chromium_edits/120.0.6099.71/chrome/browser/flag_descriptions.h.patch +++ b/chromium_edits/122.0.6194.0/chrome/browser/flag_descriptions.h.patch @@ -1,8 +1,8 @@ diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h -index 71eae84724eab..a8e4b29ee3cc5 100644 +index 65b8bcb40770c..d5ae8dca53910 100644 --- a/chrome/browser/flag_descriptions.h +++ b/chrome/browser/flag_descriptions.h -@@ -22,6 +22,7 @@ +@@ -23,6 +23,7 @@ #include "pdf/buildflags.h" #include "printing/buildflags/buildflags.h" #include "third_party/blink/public/common/buildflags.h" @@ -10,7 +10,7 @@ index 71eae84724eab..a8e4b29ee3cc5 100644 // This file declares strings used in chrome://flags. These messages are not // translated, because instead of end-users they target Chromium developers and -@@ -165,6 +166,11 @@ extern const char kDownloadWarningImprovementsDescription[]; +@@ -179,6 +180,11 @@ extern const char kDownloadWarningImprovementsDescription[]; extern const char kEnableBenchmarkingName[]; extern const char kEnableBenchmarkingDescription[]; diff --git a/chromium_edits/122.0.6194.0/chrome/browser/ipfs_extra_parts.cc b/chromium_edits/122.0.6194.0/chrome/browser/ipfs_extra_parts.cc new file mode 100644 index 00000000..90d2596f --- /dev/null +++ b/chromium_edits/122.0.6194.0/chrome/browser/ipfs_extra_parts.cc @@ -0,0 +1,10 @@ +#include "ipfs_extra_parts.h" + +#include "profiles/profile.h" + +#include + +void IpfsExtraParts::PostProfileInit(Profile* profile, bool /* is_initial_profile */ ) { + DCHECK(profile); + ipfs::InterRequestState::CreateForBrowserContext(profile, profile->GetPrefs()); +} diff --git a/chromium_edits/122.0.6194.0/chrome/browser/ipfs_extra_parts.h b/chromium_edits/122.0.6194.0/chrome/browser/ipfs_extra_parts.h new file mode 100644 index 00000000..2059c437 --- /dev/null +++ b/chromium_edits/122.0.6194.0/chrome/browser/ipfs_extra_parts.h @@ -0,0 +1,10 @@ +#ifndef IPFS_EXTRA_PART_H_ +#define IPFS_EXTRA_PART_H_ + +#include + +class IpfsExtraParts : public ChromeBrowserMainExtraParts { + void PostProfileInit(Profile* profile, bool is_initial_profile) override; +}; + +#endif // IPFS_EXTRA_PART_H_ diff --git a/chromium_edits/122.0.6194.0/chrome/browser/prefs/browser_prefs.cc.patch b/chromium_edits/122.0.6194.0/chrome/browser/prefs/browser_prefs.cc.patch new file mode 100644 index 00000000..c32f6471 --- /dev/null +++ b/chromium_edits/122.0.6194.0/chrome/browser/prefs/browser_prefs.cc.patch @@ -0,0 +1,36 @@ +diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc +index 86a09cfe49376..d8e166f76179e 100644 +--- a/chrome/browser/prefs/browser_prefs.cc ++++ b/chrome/browser/prefs/browser_prefs.cc +@@ -189,6 +189,7 @@ + #include "printing/buildflags/buildflags.h" + #include "rlz/buildflags/buildflags.h" + #include "third_party/abseil-cpp/absl/types/optional.h" ++#include "third_party/ipfs_client/ipfs_buildflags.h" + + #if BUILDFLAG(ENABLE_BACKGROUND_MODE) + #include "chrome/browser/background/background_mode_manager.h" +@@ -233,6 +234,11 @@ + #include "chrome/browser/pdf/pdf_pref_names.h" + #endif // BUILDFLAG(ENABLE_PDF) + ++#if BUILDFLAG(ENABLE_IPFS) ++#include "components/ipfs/ipfs_features.h" ++#include "components/ipfs/preferences.h" ++#endif ++ + #if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE) + #include "chrome/browser/screen_ai/pref_names.h" + #endif +@@ -1678,6 +1684,11 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry, + IncognitoModePrefs::RegisterProfilePrefs(registry); + invalidation::PerUserTopicSubscriptionManager::RegisterProfilePrefs(registry); + invalidation::InvalidatorRegistrarWithMemory::RegisterProfilePrefs(registry); ++#if BUILDFLAG(ENABLE_IPFS) ++ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { ++ ipfs::RegisterPreferences(registry); ++ } ++#endif + language::LanguagePrefs::RegisterProfilePrefs(registry); + login_detection::prefs::RegisterProfilePrefs(registry); + lookalikes::RegisterProfilePrefs(registry); diff --git a/chromium_edits/120.0.6099.71/chrome/common/chrome_content_client.cc.patch b/chromium_edits/122.0.6194.0/chrome/common/chrome_content_client.cc.patch similarity index 100% rename from chromium_edits/120.0.6099.71/chrome/common/chrome_content_client.cc.patch rename to chromium_edits/122.0.6194.0/chrome/common/chrome_content_client.cc.patch diff --git a/chromium_edits/120.0.6099.71/components/cbor/reader.cc.patch b/chromium_edits/122.0.6194.0/components/cbor/reader.cc.patch similarity index 100% rename from chromium_edits/120.0.6099.71/components/cbor/reader.cc.patch rename to chromium_edits/122.0.6194.0/components/cbor/reader.cc.patch diff --git a/chromium_edits/120.0.6099.71/components/cbor/reader.h.patch b/chromium_edits/122.0.6194.0/components/cbor/reader.h.patch similarity index 100% rename from chromium_edits/120.0.6099.71/components/cbor/reader.h.patch rename to chromium_edits/122.0.6194.0/components/cbor/reader.h.patch diff --git a/chromium_edits/120.0.6099.71/components/cbor/reader_unittest.cc.patch b/chromium_edits/122.0.6194.0/components/cbor/reader_unittest.cc.patch similarity index 100% rename from chromium_edits/120.0.6099.71/components/cbor/reader_unittest.cc.patch rename to chromium_edits/122.0.6194.0/components/cbor/reader_unittest.cc.patch diff --git a/chromium_edits/120.0.6099.71/components/cbor/values.cc.patch b/chromium_edits/122.0.6194.0/components/cbor/values.cc.patch similarity index 100% rename from chromium_edits/120.0.6099.71/components/cbor/values.cc.patch rename to chromium_edits/122.0.6194.0/components/cbor/values.cc.patch diff --git a/chromium_edits/120.0.6099.71/components/cbor/values.h.patch b/chromium_edits/122.0.6194.0/components/cbor/values.h.patch similarity index 100% rename from chromium_edits/120.0.6099.71/components/cbor/values.h.patch rename to chromium_edits/122.0.6194.0/components/cbor/values.h.patch diff --git a/chromium_edits/120.0.6099.71/components/cbor/writer.cc.patch b/chromium_edits/122.0.6194.0/components/cbor/writer.cc.patch similarity index 100% rename from chromium_edits/120.0.6099.71/components/cbor/writer.cc.patch rename to chromium_edits/122.0.6194.0/components/cbor/writer.cc.patch diff --git a/chromium_edits/120.0.6099.71/components/cbor/writer_unittest.cc.patch b/chromium_edits/122.0.6194.0/components/cbor/writer_unittest.cc.patch similarity index 100% rename from chromium_edits/120.0.6099.71/components/cbor/writer_unittest.cc.patch rename to chromium_edits/122.0.6194.0/components/cbor/writer_unittest.cc.patch diff --git a/chromium_edits/120.0.6099.71/components/open_from_clipboard/clipboard_recent_content_generic.cc.patch b/chromium_edits/122.0.6194.0/components/open_from_clipboard/clipboard_recent_content_generic.cc.patch similarity index 100% rename from chromium_edits/120.0.6099.71/components/open_from_clipboard/clipboard_recent_content_generic.cc.patch rename to chromium_edits/122.0.6194.0/components/open_from_clipboard/clipboard_recent_content_generic.cc.patch diff --git a/chromium_edits/120.0.6099.71/net/dns/dns_config_service_linux.cc.patch b/chromium_edits/122.0.6194.0/net/dns/dns_config_service_linux.cc.patch similarity index 100% rename from chromium_edits/120.0.6099.71/net/dns/dns_config_service_linux.cc.patch rename to chromium_edits/122.0.6194.0/net/dns/dns_config_service_linux.cc.patch diff --git a/chromium_edits/120.0.6099.71/third_party/blink/renderer/platform/weborigin/scheme_registry.cc.patch b/chromium_edits/122.0.6194.0/third_party/blink/renderer/platform/weborigin/scheme_registry.cc.patch similarity index 100% rename from chromium_edits/120.0.6099.71/third_party/blink/renderer/platform/weborigin/scheme_registry.cc.patch rename to chromium_edits/122.0.6194.0/third_party/blink/renderer/platform/weborigin/scheme_registry.cc.patch diff --git a/chromium_edits/120.0.6099.71/url/BUILD.gn.patch b/chromium_edits/122.0.6194.0/url/BUILD.gn.patch similarity index 87% rename from chromium_edits/120.0.6099.71/url/BUILD.gn.patch rename to chromium_edits/122.0.6194.0/url/BUILD.gn.patch index 63fb8f8b..293eb89e 100644 --- a/chromium_edits/120.0.6099.71/url/BUILD.gn.patch +++ b/chromium_edits/122.0.6194.0/url/BUILD.gn.patch @@ -1,5 +1,5 @@ diff --git a/url/BUILD.gn b/url/BUILD.gn -index c525c166979d6..ce2b1ae43c0a7 100644 +index 0ac17c08dc5bc..2497d02658bb3 100644 --- a/url/BUILD.gn +++ b/url/BUILD.gn @@ -5,6 +5,7 @@ @@ -10,7 +10,7 @@ index c525c166979d6..ce2b1ae43c0a7 100644 import("features.gni") import("//build/config/cronet/config.gni") -@@ -67,6 +68,7 @@ component("url") { +@@ -68,6 +69,7 @@ component("url") { public_deps = [ "//base", "//build:robolectric_buildflags", @@ -18,7 +18,7 @@ index c525c166979d6..ce2b1ae43c0a7 100644 ] configs += [ "//build/config/compiler:wexit_time_destructors" ] -@@ -89,6 +91,11 @@ component("url") { +@@ -90,6 +92,11 @@ component("url") { public_configs = [ "//third_party/jdk" ] } diff --git a/chromium_edits/120.0.6099.71/url/url_canon.h.patch b/chromium_edits/122.0.6194.0/url/url_canon.h.patch similarity index 91% rename from chromium_edits/120.0.6099.71/url/url_canon.h.patch rename to chromium_edits/122.0.6194.0/url/url_canon.h.patch index 24ae1ba4..7ffd3a2f 100644 --- a/chromium_edits/120.0.6099.71/url/url_canon.h.patch +++ b/chromium_edits/122.0.6194.0/url/url_canon.h.patch @@ -1,8 +1,8 @@ diff --git a/url/url_canon.h b/url/url_canon.h -index d3a7fabf09fa8..06db17242248f 100644 +index 8c48f9825d8cf..b9ad961e1b123 100644 --- a/url/url_canon.h +++ b/url/url_canon.h -@@ -697,6 +697,23 @@ bool CanonicalizeMailtoURL(const char16_t* spec, +@@ -804,6 +804,23 @@ bool CanonicalizeMailtoURL(const char16_t* spec, CanonOutput* output, Parsed* new_parsed); diff --git a/chromium_edits/120.0.6099.71/url/url_canon_ipfs.cc b/chromium_edits/122.0.6194.0/url/url_canon_ipfs.cc similarity index 62% rename from chromium_edits/120.0.6099.71/url/url_canon_ipfs.cc rename to chromium_edits/122.0.6194.0/url/url_canon_ipfs.cc index da3a5f03..d7c9fdc7 100644 --- a/chromium_edits/120.0.6099.71/url/url_canon_ipfs.cc +++ b/chromium_edits/122.0.6194.0/url/url_canon_ipfs.cc @@ -1,14 +1,10 @@ #include "url_canon_internal.h" -#include +#include #include #include -namespace m = libp2p::multi; -using Cid = m::ContentIdentifier; -using CidCodec = m::ContentIdentifierCodec; - bool url::CanonicalizeIpfsURL(const char* spec, int spec_len, const Parsed& parsed, @@ -22,37 +18,24 @@ bool url::CanonicalizeIpfsURL(const char* spec, if ( parsed.host.len < 1 ) { return false; } - std::string cid_str{ spec + parsed.host.begin, static_cast(parsed.host.len) }; - auto maybe_cid = CidCodec::fromString(cid_str); - if ( !maybe_cid.has_value() ) { - auto e = libp2p::multi::Stringify(maybe_cid.error()); - std::ostringstream err; - err << e << ' ' - << std::string_view{spec,static_cast(spec_len)}; - maybe_cid = ipfs::id_cid::forText( err.str() ); - } - auto cid = maybe_cid.value(); - if ( cid.version == Cid::Version::V0 ) { - //TODO dcheck content_type == DAG_PB && content_address.getType() == sha256 - cid = Cid{ - Cid::Version::V1, - cid.content_type, - cid.content_address - }; + std::string_view cid_str{ spec + parsed.host.begin, static_cast(parsed.host.len) }; + auto cid = ipfs::Cid(cid_str); + if ( !cid.valid() ) { + cid = ipfs::id_cid::forText( std::string{cid_str} + " is not a valid CID." ); } - auto as_str = CidCodec::toString(cid); - if ( !as_str.has_value() ) { + auto as_str = cid.to_string(); + if ( as_str.empty() ) { return false; } std::string stdurl{ spec, static_cast(parsed.host.begin) }; - stdurl.append( as_str.value() ); + stdurl.append( as_str ); stdurl.append( spec + parsed.host.end(), spec_len - parsed.host.end() ); spec = stdurl.data(); spec_len = static_cast(stdurl.size()); Parsed parsed_input; ParseStandardURL(spec, spec_len, &parsed_input); return CanonicalizeStandardURL( - spec, spec_len, + spec, parsed_input, scheme_type, charset_converter, diff --git a/chromium_edits/122.0.6194.0/url/url_util.cc.patch b/chromium_edits/122.0.6194.0/url/url_util.cc.patch new file mode 100644 index 00000000..814f4b82 --- /dev/null +++ b/chromium_edits/122.0.6194.0/url/url_util.cc.patch @@ -0,0 +1,22 @@ +diff --git a/url/url_util.cc b/url/url_util.cc +index 6f83f33c01c6b..a248e11c49445 100644 +--- a/url/url_util.cc ++++ b/url/url_util.cc +@@ -273,8 +273,15 @@ bool DoCanonicalize(const CHAR* spec, + } else if (DoCompareSchemeComponent(spec, scheme, url::kFileSystemScheme)) { + // Filesystem URLs are special. + ParseFileSystemURL(spec, spec_len, &parsed_input); +- success = CanonicalizeFileSystemURL(spec, parsed_input, charset_converter, +- output, output_parsed); ++ success = CanonicalizeFileSystemURL(spec, parsed_input, ++ charset_converter, output, ++ output_parsed); ++ ++ } else if (DoCompareSchemeComponent(spec, scheme, "ipfs")) { ++ // Switch multibase away from case-sensitive ones before continuing canonicalization. ++ ParseStandardURL(spec, spec_len, &parsed_input); ++ success = CanonicalizeIpfsURL(spec, spec_len, parsed_input, scheme_type, ++ charset_converter, output, output_parsed); + + } else if (DoIsStandard(spec, scheme, &scheme_type)) { + // All "normal" URLs. diff --git a/cmake/chromium.py b/cmake/chromium.py index 0cd38b36..f3dfc84e 100755 --- a/cmake/chromium.py +++ b/cmake/chromium.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 +from verbose import verbose from cache_vars import build_dir, vars from patch import Patcher -from verbose import verbose from glob import glob from os import environ, makedirs, remove @@ -115,13 +115,7 @@ def runpy(args, silent=False): if not isfile(join(src, '.landmines')): run([python, join(depot_tools_dir, 'gclient.py'), 'runhooks', '-j', jobs]) -with open(join(src, 'chrome', 'browser', 'BUILD.gn')) as w: - content = w.read() - if 'components/ipfs' in content: - verbose('Chromium seems to be already patched.') - else: - print('Apply patch file...', file=stderr) - patcher.apply() +patcher.apply() ipfs_dir = join(src, 'components', 'ipfs') diff --git a/cmake/inc_link.py b/cmake/inc_link.py index 529b6532..65930363 100755 --- a/cmake/inc_link.py +++ b/cmake/inc_link.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -from cache_vars import build_dir, vars, verbose +from verbose import verbose +from cache_vars import build_dir, vars from glob import glob import os @@ -35,6 +36,7 @@ def makedirs(p): # makedirs(join(inc_link,'google')) # symlink(join(chromium_src,'third_party','protobuf','src','google','protobuf'), join(inc_link,'google','protobuf')) if not exists(join(inc_link,'absl')): + makedirs(inc_link) symlink(join(chromium_src,'third_party','abseil-cpp','absl'),join(inc_link,'absl')) def quoted(inc): diff --git a/cmake/patch.py b/cmake/patch.py index 8f791d30..2bf78f3f 100755 --- a/cmake/patch.py +++ b/cmake/patch.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 import sys +from enum import auto, Enum +from glob import glob from os import listdir, makedirs, remove -from os.path import exists, dirname, isdir, join, realpath, splitext +from os.path import exists, dirname, isdir, isfile, join, realpath, relpath, splitext from shutil import copyfile, rmtree -from subprocess import check_call, check_output +from subprocess import call, check_call, check_output from sys import argv, executable, platform, stderr from time import ctime from verbose import verbose @@ -34,6 +36,19 @@ def as_int(v): return result +def content_differs(ap,bp): + if not isfile(ap) or not isfile(bp): + return True + with open(ap) as a: + with open(bp) as b: + return a.read() != b.read() + +class Result(Enum): + Output = auto() + RawOutput = auto() + OrDie = auto() + ExitCode = auto() + StrippedOutput = Output class Patcher: def __init__(self, chromium_source_dir, git_bin, build_type): @@ -46,12 +61,17 @@ def __init__(self, chromium_source_dir, git_bin, build_type): def create_patch_file(self): tag = self.tag_name() - write_dir = join(self.edir, tag) + name = tag + write_dir = join(self.edir, name) if exists(write_dir): rmtree(write_dir) - for lin in self.git(['status', '--porcelain'], and_strip=False).splitlines(): - stat = lin[0:2] - path = lin[3:] + paths = self.git(['diff', tag, '--name-only'], Result.RawOutput).splitlines() + for lin in self.git(['status', '--porcelain'], Result.RawOutput).splitlines(): + if lin[0:3] != '?? ': + continue + verbose('Unversioned', lin[3:]) + paths.append( lin[3:] ) + for path in paths: from_path = join(self.csrc, path) to_path = join(write_dir, path) to_dir = dirname(to_path) @@ -59,32 +79,36 @@ def create_patch_file(self): verbose('Not putting component into edit tree') elif 'third_party/ipfs_client' in path: verbose('Not putting library into edit tree') - elif stat == ' M': - diff_out = self.git(['diff', '--patch', tag, path], and_strip=False) + elif not self.file_in_branch(tag, path): + print('Copy', from_path, '->', to_path) + makedirs(to_dir, exist_ok=True) + copyfile(from_path, to_path) + elif not self.file_in_branch('HEAD', path): + to_path += '.rm' + print('Remembering the removal of', from_path, 'with', to_path) + makedirs(to_dir, exist_ok=True) + with open(to_path, 'w') as rm_f: + rm_f.write('//Remember to remove the corresponding file from the Chromium source tree') + else: + diff_out = self.git(['diff', '--patch', tag, path], Result.RawOutput) if diff_out: makedirs(to_dir, exist_ok=True) to_path += '.patch' with open(to_path, 'w') as to_f: to_f.write(diff_out) print(to_path) - elif stat == '??': - print('Copy', from_path, '->', to_path) - makedirs(to_dir, exist_ok=True) - copyfile(from_path, to_path) - else: - print('Unhandled git status', stat, 'for', path) - exit(32) - self.git(['add', 'url/url_canon_ipfs.cc']) - diff = self.git(['diff', '--patch', tag]) - name = tag - if self.curr_hash() != self.hash_of(tag): - print('NOT ON A TAG. Patching for hash instead') - name = self.curr_hash() + self.git(['add', 'url/url_canon_ipfs.cc'], Result.OrDie) + diff = self.git(['diff', '--patch', tag], Result.RawOutput) file_name = join(self.pdir, name+'.patch') - print('Patch file:', file_name) + print('Old patch file:', file_name) with open(file_name, 'w') as patch_file: patch_file.write(diff+"\n") + def file_in_branch(self, ref: str, path: str): + out = self.git(['ls-tree', '--name-only', ref, path], Result.Output) + verbose('ls-tree gave me', out) + return out == path + def apply(self): win = '' win_dist = 9876543210 @@ -94,29 +118,75 @@ def apply(self): if d < win_dist or (d == win_dist and len(ref) < len(win)): win_dist = d win = ref - print('Best patch file is', win, file=stderr) - patch_path = join(self.pdir, win+'.patch') - self.git(['apply', '--verbose', patch_path], out=False) - - def git(self, args: list[str], out: bool = True, and_strip: bool = True) -> str: - if out: - result = check_output([self.gbin, '-C', self.csrc] + args, text=True) - if and_strip: - return result.strip() + edits_dir = join(self.edir, win) + edit_glob = f'{edits_dir}/**/*' + print('Best edits version is', win, 'look for edits by', edit_glob, file=stderr) + for edit in glob(edit_glob, recursive=True): + if not isfile(edit): + continue + verbose('Have edit:', edit) + ext = splitext(edit)[1] + rel = relpath(edit, edits_dir) + to_path = join(self.csrc, rel) + if ext == '.patch': + self.check_patch(edit, rel, to_path) + elif ext == '.rm': + if isfile(to_path): + print("Remove", to_path, 'due to', edit) + remove(to_path) + else: + verbose(f"{to_path} already removed") + elif not isfile(to_path): + print('Copy', edit, '->', to_path) + copyfile(edit, to_path) + elif content_differs(edit, to_path): + print('Warning:', to_path, 'exists, is different from ', edit, ' and is not being overwritten.') else: - return result + verbose(f"{to_path} already copied") + verbose('Done patching') + + + def check_patch(self, patch_path: str, relative: str, target_path: str): + if 0 == self.git(['apply', '--check', '--reverse', '--verbose', patch_path], Result.ExitCode): + verbose(patch_path, 'already applied.') + return + src = splitext(relative)[0] + ec = self.git(['apply', '--verbose', patch_path], Result.ExitCode) + verbose('Applying patch', patch_path, 'gave exit code', ec) + if ec == 0: + print('Patched', src, 'with', patch_path) else: - check_call([self.gbin, '-C', self.csrc] + args, text=True) - return '' + with open(join(self.csrc,src)) as target_file: + text = target_file.read() + if 'ipfs' in text: + verbose("Patch file", patch_path, 'may have already been applied.') + else: + print("Failed to patch", src, '( at', join(self.csrc,src), ') with', patch_path) + exit(8) + + def git(self, args: list[str], result: Result) -> str: + a = [self.gbin, '-C', self.csrc] + args + verbose('Running', a) + match result: + case Result.RawOutput: + return check_output(a, text=True) + case Result.StrippedOutput: + return check_output(a, text=True).strip() + case Result.OrDie: + check_call(a) + case Result.ExitCode: + return call(a) + case _: + raise RuntimeError('result type not handled') def tag_name(self) -> str: - return self.git(['describe', '--tags', '--abbrev=0']) + return self.git(['describe', '--tags', '--abbrev=0'], Result.Output) def curr_hash(self) -> str: return self.hash_of('HEAD') def hash_of(self, ref) -> str: - return self.git(['rev-parse', ref]) + return self.git(['rev-parse', ref], Result.Output) def distance(self, ref) -> int: a, b = self.distances('HEAD', ref) @@ -146,8 +216,8 @@ def available(self): return map(lambda p: splitext(p)[0], listdir(self.pdir)) def distances(self, frm, ref): - a = int(self.git(['rev-list', '--count', frm+'..'+ref])) - b = int(self.git(['rev-list', '--count', ref+'..'+frm])) + a = int(self.git(['rev-list', '--count', frm+'..'+ref], Result.Output)) + b = int(self.git(['rev-list', '--count', ref+'..'+frm], Result.Output)) return (a, b) def maybe_newer(self, x, y): @@ -185,7 +255,7 @@ def electron_version(self, branch='main'): def unavailable(self): avail = list(map(as_int, self.available())) version_set = {} - fudge = 59888 + fudge = 59891 def check(version, version_set, s): i = as_int(version) by = (fudge,0) @@ -226,6 +296,9 @@ def out_of_date(self, p): if not Patcher.has_file_line(lines, 'chrome/browser/flag-metadata.json', '+ "name": "enable-ipfs",'): print(p, 'does not have enable-ipfs in flag-metadata.json', file_path, file=sys.stderr) return True + if not Patcher.has_file_line(lines, 'chrome/browser/chrome_content_browser_client.cc', '+ main_parts->AddParts(std::make_unique());'): + print(p, 'does not have enable-ipfs in flag-metadata.json', file_path, file=sys.stderr) + return True return False @staticmethod @@ -288,7 +361,7 @@ def list_ood(self, to_check: list[str], sense: bool): pr = Patcher(realpath(join(dirname(__file__), '..')), 'git', 'Debug') pre = '?? component/patches/' suf = 'patch' - for line in pr.git(['status','--porcelain']).splitlines(): + for line in pr.git(['status','--porcelain'], Result.RawOutput).splitlines(): if line.startswith(pre) and line.endswith(suf): end = len(line) - len(suf) - 1 pch = line[len(pre):end] diff --git a/cmake/tidy.py b/cmake/tidy.py new file mode 100755 index 00000000..64b25c85 --- /dev/null +++ b/cmake/tidy.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +import json + +from glob import glob +from os import remove +from os.path import splitext + +with open('compile_commands.json') as compile_commands_json: + keeps = {} + for command in json.load(compile_commands_json): + keeps[command['output']] = command['file'] + for obj in glob('**/*.o', recursive=True): + if not 'CMakeFiles' in obj: + continue + if not obj in keeps: + print('rm', obj) + remove(obj) + for cov in glob('**/*.gc??', recursive=True): + obj = splitext(cov)[0] + '.o' + if not obj in keeps: + print('rm', cov) + remove(cov) diff --git a/cmake/verbose.py b/cmake/verbose.py index 7de4f354..bc89fd88 100644 --- a/cmake/verbose.py +++ b/cmake/verbose.py @@ -1,4 +1,5 @@ -from sys import argv + +from sys import argv, stderr if '--verbose' in argv: argv.remove('--verbose') diff --git a/component/CMakeLists.txt b/component/CMakeLists.txt index 0248aa96..7cd3e38f 100644 --- a/component/CMakeLists.txt +++ b/component/CMakeLists.txt @@ -133,6 +133,7 @@ add_custom_command( ) add_custom_target(inc_link + ALL COMMAND "${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/cmake/inc_link.py" "${CMAKE_BINARY_DIR}" WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" DEPENDS in_tree_gen diff --git a/component/block_http_request.cc b/component/block_http_request.cc index 67e32592..76244b0a 100644 --- a/component/block_http_request.cc +++ b/component/block_http_request.cc @@ -67,7 +67,8 @@ void Self::OnResponse(std::shared_ptr, status = 408; break; default: - VLOG(2) << "NetErr " << loader_->NetError() << " for " << inf_.url; + // VLOG(2) << "NetErr " << loader_->NetError() << " for " << + // inf_.url; status = 500; } // auto sz = body ? body->size() : 0UL; @@ -77,10 +78,7 @@ void Self::OnResponse(std::shared_ptr, } auto sp = status_line_.find(' '); if (sp < status_line_.size()) { - VLOG(2) << "HTTP response status='" << status_line_ << "'."; status = std::atoi(status_line_.c_str() + sp + 1); - } else { - VLOG(2) << "Status line malformed/missing : '" << status_line_ << "'"; } if (body) { callback_(status, *body, header_accessor_); diff --git a/component/chromium_ipfs_context.cc b/component/chromium_ipfs_context.cc index 4a853033..7ef6ad8f 100644 --- a/component/chromium_ipfs_context.cc +++ b/component/chromium_ipfs_context.cc @@ -5,6 +5,7 @@ #include "chromium_json_adapter.h" #include "crypto_api.h" #include "inter_request_state.h" +#include "preferences.h" #include #include @@ -52,7 +53,7 @@ std::string Self::MimeType(std::string extension, } if (result.empty() || result == "application/octet-stream") { net::SniffMimeTypeFromLocalData({content.data(), head_size}, &result); - VLOG(1) << "Falling all the way back to content type " << result; + VLOG(2) << "Falling all the way back to content type " << result; } return result; } @@ -75,8 +76,8 @@ void Self::SendDnsTextRequest(std::string host, LOG(INFO) << "Finished resolving " << host << " via DNSLink"; dns_reqs_.erase(host); }; - dns_reqs_[host] = std::make_unique(host, res, don_wrap, - network_context_.get()); + auto* nc = state_->network_context(); + dns_reqs_[host] = std::make_unique(host, res, don_wrap, nc); } void Self::SendHttpRequest(HttpRequestDescription req_inf, HttpCompleteCallback cb) const { @@ -88,8 +89,7 @@ bool Self::VerifyKeySignature(SigningKeyType t, ByteView signature, ByteView data, ByteView key_bytes) const { - return crypto_api::VerifySignature(static_cast(t), signature, - data, key_bytes); + return crypto_api::VerifySignature(t, signature, data, key_bytes); } auto Self::ParseCbor(ipfs::ContextApi::ByteView bytes) const -> std::unique_ptr { @@ -110,11 +110,22 @@ auto Self::ParseJson(std::string_view j_str) const } return {}; } +unsigned int Self::GetGatewayRate(std::string_view prefix) { + return rates_.GetRate(prefix); +} +void Self::SetGatewayRate(std::string_view prefix, unsigned int new_rate) { + rates_.SetRate(prefix, new_rate); +} +auto Self::GetGateway(std::size_t index) const -> std::optional { + auto [gw, r] = rates_.at(index); + if (gw) { + return GatewaySpec{*gw, r}; + } + return std::nullopt; +} -Self::ChromiumIpfsContext( - InterRequestState& state, - raw_ptr network_context) - : network_context_{network_context}, state_{state} {} +Self::ChromiumIpfsContext(InterRequestState& state, PrefService* prefs) + : state_{state}, rates_{prefs} {} Self::~ChromiumIpfsContext() noexcept { LOG(WARNING) << "API dtor - are all URIs loaded?"; } diff --git a/component/chromium_ipfs_context.h b/component/chromium_ipfs_context.h index f43bed83..b274746e 100644 --- a/component/chromium_ipfs_context.h +++ b/component/chromium_ipfs_context.h @@ -2,6 +2,7 @@ #define IPFS_CHROMIUM_IPFS_CONTEXT_H_ #include "dns_txt_request.h" +#include "preferences.h" #include #include @@ -13,6 +14,8 @@ #include +class PrefService; + namespace network { class SimpleURLLoader; namespace mojom { @@ -27,9 +30,9 @@ class NetworkRequestor; class ChromiumIpfsContext final : public ContextApi { raw_ptr loader_factory_ = nullptr; - raw_ptr network_context_; raw_ref state_; std::map> dns_reqs_; + GatewayRates rates_; std::string MimeType(std::string extension, std::string_view content, @@ -48,9 +51,12 @@ class ChromiumIpfsContext final : public ContextApi { std::unique_ptr ParseCbor(ByteView) const override; std::unique_ptr ParseJson(std::string_view) const override; + std::optional GetGateway(std::size_t index) const override; + unsigned int GetGatewayRate(std::string_view) override; + void SetGatewayRate(std::string_view, unsigned int) override; + public: - ChromiumIpfsContext(InterRequestState&, - raw_ptr network_context); + ChromiumIpfsContext(InterRequestState&, PrefService* prefs); ~ChromiumIpfsContext() noexcept override; void SetLoaderFactory(network::mojom::URLLoaderFactory&); }; diff --git a/component/crypto_api.cc b/component/crypto_api.cc index d15a63f1..250c9024 100644 --- a/component/crypto_api.cc +++ b/component/crypto_api.cc @@ -7,17 +7,17 @@ #include "third_party/boringssl/src/include/openssl/evp.h" namespace { -int ToEvpKeyType(ipfs::ipns::KeyType t) { - using ipfs::ipns::KeyType; +int ToEvpKeyType(ipfs::ContextApi::SigningKeyType t) { + using T = ipfs::ContextApi::SigningKeyType; switch (t) { - case KeyType::ECDSA: + case T::ECDSA: LOG(ERROR) << "TODO Check on ECDSA key type translation."; return EVP_PKEY_EC; - case KeyType::Ed25519: + case T::Ed25519: return EVP_PKEY_ED25519; - case KeyType::RSA: + case T::RSA: return EVP_PKEY_RSA; - case KeyType::Secp256k1: + case T::Secp256k1: LOG(ERROR) << "TODO Check on Secp256k1 key type translation."; return EVP_PKEY_DSA; default: @@ -29,7 +29,7 @@ int ToEvpKeyType(ipfs::ipns::KeyType t) { namespace cpto = ipfs::crypto_api; -bool cpto::VerifySignature(ipfs::ipns::KeyType key_type, +bool cpto::VerifySignature(ipfs::ContextApi::SigningKeyType key_type, ipfs::ByteView signature, ipfs::ByteView data, ipfs::ByteView key_bytes) { diff --git a/component/crypto_api.h b/component/crypto_api.h index 1363bb1f..3b3861e1 100644 --- a/component/crypto_api.h +++ b/component/crypto_api.h @@ -1,22 +1,17 @@ #ifndef IPFS_VALIDATE_SIGNATURE_H_ #define IPFS_VALIDATE_SIGNATURE_H_ -#include "components/webcrypto/algorithm_implementation.h" - -#include "third_party/ipfs_client/keys.pb.h" - +#include #include +#include "components/webcrypto/algorithm_implementation.h" namespace ipfs::crypto_api { -/* -using Algo = std::pair>; -Algo GetAlgo(ipfs::ipns::KeyType); -*/ -bool VerifySignature(ipfs::ipns::KeyType, + +bool VerifySignature(ipfs::ContextApi::SigningKeyType, ByteView signature, ByteView data, ByteView key); + } // namespace ipfs::crypto_api #endif // IPFS_VALIDATE_SIGNATURE_H_ diff --git a/component/inter_request_state.cc b/component/inter_request_state.cc index 759b6d73..d24591e0 100644 --- a/component/inter_request_state.cc +++ b/component/inter_request_state.cc @@ -1,12 +1,13 @@ #include "inter_request_state.h" #include "chromium_ipfs_context.h" +#include "preferences.h" -#include "base/logging.h" +#include #include "content/public/browser/browser_context.h" +#include #include - #include #include @@ -16,52 +17,58 @@ namespace { constexpr char user_data_key[] = "ipfs_request_userdata"; } +void Self::CreateForBrowserContext(content::BrowserContext* c, PrefService* p) { + DCHECK(c); + DCHECK(p); + LOG(INFO) << "Creating new IPFS state for this browser context."; + auto owned = std::make_unique(c->GetPath(), p); + c->SetUserData(user_data_key, std::move(owned)); +} auto Self::FromBrowserContext(content::BrowserContext* context) -> InterRequestState& { if (!context) { LOG(WARNING) << "No browser context! Using a default IPFS state."; - static ipfs::InterRequestState static_state({}); + static ipfs::InterRequestState static_state({}, {}); return static_state; } base::SupportsUserData::Data* existing = context->GetUserData(user_data_key); if (existing) { VLOG(2) << "Re-using existing IPFS state."; return *static_cast(existing); + } else { + LOG(ERROR) << "Browser context has no IPFS state! It must be set earlier!"; + static ipfs::InterRequestState static_state({}, {}); + return static_state; } - VLOG(2) << "Creating new IPFS state for this browser context."; - auto owned = std::make_unique(context->GetPath()); - ipfs::InterRequestState* raw = owned.get(); - context->SetUserData(user_data_key, std::move(owned)); - return *raw; } std::shared_ptr Self::api() { - auto existing = api_.lock(); - if (existing) { - return existing; - } - auto created = - std::make_shared(*this, network_context_); - api_ = created; - return created; + return api_; } auto Self::cache() -> std::shared_ptr& { - if (!cache_) { - cache_ = std::make_shared(*this, disk_path_); - } + // if (!cache_) { + // cache_ = std::make_shared(*this, disk_path_); + // } return cache_; } auto Self::orchestrator() -> Orchestrator& { if (!orc_) { auto rtor = - gw::default_requestor(gateways().GenerateList(), cache(), api()); + gw::default_requestor(Gateways::DefaultGateways(), cache(), api()); orc_ = std::make_shared(rtor, api()); } return *orc_; } -void Self::set_network_context(raw_ptr val) { +void Self::network_context(network::mojom::NetworkContext* val) { network_context_ = val; } -Self::InterRequestState(base::FilePath p) : disk_path_{p} {} +network::mojom::NetworkContext* Self::network_context() const { + return network_context_; +} +Self::InterRequestState(base::FilePath p, PrefService* prefs) + : api_{std::make_shared(*this, prefs)}, disk_path_{p} { + DCHECK(prefs); +} Self::~InterRequestState() noexcept { network_context_ = nullptr; + cache_.reset(); } diff --git a/component/inter_request_state.h b/component/inter_request_state.h index 417e609c..3471cfe5 100644 --- a/component/inter_request_state.h +++ b/component/inter_request_state.h @@ -10,6 +10,8 @@ #include "base/supports_user_data.h" #include "services/network/network_context.h" +class PrefService; + namespace content { class BrowserContext; } @@ -17,10 +19,10 @@ class BrowserContext; namespace ipfs { class Scheduler; class ChromiumIpfsContext; -class InterRequestState : public base::SupportsUserData::Data { - Gateways gws_; +class COMPONENT_EXPORT(IPFS) InterRequestState + : public base::SupportsUserData::Data { IpnsNames names_; - std::weak_ptr api_; + std::shared_ptr api_; std::time_t last_discovery_ = 0; std::shared_ptr cache_; base::FilePath const disk_path_; @@ -30,17 +32,18 @@ class InterRequestState : public base::SupportsUserData::Data { std::shared_ptr& cache(); public: - InterRequestState(base::FilePath); + InterRequestState(base::FilePath, PrefService*); ~InterRequestState() noexcept override; - Gateways& gateways() { return gws_; } IpnsNames& names() { return names_; } Scheduler& scheduler(); std::shared_ptr api(); std::array,2> serialized_caches(); Orchestrator& orchestrator(); - void set_network_context(raw_ptr); + void network_context(network::mojom::NetworkContext*); + network::mojom::NetworkContext* network_context() const; + static void CreateForBrowserContext(content::BrowserContext*, PrefService*); static InterRequestState& FromBrowserContext(content::BrowserContext*); }; } // namespace ipfs diff --git a/component/interceptor.cc b/component/interceptor.cc index 02d47638..91493687 100644 --- a/component/interceptor.cc +++ b/component/interceptor.cc @@ -19,7 +19,8 @@ void Interceptor::MaybeCreateLoader(network::ResourceRequest const& req, content::BrowserContext* context, LoaderCallback loader_callback) { auto& state = InterRequestState::FromBrowserContext(context); - state.set_network_context(network_context_); + state.network_context(network_context_); + LOG(INFO) << "MaybeCreateLoader " << req.url.spec(); if (req.url.SchemeIs("ipfs") || req.url.SchemeIs("ipns")) { auto hdr_str = req.headers.ToString(); std::replace(hdr_str.begin(), hdr_str.end(), '\r', ' '); diff --git a/component/interceptor.h b/component/interceptor.h index 38d5ff39..0321ea54 100644 --- a/component/interceptor.h +++ b/component/interceptor.h @@ -3,6 +3,7 @@ #include "content/public/browser/url_loader_request_interceptor.h" +class PrefService; namespace network::mojom { class URLLoaderFactory; class NetworkContext; @@ -14,6 +15,7 @@ class COMPONENT_EXPORT(IPFS) Interceptor final : public content::URLLoaderRequestInterceptor { raw_ptr loader_factory_; raw_ptr network_context_; + raw_ptr pref_svc_; void MaybeCreateLoader(network::ResourceRequest const&, content::BrowserContext*, diff --git a/component/ipfs_url_loader.cc b/component/ipfs_url_loader.cc index afc97dc4..5b98b08b 100644 --- a/component/ipfs_url_loader.cc +++ b/component/ipfs_url_loader.cc @@ -62,6 +62,7 @@ void ipfs::IpfsUrlLoader::StartRequest( mojo::PendingRemote client) { DCHECK(!me->receiver_.is_bound()); DCHECK(!me->client_.is_bound()); + VLOG(1) << "StartRequest(" << resource_request.url.spec() << ")"; me->receiver_.Bind(std::move(receiver)); me->client_.Bind(std::move(client)); if (me->original_url_.empty()) { @@ -134,9 +135,6 @@ void ipfs::IpfsUrlLoader::BlocksComplete(std::string mime_type) { head->content_length = byte_count; head->headers = net::HttpResponseHeaders::TryToCreate("access-control-allow-origin: *"); - if (resp_loc_.size()) { - head->headers->AddHeader("Location", resp_loc_); - } if (!head->headers) { LOG(ERROR) << "\n\tFailed to create headers!\n"; return; @@ -155,11 +153,18 @@ void ipfs::IpfsUrlLoader::BlocksComplete(std::string mime_type) { VLOG(1) << "Appending 'additional' header:" << n << '=' << v << '.'; head->headers->AddHeader(n, v); } - VLOG(1) << "Calling PopulateParsedHeaders"; + if (resp_loc_.size()) { + head->headers->AddHeader("Location", resp_loc_); + LOG(INFO) << "Sending response for " << original_url_ << " with mime type " + << head->mime_type << " and status line '" << status_line + << "' @location '" << resp_loc_ << "'"; + } else { + VLOG(1) << "Sending response for " << original_url_ << " with mime type " + << head->mime_type << " and status line '" << status_line + << "' with no location header."; + } head->parsed_headers = network::PopulateParsedHeaders(head->headers.get(), GURL{original_url_}); - VLOG(1) << "Sending response for " << original_url_ << " with mime type " - << head->mime_type << " and status line " << status_line; if (status_ / 100 == 3 && resp_loc_.size()) { auto ri = net::RedirectInfo::ComputeRedirectInfo( "GET", GURL{original_url_}, net::SiteForCookies{}, diff --git a/component/patches/122.0.6182.0.patch b/component/patches/122.0.6182.0.patch index 87ae89c8..00ebf668 100644 --- a/component/patches/122.0.6182.0.patch +++ b/component/patches/122.0.6182.0.patch @@ -1,5 +1,5 @@ diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn -index a188528a9e262..4b945eb891895 100644 +index a188528a9e262..88df13b162858 100644 --- a/chrome/browser/BUILD.gn +++ b/chrome/browser/BUILD.gn @@ -40,6 +40,7 @@ import("//rlz/buildflags/buildflags.gni") @@ -10,11 +10,23 @@ index a188528a9e262..4b945eb891895 100644 import("//third_party/protobuf/proto_library.gni") import("//third_party/webrtc/webrtc.gni") import("//third_party/widevine/cdm/widevine.gni") -@@ -2604,6 +2605,10 @@ static_library("browser") { +@@ -1912,7 +1913,6 @@ static_library("browser") { + "user_education/user_education_service_factory.h", + ] + } +- + configs += [ + "//build/config/compiler:wexit_time_destructors", + "//build/config:precompiled_headers", +@@ -2604,6 +2604,14 @@ static_library("browser") { ] } + if (enable_ipfs) { ++ sources += [ ++ "ipfs_extra_parts.cc", ++ "ipfs_extra_parts.h", ++ ] + deps += [ "//components/ipfs" ] + } + @@ -110,7 +122,7 @@ index 4c88614c68c25..f8bb12a3b0c2e 100644 // Also check for schemes registered via registerProtocolHandler(), which diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc -index d3d67d83a514e..a7f34d6dde492 100644 +index d3d67d83a514e..a5b1ef6339211 100644 --- a/chrome/browser/chrome_content_browser_client.cc +++ b/chrome/browser/chrome_content_browser_client.cc @@ -378,6 +378,7 @@ @@ -121,11 +133,12 @@ index d3d67d83a514e..a7f34d6dde492 100644 #include "third_party/widevine/cdm/buildflags.h" #include "ui/base/clipboard/clipboard_format_type.h" #include "ui/base/l10n/l10n_util.h" -@@ -500,6 +501,12 @@ +@@ -500,6 +501,13 @@ #include "chrome/browser/fuchsia/chrome_browser_main_parts_fuchsia.h" #endif +#if BUILDFLAG(ENABLE_IPFS) ++#include "chrome/browser/ipfs_extra_parts.h" +#include "components/ipfs/interceptor.h" +#include "components/ipfs/ipfs_features.h" +#include "components/ipfs/url_loader_factory.h" @@ -134,7 +147,19 @@ index d3d67d83a514e..a7f34d6dde492 100644 #if BUILDFLAG(IS_CHROMEOS) #include "base/debug/leak_annotations.h" #include "chrome/browser/apps/app_service/app_install/app_install_navigation_throttle.h" -@@ -6084,12 +6091,23 @@ void ChromeContentBrowserClient:: +@@ -1712,6 +1720,11 @@ ChromeContentBrowserClient::CreateBrowserMainParts(bool is_integration_test) { + main_parts->AddParts( + std::make_unique()); + ++#if BUILDFLAG(ENABLE_IPFS) ++ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { ++ main_parts->AddParts(std::make_unique()); ++ } ++#endif + return main_parts; + } + +@@ -6084,12 +6097,25 @@ void ChromeContentBrowserClient:: const absl::optional& request_initiator_origin, NonNetworkURLLoaderFactoryMap* factories) { #if BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(ENABLE_EXTENSIONS) || \ @@ -149,18 +174,20 @@ index d3d67d83a514e..a7f34d6dde492 100644 +#if BUILDFLAG(ENABLE_IPFS) + if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { + network::mojom::URLLoaderFactory* default_factory = g_browser_process->system_network_context_manager()->GetURLLoaderFactory(); ++ auto* context = web_contents->GetBrowserContext(); + ipfs::IpfsURLLoaderFactory::Create( + factories, -+ web_contents->GetBrowserContext(), ++ context, + default_factory, -+ GetSystemNetworkContext() ++ GetSystemNetworkContext(), ++ Profile::FromBrowserContext(context)->GetPrefs() + ); + } +#endif // BUILDFLAG(ENABLE_IPFS) #if BUILDFLAG(IS_CHROMEOS_ASH) if (web_contents) { -@@ -6231,6 +6249,11 @@ ChromeContentBrowserClient::WillCreateURLLoaderRequestInterceptors( +@@ -6231,6 +6257,11 @@ ChromeContentBrowserClient::WillCreateURLLoaderRequestInterceptors( scoped_refptr navigation_response_task_runner) { std::vector> interceptors; @@ -228,6 +255,42 @@ index ad76d832395a1..438facecff519 100644 #if BUILDFLAG(USE_FONTATIONS_BACKEND) extern const char kFontationsFontBackendName[]; extern const char kFontationsFontBackendDescription[]; +diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc +index fc9fcf1ff478a..800961b3c8767 100644 +--- a/chrome/browser/prefs/browser_prefs.cc ++++ b/chrome/browser/prefs/browser_prefs.cc +@@ -190,6 +190,7 @@ + #include "printing/buildflags/buildflags.h" + #include "rlz/buildflags/buildflags.h" + #include "third_party/abseil-cpp/absl/types/optional.h" ++#include "third_party/ipfs_client/ipfs_buildflags.h" + + #if BUILDFLAG(ENABLE_BACKGROUND_MODE) + #include "chrome/browser/background/background_mode_manager.h" +@@ -241,6 +242,11 @@ + #include "chrome/browser/pdf/pdf_pref_names.h" + #endif // BUILDFLAG(ENABLE_PDF) + ++#if BUILDFLAG(ENABLE_IPFS) ++#include "components/ipfs/ipfs_features.h" ++#include "components/ipfs/preferences.h" ++#endif ++ + #if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE) + #include "chrome/browser/screen_ai/pref_names.h" + #endif +@@ -1658,6 +1664,11 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry, + IncognitoModePrefs::RegisterProfilePrefs(registry); + invalidation::PerUserTopicSubscriptionManager::RegisterProfilePrefs(registry); + invalidation::InvalidatorRegistrarWithMemory::RegisterProfilePrefs(registry); ++#if BUILDFLAG(ENABLE_IPFS) ++ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { ++ ipfs::RegisterPreferences(registry); ++ } ++#endif + language::LanguagePrefs::RegisterProfilePrefs(registry); + login_detection::prefs::RegisterProfilePrefs(registry); + lookalikes::RegisterProfilePrefs(registry); diff --git a/chrome/common/chrome_content_client.cc b/chrome/common/chrome_content_client.cc index 246ec9c5c911f..5d66d133a7907 100644 --- a/chrome/common/chrome_content_client.cc @@ -636,164 +699,10512 @@ index e3bffe20734bc..0ed569ae164a0 100644 +} + } // namespace cbor -diff --git a/components/open_from_clipboard/clipboard_recent_content_generic.cc b/components/open_from_clipboard/clipboard_recent_content_generic.cc -index 4dcafecbc66c6..d205209c08162 100644 ---- a/components/open_from_clipboard/clipboard_recent_content_generic.cc -+++ b/components/open_from_clipboard/clipboard_recent_content_generic.cc -@@ -20,7 +20,7 @@ - namespace { - // Schemes appropriate for suggestion by ClipboardRecentContent. - const char* kAuthorizedSchemes[] = { -- url::kAboutScheme, url::kDataScheme, url::kHttpScheme, url::kHttpsScheme, -+ url::kAboutScheme, url::kDataScheme, url::kHttpScheme, url::kHttpsScheme, "ipfs", "ipns" - // TODO(mpearson): add support for chrome:// URLs. Right now the scheme - // for that lives in content and is accessible via - // GetEmbedderRepresentationOfAboutScheme() or content::kChromeUIScheme -diff --git a/net/dns/dns_config_service_linux.cc b/net/dns/dns_config_service_linux.cc -index 5273da5190277..12b28b86a4c00 100644 ---- a/net/dns/dns_config_service_linux.cc -+++ b/net/dns/dns_config_service_linux.cc -@@ -272,11 +272,11 @@ bool IsNsswitchConfigCompatible( - // Ignore any entries after `kDns` because Chrome will fallback to the - // system resolver if a result was not found in DNS. - return true; -- -+ case NsswitchReader::Service::kResolve: -+ break; - case NsswitchReader::Service::kMdns: - case NsswitchReader::Service::kMdns4: - case NsswitchReader::Service::kMdns6: -- case NsswitchReader::Service::kResolve: - case NsswitchReader::Service::kNis: - RecordIncompatibleNsswitchReason( - IncompatibleNsswitchReason::kIncompatibleService, -diff --git a/third_party/blink/renderer/platform/weborigin/scheme_registry.cc b/third_party/blink/renderer/platform/weborigin/scheme_registry.cc -index 4eadf46ea0c24..d62fc7fb14e01 100644 ---- a/third_party/blink/renderer/platform/weborigin/scheme_registry.cc -+++ b/third_party/blink/renderer/platform/weborigin/scheme_registry.cc -@@ -67,7 +67,7 @@ class URLSchemesRegistry final { - // is considered secure. Additional checks are performed to ensure that - // other http pages are filtered out. - service_worker_schemes({"http", "https"}), -- fetch_api_schemes({"http", "https"}), -+ fetch_api_schemes({"http", "https", "ipfs", "ipns"}), - allowed_in_referrer_schemes({"http", "https"}) { - for (auto& scheme : url::GetCorsEnabledSchemes()) - cors_enabled_schemes.insert(scheme.c_str()); -diff --git a/url/BUILD.gn b/url/BUILD.gn -index c525c166979d6..ce2b1ae43c0a7 100644 ---- a/url/BUILD.gn -+++ b/url/BUILD.gn -@@ -5,6 +5,7 @@ - import("//build/buildflag_header.gni") - import("//testing/libfuzzer/fuzzer_test.gni") - import("//testing/test.gni") +diff --git a/components/ipfs/BUILD.gn b/components/ipfs/BUILD.gn +new file mode 100644 +index 0000000000000..572e93e493e7a +--- /dev/null ++++ b/components/ipfs/BUILD.gn +@@ -0,0 +1,62 @@ ++import("//testing/test.gni") +import("//third_party/ipfs_client/args.gni") - import("features.gni") - - import("//build/config/cronet/config.gni") -@@ -67,6 +68,7 @@ component("url") { - public_deps = [ - "//base", - "//build:robolectric_buildflags", -+ "//third_party/ipfs_client:ipfs_buildflags", - ] - - configs += [ "//build/config/compiler:wexit_time_destructors" ] -@@ -89,6 +91,11 @@ component("url") { - public_configs = [ "//third_party/jdk" ] - } - -+ if (enable_ipfs) { -+ sources += [ "url_canon_ipfs.cc" ] -+ deps += [ "//third_party/ipfs_client:ipfs_client" ] ++ ++if (enable_ipfs) { ++ ++ component("ipfs") { ++ sources = [ ++ "block_http_request.cc", ++ "block_http_request.h", ++ "cache_requestor.cc", ++ "cache_requestor.h", ++ "chromium_cbor_adapter.cc", ++ "chromium_cbor_adapter.h", ++ "chromium_ipfs_context.cc", ++ "chromium_ipfs_context.h", ++ "chromium_json_adapter.cc", ++ "chromium_json_adapter.h", ++ "crypto_api.cc", ++ "crypto_api.h", ++ "dns_txt_request.cc", ++ "dns_txt_request.h", ++ "export.h", ++ "inter_request_state.cc", ++ "inter_request_state.h", ++ "interceptor.cc", ++ "interceptor.h", ++ "ipfs_features.cc", ++ "ipfs_features.h", ++ "ipfs_url_loader.cc", ++ "ipfs_url_loader.h", ++ "preferences.cc", ++ "preferences.h", ++ "url_loader_factory.cc", ++ "url_loader_factory.h", ++ ] ++ defines = [ ] ++ include_dirs = [ ++ ".", ++ "ipfs_client", ++ "ipfs_client/unix_fs", ++ ] ++ deps = [ ++ "//content", ++ "//crypto", ++ "//base", ++ "//components/cbor", ++ "//components/prefs", ++ "//components/webcrypto:webcrypto", ++ "//mojo/public/cpp/bindings", ++ "//services/network:network_service", ++ "//services/network/public/cpp:cpp", ++ "//services/network/public/mojom:url_loader_base", ++ "//url", ++ "//third_party/blink/public:blink", ++ ] ++ public_deps = [ ++ "//third_party/ipfs_client", ++ ] ++ defines = [ "IS_IPFS_IMPL" ] ++ } ++ ++} +diff --git a/components/ipfs/README.md b/components/ipfs/README.md +new file mode 100644 +index 0000000000000..1333ed77b7e1e +--- /dev/null ++++ b/components/ipfs/README.md +@@ -0,0 +1 @@ ++TODO +diff --git a/components/ipfs/block_http_request.cc b/components/ipfs/block_http_request.cc +new file mode 100644 +index 0000000000000..c48ddd8f77c8d +--- /dev/null ++++ b/components/ipfs/block_http_request.cc +@@ -0,0 +1,102 @@ ++#include "block_http_request.h" ++ ++#include ++#include ++#include ++ ++using Self = ipfs::BlockHttpRequest; ++ ++namespace { ++constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation = ++ net::DefineNetworkTrafficAnnotation("ipfs_gateway_request", R"( ++ semantics { ++ sender: "IPFS component" ++ description: ++ "Sends a request to an IPFS gateway." ++ trigger: ++ "Processing of an ipfs:// or ipns:// URL." ++ data: "None" ++ destination: WEBSITE ++ } ++ policy { ++ cookies_allowed: NO ++ setting: "EnableIpfs" ++ } ++ )"); ++} ++ ++Self::BlockHttpRequest(ipfs::HttpRequestDescription req_inf, ++ HttpCompleteCallback cb) ++ : inf_{req_inf}, callback_{cb} {} ++Self::~BlockHttpRequest() noexcept {} ++ ++void Self::send(raw_ptr loader_factory) { ++ auto req = std::make_unique(); ++ req->url = GURL{inf_.url}; ++ req->priority = net::HIGHEST; // TODO ++ if (!inf_.accept.empty()) { ++ req->headers.SetHeader("Accept", inf_.accept); ++ } ++ using L = network::SimpleURLLoader; ++ loader_ = L::Create(std::move(req), kTrafficAnnotation, FROM_HERE); ++ loader_->SetTimeoutDuration(base::Seconds(inf_.timeout_seconds)); ++ loader_->SetAllowHttpErrorResults(true); ++ loader_->SetOnResponseStartedCallback( ++ base::BindOnce(&Self::OnResponseHead, base::Unretained(this))); ++ auto bound = base::BindOnce(&Self::OnResponse, base::Unretained(this), ++ shared_from_this()); ++ DCHECK(loader_factory); ++ if (auto sz = inf_.max_response_size) { ++ loader_->DownloadToString(loader_factory, std::move(bound), sz.value()); ++ } else { ++ loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(loader_factory, ++ std::move(bound)); + } ++} ++void Self::OnResponse(std::shared_ptr, ++ std::unique_ptr body) { ++ DCHECK(loader_); ++ int status; ++ switch (loader_->NetError()) { ++ case net::Error::OK: ++ status = 200; ++ break; ++ case net::Error::ERR_TIMED_OUT: ++ VLOG(2) << "HTTP request timed out: " << inf_.url << " after " ++ << inf_.timeout_seconds << "s."; ++ status = 408; ++ break; ++ default: ++ VLOG(2) << "NetErr " << loader_->NetError() << " for " << inf_.url; ++ status = 500; ++ } ++ // auto sz = body ? body->size() : 0UL; ++ auto const* head = loader_->ResponseInfo(); ++ if (head) { ++ OnResponseHead({}, *head); ++ } ++ auto sp = status_line_.find(' '); ++ if (sp < status_line_.size()) { ++ VLOG(2) << "HTTP response status='" << status_line_ << "'."; ++ status = std::atoi(status_line_.c_str() + sp + 1); ++ } ++ if (body) { ++ callback_(status, *body, header_accessor_); ++ } else { ++ callback_(status, "", header_accessor_); ++ } ++} ++void Self::OnResponseHead( ++ GURL const&, ++ network::mojom::URLResponseHead const& response_head) { ++ if (!response_head.headers) { ++ return; ++ } ++ auto head = response_head.headers; ++ status_line_ = head->GetStatusLine(); ++ header_accessor_ = [head](std::string_view k) { ++ std::string val; ++ head->EnumerateHeader(nullptr, k, &val); ++ return val; ++ }; ++} +diff --git a/components/ipfs/block_http_request.h b/components/ipfs/block_http_request.h +new file mode 100644 +index 0000000000000..a34d88b7a54cf +--- /dev/null ++++ b/components/ipfs/block_http_request.h +@@ -0,0 +1,46 @@ ++#ifndef IPFS_BLOCK_HTTP_REQUEST_H_ ++#define IPFS_BLOCK_HTTP_REQUEST_H_ + - if (is_win) { - # Don't conflict with Windows' "url.dll". - output_name = "url_lib" -diff --git a/url/url_canon.h b/url/url_canon.h -index 913b3685c6fec..3c3c55e580564 100644 ---- a/url/url_canon.h -+++ b/url/url_canon.h -@@ -792,6 +792,23 @@ bool CanonicalizeMailtoURL(const char16_t* spec, - CanonOutput* output, - Parsed* new_parsed); - -+COMPONENT_EXPORT(URL) -+bool CanonicalizeIpfsURL(const char* spec, -+ int spec_len, -+ const Parsed& parsed, -+ SchemeType scheme_type, -+ CharsetConverter* query_converter, -+ CanonOutput* output, -+ Parsed* new_parsed); -+COMPONENT_EXPORT(URL) -+bool CanonicalizeIpfsURL(const char16_t* spec, -+ int spec_len, -+ const Parsed& parsed, -+ SchemeType scheme_type, -+ CharsetConverter* query_converter, -+ CanonOutput* output, -+ Parsed* new_parsed); ++#include + - // Part replacer -------------------------------------------------------------- - - // Internal structure used for storing separate strings for each component. -diff --git a/url/url_canon_ipfs.cc b/url/url_canon_ipfs.cc ++#include ++#include ++ ++namespace network { ++struct ResourceRequest; ++class SimpleURLLoader; ++} // namespace network ++namespace network::mojom { ++class URLLoaderFactory; ++class URLResponseHead; ++} // namespace network::mojom ++class GURL; ++ ++namespace ipfs { ++class BlockHttpRequest : public std::enable_shared_from_this { ++ // TODO ween oneself off of SimpleURLLoader ++ // std::array buffer_; ++ std::unique_ptr loader_; ++ ++ public: ++ using HttpCompleteCallback = ipfs::ContextApi::HttpCompleteCallback; ++ BlockHttpRequest(ipfs::HttpRequestDescription, HttpCompleteCallback); ++ ~BlockHttpRequest() noexcept; ++ ++ void send(raw_ptr); ++ ++ private: ++ ipfs::HttpRequestDescription const inf_; ++ HttpCompleteCallback callback_; ++ std::string status_line_; ++ ContextApi::HeaderAccess header_accessor_ = [](auto) { ++ return std::string{}; ++ }; ++ ++ void OnResponseHead(GURL const&, network::mojom::URLResponseHead const&); ++ void OnResponse(std::shared_ptr, ++ std::unique_ptr body); ++}; ++} // namespace ipfs ++ ++#endif // IPFS_BLOCK_HTTP_REQUEST_H_ +diff --git a/components/ipfs/cache_requestor.cc b/components/ipfs/cache_requestor.cc new file mode 100644 -index 0000000000000..9511e3f5e6f5c +index 0000000000000..ce446b608080a --- /dev/null -+++ b/url/url_canon_ipfs.cc -@@ -0,0 +1,55 @@ -+#include "url_canon_internal.h" ++++ b/components/ipfs/cache_requestor.cc +@@ -0,0 +1,219 @@ ++#include "cache_requestor.h" + -+#include -+#include ++#include "chromium_ipfs_context.h" ++#include "inter_request_state.h" + -+#include ++#include ++#include + -+namespace m = libp2p::multi; -+using Cid = m::ContentIdentifier; -+using CidCodec = m::ContentIdentifierCodec; ++using Self = ipfs::CacheRequestor; ++namespace dc = disk_cache; + -+bool url::CanonicalizeIpfsURL(const char* spec, -+ int spec_len, -+ const Parsed& parsed, -+ SchemeType scheme_type, -+ CharsetConverter* charset_converter, -+ CanonOutput* output, -+ Parsed* output_parsed) { -+ if ( spec_len < 1 || !spec ) { -+ return false; ++std::string_view Self::name() const { ++ return "Disk Cache"; ++} ++Self::CacheRequestor(InterRequestState& state, base::FilePath base) ++ : state_{state} { ++ if (!base.empty()) { ++ path_ = base.AppendASCII("IpfsBlockCache"); + } -+ if ( parsed.host.len < 1 ) { -+ return false; ++ Start(); ++} ++void Self::Start() { ++ if (pending_) { ++ return; ++ } ++ auto result = dc::CreateCacheBackend( ++ net::CacheType::DISK_CACHE, net::CACHE_BACKEND_DEFAULT, {}, path_, 0, ++ dc::ResetHandling::kNeverReset, ++ // dc::ResetHandling::kResetOnError, ++ nullptr, base::BindOnce(&Self::Assign, base::Unretained(this))); ++ LOG(INFO) << "Start(" << result.net_error << ')' << result.net_error; ++ pending_ = result.net_error == net::ERR_IO_PENDING; ++ if (!pending_) { ++ Assign(std::move(result)); ++ } ++} ++Self::~CacheRequestor() noexcept = default; ++ ++void Self::Assign(dc::BackendResult res) { ++ pending_ = false; ++ if (res.net_error == net::OK) { ++ LOG(INFO) << "Initialized disk cache"; ++ cache_.swap(res.backend); ++ } else { ++ LOG(ERROR) << "Trouble opening " << name() << ": " << res.net_error; ++ Start(); ++ } ++} ++auto Self::handle(RequestPtr req) -> HandleOutcome { ++ if (pending_) { ++ return HandleOutcome::NOT_HANDLED; ++ } ++ Task task; ++ task.key = req->main_param; ++ task.request = req; ++ StartFetch(task, net::MAXIMUM_PRIORITY); ++ return HandleOutcome::PENDING; ++} ++void Self::StartFetch(Task& task, net::RequestPriority priority) { ++ if (pending_) { ++ Start(); ++ Miss(task); ++ return; ++ } ++ auto bound = base::BindOnce(&Self::OnOpen, base::Unretained(this), task); ++ auto res = cache_->OpenEntry(task.key, priority, std::move(bound)); ++ if (res.net_error() != net::ERR_IO_PENDING) { ++ OnOpen(task, std::move(res)); ++ } ++} ++void Self::Miss(Task& task) { ++ if (task.request) { ++ VLOG(2) << "Cache miss on " << task.request->debug_string(); ++ auto req = task.request; ++ task.request->Hook([this, req](std::string_view bytes) { ++ Store(req->main_param, "TODO", std::string{bytes}); ++ }); ++ forward(req); ++ } ++} ++namespace { ++std::shared_ptr GetEntry(dc::EntryResult& result) { ++ auto* e = result.ReleaseEntry(); ++ auto deleter = [](auto e) { ++ if (e) { ++ e->Close(); ++ } ++ }; ++ return {e, deleter}; ++} ++} // namespace ++ ++void Self::OnOpen(Task task, dc::EntryResult res) { ++ VLOG(2) << "OnOpen(" << res.net_error() << ")"; ++ if (res.net_error() != net::OK) { ++ VLOG(2) << "Failed to find " << task.key << " in " << name(); ++ Miss(task); ++ return; ++ } ++ task.entry = GetEntry(res); ++ DCHECK(task.entry); ++ task.buf = base::MakeRefCounted(2 * 1024 * 1024); ++ DCHECK(task.buf); ++ auto bound = ++ base::BindOnce(&Self::OnHeaderRead, base::Unretained(this), task); ++ auto code = task.entry->ReadData(0, 0, task.buf.get(), task.buf->size(), ++ std::move(bound)); ++ if (code != net::ERR_IO_PENDING) { ++ OnHeaderRead(task, code); ++ } ++} ++void Self::OnHeaderRead(Task task, int code) { ++ if (code <= 0) { ++ LOG(ERROR) << "Failed to read headers for entry " << task.key << " in " ++ << name() << " " << code; ++ // Miss(task); ++ // return; ++ } ++ task.header.assign(task.buf->data(), static_cast(code)); ++ auto bound = base::BindOnce(&Self::OnBodyRead, base::Unretained(this), task); ++ code = task.entry->ReadData(1, 0, task.buf.get(), task.buf->size(), ++ std::move(bound)); ++ if (code != net::ERR_IO_PENDING) { ++ OnBodyRead(task, code); ++ } ++} ++void Self::OnBodyRead(Task task, int code) { ++ if (code <= 0) { ++ LOG(INFO) << "Failed to read body for entry " << task.key << " in " ++ << name(); ++ Miss(task); ++ return; ++ } ++ task.body.assign(task.buf->data(), static_cast(code)); ++ if (task.request) { ++ task.SetHeaders(name()); ++ if (task.request->RespondSuccessfully(task.body, api_)) { ++ VLOG(2) << "Cache hit on " << task.key << " for " ++ << task.request->debug_string(); ++ } else { ++ LOG(ERROR) << "Had a BAD cached response for " << task.key; ++ Expire(task.key); ++ Miss(task); ++ } ++ } ++} ++void Self::Store(std::string cid, std::string headers, std::string body) { ++ VLOG(1) << "Store(" << name() << ',' << cid << ',' << headers.size() << ',' ++ << body.size() << ')'; ++ auto bound = base::BindOnce(&Self::OnEntryCreated, base::Unretained(this), ++ cid, headers, body); ++ auto res = cache_->OpenOrCreateEntry(cid, net::LOW, std::move(bound)); ++ if (res.net_error() != net::ERR_IO_PENDING) { ++ OnEntryCreated(cid, headers, body, std::move(res)); ++ } ++} ++void Self::OnEntryCreated(std::string cid, ++ std::string headers, ++ std::string body, ++ disk_cache::EntryResult result) { ++ if (result.opened()) { ++ VLOG(1) << "No need to write an entry for " << cid << " in " << name() ++ << " as it is already there and immutable."; ++ } else if (result.net_error() == net::OK) { ++ auto entry = GetEntry(result); ++ auto buf = base::MakeRefCounted(headers); ++ DCHECK(buf); ++ auto bound = base::BindOnce(&Self::OnHeaderWritten, base::Unretained(this), ++ buf, body, entry); ++ auto code = ++ entry->WriteData(0, 0, buf.get(), buf->size(), std::move(bound), true); ++ if (code != net::ERR_IO_PENDING) { ++ OnHeaderWritten(buf, body, entry, code); ++ } ++ } else { ++ LOG(ERROR) << "Failed to create an entry for " << cid << " in " << name(); ++ } ++} ++void Self::OnHeaderWritten(scoped_refptr buf, ++ std::string body, ++ std::shared_ptr entry, ++ int code) { ++ if (code < 0) { ++ LOG(ERROR) << "Failed to write header info for " << entry->GetKey() ++ << " in " << name(); ++ return; ++ } ++ buf = base::MakeRefCounted(body); ++ DCHECK(buf); ++ auto f = [](scoped_refptr, int c) { ++ VLOG(1) << "body write " << c; ++ }; ++ auto bound = base::BindOnce(f, buf); ++ entry->WriteData(1, 0, buf.get(), buf->size(), std::move(bound), true); ++} ++ ++void Self::Task::SetHeaders(std::string_view source) { ++ auto heads = base::MakeRefCounted(header); ++ DCHECK(heads); ++ std::string value{"blockcache-"}; ++ value.append(key); ++ value.append(";desc=\"Load from local browser block cache\";dur="); ++ auto dur = base::TimeTicks::Now() - start; ++ value.append(std::to_string(dur.InMillisecondsRoundedUp())); ++ heads->SetHeader("Server-Timing", value); ++ VLOG(2) << "From cache: Server-Timing: " << value << "; Block-Cache-" << key ++ << ": " << source; ++ heads->SetHeader("Block-Cache-" + key, {source.data(), source.size()}); ++ header = heads->raw_headers(); ++} ++void Self::Expire(std::string const& key) { ++ if (cache_ && !pending_) { ++ cache_->DoomEntry(key, net::RequestPriority::LOWEST, base::DoNothing()); ++ } ++} ++ ++Self::Task::Task() = default; ++Self::Task::Task(Task const&) = default; ++Self::Task::~Task() noexcept = default; +diff --git a/components/ipfs/cache_requestor.h b/components/ipfs/cache_requestor.h +new file mode 100644 +index 0000000000000..b8c31d371ecb4 +--- /dev/null ++++ b/components/ipfs/cache_requestor.h +@@ -0,0 +1,71 @@ ++#ifndef CACHE_REQUESTOR_H_ ++#define CACHE_REQUESTOR_H_ ++ ++#include ++#include ++#include ++ ++#include ++#include ++#include ++ ++#include ++ ++#include ++ ++namespace ipfs { ++ ++class BlockStorage; ++class InterRequestState; ++ ++class CacheRequestor : public gw::Requestor { ++ public: ++ CacheRequestor(InterRequestState&, base::FilePath); ++ ~CacheRequestor() noexcept override; ++ void Store(std::string cid, std::string headers, std::string body); ++ void Expire(std::string const& key); ++ ++ std::string_view name() const override; ++ ++ private: ++ struct Task { ++ Task(); ++ Task(Task const&); ++ ~Task() noexcept; ++ std::string key; ++ base::TimeTicks start = base::TimeTicks::Now(); ++ std::string header; ++ std::string body; ++ scoped_refptr buf; ++ std::shared_ptr entry; ++ gw::RequestPtr request; ++ ++ void SetHeaders(std::string_view); ++ }; ++ raw_ref state_; ++ std::unique_ptr cache_; ++ bool pending_ = false; ++ base::FilePath path_; ++ ++ void Start(); ++ ++ void StartFetch(Task& t, net::RequestPriority priority); ++ void Assign(disk_cache::BackendResult); ++ void OnOpen(Task, disk_cache::EntryResult); ++ void OnHeaderRead(Task, int); ++ void OnBodyRead(Task, int); ++ ++ void OnEntryCreated(std::string c, ++ std::string h, ++ std::string b, ++ disk_cache::EntryResult); ++ void OnHeaderWritten(scoped_refptr buf, ++ std::string body, ++ std::shared_ptr entry, ++ int); ++ void Miss(Task&); ++ HandleOutcome handle(RequestPtr) override; ++}; ++} // namespace ipfs ++ ++#endif // CACHE_REQUESTOR_H_ +diff --git a/components/ipfs/chromium_cbor_adapter.cc b/components/ipfs/chromium_cbor_adapter.cc +new file mode 100644 +index 0000000000000..d7d43b81be96b +--- /dev/null ++++ b/components/ipfs/chromium_cbor_adapter.cc +@@ -0,0 +1,91 @@ ++#include "chromium_cbor_adapter.h" ++ ++#include ++ ++using Self = ipfs::ChromiumCborAdapter; ++ ++bool Self::is_map() const { ++ return cbor_.is_map(); ++} ++bool Self::is_array() const { ++ return cbor_.is_array(); ++} ++auto Self::at(std::string_view key) const -> std::unique_ptr { ++ if (is_map()) { ++ auto& m = cbor_.GetMap(); ++ auto it = m.find(cbor::Value{base::StringPiece{key}}); ++ if (m.end() != it) { ++ return std::make_unique(it->second.Clone()); ++ } ++ } ++ return {}; ++} ++std::optional Self::as_unsigned() const { ++ if (cbor_.is_unsigned()) { ++ return cbor_.GetUnsigned(); ++ } ++ return std::nullopt; ++} ++std::optional Self::as_signed() const { ++ if (cbor_.is_integer()) { ++ return cbor_.GetInteger(); ++ } ++ return {}; ++} ++std::optional Self::as_float() const { ++ return {}; ++} ++ ++std::optional Self::as_string() const { ++ if (cbor_.is_string()) { ++ return cbor_.GetString(); ++ } ++ return std::nullopt; ++} ++auto Self::as_bytes() const -> std::optional> { ++ if (cbor_.is_bytestring()) { ++ return cbor_.GetBytestring(); ++ } ++ return std::nullopt; ++} ++auto Self::as_link() const -> std::optional { ++ VLOG(1) << "Trying to do an as_link(" << static_cast(cbor_.type()) << ',' << std::boolalpha << cbor_.has_tag() << ")"; ++ if (!cbor_.has_tag() || cbor_.GetTag() != 42UL || !cbor_.is_bytestring()) { ++ VLOG(1) << "This is not a link."; ++ return std::nullopt; ++ } ++ auto& bytes = cbor_.GetBytestring(); ++ auto* byte_ptr = reinterpret_cast(bytes.data()) + 1; ++ auto result = Cid(ByteView{byte_ptr, bytes.size() - 1UL}); ++ if (result.valid()) { ++ return result; ++ } else { ++ LOG(ERROR) << "Unable to decode bytes from DAG-CBOR Link as CID."; ++ return std::nullopt; ++ } ++} ++std::optional Self::as_bool() const { ++ if (cbor_.is_bool()) { ++ return cbor_.GetBool(); ++ } ++ return std::nullopt; ++} ++void Self::iterate_map(MapElementCallback cb) const { ++ auto& m = cbor_.GetMap(); ++ for (auto& [k,v] : m) { ++ cb(k.GetString(), Self{v}); ++ } ++} ++void Self::iterate_array(ArrayElementCallback cb) const { ++ auto& a = cbor_.GetArray(); ++ for (auto& e : a) { ++ cb(Self{e}); ++ } ++} ++ ++Self::ChromiumCborAdapter(cbor::Value const& v) : cbor_{v.Clone()} {} ++Self::ChromiumCborAdapter(cbor::Value&& v) : cbor_{std::move(v)} {} ++Self::ChromiumCborAdapter(ChromiumCborAdapter const& rhs) ++ : cbor_{rhs.cbor_.Clone()} {} ++ ++Self::~ChromiumCborAdapter() noexcept {} +diff --git a/components/ipfs/chromium_cbor_adapter.h b/components/ipfs/chromium_cbor_adapter.h +new file mode 100644 +index 0000000000000..65c2d746e6630 +--- /dev/null ++++ b/components/ipfs/chromium_cbor_adapter.h +@@ -0,0 +1,33 @@ ++#ifndef IPFS_CHROMIUM_CBOR_ADAPTER_H_ ++#define IPFS_CHROMIUM_CBOR_ADAPTER_H_ ++ ++#include ++ ++#include ++ ++namespace ipfs { ++class ChromiumCborAdapter final : public DagCborValue { ++ cbor::Value cbor_; ++ ++ std::unique_ptr at(std::string_view) const override; ++ std::optional as_unsigned() const override; ++ std::optional as_signed() const override; ++ std::optional as_float() const override; ++ std::optional as_string() const override; ++ std::optional> as_bytes() const override; ++ std::optional as_link() const override; ++ std::optional as_bool() const override; ++ bool is_map() const override; ++ bool is_array() const override; ++ void iterate_map(MapElementCallback) const override; ++ void iterate_array(ArrayElementCallback) const override; ++ ++ public: ++ ChromiumCborAdapter(cbor::Value&&); ++ ChromiumCborAdapter(cbor::Value const&); ++ ChromiumCborAdapter(ChromiumCborAdapter const& rhs); ++ ~ChromiumCborAdapter() noexcept override; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_CHROMIUM_CBOR_ADAPTER_H_ +diff --git a/components/ipfs/chromium_ipfs_context.cc b/components/ipfs/chromium_ipfs_context.cc +new file mode 100644 +index 0000000000000..92a56994633bf +--- /dev/null ++++ b/components/ipfs/chromium_ipfs_context.cc +@@ -0,0 +1,133 @@ ++#include "chromium_ipfs_context.h" ++ ++#include "block_http_request.h" ++#include "chromium_cbor_adapter.h" ++#include "chromium_json_adapter.h" ++#include "crypto_api.h" ++#include "inter_request_state.h" ++#include "preferences.h" ++ ++#include ++#include ++#include ++ ++#include ++#include ++ ++#include ++ ++#include ++#include ++ ++#include ++ ++#include ++#include ++ ++ ++using Self = ipfs::ChromiumIpfsContext; ++ ++void Self::SetLoaderFactory(network::mojom::URLLoaderFactory& lf) { ++ loader_factory_ = &lf; ++} ++ ++std::string Self::MimeType(std::string extension, ++ std::string_view content, ++ std::string const& url) const { ++ std::string result; ++ auto fp_ext = base::FilePath::FromUTF8Unsafe(extension).value(); ++ VLOG(2) << "extension=" << extension << "content.size()=" << content.size() ++ << "(as-if) url for mime type:" << url; ++ if (extension.empty()) { ++ result.clear(); ++ } else if (net::GetWellKnownMimeTypeFromExtension(fp_ext, &result)) { ++ VLOG(2) << "Got " << result << " from extension " << extension << " for " ++ << url; ++ } else { ++ result.clear(); ++ } ++ auto head_size = std::min(content.size(), 999'999UL); ++ if (net::SniffMimeType({content.data(), head_size}, GURL{url}, result, ++ net::ForceSniffFileUrlsForHtml::kDisabled, &result)) { ++ VLOG(2) << "Got " << result << " from content of " << url; ++ } ++ if (result.empty() || result == "application/octet-stream") { ++ net::SniffMimeTypeFromLocalData({content.data(), head_size}, &result); ++ VLOG(2) << "Falling all the way back to content type " << result; ++ } ++ return result; ++} ++std::string Self::UnescapeUrlComponent(std::string_view comp) const { ++ using Rule = base::UnescapeRule; ++ auto rules = Rule::PATH_SEPARATORS | ++ Rule::URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS | Rule::SPACES; ++ auto result = base::UnescapeURLComponent({comp.data(), comp.size()}, rules); ++ return result; ++} ++void Self::SendDnsTextRequest(std::string host, ++ DnsTextResultsCallback res, ++ DnsTextCompleteCallback don) { ++ if (dns_reqs_.find(host) != dns_reqs_.end()) { ++ LOG(ERROR) << "Requested resolution of DNSLink host " << host ++ << " multiple times."; ++ } ++ auto don_wrap = [don, this, host]() { ++ don(); ++ LOG(INFO) << "Finished resolving " << host << " via DNSLink"; ++ dns_reqs_.erase(host); ++ }; ++ auto* nc = state_->network_context(); ++ dns_reqs_[host] = std::make_unique(host, res, don_wrap, nc); ++} ++void Self::SendHttpRequest(HttpRequestDescription req_inf, ++ HttpCompleteCallback cb) const { ++ DCHECK(loader_factory_); ++ auto ptr = std::make_shared(req_inf, cb); ++ ptr->send(loader_factory_); ++} ++bool Self::VerifyKeySignature(SigningKeyType t, ++ ByteView signature, ++ ByteView data, ++ ByteView key_bytes) const { ++ return crypto_api::VerifySignature(static_cast(t), signature, ++ data, key_bytes); ++} ++auto Self::ParseCbor(ipfs::ContextApi::ByteView bytes) const ++ -> std::unique_ptr { ++ cbor::Reader::Config cfg; ++ cfg.parse_tags = true; ++ auto parsed = cbor::Reader::Read(as_octets(bytes), cfg); ++ if (parsed.has_value()) { ++ return std::make_unique(std::move(parsed.value())); ++ } ++ LOG(ERROR) << "Failed to parse CBOR."; ++ return {}; ++} ++auto Self::ParseJson(std::string_view j_str) const ++ -> std::unique_ptr { ++ auto d = base::JSONReader::Read(j_str, base::JSON_ALLOW_TRAILING_COMMAS); ++ if (d) { ++ return std::make_unique(std::move(d.value())); ++ } ++ return {}; ++} ++unsigned int Self::GetGatewayRate(std::string_view prefix) { ++ return rates_.GetRate(prefix); ++} ++void Self::SetGatewayRate(std::string_view prefix, unsigned int new_rate) { ++ rates_.SetRate(prefix, new_rate); ++} ++auto Self::GetGateway(std::size_t index) const -> std::optional { ++ auto [gw, r] = rates_.at(index); ++ if (gw) { ++ return GatewaySpec{*gw, r}; ++ } ++ return std::nullopt; ++} ++ ++Self::ChromiumIpfsContext(InterRequestState& state, PrefService* prefs) ++ : state_{state}, rates_{prefs} {} ++Self::~ChromiumIpfsContext() noexcept { ++ LOG(WARNING) << "API dtor - are all URIs loaded?"; ++} ++ +diff --git a/components/ipfs/chromium_ipfs_context.h b/components/ipfs/chromium_ipfs_context.h +new file mode 100644 +index 0000000000000..b274746efac42 +--- /dev/null ++++ b/components/ipfs/chromium_ipfs_context.h +@@ -0,0 +1,66 @@ ++#ifndef IPFS_CHROMIUM_IPFS_CONTEXT_H_ ++#define IPFS_CHROMIUM_IPFS_CONTEXT_H_ ++ ++#include "dns_txt_request.h" ++#include "preferences.h" ++ ++#include ++#include ++ ++#include ++#include ++ ++#include ++ ++#include ++ ++class PrefService; ++ ++namespace network { ++class SimpleURLLoader; ++namespace mojom { ++class URLLoaderFactory; ++} ++} // namespace network ++ ++namespace ipfs { ++class InterRequestState; ++class IpfsRequest; ++class NetworkRequestor; ++ ++class ChromiumIpfsContext final : public ContextApi { ++ raw_ptr loader_factory_ = nullptr; ++ raw_ref state_; ++ std::map> dns_reqs_; ++ GatewayRates rates_; ++ ++ std::string MimeType(std::string extension, ++ std::string_view content, ++ std::string const& url) const override; ++ std::string UnescapeUrlComponent(std::string_view) const override; ++ void SendDnsTextRequest(std::string, ++ DnsTextResultsCallback, ++ DnsTextCompleteCallback) override; ++ void SendHttpRequest(HttpRequestDescription req_inf, ++ HttpCompleteCallback cb) const override; ++ bool VerifyKeySignature(SigningKeyType, ++ ByteView signature, ++ ByteView data, ++ ByteView key_bytes) const override; ++ ++ std::unique_ptr ParseCbor(ByteView) const override; ++ std::unique_ptr ParseJson(std::string_view) const override; ++ ++ std::optional GetGateway(std::size_t index) const override; ++ unsigned int GetGatewayRate(std::string_view) override; ++ void SetGatewayRate(std::string_view, unsigned int) override; ++ ++ public: ++ ChromiumIpfsContext(InterRequestState&, PrefService* prefs); ++ ~ChromiumIpfsContext() noexcept override; ++ void SetLoaderFactory(network::mojom::URLLoaderFactory&); ++}; ++ ++} // namespace ipfs ++ ++#endif // IPFS_CHROMIUM_IPFS_CONTEXT_H_ +diff --git a/components/ipfs/chromium_json_adapter.cc b/components/ipfs/chromium_json_adapter.cc +new file mode 100644 +index 0000000000000..92c1c19aa35ce +--- /dev/null ++++ b/components/ipfs/chromium_json_adapter.cc +@@ -0,0 +1,48 @@ ++#include "chromium_json_adapter.h" ++ ++using Self = ipfs::ChromiumJsonAdapter; ++ ++Self::ChromiumJsonAdapter(base::Value d) : data_(std::move(d)) {} ++Self::~ChromiumJsonAdapter() noexcept {} ++std::string Self::pretty_print() const { ++ return data_.DebugString(); ++} ++std::optional Self::get_if_string() const { ++ auto* s = data_.GetIfString(); ++ if (s) { ++ return *s; ++ } else { ++ return std::nullopt; ++ } ++} ++auto Self::operator[](std::string_view k) const ++ -> std::unique_ptr { ++ if (auto* m = data_.GetIfDict()) { ++ if (auto* v = m->Find(k)) { ++ return std::make_unique(v->Clone()); ++ } ++ } ++ return {}; ++} ++bool Self::iterate_list(std::function cb) const { ++ auto* l = data_.GetIfList(); ++ if (!l) { ++ return false; ++ } ++ for (auto& v : *l) { ++ Self wrap(v.Clone()); ++ cb(wrap); ++ } ++ return true; ++} ++std::optional> Self::object_keys() const { ++ auto* m = data_.GetIfDict(); ++ if (!m) { ++ return std::nullopt; ++ } ++ std::vector rv; ++ for (auto [k, v] : *m) { ++ rv.push_back(k); ++ } ++ return rv; ++} +\ No newline at end of file +diff --git a/components/ipfs/chromium_json_adapter.h b/components/ipfs/chromium_json_adapter.h +new file mode 100644 +index 0000000000000..8e5e26aa3150e +--- /dev/null ++++ b/components/ipfs/chromium_json_adapter.h +@@ -0,0 +1,22 @@ ++#ifndef IPFS_CHROMIUM_JSON_ADAPTER_H_ ++#define IPFS_CHROMIUM_JSON_ADAPTER_H_ ++ ++#include ++#include ++ ++namespace ipfs { ++class ChromiumJsonAdapter final : public ipfs::DagJsonValue { ++ base::Value data_; ++ std::string pretty_print() const override; ++ std::unique_ptr operator[](std::string_view) const override; ++ std::optional get_if_string() const override; ++ std::optional> object_keys() const override; ++ bool iterate_list(std::function) const override; ++ ++ public: ++ ChromiumJsonAdapter(base::Value); ++ ~ChromiumJsonAdapter() noexcept override; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_CHROMIUM_JSON_ADAPTER_H_ +diff --git a/components/ipfs/crypto_api.cc b/components/ipfs/crypto_api.cc +new file mode 100644 +index 0000000000000..d15a63f1f577c +--- /dev/null ++++ b/components/ipfs/crypto_api.cc +@@ -0,0 +1,62 @@ ++#include "crypto_api.h" ++ ++#include "base/logging.h" ++#include "components/webcrypto/algorithm_implementations.h" ++#include "components/webcrypto/status.h" ++#include "third_party/blink/public/platform/web_crypto_key.h" ++#include "third_party/boringssl/src/include/openssl/evp.h" ++ ++namespace { ++int ToEvpKeyType(ipfs::ipns::KeyType t) { ++ using ipfs::ipns::KeyType; ++ switch (t) { ++ case KeyType::ECDSA: ++ LOG(ERROR) << "TODO Check on ECDSA key type translation."; ++ return EVP_PKEY_EC; ++ case KeyType::Ed25519: ++ return EVP_PKEY_ED25519; ++ case KeyType::RSA: ++ return EVP_PKEY_RSA; ++ case KeyType::Secp256k1: ++ LOG(ERROR) << "TODO Check on Secp256k1 key type translation."; ++ return EVP_PKEY_DSA; ++ default: ++ LOG(ERROR) << "Invalid key type: " << static_cast(t); ++ return EVP_PKEY_NONE; ++ } ++} ++} // namespace ++ ++namespace cpto = ipfs::crypto_api; ++ ++bool cpto::VerifySignature(ipfs::ipns::KeyType key_type, ++ ipfs::ByteView signature, ++ ipfs::ByteView data, ++ ipfs::ByteView key_bytes) { ++ auto* key_p = reinterpret_cast(key_bytes.data()); ++ auto* data_p = reinterpret_cast(data.data()); ++ auto* sig_p = reinterpret_cast(signature.data()); ++ auto kt = ToEvpKeyType(key_type); ++ std::clog << "data:"; ++ for (auto b : data) { ++ std::clog << ' ' << std::hex << static_cast(b); ++ } ++ std::clog << ' ' << data.size() << " bytes.\n"; ++ bssl::UniquePtr pkey(EVP_PKEY_new_raw_public_key( ++ kt, /*engine*/ nullptr, key_p, key_bytes.size())); ++ bssl::ScopedEVP_MD_CTX ctx; ++ if (!EVP_DigestVerifyInit(ctx.get(), /*pctx=*/nullptr, /*type=*/nullptr, ++ /*e=*/nullptr, pkey.get())) { ++ LOG(ERROR) << "EVP_DigestVerifyInit failed"; ++ return false; ++ } ++ // auto* prefix = reinterpret_cast( ++ // "\x69\x70\x6e\x73\x2d\x73\x69\x67\x6e\x61\x74\x75\x72\x65\x3a"); ++ // std::basic_string to_verify = prefix; ++ // to_verify.append(data_p, data.size()); ++ auto result = ++ EVP_DigestVerify(ctx.get(), sig_p, signature.size(), data_p, data.size()); ++ // to_verify.data(), to_verify.size()); ++ LOG(INFO) << "EVP_DigestVerify returned " << result; ++ return result == 1; ++} +diff --git a/components/ipfs/crypto_api.h b/components/ipfs/crypto_api.h +new file mode 100644 +index 0000000000000..1363bb1fec6df +--- /dev/null ++++ b/components/ipfs/crypto_api.h +@@ -0,0 +1,22 @@ ++#ifndef IPFS_VALIDATE_SIGNATURE_H_ ++#define IPFS_VALIDATE_SIGNATURE_H_ ++ ++#include "components/webcrypto/algorithm_implementation.h" ++ ++#include "third_party/ipfs_client/keys.pb.h" ++ ++#include ++ ++namespace ipfs::crypto_api { ++/* ++using Algo = std::pair>; ++Algo GetAlgo(ipfs::ipns::KeyType); ++*/ ++bool VerifySignature(ipfs::ipns::KeyType, ++ ByteView signature, ++ ByteView data, ++ ByteView key); ++} // namespace ipfs::crypto_api ++ ++#endif // IPFS_VALIDATE_SIGNATURE_H_ +diff --git a/components/ipfs/dns_txt_request.cc b/components/ipfs/dns_txt_request.cc +new file mode 100644 +index 0000000000000..c7e8e667d5f05 +--- /dev/null ++++ b/components/ipfs/dns_txt_request.cc +@@ -0,0 +1,39 @@ ++#include "dns_txt_request.h" ++ ++#include ++#include ++ ++namespace moj = network::mojom; ++using Self = ipfs::DnsTxtRequest; ++ ++Self::DnsTxtRequest(std::string host, ++ ipfs::ContextApi::DnsTextResultsCallback res, ++ ipfs::ContextApi::DnsTextCompleteCallback don, ++ moj::NetworkContext* network_context) ++ : results_callback_{res}, completion_callback_{don} { ++ auto params = moj::ResolveHostParameters::New(); ++ params->dns_query_type = net::DnsQueryType::TXT; ++ params->initial_priority = net::RequestPriority::HIGHEST; ++ params->source = net::HostResolverSource::ANY; ++ params->cache_usage = moj::ResolveHostParameters_CacheUsage::STALE_ALLOWED; ++ params->secure_dns_policy = moj::SecureDnsPolicy::ALLOW; ++ params->purpose = moj::ResolveHostParameters::Purpose::kUnspecified; ++ LOG(INFO) << "Querying DNS for TXT records on " << host; ++ auto hrh = moj::HostResolverHost::NewHostPortPair({host, 0}); ++ auto nak = net::NetworkAnonymizationKey::CreateTransient(); ++ network_context->ResolveHost(std::move(hrh), nak, std::move(params), ++ recv_.BindNewPipeAndPassRemote()); ++} ++Self::~DnsTxtRequest() {} ++ ++void Self::OnTextResults(std::vector const& results) { ++ LOG(INFO) << "Hit " << results.size() << " DNS TXT results."; ++ results_callback_(results); ++} ++void Self::OnComplete(int32_t result, ++ const ::net::ResolveErrorInfo&, ++ const absl::optional<::net::AddressList>&, ++ const absl::optional&) { ++ LOG(INFO) << "DNS Results done with code: " << result; ++ completion_callback_(); ++} +diff --git a/components/ipfs/dns_txt_request.h b/components/ipfs/dns_txt_request.h +new file mode 100644 +index 0000000000000..a6c72e467d11f +--- /dev/null ++++ b/components/ipfs/dns_txt_request.h +@@ -0,0 +1,36 @@ ++#ifndef IPFS_DNS_TXT_REQUEST_H_ ++#define IPFS_DNS_TXT_REQUEST_H_ ++ ++#include ++ ++#include ++#include ++ ++namespace network::mojom { ++class NetworkContext; ++} ++ ++namespace ipfs { ++class DnsTxtRequest final : public network::ResolveHostClientBase { ++ ipfs::ContextApi::DnsTextResultsCallback results_callback_; ++ ipfs::ContextApi::DnsTextCompleteCallback completion_callback_; ++ mojo::Receiver recv_{this}; ++ ++ using Endpoints = std::vector<::net::HostResolverEndpointResult>; ++ void OnTextResults(std::vector const&) override; ++ void OnComplete(int32_t result, ++ ::net::ResolveErrorInfo const&, ++ absl::optional<::net::AddressList> const&, ++ absl::optional const&) override; ++ ++ public: ++ DnsTxtRequest(std::string, ++ ipfs::ContextApi::DnsTextResultsCallback, ++ ipfs::ContextApi::DnsTextCompleteCallback, ++ network::mojom::NetworkContext*); ++ DnsTxtRequest(DnsTxtRequest&&) = delete; ++ ~DnsTxtRequest() noexcept override; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_DNS_TXT_REQUEST_H_ +diff --git a/components/ipfs/export.h b/components/ipfs/export.h +new file mode 100644 +index 0000000000000..8161da33aca16 +--- /dev/null ++++ b/components/ipfs/export.h +@@ -0,0 +1,16 @@ ++#ifndef IPFS_EXPORT_H_ ++#define IPFS_EXPORT_H_ ++ ++#if __has_include() ++#include ++#else ++ ++#ifndef IS_IPFS_IMPL ++#if !defined(COMPONENT_EXPORT) ++#define COMPONENT_EXPORT(IPFS) ++#endif ++#endif ++ ++#endif ++ ++#endif // IPFS_EXPORT_H_ +diff --git a/components/ipfs/inter_request_state.cc b/components/ipfs/inter_request_state.cc +new file mode 100644 +index 0000000000000..4f704d5ebce46 +--- /dev/null ++++ b/components/ipfs/inter_request_state.cc +@@ -0,0 +1,74 @@ ++#include "inter_request_state.h" ++ ++#include "chromium_ipfs_context.h" ++#include "preferences.h" ++ ++#include ++#include "content/public/browser/browser_context.h" ++ ++#include ++#include ++#include ++#include ++ ++using Self = ipfs::InterRequestState; ++ ++namespace { ++constexpr char user_data_key[] = "ipfs_request_userdata"; ++} ++ ++void Self::CreateForBrowserContext(content::BrowserContext* c, PrefService* p) { ++ DCHECK(c); ++ DCHECK(p); ++ LOG(INFO) << "Creating new IPFS state for this browser context."; ++ auto owned = std::make_unique(c->GetPath(), p); ++ c->SetUserData(user_data_key, std::move(owned)); ++} ++auto Self::FromBrowserContext(content::BrowserContext* context) ++ -> InterRequestState& { ++ if (!context) { ++ LOG(WARNING) << "No browser context! Using a default IPFS state."; ++ static ipfs::InterRequestState static_state({}, {}); ++ return static_state; ++ } ++ base::SupportsUserData::Data* existing = context->GetUserData(user_data_key); ++ if (existing) { ++ VLOG(2) << "Re-using existing IPFS state."; ++ return *static_cast(existing); ++ } else { ++ LOG(ERROR) << "Browser context has no IPFS state! It must be set earlier!"; ++ static ipfs::InterRequestState static_state({}, {}); ++ return static_state; ++ } ++} ++std::shared_ptr Self::api() { ++ return api_; ++} ++auto Self::cache() -> std::shared_ptr& { ++ if (!cache_) { ++ cache_ = std::make_shared(*this, disk_path_); ++ } ++ return cache_; ++} ++auto Self::orchestrator() -> Orchestrator& { ++ if (!orc_) { ++ auto rtor = ++ gw::default_requestor(Gateways::DefaultGateways(), cache(), api()); ++ orc_ = std::make_shared(rtor, api()); ++ } ++ return *orc_; ++} ++void Self::network_context(network::mojom::NetworkContext* val) { ++ network_context_ = val; ++} ++network::mojom::NetworkContext* Self::network_context() const { ++ return network_context_; ++} ++Self::InterRequestState(base::FilePath p, PrefService* prefs) ++ : api_{std::make_shared(*this, prefs)}, disk_path_{p} { ++ DCHECK(prefs); ++} ++Self::~InterRequestState() noexcept { ++ network_context_ = nullptr; ++ cache_.reset(); ++} +diff --git a/components/ipfs/inter_request_state.h b/components/ipfs/inter_request_state.h +new file mode 100644 +index 0000000000000..3471cfe5d15c9 +--- /dev/null ++++ b/components/ipfs/inter_request_state.h +@@ -0,0 +1,51 @@ ++#ifndef IPFS_INTER_REQUEST_STATE_H_ ++#define IPFS_INTER_REQUEST_STATE_H_ ++ ++#include "cache_requestor.h" ++ ++#include "ipfs_client/gateways.h" ++#include "ipfs_client/ipns_names.h" ++#include "ipfs_client/orchestrator.h" ++ ++#include "base/supports_user_data.h" ++#include "services/network/network_context.h" ++ ++class PrefService; ++ ++namespace content { ++class BrowserContext; ++} ++ ++namespace ipfs { ++class Scheduler; ++class ChromiumIpfsContext; ++class COMPONENT_EXPORT(IPFS) InterRequestState ++ : public base::SupportsUserData::Data { ++ IpnsNames names_; ++ std::shared_ptr api_; ++ std::time_t last_discovery_ = 0; ++ std::shared_ptr cache_; ++ base::FilePath const disk_path_; ++ std::shared_ptr orc_; // TODO - map of origin to Orchestrator ++ raw_ptr network_context_; ++ ++ std::shared_ptr& cache(); ++ ++ public: ++ InterRequestState(base::FilePath, PrefService*); ++ ~InterRequestState() noexcept override; ++ ++ IpnsNames& names() { return names_; } ++ Scheduler& scheduler(); ++ std::shared_ptr api(); ++ std::array,2> serialized_caches(); ++ Orchestrator& orchestrator(); ++ void network_context(network::mojom::NetworkContext*); ++ network::mojom::NetworkContext* network_context() const; ++ ++ static void CreateForBrowserContext(content::BrowserContext*, PrefService*); ++ static InterRequestState& FromBrowserContext(content::BrowserContext*); ++}; ++} // namespace ipfs ++ ++#endif // IPFS_INTER_REQUEST_STATE_H_ +diff --git a/components/ipfs/interceptor.cc b/components/ipfs/interceptor.cc +new file mode 100644 +index 0000000000000..39b5de32b87ef +--- /dev/null ++++ b/components/ipfs/interceptor.cc +@@ -0,0 +1,36 @@ ++#include "interceptor.h" ++ ++#include "inter_request_state.h" ++#include "ipfs_url_loader.h" ++ ++#include "base/logging.h" ++#include "services/network/public/cpp/resource_request.h" ++#include "services/network/public/mojom/url_response_head.mojom.h" ++#include "services/network/url_loader_factory.h" ++#include "url/url_util.h" ++ ++using Interceptor = ipfs::Interceptor; ++ ++Interceptor::Interceptor(network::mojom::URLLoaderFactory* handles_http, ++ network::mojom::NetworkContext* network_context) ++ : loader_factory_{handles_http}, network_context_{network_context} {} ++ ++void Interceptor::MaybeCreateLoader(network::ResourceRequest const& req, ++ content::BrowserContext* context, ++ LoaderCallback loader_callback) { ++ auto& state = InterRequestState::FromBrowserContext(context); ++ state.network_context(network_context_); ++ if (req.url.SchemeIs("ipfs") || req.url.SchemeIs("ipns")) { ++ auto hdr_str = req.headers.ToString(); ++ std::replace(hdr_str.begin(), hdr_str.end(), '\r', ' '); ++ VLOG(1) << req.url.spec() << " getting intercepted! Headers: \n" << hdr_str; ++ DCHECK(context); ++ auto loader = ++ std::make_shared(*loader_factory_, state); ++ std::move(loader_callback) ++ .Run(base::BindOnce(&ipfs::IpfsUrlLoader::StartRequest, loader)); ++ ++ } else { ++ std::move(loader_callback).Run({}); // SEP ++ } ++} +diff --git a/components/ipfs/interceptor.h b/components/ipfs/interceptor.h +new file mode 100644 +index 0000000000000..0321ea5481864 +--- /dev/null ++++ b/components/ipfs/interceptor.h +@@ -0,0 +1,30 @@ ++#ifndef IPFS_INTERCEPTOR_H_ ++#define IPFS_INTERCEPTOR_H_ ++ ++#include "content/public/browser/url_loader_request_interceptor.h" ++ ++class PrefService; ++namespace network::mojom { ++class URLLoaderFactory; ++class NetworkContext; ++} // namespace network::mojom ++ ++namespace ipfs { ++ ++class COMPONENT_EXPORT(IPFS) Interceptor final ++ : public content::URLLoaderRequestInterceptor { ++ raw_ptr loader_factory_; ++ raw_ptr network_context_; ++ raw_ptr pref_svc_; ++ ++ void MaybeCreateLoader(network::ResourceRequest const&, ++ content::BrowserContext*, ++ LoaderCallback) override; ++ ++ public: ++ Interceptor(network::mojom::URLLoaderFactory* handles_http, ++ network::mojom::NetworkContext*); ++}; ++} // namespace ipfs ++ ++#endif // IPFS_INTERCEPTOR_H_ +diff --git a/components/ipfs/ipfs_features.cc b/components/ipfs/ipfs_features.cc +new file mode 100644 +index 0000000000000..a0a729d5aa8e6 +--- /dev/null ++++ b/components/ipfs/ipfs_features.cc +@@ -0,0 +1,7 @@ ++#include "ipfs_features.h" ++ ++namespace ipfs { ++ ++BASE_FEATURE(kEnableIpfs, "EnableIpfs", base::FEATURE_DISABLED_BY_DEFAULT); ++ ++} +diff --git a/components/ipfs/ipfs_features.h b/components/ipfs/ipfs_features.h +new file mode 100644 +index 0000000000000..2e54462b135a9 +--- /dev/null ++++ b/components/ipfs/ipfs_features.h +@@ -0,0 +1,13 @@ ++#ifndef IPFS_IPFS_FEATURES_H_ ++#define IPFS_IPFS_FEATURES_H_ ++ ++#include "base/component_export.h" ++#include "base/feature_list.h" ++ ++namespace ipfs { ++ ++COMPONENT_EXPORT(IPFS) BASE_DECLARE_FEATURE(kEnableIpfs); ++ ++} // namespace ipfs ++ ++#endif // IPFS_IPFS_FEATURES_H_ +diff --git a/components/ipfs/ipfs_url_loader.cc b/components/ipfs/ipfs_url_loader.cc +new file mode 100644 +index 0000000000000..afc97dc425b4f +--- /dev/null ++++ b/components/ipfs/ipfs_url_loader.cc +@@ -0,0 +1,194 @@ ++#include "ipfs_url_loader.h" ++ ++#include "chromium_ipfs_context.h" ++#include "inter_request_state.h" ++ ++#include "ipfs_client/gateways.h" ++#include "ipfs_client/ipfs_request.h" ++ ++#include "base/debug/stack_trace.h" ++#include "base/notreached.h" ++#include "base/strings/stringprintf.h" ++#include "base/threading/platform_thread.h" ++#include "net/http/http_status_code.h" ++#include "services/network/public/cpp/parsed_headers.h" ++#include "services/network/public/cpp/simple_url_loader.h" ++#include "services/network/public/mojom/url_loader_factory.mojom.h" ++#include "services/network/public/mojom/url_response_head.mojom.h" ++#include "services/network/url_loader_factory.h" ++ ++#include ++ ++ipfs::IpfsUrlLoader::IpfsUrlLoader( ++ network::mojom::URLLoaderFactory& handles_http, ++ InterRequestState& state) ++ : state_{state}, lower_loader_factory_{handles_http}, api_{state_->api()} {} ++ipfs::IpfsUrlLoader::~IpfsUrlLoader() noexcept { ++ if (!complete_) { ++ LOG(ERROR) << "Premature IPFS URLLoader dtor, uri was '" << original_url_ ++ << "' " << base::debug::StackTrace(); ++ } ++} ++ ++void ipfs::IpfsUrlLoader::FollowRedirect( ++ std::vector const& // removed_headers ++ , ++ net::HttpRequestHeaders const& // modified_headers ++ , ++ net::HttpRequestHeaders const& // modified_cors_exempt_headers ++ , ++ absl::optional<::GURL> const& // new_url ++) { ++ NOTIMPLEMENTED(); ++} ++ ++void ipfs::IpfsUrlLoader::SetPriority(net::RequestPriority priority, ++ int32_t intra_prio_val) { ++ VLOG(1) << "TODO SetPriority(" << priority << ',' << intra_prio_val << ')'; ++} ++ ++void ipfs::IpfsUrlLoader::PauseReadingBodyFromNet() { ++ NOTIMPLEMENTED(); ++} ++ ++void ipfs::IpfsUrlLoader::ResumeReadingBodyFromNet() { ++ NOTIMPLEMENTED(); ++} ++ ++void ipfs::IpfsUrlLoader::StartRequest( ++ std::shared_ptr me, ++ network::ResourceRequest const& resource_request, ++ mojo::PendingReceiver receiver, ++ mojo::PendingRemote client) { ++ DCHECK(!me->receiver_.is_bound()); ++ DCHECK(!me->client_.is_bound()); ++ me->receiver_.Bind(std::move(receiver)); ++ me->client_.Bind(std::move(client)); ++ if (me->original_url_.empty()) { ++ me->original_url_ = resource_request.url.spec(); ++ } ++ if (resource_request.url.SchemeIs("ipfs") || ++ resource_request.url.SchemeIs("ipns")) { ++ auto ns = resource_request.url.scheme(); ++ auto cid_str = resource_request.url.host(); ++ auto path = resource_request.url.path(); ++ auto abs_path = "/" + ns + "/" + cid_str + path; ++ VLOG(1) << resource_request.url.spec() << " -> " << abs_path; ++ me->root_ = cid_str; ++ me->api_->SetLoaderFactory(*(me->lower_loader_factory_)); ++ auto whendone = [me](IpfsRequest const& req, ipfs::Response const& res) { ++ VLOG(1) << "whendone(" << req.path().to_string() << ',' << res.status_ ++ << ',' << res.body_.size() << "B mime=" << res.mime_ << ')'; ++ if (!res.body_.empty()) { ++ me->ReceiveBlockBytes(res.body_); ++ } ++ me->status_ = res.status_; ++ me->resp_loc_ = res.location_; ++ if (res.status_ == Response::IMMUTABLY_GONE.status_) { ++ auto p = req.path(); ++ p.pop(); ++ std::string cid{p.pop()}; ++ me->DoesNotExist(cid, p.to_string()); ++ } else { ++ me->BlocksComplete(res.mime_); ++ } ++ DCHECK(me->complete_); ++ }; ++ auto req = std::make_shared(abs_path, whendone); ++ me->state_->orchestrator().build_response(req); ++ } else { ++ LOG(ERROR) << "Wrong scheme: " << resource_request.url.scheme(); ++ } ++} ++ ++void ipfs::IpfsUrlLoader::OverrideUrl(GURL u) { ++ original_url_ = u.spec(); ++} ++void ipfs::IpfsUrlLoader::AddHeader(std::string_view a, std::string_view b) { ++ VLOG(1) << "AddHeader(" << a << ',' << b << ')'; ++ additional_outgoing_headers_.emplace_back(a, b); ++} ++ ++void ipfs::IpfsUrlLoader::BlocksComplete(std::string mime_type) { ++ VLOG(1) << "Resolved from unix-fs dag a file of type: " << mime_type ++ << " will report it as " << original_url_; ++ if (complete_) { ++ return; ++ } ++ auto result = ++ mojo::CreateDataPipe(partial_block_.size(), pipe_prod_, pipe_cons_); ++ if (result) { ++ LOG(ERROR) << " ERROR: TaskFailed to create data pipe: " << result; ++ return; ++ } ++ complete_ = true; ++ auto head = network::mojom::URLResponseHead::New(); ++ if (mime_type.size()) { ++ head->mime_type = mime_type; ++ } ++ std::uint32_t byte_count = partial_block_.size(); ++ VLOG(1) << "Calling WriteData(" << byte_count << ")"; ++ pipe_prod_->WriteData(partial_block_.data(), &byte_count, ++ MOJO_BEGIN_WRITE_DATA_FLAG_ALL_OR_NONE); ++ VLOG(1) << "Called WriteData(" << byte_count << ")"; ++ head->content_length = byte_count; ++ head->headers = ++ net::HttpResponseHeaders::TryToCreate("access-control-allow-origin: *"); ++ if (resp_loc_.size()) { ++ head->headers->AddHeader("Location", resp_loc_); ++ } ++ if (!head->headers) { ++ LOG(ERROR) << "\n\tFailed to create headers!\n"; ++ return; ++ } ++ auto* reason = ++ net::GetHttpReasonPhrase(static_cast(status_)); ++ auto status_line = base::StringPrintf("HTTP/1.1 %d %s", status_, reason); ++ VLOG(1) << "Returning with status line '" << status_line << "'.\n"; ++ head->headers->ReplaceStatusLine(status_line); ++ if (mime_type.size()) { ++ head->headers->SetHeader("Content-Type", mime_type); ++ } ++ head->headers->SetHeader("Access-Control-Allow-Origin", "*"); ++ head->was_fetched_via_spdy = false; ++ for (auto& [n, v] : additional_outgoing_headers_) { ++ VLOG(1) << "Appending 'additional' header:" << n << '=' << v << '.'; ++ head->headers->AddHeader(n, v); ++ } ++ VLOG(1) << "Calling PopulateParsedHeaders"; ++ head->parsed_headers = ++ network::PopulateParsedHeaders(head->headers.get(), GURL{original_url_}); ++ VLOG(1) << "Sending response for " << original_url_ << " with mime type " ++ << head->mime_type << " and status line " << status_line; ++ if (status_ / 100 == 3 && resp_loc_.size()) { ++ auto ri = net::RedirectInfo::ComputeRedirectInfo( ++ "GET", GURL{original_url_}, net::SiteForCookies{}, ++ net::RedirectInfo::FirstPartyURLPolicy::UPDATE_URL_ON_REDIRECT, ++ net::ReferrerPolicy::NO_REFERRER, "", status_, GURL{resp_loc_}, ++ std::nullopt, false); ++ client_->OnReceiveRedirect(ri, std::move(head)); ++ } else { ++ client_->OnReceiveResponse(std::move(head), std::move(pipe_cons_), ++ absl::nullopt); ++ } ++ client_->OnComplete(network::URLLoaderCompletionStatus{}); ++ stepper_.reset(); ++} ++ ++void ipfs::IpfsUrlLoader::DoesNotExist(std::string_view cid, ++ std::string_view path) { ++ LOG(ERROR) << "Immutable data 404 for " << cid << '/' << path; ++ complete_ = true; ++ client_->OnComplete( ++ network::URLLoaderCompletionStatus{net::ERR_FILE_NOT_FOUND}); ++ stepper_.reset(); ++} ++void ipfs::IpfsUrlLoader::NotHere(std::string_view cid, std::string_view path) { ++ LOG(INFO) << "TODO " << __func__ << '(' << cid << ',' << path << ')'; ++} ++ ++void ipfs::IpfsUrlLoader::ReceiveBlockBytes(std::string_view content) { ++ partial_block_.append(content); ++ VLOG(2) << "Recived a block of size " << content.size() << " now have " ++ << partial_block_.size() << " bytes."; ++} +diff --git a/components/ipfs/ipfs_url_loader.h b/components/ipfs/ipfs_url_loader.h +new file mode 100644 +index 0000000000000..dc324f7b11f2d +--- /dev/null ++++ b/components/ipfs/ipfs_url_loader.h +@@ -0,0 +1,96 @@ ++#ifndef COMPONENTS_IPFS_URL_LOADER_H_ ++#define COMPONENTS_IPFS_URL_LOADER_H_ 1 ++ ++#include "base/debug/debugging_buildflags.h" ++#include "base/timer/timer.h" ++#include "mojo/public/cpp/bindings/receiver_set.h" ++#include "mojo/public/cpp/system/data_pipe.h" ++#include "net/http/http_request_headers.h" ++#include "services/network/public/cpp/resolve_host_client_base.h" ++#include "services/network/public/cpp/resource_request.h" ++#include "services/network/public/mojom/url_loader.mojom.h" ++ ++#include ++ ++namespace ipfs { ++class ChromiumIpfsContext; ++} // namespace ipfs ++ ++namespace network::mojom { ++class URLLoaderFactory; ++class HostResolver; ++class NetworkContext; ++} // namespace network::mojom ++namespace network { ++class SimpleURLLoader; ++} ++ ++namespace ipfs { ++class InterRequestState; ++ ++class IpfsUrlLoader final : public network::mojom::URLLoader { ++ void FollowRedirect( ++ std::vector const& removed_headers, ++ net::HttpRequestHeaders const& modified_headers, ++ net::HttpRequestHeaders const& modified_cors_exempt_headers, ++ absl::optional<::GURL> const& new_url) override; ++ void SetPriority(net::RequestPriority priority, ++ int32_t intra_priority_value) override; ++ void PauseReadingBodyFromNet() override; ++ void ResumeReadingBodyFromNet() override; ++ ++ public: ++ explicit IpfsUrlLoader(network::mojom::URLLoaderFactory& handles_http, ++ InterRequestState& state); ++ ~IpfsUrlLoader() noexcept override; ++ ++ using ptr = std::shared_ptr; ++ ++ // Passed as the RequestHandler for ++ // Interceptor::MaybeCreateLoader. ++ static void StartRequest( ++ ptr, ++ network::ResourceRequest const& resource_request, ++ mojo::PendingReceiver receiver, ++ mojo::PendingRemote client); ++ ++ void OverrideUrl(GURL); ++ void AddHeader(std::string_view,std::string_view); ++ void extra(std::shared_ptr xtra) { extra_ = xtra; } ++ ++ private: ++ using RequestHandle = std::unique_ptr; ++ ++ raw_ref state_; ++ mojo::Receiver receiver_{this}; ++ mojo::Remote client_; ++ raw_ref lower_loader_factory_; ++ mojo::ScopedDataPipeProducerHandle pipe_prod_ = {}; ++ mojo::ScopedDataPipeConsumerHandle pipe_cons_ = {}; ++ bool complete_ = false; ++ std::shared_ptr api_; ++ std::string original_url_; ++ std::string partial_block_; ++ std::vector> additional_outgoing_headers_; ++ std::shared_ptr extra_; ++ std::unique_ptr stepper_; ++ std::string root_; ++ int status_ = 200; ++ std::string resp_loc_; ++ ++ void CreateBlockRequest(std::string cid); ++ ++ void ReceiveBlockBytes(std::string_view); ++ void BlocksComplete(std::string mime_type); ++ void DoesNotExist(std::string_view cid, std::string_view path); ++ void NotHere(std::string_view cid, std::string_view path); ++ ++ void StartUnixFsProc(ptr, std::string_view); ++ void AppendGatewayHeaders(std::vector const& cids, net::HttpResponseHeaders&); ++ void AppendGatewayInfoHeader(std::string const&, net::HttpResponseHeaders&); ++ void TakeStep(); ++}; ++ ++} // namespace ipfs ++ ++#endif +diff --git a/components/ipfs/url_loader_factory.cc b/components/ipfs/url_loader_factory.cc +new file mode 100644 +index 0000000000000..9a80284098748 +--- /dev/null ++++ b/components/ipfs/url_loader_factory.cc +@@ -0,0 +1,56 @@ ++#include "url_loader_factory.h" ++ ++#include "inter_request_state.h" ++#include "ipfs_url_loader.h" ++ ++void ipfs::IpfsURLLoaderFactory::Create( ++ NonNetworkURLLoaderFactoryMap* in_out, ++ content::BrowserContext* context, ++ URLLoaderFactory* default_factory, ++ network::mojom::NetworkContext* net_ctxt, ++ PrefService* pref_svc) { ++ for (char const* scheme : {"ipfs", "ipns"}) { ++ mojo::PendingRemote pending; ++ new IpfsURLLoaderFactory(scheme, pending.InitWithNewPipeAndPassReceiver(), ++ context, default_factory, net_ctxt, pref_svc); ++ in_out->emplace(scheme, std::move(pending)); ++ } ++} ++ ++ipfs::IpfsURLLoaderFactory::IpfsURLLoaderFactory( ++ std::string scheme, ++ mojo::PendingReceiver factory_receiver, ++ content::BrowserContext* context, ++ URLLoaderFactory* default_factory, ++ network::mojom::NetworkContext* net_ctxt, ++ PrefService* pref_svc) ++ : network::SelfDeletingURLLoaderFactory(std::move(factory_receiver)), ++ scheme_{scheme}, ++ context_{context}, ++ default_factory_{default_factory}, ++ network_context_{net_ctxt}, ++ pref_svc_{pref_svc} {} ++ ++ipfs::IpfsURLLoaderFactory::~IpfsURLLoaderFactory() noexcept { ++ context_ = nullptr; ++ default_factory_ = nullptr; ++ network_context_ = nullptr; ++} ++ ++void ipfs::IpfsURLLoaderFactory::CreateLoaderAndStart( ++ mojo::PendingReceiver loader, ++ int32_t /*request_id*/, ++ uint32_t /*options*/, ++ network::ResourceRequest const& request, ++ mojo::PendingRemote client, ++ net::MutableNetworkTrafficAnnotationTag const& // traffic_annotation ++) { ++ VLOG(2) << "IPFS subresource: case=" << scheme_ ++ << " url=" << request.url.spec(); ++ DCHECK(default_factory_); ++ if (scheme_ == "ipfs" || scheme_ == "ipns") { ++ auto ptr = std::make_shared( ++ *default_factory_, InterRequestState::FromBrowserContext(context_)); ++ ptr->StartRequest(ptr, request, std::move(loader), std::move(client)); ++ } ++} +diff --git a/components/ipfs/url_loader_factory.h b/components/ipfs/url_loader_factory.h +new file mode 100644 +index 0000000000000..01cd66ea6ed8f +--- /dev/null ++++ b/components/ipfs/url_loader_factory.h +@@ -0,0 +1,58 @@ ++#ifndef IPFS_URL_LOADER_FACTORY_H_ ++#define IPFS_URL_LOADER_FACTORY_H_ ++ ++#include "services/network/public/cpp/self_deleting_url_loader_factory.h" ++#include "services/network/public/mojom/url_loader_factory.mojom.h" ++ ++#include ++ ++class PrefService; ++namespace content { ++class BrowserContext; ++} ++namespace network { ++namespace mojom { ++class NetworkContext; ++} ++} // namespace network ++ ++namespace ipfs { ++using NonNetworkURLLoaderFactoryMap = ++ std::map>; ++ ++class COMPONENT_EXPORT(IPFS) IpfsURLLoaderFactory ++ : public network::SelfDeletingURLLoaderFactory { ++ public: ++ static void Create(NonNetworkURLLoaderFactoryMap* in_out, ++ content::BrowserContext*, ++ URLLoaderFactory*, ++ network::mojom::NetworkContext*, ++ PrefService*); ++ ++ private: ++ IpfsURLLoaderFactory(std::string, ++ mojo::PendingReceiver, ++ content::BrowserContext*, ++ network::mojom::URLLoaderFactory*, ++ network::mojom::NetworkContext*, ++ PrefService*); ++ ~IpfsURLLoaderFactory() noexcept override; ++ void CreateLoaderAndStart( ++ mojo::PendingReceiver loader, ++ int32_t request_id, ++ uint32_t options, ++ network::ResourceRequest const& request, ++ mojo::PendingRemote client, ++ net::MutableNetworkTrafficAnnotationTag const& traffic_annotation) ++ override; ++ ++ std::string scheme_; ++ raw_ptr context_; ++ raw_ptr default_factory_; ++ raw_ptr network_context_; ++ raw_ptr pref_svc_; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_URL_LOADER_FACTORY_H_ +diff --git a/components/open_from_clipboard/clipboard_recent_content_generic.cc b/components/open_from_clipboard/clipboard_recent_content_generic.cc +index 4dcafecbc66c6..d205209c08162 100644 +--- a/components/open_from_clipboard/clipboard_recent_content_generic.cc ++++ b/components/open_from_clipboard/clipboard_recent_content_generic.cc +@@ -20,7 +20,7 @@ + namespace { + // Schemes appropriate for suggestion by ClipboardRecentContent. + const char* kAuthorizedSchemes[] = { +- url::kAboutScheme, url::kDataScheme, url::kHttpScheme, url::kHttpsScheme, ++ url::kAboutScheme, url::kDataScheme, url::kHttpScheme, url::kHttpsScheme, "ipfs", "ipns" + // TODO(mpearson): add support for chrome:// URLs. Right now the scheme + // for that lives in content and is accessible via + // GetEmbedderRepresentationOfAboutScheme() or content::kChromeUIScheme +diff --git a/net/dns/dns_config_service_linux.cc b/net/dns/dns_config_service_linux.cc +index 5273da5190277..12b28b86a4c00 100644 +--- a/net/dns/dns_config_service_linux.cc ++++ b/net/dns/dns_config_service_linux.cc +@@ -272,11 +272,11 @@ bool IsNsswitchConfigCompatible( + // Ignore any entries after `kDns` because Chrome will fallback to the + // system resolver if a result was not found in DNS. + return true; +- ++ case NsswitchReader::Service::kResolve: ++ break; + case NsswitchReader::Service::kMdns: + case NsswitchReader::Service::kMdns4: + case NsswitchReader::Service::kMdns6: +- case NsswitchReader::Service::kResolve: + case NsswitchReader::Service::kNis: + RecordIncompatibleNsswitchReason( + IncompatibleNsswitchReason::kIncompatibleService, +diff --git a/third_party/blink/renderer/platform/weborigin/scheme_registry.cc b/third_party/blink/renderer/platform/weborigin/scheme_registry.cc +index 4eadf46ea0c24..d62fc7fb14e01 100644 +--- a/third_party/blink/renderer/platform/weborigin/scheme_registry.cc ++++ b/third_party/blink/renderer/platform/weborigin/scheme_registry.cc +@@ -67,7 +67,7 @@ class URLSchemesRegistry final { + // is considered secure. Additional checks are performed to ensure that + // other http pages are filtered out. + service_worker_schemes({"http", "https"}), +- fetch_api_schemes({"http", "https"}), ++ fetch_api_schemes({"http", "https", "ipfs", "ipns"}), + allowed_in_referrer_schemes({"http", "https"}) { + for (auto& scheme : url::GetCorsEnabledSchemes()) + cors_enabled_schemes.insert(scheme.c_str()); +diff --git a/third_party/ipfs_client/BUILD.gn b/third_party/ipfs_client/BUILD.gn +new file mode 100644 +index 0000000000000..39c8128a02161 +--- /dev/null ++++ b/third_party/ipfs_client/BUILD.gn +@@ -0,0 +1,202 @@ ++import("args.gni") ++import("//build/buildflag_header.gni") ++ ++buildflag_header("ipfs_buildflags") { ++ header = "ipfs_buildflags.h" ++ flags = [ "ENABLE_IPFS=$enable_ipfs" ] ++} ++ ++config("external_config") { ++ include_dirs = [ ++ "include", ++ ] ++} ++ ++if (enable_ipfs) { ++ cxx_sources = [ ++ "include/ipfs_client/block_requestor.h", ++ "include/ipfs_client/block_storage.h", ++ "include/ipfs_client/cid.h", ++ "include/ipfs_client/context_api.h", ++ "include/ipfs_client/crypto/hasher.h", ++ "include/ipfs_client/dag_cbor_value.h", ++ "include/ipfs_client/dag_json_value.h", ++ "include/ipfs_client/gateway_spec.h", ++ "include/ipfs_client/gateways.h", ++ "include/ipfs_client/gw/block_request_splitter.h", ++ "include/ipfs_client/gw/default_requestor.h", ++ "include/ipfs_client/gw/dnslink_requestor.h", ++ "include/ipfs_client/gw/gateway_request.h", ++ "include/ipfs_client/gw/inline_request_handler.h", ++ "include/ipfs_client/gw/requestor.h", ++ "include/ipfs_client/gw/terminating_requestor.h", ++ "include/ipfs_client/http_request_description.h", ++ "include/ipfs_client/identity_cid.h", ++ "include/ipfs_client/ipfs_request.h", ++ "include/ipfs_client/ipld/dag_node.h", ++ "include/ipfs_client/ipld/link.h", ++ "include/ipfs_client/ipld/resolution_state.h", ++ "include/ipfs_client/ipns_cbor_entry.h", ++ "include/ipfs_client/ipns_names.h", ++ "include/ipfs_client/ipns_record.h", ++ "include/ipfs_client/json_cbor_adapter.h", ++ "include/ipfs_client/logger.h", ++ "include/ipfs_client/multi_base.h", ++ "include/ipfs_client/multi_hash.h", ++ "include/ipfs_client/multicodec.h", ++ "include/ipfs_client/orchestrator.h", ++ "include/ipfs_client/pb_dag.h", ++ "include/ipfs_client/response.h", ++ "include/ipfs_client/signing_key_type.h", ++ "include/ipfs_client/url_spec.h", ++ "include/libp2p/common/types.hpp", ++ "include/libp2p/crypto/key.h", ++ "include/libp2p/crypto/protobuf/protobuf_key.hpp", ++ "include/libp2p/multi/multibase_codec.hpp", ++ "include/libp2p/multi/multibase_codec/codecs/base16.h", ++ "include/libp2p/multi/multibase_codec/codecs/base32.hpp", ++ "include/libp2p/multi/multibase_codec/codecs/base_error.hpp", ++ "include/libp2p/multi/multicodec_type.hpp", ++ "include/libp2p/multi/uvarint.hpp", ++ "include/multibase/algorithm.h", ++ "include/multibase/basic_algorithm.h", ++ "include/multibase/encoding.h", ++ "include/smhasher/MurmurHash3.h", ++ "include/vocab/byte.h", ++ "include/vocab/byte_view.h", ++ "include/vocab/endian.h", ++ "include/vocab/expected.h", ++ "include/vocab/flat_mapset.h", ++ "include/vocab/html_escape.h", ++ "include/vocab/i128.h", ++ "include/vocab/raw_ptr.h", ++ "include/vocab/slash_delimited.h", ++ "include/vocab/span.h", ++ "include/vocab/stringify.h", ++ "src/ipfs_client/bases/b16_upper.h", ++ "src/ipfs_client/bases/b32.h", ++ "src/ipfs_client/block_requestor.cc", ++ "src/ipfs_client/car.cc", ++ "src/ipfs_client/car.h", ++ "src/ipfs_client/cid.cc", ++ "src/ipfs_client/context_api.cc", ++ "src/ipfs_client/crypto/openssl_sha2_256.cc", ++ "src/ipfs_client/crypto/openssl_sha2_256.h", ++ "src/ipfs_client/dag_cbor_value.cc", ++ "src/ipfs_client/dag_json_value.cc", ++ "src/ipfs_client/gateways.cc", ++ "src/ipfs_client/generated_directory_listing.cc", ++ "src/ipfs_client/generated_directory_listing.h", ++ "src/ipfs_client/gw/block_request_splitter.cc", ++ "src/ipfs_client/gw/default_requestor.cc", ++ "src/ipfs_client/gw/dnslink_requestor.cc", ++ "src/ipfs_client/gw/gateway_http_requestor.cc", ++ "src/ipfs_client/gw/gateway_http_requestor.h", ++ "src/ipfs_client/gw/gateway_request.cc", ++ "src/ipfs_client/gw/gateway_state.cc", ++ "src/ipfs_client/gw/gateway_state.h", ++ "src/ipfs_client/gw/inline_request_handler.cc", ++ "src/ipfs_client/gw/multi_gateway_requestor.cc", ++ "src/ipfs_client/gw/multi_gateway_requestor.h", ++ "src/ipfs_client/gw/requestor.cc", ++ "src/ipfs_client/gw/requestor_pool.cc", ++ "src/ipfs_client/gw/requestor_pool.h", ++ "src/ipfs_client/gw/terminating_requestor.cc", ++ "src/ipfs_client/http_request_description.cc", ++ "src/ipfs_client/identity_cid.cc", ++ "src/ipfs_client/ipfs_request.cc", ++ "src/ipfs_client/ipld/chunk.cc", ++ "src/ipfs_client/ipld/chunk.h", ++ "src/ipfs_client/ipld/dag_cbor_node.cc", ++ "src/ipfs_client/ipld/dag_cbor_node.h", ++ "src/ipfs_client/ipld/dag_json_node.cc", ++ "src/ipfs_client/ipld/dag_json_node.h", ++ "src/ipfs_client/ipld/dag_node.cc", ++ "src/ipfs_client/ipld/directory_shard.cc", ++ "src/ipfs_client/ipld/directory_shard.h", ++ "src/ipfs_client/ipld/ipns_name.cc", ++ "src/ipfs_client/ipld/ipns_name.h", ++ "src/ipfs_client/ipld/link.cc", ++ "src/ipfs_client/ipld/resolution_state.cc", ++ "src/ipfs_client/ipld/root.cc", ++ "src/ipfs_client/ipld/root.h", ++ "src/ipfs_client/ipld/small_directory.cc", ++ "src/ipfs_client/ipld/small_directory.h", ++ "src/ipfs_client/ipld/symlink.cc", ++ "src/ipfs_client/ipld/symlink.h", ++ "src/ipfs_client/ipld/unixfs_file.cc", ++ "src/ipfs_client/ipld/unixfs_file.h", ++ "src/ipfs_client/ipns_names.cc", ++ "src/ipfs_client/ipns_record.cc", ++ "src/ipfs_client/logger.cc", ++ "src/ipfs_client/multi_base.cc", ++ "src/ipfs_client/multi_hash.cc", ++ "src/ipfs_client/multicodec.cc", ++ "src/ipfs_client/orchestrator.cc", ++ "src/ipfs_client/path2url.cc", ++ "src/ipfs_client/path2url.h", ++ "src/ipfs_client/pb_dag.cc", ++ "src/ipfs_client/redirects.cc", ++ "src/ipfs_client/redirects.h", ++ "src/ipfs_client/response.cc", ++ "src/ipfs_client/signing_key_type.cc", ++ "src/libp2p/crypto/protobuf_key.hpp", ++ "src/libp2p/multi/multibase_codec/codecs/base16.cc", ++ "src/libp2p/multi/uvarint.cc", ++ "src/log_macros.h", ++ "src/smhasher/MurmurHash3.cc", ++ "src/vocab/byte_view.cc", ++ "src/vocab/slash_delimited.cc", ++ ] ++ static_library("ipfs_client") { ++ if (is_nacl) { ++ sources = cxx_sources - [ ++ "src/ipfs_client/dag_block.cc", ++ "src/ipfs_client/gw/gateway_request.cc", ++ "src/ipfs_client/gw/gateway_http_requestor.cc", ++ "src/ipfs_client/gw/requestor.cc", ++ "src/ipfs_client/ipld/dag_node.cc", ++ "src/ipfs_client/ipns_names.cc", ++ "src/ipfs_client/ipns_record.cc", ++ "src/ipfs_client/logger.cc", ++ "src/ipfs_client/signing_key_type.cc", ++ ] ++ } else { ++ sources = cxx_sources ++ } ++ include_dirs = [ ++ "include", ++ "src", ++ "..", ++ "../boringssl/src/include" ++ ] ++ public_configs = [ ++ ":external_config" ++ ] ++ public_deps = [ ++ "//third_party/abseil-cpp:absl", ++ "//base", ++ ] ++ deps = [ ++ "//third_party/abseil-cpp:absl", ++ "//base", ++ ] ++ if (!is_nacl) { ++ public_deps += [ ++ ":protos", ++ "//third_party/protobuf:protobuf_lite", ++ ] ++ } ++ } ++} ++ ++import("//third_party/protobuf/proto_library.gni") ++ ++proto_library("protos") { ++ sources = [ ++ "ipns_record.proto", ++ "keys.proto", ++ "pb_dag.proto", ++ "unix_fs.proto", ++ ] ++} +diff --git a/third_party/ipfs_client/README.chromium b/third_party/ipfs_client/README.chromium +new file mode 100644 +index 0000000000000..e69de29bb2d1d +diff --git a/third_party/ipfs_client/README.md b/third_party/ipfs_client/README.md +new file mode 100644 +index 0000000000000..0e6ffadd2ebbc +--- /dev/null ++++ b/third_party/ipfs_client/README.md +@@ -0,0 +1,6 @@ ++# ipfs-client ++ ++## TODO ++ ++Need to fill out this README to explain how to use ipfs-client in other contexts. ++ +diff --git a/third_party/ipfs_client/args.gni b/third_party/ipfs_client/args.gni +new file mode 100644 +index 0000000000000..bb13519b23e89 +--- /dev/null ++++ b/third_party/ipfs_client/args.gni +@@ -0,0 +1,3 @@ ++declare_args() { ++ enable_ipfs = false ++} +diff --git a/third_party/ipfs_client/conanfile.py b/third_party/ipfs_client/conanfile.py +new file mode 100644 +index 0000000000000..289e3b48f8ad1 +--- /dev/null ++++ b/third_party/ipfs_client/conanfile.py +@@ -0,0 +1,79 @@ ++from conan import ConanFile ++from conan.tools.cmake import CMake, CMakeToolchain, CMakeDeps, cmake_layout ++from shutil import copyfile, which ++import sys ++from os.path import dirname, isfile, join, realpath ++ ++here = realpath(dirname(__file__)) ++sys.path.append(realpath(join(here, '..', 'cmake'))) ++sys.path.append(here) ++ ++try: ++ import version ++ VERSION = version.deduce() ++except ImportError: ++ VERSION = open(join(here,'version.txt'), 'r').read().strip() ++ ++ ++class IpfsChromium(ConanFile): ++ name = "ipfs_client" ++ version = VERSION ++ settings = "os", "compiler", "build_type", "arch" ++ # generators = "CMakeDeps", 'CMakeToolchain' ++ _PB = 'protobuf/3.20.0' ++ require_transitively = [ ++ 'abseil/20230125.3', ++ 'boost/1.81.0', ++ 'bzip2/1.0.8', ++ 'c-ares/1.22.1', ++ 'nlohmann_json/3.11.2', ++ 'openssl/1.1.1t', ++ _PB, ++ ] ++ # default_options = {"boost/*:header_only": True} ++ default_options = { ++ "boost/*:bzip2": True, ++ "boost/*:with_stacktrace_backtrace": True ++ } ++ tool_requires = [ ++ 'cmake/3.22.6', ++ 'ninja/1.11.1', ++ _PB, ++ ] ++ extensions = ['h', 'cc', 'hpp', 'proto'] ++ exports_sources = [ '*.txt' ] + [f'**/*.{e}' for e in extensions] ++ exports = 'version.txt' ++ package_type = 'static-library' ++ ++ ++ def generate(self): ++ tc = CMakeToolchain(self, 'Ninja') ++ tc.generate() ++ d = CMakeDeps(self) ++ d.generate() ++ ++ def build(self): ++ cmake = CMake(self) ++ cmake.configure(variables={ ++ "CXX_VERSION": 20, ++ "INSIDE_CONAN": True ++ }) ++ cmake.build(build_tool_args=['--verbose']) ++ ++ def package(self): ++ cmake = CMake(self) ++ cmake.install() ++ print(self.cpp_info.objects) ++ ++ def package_info(self): ++ self.cpp_info.libs = ["ipfs_client"] ++ ++ def build_requirements(self): ++ if not which("doxygen"): ++ self.tool_requires("doxygen/1.9.4") ++ def layout(self): ++ cmake_layout(self) ++ ++ def requirements(self): ++ for l in self.require_transitively: ++ self.requires(l, transitive_headers=True) +diff --git a/third_party/ipfs_client/include/ipfs_client/block_requestor.h b/third_party/ipfs_client/include/ipfs_client/block_requestor.h +new file mode 100644 +index 0000000000000..42ae26e519760 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/block_requestor.h +@@ -0,0 +1,48 @@ ++#ifndef BLOCK_REQUESTOR_H_ ++#define BLOCK_REQUESTOR_H_ ++ ++#include ++ ++#include ++#include ++#include ++ ++namespace ipfs { ++ ++/*! ++ * \brief The urgency of a gateway request ++ * \details Determines how many gateways should be involved, and how burdened a ++ * gateway should be before not also taking this one on concurrently. Zero is ++ * a special value that indicates the block isn't actually required now, but ++ * rather might be required soonish (prefetch). There are some cases of ++ * special handling for that. ++ */ ++using Priority = std::uint_least16_t; ++ ++class DagListener; ++ ++/*! ++ * \brief Interface for classes that can asynchronously fetch a block for a CID ++ * \details This is one of the interfaces using code is meant to implement. ++ * Common usages: ++ * * A class that requests blocks from gateways ++ * * A cache that must act asynchronously (perhaps on-disk) ++ * * ChainedRequestors : a chain-of-responsibility combining multiple ++ */ ++class BlockRequestor { ++ public: ++ /** ++ * \brief Request a single block from gateway(s). ++ * \param cid - MB-MH string representation of the Content IDentifier ++ * \param dl - Someone who may be interested ++ * \param priority - Urgency of the request ++ * \note The DagListener is mostly about lifetime extension, since it's ++ * waiting on something which is waiting on this ++ */ ++ virtual void RequestByCid(std::string cid, ++ std::shared_ptr dl, ++ Priority priority) = 0; ++}; ++} // namespace ipfs ++ ++#endif // BLOCK_REQUESTOR_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/block_storage.h b/third_party/ipfs_client/include/ipfs_client/block_storage.h +new file mode 100644 +index 0000000000000..525bae463f50d +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/block_storage.h +@@ -0,0 +1,144 @@ ++#ifndef IPFS_BLOCKS_H_ ++#define IPFS_BLOCKS_H_ ++ ++#include "pb_dag.h" ++#include "vocab/flat_mapset.h" ++ ++#include ++#include ++#include ++ ++namespace libp2p::multi { ++struct ContentIdentifier; ++} ++ ++namespace ipfs { ++class DagListener; ++class ContextApi; ++ ++class UnixFsPathResolver; ++ ++/*! ++ * \brief Immediate access to recently-accessed blocks ++ * \details Blocks are held in-memory, using pretty standard containers, as ++ * already-parsed ipfs::Block objects. ++ */ ++class BlockStorage { ++ public: ++ BlockStorage(); ++ ++ BlockStorage(BlockStorage const&) = delete; ++ ++ ~BlockStorage() noexcept; ++ ++ /*! ++ * \brief Store a Block for later access. ++ * \param cid_str - The string representation of cid ++ * \param cid - The Content IDentifier ++ * \param headers - Associated HTTP headers ++ * \param body - The raw bytes of the block ++ * \param block - The block being stored ++ * \return Whether this block is now stored in *this ++ */ ++ bool Store(std::string cid_str, ++ Cid const& cid, ++ std::string headers, ++ std::string const& body, ++ PbDag&& block); ++ ++ /*! ++ * \name Store (Convenience) ++ * Convenience functions for ++ * ipfs::BlockStorage::Store(std::string,Cid const&,std::string,std::string ++ * const&,Block&&) ++ */ ++ ///@{ ++ bool Store(std::string headers, std::string const& body, PbDag&& block); ++ bool Store(std::string const& cid, std::string headers, std::string body); ++ bool Store(std::string cid_str, ++ Cid const& cid, ++ std::string headers, ++ std::string body); ++ bool Store(Cid const& cid, ++ std::string headers, ++ std::string const& body, ++ PbDag&&); ++ ///@} ++ ++ /*! ++ * \brief Get a block! ++ * \details cid must match string-wise exactly: same multibase & all. ++ * For identity codecs, returns the data even if not stored. ++ * \param cid - String representation of the CID for the block. ++ * \return Non-owning pointer if found, nullptr ++ * otherwise ++ */ ++ PbDag const* Get(std::string const& cid); ++ ++ /*! ++ * \brief Get HTTP headers associated with the block ++ * \param cid - String representation of the CID for the block. ++ * \return nullptr iff ! Get(cid) ; ++ * Empty string if the headers have never been set ; ++ * Otherwise, application-specific std::string (as-stored) ++ */ ++ std::string const* GetHeaders(std::string const& cid); ++ ++ /*! ++ * \brief Indicate that a particular path resolver is waiting on a CID to ++ * become available ++ */ ++ void AddListening(UnixFsPathResolver*); ++ ++ /*! ++ * \brief Indicate that a particular path resolver is no longer waiting ++ */ ++ void StopListening(UnixFsPathResolver*); ++ ++ /*! ++ * \brief Normally called internally ++ * \details Checks to see if any listening path resolver appears to be waiting ++ * on a CID which is now available. ++ */ ++ void CheckListening(); ++ ++ /*! ++ * \brief Type for callbacks about new blocks ++ * \details The parameters to the hook are ++ * * CID string ++ * * HTTP headers ++ * * raw bytes of the block ++ */ ++ using SerializedStorageHook = ++ std::function; ++ ++ /*! ++ * \brief Register a callback that will be called when any new block goes into ++ * storage ++ */ ++ void AddStorageHook(SerializedStorageHook); ++ ++ private: ++ struct Record { ++ Record(); ++ ~Record() noexcept; ++ std::time_t last_access = 0L; ++ std::string cid_str = {}; ++ PbDag block = {}; ++ std::string headers = {}; ++ }; ++ std::list records_ = std::list(0xFFUL); ++ using Iter = decltype(records_)::iterator; ++ flat_map cid2record_; ++ flat_set listening_; ++ bool checking_ = false; ++ std::vector hooks_; ++ ++ Record const* GetInternal(std::string const&); ++ Record* FindFree(std::time_t); ++ Record* Allocate(); ++ Record* StoreIdentity(std::string const&, Cid const&); ++}; ++} // namespace ipfs ++ ++#endif // IPFS_BLOCKS_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/cid.h b/third_party/ipfs_client/include/ipfs_client/cid.h +new file mode 100644 +index 0000000000000..d957d23e5e7e4 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/cid.h +@@ -0,0 +1,38 @@ ++#ifndef IPFS_CID_H_ ++#define IPFS_CID_H_ ++ ++#include "multi_hash.h" ++#include "multicodec.h" ++ ++#include ++ ++#include ++#include ++ ++namespace ipfs { ++class Cid { ++ MultiCodec codec_ = MultiCodec::INVALID; ++ MultiHash hash_; ++ ++ public: ++ Cid() = default; ++ Cid(MultiCodec, MultiHash); ++ explicit Cid(std::string_view); ++ explicit Cid(ByteView); ++ bool ReadStart(ByteView&); ++ ++ bool valid() const; ++ MultiCodec codec() const { return codec_; } ++ MultiHash const& multi_hash() const { return hash_; } ++ ByteView hash() const; ++ HashType hash_type() const; ++ ++ std::string to_string() const; ++ ++ constexpr static std::size_t MinSerializedLength = ++ 1 /*cid version*/ + 1 /*codec*/ + 1 /*hash type*/ + ++ 1 /*hash len, could be zero*/; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_CID_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/context_api.h b/third_party/ipfs_client/include/ipfs_client/context_api.h +new file mode 100644 +index 0000000000000..dc46f903e17ec +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/context_api.h +@@ -0,0 +1,91 @@ ++#ifndef IPFS_CONTEXT_API_H_ ++#define IPFS_CONTEXT_API_H_ ++ ++#include "crypto/hasher.h" ++#include "dag_cbor_value.h" ++#include "gateway_spec.h" ++#include "http_request_description.h" ++#include "ipns_cbor_entry.h" ++#include "multi_hash.h" ++#include "signing_key_type.h" ++ ++#include ++ ++#include ++#include ++#include ++#include ++#include ++ ++namespace ipfs { ++class IpfsRequest; ++class DagJsonValue; ++ ++/** ++ * \brief Interface that provides functionality from whatever ++ * environment you're using this library in. ++ * \note A user of this library must implement this, but will probably do so ++ * only once. ++ */ ++class ContextApi : public std::enable_shared_from_this { ++ public: ++ ContextApi(); ++ virtual ~ContextApi() noexcept {} ++ ++ using HttpRequestDescription = ::ipfs::HttpRequestDescription; ++ using HeaderAccess = std::function; ++ using HttpCompleteCallback = ++ std::function; ++ virtual void SendHttpRequest(HttpRequestDescription, ++ HttpCompleteCallback cb) const = 0; ++ ++ using DnsTextResultsCallback = ++ std::function const&)>; ++ using DnsTextCompleteCallback = std::function; ++ virtual void SendDnsTextRequest(std::string hostname, ++ DnsTextResultsCallback, ++ DnsTextCompleteCallback) = 0; ++ ++ /*! ++ * \brief Determine a mime type for a given file. ++ * \param extension - "File extension" not including ., e.g. "html" ++ * \param content - The content of the resource or a large prefix thereof ++ * \param url - A URL it was fetched from (of any sort, ipfs:// is fine) ++ */ ++ virtual std::string MimeType(std::string extension, ++ std::string_view content, ++ std::string const& url) const = 0; ++ ++ /*! ++ * \brief Remove URL escaping, e.g. %20 ++ * \param url_comp - a single component of the URL, e.g. a element of the path ++ * not including / ++ * \return The unescaped string ++ */ ++ virtual std::string UnescapeUrlComponent(std::string_view url_comp) const = 0; ++ ++ virtual std::unique_ptr ParseCbor(ByteView) const = 0; ++ virtual std::unique_ptr ParseJson(std::string_view) const = 0; ++ ++ using IpnsCborEntry = ::ipfs::IpnsCborEntry; ++ ++ using SigningKeyType = ::ipfs::SigningKeyType; ++ using ByteView = ::ipfs::ByteView; ++ virtual bool VerifyKeySignature(SigningKeyType, ++ ByteView signature, ++ ByteView data, ++ ByteView key_bytes) const = 0; ++ ++ std::optional> Hash(HashType, ByteView data); ++ ++ virtual std::optional GetGateway(std::size_t index) const = 0; ++ virtual unsigned GetGatewayRate(std::string_view); ++ virtual void SetGatewayRate(std::string_view, unsigned); ++ ++ protected: ++ std::unordered_map> hashers_; ++}; ++ ++} // namespace ipfs ++ ++#endif +diff --git a/third_party/ipfs_client/include/ipfs_client/crypto/hasher.h b/third_party/ipfs_client/include/ipfs_client/crypto/hasher.h +new file mode 100644 +index 0000000000000..5222d622ce998 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/crypto/hasher.h +@@ -0,0 +1,18 @@ ++#ifndef IPFS_HASHER_H_ ++#define IPFS_HASHER_H_ ++ ++#include ++ ++#include ++#include ++ ++namespace ipfs::crypto { ++class Hasher { ++ public: ++ virtual ~Hasher() noexcept {} ++ ++ virtual std::optional> hash(ByteView) = 0; ++}; ++} // namespace ipfs::crypto ++ ++#endif // IPFS_HASHER_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/dag_cbor_value.h b/third_party/ipfs_client/include/ipfs_client/dag_cbor_value.h +new file mode 100644 +index 0000000000000..71cb538776361 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/dag_cbor_value.h +@@ -0,0 +1,35 @@ ++#ifndef IPFS_DAG_CBOR_VALUE_H_ ++#define IPFS_DAG_CBOR_VALUE_H_ ++ ++#include ++ ++#include ++#include ++#include ++#include ++#include ++ ++namespace ipfs { ++class DagCborValue { ++ public: ++ virtual std::unique_ptr at(std::string_view) const = 0; ++ virtual std::optional as_unsigned() const = 0; ++ virtual std::optional as_signed() const = 0; ++ virtual std::optional as_float() const = 0; ++ virtual std::optional as_string() const = 0; ++ virtual std::optional> as_bytes() const = 0; ++ virtual std::optional as_bool() const = 0; ++ virtual std::optional as_link() const = 0; ++ virtual bool is_map() const = 0; ++ virtual bool is_array() const = 0; ++ using MapElementCallback = std::function; ++ using ArrayElementCallback = std::function; ++ virtual void iterate_map(MapElementCallback) const = 0; ++ virtual void iterate_array(ArrayElementCallback) const = 0; ++ std::string html() const; ++ void html(std::ostream&) const; ++ virtual ~DagCborValue() noexcept {} ++}; ++} ++ ++#endif // IPFS_DAG_CBOR_VALUE_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/dag_json_value.h b/third_party/ipfs_client/include/ipfs_client/dag_json_value.h +new file mode 100644 +index 0000000000000..32e170c439438 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/dag_json_value.h +@@ -0,0 +1,26 @@ ++#ifndef IPFS_DAG_JSON_VALUE_H_ ++#define IPFS_DAG_JSON_VALUE_H_ ++ ++#include ++ ++#include ++#include ++#include ++#include ++#include ++ ++namespace ipfs { ++class DagJsonValue { ++ public: ++ virtual std::string pretty_print() const = 0; ++ virtual std::unique_ptr operator[](std::string_view) const = 0; ++ virtual std::optional get_if_string() const = 0; ++ virtual std::optional> object_keys() const = 0; ++ virtual bool iterate_list(std::function) const = 0; ++ virtual ~DagJsonValue() noexcept; ++ ++ std::optional get_if_link() const; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_DAG_JSON_VALUE_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/gateways.h b/third_party/ipfs_client/include/ipfs_client/gateways.h +new file mode 100644 +index 0000000000000..9852971b36199 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/gateways.h +@@ -0,0 +1,57 @@ ++#ifndef CHROMIUM_IPFS_GATEWAYS_H_ ++#define CHROMIUM_IPFS_GATEWAYS_H_ ++ ++#include "gateway_spec.h" ++#include "vocab/flat_mapset.h" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace ipfs { ++using GatewayList = std::vector; ++class ContextApi; ++ ++/*! ++ * \brief All known IPFS gateways ++ */ ++class Gateways { ++ flat_map known_gateways_; ++ std::default_random_engine random_engine_; ++ std::geometric_distribution dist_; ++ int up_log_ = 1; ++ ++ public: ++ /*! ++ * \brief The hard-coded list of gateways at startup ++ */ ++ static GatewayList DefaultGateways(); ++ ++ Gateways(); ++ ~Gateways(); ++ GatewayList GenerateList(); ///< Get a sorted list of gateways for requesting ++ ++ /*! ++ * \brief Good gateway, handle more! ++ * \param prefix - identify the gateway by its URL prefix ++ */ ++ void promote(std::string const& prefix); ++ ++ /*! ++ * \brief Bad gateway, move toward the back of the line. ++ * \param prefix - identify the gateway by its URL prefix ++ */ ++ void demote(std::string const& prefix); ++ ++ /*! ++ * \brief Bulk load a bunch of new gateways ++ * \param prefices - list of URL gateways by prefix ++ */ ++ void AddGateways(std::vector prefices); ++}; ++} // namespace ipfs ++ ++#endif // CHROMIUM_IPFS_GATEWAYS_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/gw/block_request_splitter.h b/third_party/ipfs_client/include/ipfs_client/gw/block_request_splitter.h +new file mode 100644 +index 0000000000000..0f308a996d360 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/gw/block_request_splitter.h +@@ -0,0 +1,17 @@ ++#ifndef IPFS_BLOCK_REQUEST_SPLITTER_H_ ++#define IPFS_BLOCK_REQUEST_SPLITTER_H_ ++ ++#include "requestor.h" ++ ++namespace ipfs { ++class ContextApi; ++} ++ ++namespace ipfs::gw { ++class BlockRequestSplitter final : public Requestor { ++ HandleOutcome handle(RequestPtr) override; ++ std::string_view name() const override; ++}; ++} // namespace ipfs::gw ++ ++#endif // IPFS_BLOCK_REQUEST_SPLITTER_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/gw/default_requestor.h b/third_party/ipfs_client/include/ipfs_client/gw/default_requestor.h +new file mode 100644 +index 0000000000000..06b5970e1d103 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/gw/default_requestor.h +@@ -0,0 +1,14 @@ ++#ifndef IPFS_DEFAULT_REQUESTOR_LIST_H_ ++#define IPFS_DEFAULT_REQUESTOR_LIST_H_ ++ ++#include "requestor.h" ++ ++#include ++ ++namespace ipfs::gw { ++std::shared_ptr default_requestor(GatewayList, ++ std::shared_ptr early, ++ std::shared_ptr); ++} ++ ++#endif // IPFS_DEFAULT_REQUESTOR_LIST_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/gw/dnslink_requestor.h b/third_party/ipfs_client/include/ipfs_client/gw/dnslink_requestor.h +new file mode 100644 +index 0000000000000..4910fe61976c8 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/gw/dnslink_requestor.h +@@ -0,0 +1,20 @@ ++#ifndef IPFS_DNSLINK_REQUESTOR_H_ ++#define IPFS_DNSLINK_REQUESTOR_H_ ++ ++#include "requestor.h" ++ ++namespace ipfs { ++class ContextApi; ++} ++ ++namespace ipfs::gw { ++class DnsLinkRequestor final : public Requestor { ++ public: ++ explicit DnsLinkRequestor(std::shared_ptr); ++ ++ HandleOutcome handle(RequestPtr) override; ++ std::string_view name() const override; ++}; ++} // namespace ipfs::gw ++ ++#endif // IPFS_DNSLINK_REQUESTOR_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/gw/gateway_request.h b/third_party/ipfs_client/include/ipfs_client/gw/gateway_request.h +new file mode 100644 +index 0000000000000..2e792ae9ed044 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/gw/gateway_request.h +@@ -0,0 +1,79 @@ ++#ifndef IPFS_TRUSTLESS_REQUEST_H_ ++#define IPFS_TRUSTLESS_REQUEST_H_ ++ ++#include ++#include ++ ++#include ++#include ++ ++#include ++#include ++#include ++#include ++ ++namespace ipfs { ++class IpfsRequest; ++class Orchestrator; ++namespace ipld { ++class DagNode; ++} ++} // namespace ipfs ++ ++namespace ipfs::gw { ++class Requestor; ++ ++enum class Type : char { ++ Block, ++ Car, ++ Ipns, ++ DnsLink, ++ Providers, ++ Identity, ++ Zombie ++}; ++std::string_view name(Type); ++ ++constexpr std::size_t BLOCK_RESPONSE_BUFFER_SIZE = 2 * 1024 * 1024; ++ ++class GatewayRequest { ++ std::shared_ptr orchestrator_; ++ std::vector> bytes_received_hooks; ++ ++ void ParseNodes(std::string_view, ContextApi* api); ++ ++ public: ++ Type type; ++ std::string main_param; ///< CID, IPNS name, hostname ++ std::string path; ///< For CAR requests ++ std::shared_ptr dependent; ++ std::optional cid; ++ short parallel = 0; ++ std::string affinity; ++ flat_set failures; ++ ++ std::string url_suffix() const; ++ std::string_view accept() const; ++ std::string_view identity_data() const; ++ short timeout_seconds() const; ++ bool is_http() const; ++ std::optional max_response_size() const; ++ std::optional describe_http(std::string_view) const; ++ std::string debug_string() const; ++ void orchestrator(std::shared_ptr const&); ++ ++ bool RespondSuccessfully(std::string_view, ++ std::shared_ptr const& api); ++ void Hook(std::function); ++ bool PartiallyRedundant() const; ++ ++ static std::shared_ptr fromIpfsPath(SlashDelimited); ++}; ++ ++} // namespace ipfs::gw ++ ++inline std::ostream& operator<<(std::ostream& s, ipfs::gw::Type t) { ++ return s << name(t); ++} ++ ++#endif // IPFS_TRUSTLESS_REQUEST_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/gw/inline_request_handler.h b/third_party/ipfs_client/include/ipfs_client/gw/inline_request_handler.h +new file mode 100644 +index 0000000000000..0301c561c5735 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/gw/inline_request_handler.h +@@ -0,0 +1,14 @@ ++#ifndef IPFS_INLINE_REQUEST_HANDLER_H_ ++#define IPFS_INLINE_REQUEST_HANDLER_H_ ++ ++#include "requestor.h" ++ ++namespace ipfs::gw { ++class InlineRequestHandler final : public Requestor { ++ public: ++ HandleOutcome handle(RequestPtr) override; ++ std::string_view name() const override; ++}; ++} // namespace ipfs::gw ++ ++#endif // IPFS_INLINE_REQUEST_HANDLER_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/gw/requestor.h b/third_party/ipfs_client/include/ipfs_client/gw/requestor.h +new file mode 100644 +index 0000000000000..634c36730b1ea +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/gw/requestor.h +@@ -0,0 +1,55 @@ ++#ifndef IPFS_REQUESTOR_H_ ++#define IPFS_REQUESTOR_H_ ++ ++#include ++#include ++#include ++ ++namespace ipfs::ipld { ++class DagNode; ++} ++namespace ipfs { ++class ContextApi; ++struct Response; ++} // namespace ipfs ++ ++namespace ipfs::gw { ++class GatewayRequest; ++using RequestPtr = std::shared_ptr; ++ ++class Requestor : public std::enable_shared_from_this { ++ protected: ++ Requestor() {} ++ ++ friend class RequestorPool; ++ enum class HandleOutcome : char { ++ NOT_HANDLED = 'N', ++ PENDING = 'P', ++ DONE = 'D', ++ PARALLEL = 'L', ++ MAYBE_LATER = 'M' ++ }; ++ virtual HandleOutcome handle(RequestPtr) = 0; ++ ++ void definitive_failure(RequestPtr) const; ++ void forward(RequestPtr) const; ++ ++ std::shared_ptr api_; ++ ++ public: ++ using RequestPtr = ::ipfs::gw::RequestPtr; ++ virtual std::string_view name() const = 0; ++ ++ virtual ~Requestor() noexcept {} ++ void request(std::shared_ptr); ++ Requestor& or_else(std::shared_ptr p); ++ void api(std::shared_ptr); ++ ++ void TestAccess(void*); ++ ++ private: ++ std::shared_ptr next_; ++}; ++} // namespace ipfs::gw ++ ++#endif // IPFS_REQUESTOR_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/gw/terminating_requestor.h b/third_party/ipfs_client/include/ipfs_client/gw/terminating_requestor.h +new file mode 100644 +index 0000000000000..3fe7a01e752f5 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/gw/terminating_requestor.h +@@ -0,0 +1,15 @@ ++#ifndef IPFS_TERMINATING_REQUESTOR_H_ ++#define IPFS_TERMINATING_REQUESTOR_H_ ++ ++#include "requestor.h" ++ ++namespace ipfs::gw { ++class TerminatingRequestor : public Requestor { ++ public: ++ using HandleOutcome = Requestor::HandleOutcome; ++ std::string_view name() const override; ++ HandleOutcome handle(RequestPtr) override; ++}; ++} // namespace ipfs::gw ++ ++#endif // IPFS_TERMINATING_REQUESTOR_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/http_request_description.h b/third_party/ipfs_client/include/ipfs_client/http_request_description.h +new file mode 100644 +index 0000000000000..f3f07d58ea199 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/http_request_description.h +@@ -0,0 +1,20 @@ ++#ifndef IPFS_HTTP_REQUEST_DESCRIPTION_H_ ++#define IPFS_HTTP_REQUEST_DESCRIPTION_H_ ++ ++#include ++#include ++ ++#include ++ ++namespace ipfs { ++struct HttpRequestDescription { ++ std::string url; ++ int timeout_seconds; ++ std::string accept; ++ std::optional max_response_size; ++ bool operator==(HttpRequestDescription const&) const; ++ bool operator<(HttpRequestDescription const&) const; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_HTTP_REQUEST_DESCRIPTION_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/identity_cid.h b/third_party/ipfs_client/include/ipfs_client/identity_cid.h +new file mode 100644 +index 0000000000000..29efd30d1c6b2 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/identity_cid.h +@@ -0,0 +1,14 @@ ++#ifndef IPFS_IDENTITY_CID_H_ ++#define IPFS_IDENTITY_CID_H_ 1 ++ ++#include ++ ++#include ++ ++namespace ipfs { ++namespace id_cid { ++ipfs::Cid forText(std::string_view); ++} // namespace id_cid ++} // namespace ipfs ++ ++#endif +diff --git a/third_party/ipfs_client/include/ipfs_client/ipfs_request.h b/third_party/ipfs_client/include/ipfs_client/ipfs_request.h +new file mode 100644 +index 0000000000000..eda8bdfa7010b +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/ipfs_request.h +@@ -0,0 +1,33 @@ ++#ifndef IPFS_IPFS_REQUEST_H_ ++#define IPFS_IPFS_REQUEST_H_ ++ ++#include ++ ++#include ++#include ++#include ++ ++namespace ipfs { ++struct Response; ++class IpfsRequest { ++ public: ++ using Finisher = std::function; ++ ++ private: ++ std::string path_; ++ Finisher callback_; ++ std::size_t waiting_ = 0UL; ++ ++ public: ++ IpfsRequest(std::string path, Finisher); ++ SlashDelimited path() const { return SlashDelimited{path_}; } ++ void finish(Response& r); ++ void till_next(std::size_t); ++ bool ready_after(); ++ void new_path(std::string_view); ++ ++ static std::shared_ptr fromUrl(std::string url, Finisher); ++}; ++} // namespace ipfs ++ ++#endif // IPFS_IPFS_REQUEST_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/ipld/dag_node.h b/third_party/ipfs_client/include/ipfs_client/ipld/dag_node.h +new file mode 100644 +index 0000000000000..1c66f4fd1c755 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/ipld/dag_node.h +@@ -0,0 +1,102 @@ ++#ifndef IPFS_DAG_NODE_H_ ++#define IPFS_DAG_NODE_H_ ++ ++#include "link.h" ++#include "resolution_state.h" ++ ++#include ++#include ++#include ++#include ++ ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace ipfs { ++class PbDag; ++class ContextApi; ++struct ValidatedIpns; ++} // namespace ipfs ++namespace libp2p::multi { ++struct ContentIdentifier; ++} ++namespace ipfs::ipld { ++ ++using NodePtr = std::shared_ptr; ++class DirShard; ++ ++struct MoreDataNeeded { ++ MoreDataNeeded(std::string one) : ipfs_abs_paths_{{one}} {} ++ template ++ MoreDataNeeded(Range const& many) ++ : ipfs_abs_paths_(many.begin(), many.end()) {} ++ std::vector ipfs_abs_paths_; ++ bool insist_on_car = false; ++}; ++enum class ProvenAbsent {}; ++struct PathChange { ++ std::string new_path; ++}; ++ ++using ResolveResult = ++ std::variant; ++/** ++ * @brief A block, an IPNS record, etc. ++ */ ++class DagNode : public std::enable_shared_from_this { ++ Link* FindChild(std::string_view); ++ static void Descend(ResolutionState&); ++ ++ protected: ++ std::vector> links_; ++ std::shared_ptr api_; ++ ++ ///< When the next path element is what's needed, and it should already be a ++ ///< link known about... ++ ResolveResult CallChild(ResolutionState&); ++ ++ ///< As before, but it might be possible to create on the fly if not known ++ ResolveResult CallChild(ResolutionState&, ++ std::function gen_child); ++ ++ ///< When the child's name is not the next element in the path, but it must be ++ ///< known about. e.g. index.html for a path ending in a directory ++ ResolveResult CallChild(ResolutionState&, std::string_view link_key); ++ ++ ///< Add the link if not present, then CallChild(ResolutionState) ++ ResolveResult CallChild(ResolutionState&, ++ std::string_view link_key, ++ std::string_view block_key); ++ ++ public: ++ virtual ResolveResult resolve(ResolutionState& params) = 0; ++ ResolveResult resolve(SlashDelimited initial_path, BlockLookup); ++ ++ static NodePtr fromBytes(std::shared_ptr const& api, ++ Cid const&, ++ ByteView bytes); ++ static NodePtr fromBytes(std::shared_ptr const& api, ++ Cid const&, ++ std::string_view bytes); ++ static NodePtr fromBlock(PbDag const&); ++ static NodePtr fromIpnsRecord(ValidatedIpns const&); ++ ++ virtual ~DagNode() noexcept {} ++ ++ virtual NodePtr rooted(); ++ virtual NodePtr deroot(); ++ virtual DirShard* as_hamt(); // Wish I had access to dynamic_cast ++ ++ void set_api(std::shared_ptr); ++}; ++} // namespace ipfs::ipld ++ ++std::ostream& operator<<(std::ostream&, ipfs::ipld::PathChange const&); ++ ++#endif // IPFS_DAG_NODE_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/ipld/link.h b/third_party/ipfs_client/include/ipfs_client/ipld/link.h +new file mode 100644 +index 0000000000000..a0d290b25dd3d +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/ipld/link.h +@@ -0,0 +1,22 @@ ++#ifndef IPFS_LINK_H_ ++#define IPFS_LINK_H_ ++ ++#include ++#include ++ ++namespace ipfs::ipld { ++ ++class DagNode; ++using Ptr = std::shared_ptr; ++ ++class Link { ++ public: ++ std::string cid; ++ Ptr node; ++ ++ Link(std::string); ++ explicit Link(std::string, std::shared_ptr); ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_LINK_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/ipld/resolution_state.h b/third_party/ipfs_client/include/ipfs_client/ipld/resolution_state.h +new file mode 100644 +index 0000000000000..82e330cea4355 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/ipld/resolution_state.h +@@ -0,0 +1,36 @@ ++#ifndef IPFS_RESOLUTION_STATE_H_ ++#define IPFS_RESOLUTION_STATE_H_ ++ ++#include ++ ++#include ++#include ++ ++namespace ipfs { ++class ContextApi; ++} ++ ++namespace ipfs::ipld { ++class DagNode; ++using NodePtr = std::shared_ptr; ++using BlockLookup = std::function; ++ ++class ResolutionState { ++ friend class DagNode; ++ std::string resolved_path_components; ++ SlashDelimited unresolved_path; ++ BlockLookup get_available_block; ++ ++ public: ++ SlashDelimited MyPath() const; ++ SlashDelimited PathToResolve() const; ++ bool IsFinalComponent() const; ++ std::string NextComponent(ContextApi const*) const; ++ NodePtr GetBlock(std::string const& block_key) const; ++ ++ ResolutionState WithPath(std::string_view) const; ++ ResolutionState RestartResolvedPath() const; ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_RESOLUTION_STATE_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/ipns_cbor_entry.h b/third_party/ipfs_client/include/ipfs_client/ipns_cbor_entry.h +new file mode 100644 +index 0000000000000..230339793543c +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/ipns_cbor_entry.h +@@ -0,0 +1,21 @@ ++#ifndef IPFS_IPNS_CBOR_ENTRY_H_ ++#define IPFS_IPNS_CBOR_ENTRY_H_ ++ ++#include ++#include ++ ++namespace ipfs { ++/*! ++ * \brief Parsed out data contained in the CBOR data of an IPNS record. ++ */ ++struct IpnsCborEntry { ++ std::string value; ///< The "value" (target) the name points at ++ std::string validity; ///< Value to compare for validity (i.e. expiration) ++ std::uint64_t validityType; ///< Way to deterimine current validity ++ std::uint64_t sequence; ///< Distinguish other IPNS records for the same name ++ std::uint64_t ttl; ///< Recommended caching time ++}; ++ ++} // namespace ipfs ++ ++#endif // IPFS_IPNS_CBOR_ENTRY_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/ipns_names.h b/third_party/ipfs_client/include/ipfs_client/ipns_names.h +new file mode 100644 +index 0000000000000..b611365b87874 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/ipns_names.h +@@ -0,0 +1,69 @@ ++#ifndef IPNS_NAME_RESOLVER_H_ ++#define IPNS_NAME_RESOLVER_H_ ++ ++#include ++#include ++ ++#include ++ ++namespace ipfs { ++ ++/*! ++ * \brief Fast synchronous access to IPNS & DNSLink name resolution ++ */ ++class IpnsNames { ++ flat_map names_; ++ ++ public: ++ IpnsNames(); ++ ~IpnsNames(); ++ ++ /*! ++ * \brief Get the already-known "value"/target of a given name ++ * \param name - either a mb-mf IPNS (key) name, or a host with DNSLink ++ * \return ++ * * if resolution is incomplete: "" ++ * * if it is known not to resolve: kNoSuchName ++ * * otherwise an IPFS path witout leading /, e.g.: ++ * - ipfs/bafybeicfqz46dj67nkhxaylqd5sknnidsr4oaw4hhsjrgdmcwt73sow2d4/ ++ * - ipns/k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8 ++ */ ++ std::string_view NameResolvedTo(std::string_view name) const; ++ ++ /*! ++ * \brief Store an IPNS record that already validated for this name ++ * \param name - The name that resolves with this ++ * \param rec - The record modulo validation bits ++ */ ++ void AssignName(std::string const& name, ValidatedIpns rec); ++ ++ /*! ++ * \brief Assign a target path to a DNSLink host ++ * \param host - The original host NOT including a "_dnslink." prefix ++ * \param target - an IPFS path witout leading / ++ */ ++ void AssignDnsLink(std::string const& host, std::string_view target); ++ ++ /*! ++ * \brief Store the definitive absence of a resolution ++ * \details This is useful because code will check resolution here before ++ * trying to resolve it fresh again, and you can stop that if you know ++ * it will never work. ++ */ ++ void NoSuchName(std::string const& name); ++ ++ /*! ++ * \brief Fetch the all the stored IPNS record data ++ * \param name - the IPNS name it was stored with ++ * \return nullptr if missing, otherwise non-owning pointer to record ++ */ ++ ValidatedIpns const* Entry(std::string const& name); ++ ++ /*! ++ * \brief A special value constant ++ */ ++ static constexpr std::string_view kNoSuchName{"NO_SUCH_NAME"}; ++}; ++} // namespace ipfs ++ ++#endif // IPNS_NAME_RESOLVER_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/ipns_record.h b/third_party/ipfs_client/include/ipfs_client/ipns_record.h +new file mode 100644 +index 0000000000000..a6bd168a4af60 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/ipns_record.h +@@ -0,0 +1,75 @@ ++#ifndef IPFS_IPNS_RECORD_H_ ++#define IPFS_IPNS_RECORD_H_ ++ ++#include ++ ++#include ++ ++#if __has_include() ++#include ++#else ++#include "ipfs_client/keys.pb.h" ++#endif ++ ++#include ++#include ++ ++namespace libp2p::peer { ++class PeerId; ++} ++namespace libp2p::multi { ++struct ContentIdentifier; ++} ++ ++namespace ipfs { ++ ++class Cid; ++class ContextApi; ++ ++constexpr static std::size_t MAX_IPNS_PB_SERIALIZED_SIZE = 10 * 1024; ++ ++std::optional ValidateIpnsRecord(ByteView top_level_bytes, ++ Cid const& name, ++ ContextApi&); ++ ++/*! ++ * \brief Data from IPNS record modulo the verification parts ++ */ ++struct ValidatedIpns { ++ std::string value; ///< The path the record claims the IPNS name points to ++ std::time_t use_until; ///< An expiration timestamp ++ std::time_t cache_until; ///< Inspired by TTL ++ ++ /*! ++ * \brief The version of the record ++ * \details Higher sequence numbers obsolete lower ones ++ */ ++ std::uint64_t sequence; ++ std::int64_t resolution_ms; ///< How long it took to fetch the record ++ ++ /*! ++ * \brief When the record was fetched ++ */ ++ std::time_t fetch_time = std::time(nullptr); ++ std::string gateway_source; ///< Who gave us this record? ++ ++ ValidatedIpns(); ///< Create an invalid default object ++ ValidatedIpns(IpnsCborEntry const&); ++ ValidatedIpns(ValidatedIpns&&); ++ ValidatedIpns(ValidatedIpns const&); ++ ValidatedIpns& operator=(ValidatedIpns const&); ++ ++ std::string Serialize() const; ///< Turn into a well-defined list of bytes ++ ++ /*! ++ * \brief Create a ValidatedIpns from untyped bytes ++ * \param bytes - Output from a former call to Serialize() ++ * \note Is used by disk cache ++ * \return Recreation of the old object ++ */ ++ static ValidatedIpns Deserialize(std::string bytes); ++}; ++ ++} // namespace ipfs ++ ++#endif // IPFS_IPNS_RECORD_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/json_cbor_adapter.h b/third_party/ipfs_client/include/ipfs_client/json_cbor_adapter.h +new file mode 100644 +index 0000000000000..5ed52ad465b0c +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/json_cbor_adapter.h +@@ -0,0 +1,155 @@ ++#ifndef IPFS_JSON_CBOR_ADAPTER_H_ ++#define IPFS_JSON_CBOR_ADAPTER_H_ ++ ++#include ++#include ++ ++#include ++#include ++ ++#if __has_include() ++ ++#include ++#define HAS_JSON_CBOR_ADAPTER 1 ++ ++namespace ipfs { ++// LCOV_EXCL_START ++class JsonCborAdapter final : public DagCborValue, public DagJsonValue { ++ nlohmann::json data_; ++ ++ public: ++ using Cid = ipfs::Cid; ++ JsonCborAdapter(nlohmann::json data) : data_{data} { ++ if (data_.is_array() && data_.size() == 1UL) { ++ data_ = data_[0]; ++ } ++ } ++ std::unique_ptr at(std::string_view k) const override { ++ if (data_.is_object() && data_.contains(k)) { ++ return std::make_unique(data_.at(k)); ++ } ++ return {}; ++ } ++ std::unique_ptr operator[](std::string_view k) const override { ++ if (data_.is_object() && data_.contains(k)) { ++ return std::make_unique(data_[k]); ++ } ++ return {}; ++ } ++ std::optional as_unsigned() const override { ++ if (data_.is_number_unsigned()) { ++ return data_.get(); ++ } ++ return std::nullopt; ++ } ++ std::optional as_signed() const { ++ if (data_.is_number_integer()) { ++ return data_.get(); ++ } else if (auto ui = as_unsigned()) { ++ if (*ui <= std::numeric_limits::max()) { ++ return static_cast(*ui); ++ } ++ } ++ return std::nullopt; ++ } ++ std::optional as_float() const override { ++ if (data_.is_number_float()) { ++ return data_.get(); ++ } ++ return std::nullopt; ++ } ++ std::optional as_string() const override { ++ if (data_.is_string()) { ++ return data_.get(); ++ } ++ return std::nullopt; ++ } ++ std::optional get_if_string() const override { ++ return as_string(); ++ } ++ std::optional as_bool() const override { ++ if (data_.is_boolean()) { ++ return data_.get(); ++ } ++ return std::nullopt; ++ } ++ std::optional> as_bytes() const override { ++ if (data_.is_binary()) { ++ return data_.get_binary(); ++ } ++ return std::nullopt; ++ } ++ std::optional as_link() const override { ++ if (!data_.is_binary()) { ++ return std::nullopt; ++ } ++ auto& bin = data_.get_binary(); ++ if (!bin.has_subtype() || bin.subtype() != 42) { ++ return std::nullopt; ++ } ++ if (bin.size() < 6) { ++ return std::nullopt; ++ } ++ if (bin[0]) { ++ return std::nullopt; ++ } ++ auto p = reinterpret_cast(bin.data()) + 1UL; ++ Cid from_binary(ByteView{p, bin.size() - 1UL}); ++ if (from_binary.valid()) { ++ return from_binary; ++ } else { ++ return std::nullopt; ++ } ++ } ++ bool is_map() const override {return data_.is_object();} ++ bool is_array() const override {return data_.is_array();} ++ void iterate_map(MapElementCallback cb) const override { ++ if (!is_map()) { ++ return; ++ } ++ for (auto& [k,v] : data_.items()) { ++ JsonCborAdapter el(v); ++ cb(k, el); ++ } ++ } ++ void iterate_array(ArrayElementCallback cb) const override { ++ if (!is_array()) { ++ return; ++ } ++ for (auto& v : data_) { ++ JsonCborAdapter el(v); ++ cb(el); ++ } ++ } ++ std::string pretty_print() const override { ++ std::ostringstream result; ++ result << std::setw(2) << data_; ++ return result.str(); ++ } ++ std::optional> object_keys() const override { ++ if (!data_.is_object()) { ++ return std::nullopt; ++ } ++ std::vector rv; ++ for (auto& [k, v] : data_.items()) { ++ rv.push_back(k); ++ } ++ return rv; ++ } ++ bool iterate_list( ++ std::function cb) const override { ++ if (!data_.is_array()) { ++ return false; ++ } ++ for (auto& v : data_) { ++ JsonCborAdapter wrap(v); ++ cb(wrap); ++ } ++ return true; ++ } ++}; ++} // namespace ipfs ++ ++#endif ++ ++#endif // IPFS_JSON_CBOR_ADAPTER_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/logger.h b/third_party/ipfs_client/include/ipfs_client/logger.h +new file mode 100644 +index 0000000000000..35191ac5f832c +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/logger.h +@@ -0,0 +1,34 @@ ++#ifndef IPFS_LOGGER_H_ ++#define IPFS_LOGGER_H_ ++ ++#include ++ ++namespace ipfs::log { ++ ++enum class Level { ++ TRACE = -2, ++ DEBUG = -1, ++ INFO = 0, ++ WARN = 1, ++ ERROR = 2, ++ FATAL = 3, ++ OFF ++}; ++ ++void SetLevel(Level); ++ ++using Handler = void (*)(std::string const&, char const*, int, Level); ++void SetHandler(Handler); ++ ++void DefaultHandler(std::string const& message, ++ char const* source_file, ++ int source_line, ++ Level for_prefix); ++ ++std::string_view LevelDescriptor(Level); ++ ++bool IsInitialized(); ++ ++} // namespace ipfs::log ++ ++#endif // LOGGER_H +diff --git a/third_party/ipfs_client/include/ipfs_client/multi_base.h b/third_party/ipfs_client/include/ipfs_client/multi_base.h +new file mode 100644 +index 0000000000000..8c09b97345635 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/multi_base.h +@@ -0,0 +1,42 @@ ++#ifndef IPFS_MB_PREFIXES_H_ ++#define IPFS_MB_PREFIXES_H_ ++ ++#include ++ ++#include ++#include ++#include ++#include ++ ++namespace ipfs::mb { ++ ++// https://github.com/multiformats/multibase/blob/master/multibase.csv ++enum class Code : char { ++ IDENTITY = '\0', ++ UNSUPPORTED = '1', ++ BASE16_LOWER = 'f', ++ BASE16_UPPER = 'F', ++ BASE32_LOWER = 'b', ++ BASE32_UPPER = 'B', ++ BASE36_LOWER = 'k', ++ BASE36_UPPER = 'K', ++ BASE58_BTC = 'z', ++ BASE64 = 'm' ++}; ++Code CodeFromPrefix(char c); ++std::string_view GetName(Code); ++ ++using Decoder = std::vector (*)(std::string_view); ++using Encoder = std::string (*)(ByteView); ++struct Codec { ++ Decoder const decode; ++ Encoder const encode; ++ std::string_view const name; ++ static Codec const* Get(Code); ++}; ++ ++std::string encode(Code, ByteView); ++std::optional> decode(std::string_view mb_str); ++} // namespace ipfs::mb ++ ++#endif // IPFS_MB_PREFIXES_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/multi_hash.h b/third_party/ipfs_client/include/ipfs_client/multi_hash.h +new file mode 100644 +index 0000000000000..6ed78f5e674dc +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/multi_hash.h +@@ -0,0 +1,32 @@ ++#ifndef IPFS_MULTI_HASH_H_ ++#define IPFS_MULTI_HASH_H_ ++ ++#include ++ ++#include ++ ++namespace ipfs { ++enum class HashType { INVALID = -1, IDENTITY = 0, SHA2_256 = 0X12 }; ++constexpr std::uint16_t MaximumHashLength = 127; ++ ++HashType Validate(HashType); ++std::string_view GetName(HashType); ++class MultiHash { ++ public: ++ MultiHash() = default; ++ explicit MultiHash(ByteView); ++ explicit MultiHash(HashType, ByteView digest); ++ ++ bool ReadPrefix(ByteView&); ++ ++ bool valid() const; ++ HashType type() const { return type_; } ++ ByteView digest() const { return hash_; } ++ ++ private: ++ HashType type_ = HashType::INVALID; ++ std::vector hash_; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_MULTI_HASH_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/multicodec.h b/third_party/ipfs_client/include/ipfs_client/multicodec.h +new file mode 100644 +index 0000000000000..bf8d89b6c27e2 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/multicodec.h +@@ -0,0 +1,23 @@ ++#ifndef IPFS_MUTLICODEC_H_ ++#define IPFS_MUTLICODEC_H_ ++ ++#include ++#include ++ ++#include ++ ++namespace ipfs { ++enum class MultiCodec : std::uint32_t { ++ INVALID = std::numeric_limits::max(), ++ IDENTITY = 0x00, ++ RAW = 0x55, ++ DAG_PB = 0x70, ++ DAG_CBOR = 0x71, ++ LIBP2P_KEY = 0x72, ++ DAG_JSON = 0x0129, ++}; ++MultiCodec Validate(MultiCodec); ++std::string_view GetName(MultiCodec); ++} // namespace ipfs ++ ++#endif // IPFS_MUTLICODEC_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/orchestrator.h b/third_party/ipfs_client/include/ipfs_client/orchestrator.h +new file mode 100644 +index 0000000000000..f204dde799b3e +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/orchestrator.h +@@ -0,0 +1,45 @@ ++#ifndef IPFS_ORCHESTRATOR_H_ ++#define IPFS_ORCHESTRATOR_H_ ++ ++#include "ipfs_client/ipld/dag_node.h" ++ ++#include ++#include ++ ++#include ++#include ++ ++namespace ipfs { ++ ++class ContextApi; ++ ++class Orchestrator : public std::enable_shared_from_this { ++ public: ++ using GatewayAccess = ++ std::function)>; ++ using MimeDetection = std::function< ++ std::string(std::string, std::string_view, std::string const&)>; ++ explicit Orchestrator(std::shared_ptr requestor, ++ std::shared_ptr = {}); ++ void build_response(std::shared_ptr); ++ bool add_node(std::string key, ipld::NodePtr); ++ bool has_key(std::string const& k) const; ++ ++ private: ++ flat_map dags_; ++ // GatewayAccess gw_requestor_; ++ std::shared_ptr api_; ++ std::shared_ptr requestor_; ++ ++ void from_tree(std::shared_ptr, ++ ipld::NodePtr&, ++ SlashDelimited, ++ std::string const&); ++ bool gw_request(std::shared_ptr, ++ SlashDelimited path, ++ std::string const& aff); ++ std::string sniff(SlashDelimited, std::string const&) const; ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_ORCHESTRATOR_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/response.h b/third_party/ipfs_client/include/ipfs_client/response.h +new file mode 100644 +index 0000000000000..3c277994d8b9c +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/response.h +@@ -0,0 +1,27 @@ ++#ifndef IPFS_RESPONSE_H_ ++#define IPFS_RESPONSE_H_ ++ ++#include ++ ++#include ++#include ++#include ++ ++namespace ipfs { ++ ++struct Response { ++ std::string mime_; ++ std::uint16_t status_; ++ std::string body_; ++ std::string location_; ++ ++ static Response PLAIN_NOT_FOUND; ++ static Response IMMUTABLY_GONE; ++ static Response HOST_NOT_FOUND; ++ ++ constexpr static std::uint16_t HOST_NOT_FOUND_STATUS = 503; ++}; ++ ++} // namespace ipfs ++ ++#endif // IPFS_RESPONSE_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/signing_key_type.h b/third_party/ipfs_client/include/ipfs_client/signing_key_type.h +new file mode 100644 +index 0000000000000..4a74ad0f6967b +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/signing_key_type.h +@@ -0,0 +1,14 @@ ++#ifndef IPFS_SIGNING_KEY_TYPE_H_ ++#define IPFS_SIGNING_KEY_TYPE_H_ ++ ++namespace ipfs { ++enum class SigningKeyType : int { ++ RSA, ++ Ed25519, ++ Secp256k1, ++ ECDSA, ++ KeyTypeCount ++}; ++} ++ ++#endif // IPFS_SIGNING_KEY_TYPE_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/url_spec.h b/third_party/ipfs_client/include/ipfs_client/url_spec.h +new file mode 100644 +index 0000000000000..a61aec25d5968 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/url_spec.h +@@ -0,0 +1,24 @@ ++#ifndef IPFS_URL_SPEC_H_ ++#define IPFS_URL_SPEC_H_ ++ ++// TODO - Give more thought to how this interplays with gw::Request ++ ++#include ++#include ++ ++namespace ipfs { ++struct UrlSpec { ++ std::string suffix; ++ std::string_view accept; ++ ++ bool operator<(UrlSpec const& rhs) const { ++ if (suffix != rhs.suffix) { ++ return suffix < rhs.suffix; ++ } ++ return accept < rhs.accept; ++ } ++ bool none() const { return suffix.empty(); } ++}; ++} // namespace ipfs ++ ++#endif // IPFS_URL_SPEC_H_ +diff --git a/third_party/ipfs_client/include/libp2p/common/types.hpp b/third_party/ipfs_client/include/libp2p/common/types.hpp +new file mode 100644 +index 0000000000000..a112d1bf5d3db +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/common/types.hpp +@@ -0,0 +1,39 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef LIBP2P_P2P_COMMON_TYPES_HPP ++#define LIBP2P_P2P_COMMON_TYPES_HPP ++ ++#include "vocab/byte_view.h" ++ ++#include ++#include ++#include ++#include ++ ++namespace libp2p::common { ++/** ++ * Sequence of bytes ++ */ ++using ByteArray = std::vector; ++// using ByteArray = std::string; ++ ++template ++void append(Collection& c, Item&& g) { ++ c.insert(c.end(), g.begin(), g.end()); ++} ++ ++template ++void append(Collection& c, char g) { ++ c.push_back(g); ++} ++ ++/// Hash256 as a sequence of 32 bytes ++using Hash256 = std::array; ++/// Hash512 as a sequence of 64 bytes ++using Hash512 = std::array; ++} // namespace libp2p::common ++ ++#endif // LIBP2P_P2P_COMMON_TYPES_HPP +diff --git a/third_party/ipfs_client/include/libp2p/crypto/key.h b/third_party/ipfs_client/include/libp2p/crypto/key.h +new file mode 100644 +index 0000000000000..8198e41122fdd +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/crypto/key.h +@@ -0,0 +1,100 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef LIBP2P_LIBP2P_CRYPTO_KEY_HPP ++#define LIBP2P_LIBP2P_CRYPTO_KEY_HPP ++ ++#include ++ ++#include "libp2p/common/types.hpp" ++ ++namespace libp2p::crypto { ++ ++using Buffer = libp2p::common::ByteArray; ++ ++struct Key { ++ /** ++ * Supported types of all keys ++ */ ++ enum class Type { ++ UNSPECIFIED = 100, ++ RSA = 0, ++ Ed25519 = 1, ++ Secp256k1 = 2, ++ ECDSA = 3 ++ }; ++ ++ Key(Type, std::vector); ++ ~Key() noexcept; ++ Type type = Type::UNSPECIFIED; ///< key type ++ std::vector data{}; ///< key content ++}; ++ ++inline bool operator==(const Key& lhs, const Key& rhs) { ++ return lhs.type == rhs.type && lhs.data == rhs.data; ++} ++ ++inline bool operator!=(const Key& lhs, const Key& rhs) { ++ return !(lhs == rhs); ++} ++ ++struct PublicKey : public Key {}; ++ ++struct PrivateKey : public Key {}; ++ ++struct KeyPair { ++ PublicKey publicKey; ++ PrivateKey privateKey; ++}; ++ ++using Signature = std::vector; ++ ++inline bool operator==(const KeyPair& a, const KeyPair& b) { ++ return a.publicKey == b.publicKey && a.privateKey == b.privateKey; ++} ++ ++/** ++ * Result of ephemeral key generation ++ * ++struct EphemeralKeyPair { ++ Buffer ephemeral_public_key; ++ std::function(Buffer)> shared_secret_generator; ++}; ++*/ ++ ++/** ++ * Type of the stretched key ++ * ++struct StretchedKey { ++ Buffer iv; ++ Buffer cipher_key; ++ Buffer mac_key; ++}; ++*/ ++} // namespace libp2p::crypto ++ ++namespace std { ++template <> ++struct hash { ++ size_t operator()(const libp2p::crypto::Key& x) const; ++}; ++ ++template <> ++struct hash { ++ size_t operator()(const libp2p::crypto::PrivateKey& x) const; ++}; ++ ++template <> ++struct hash { ++ size_t operator()(const libp2p::crypto::PublicKey& x) const; ++}; ++ ++template <> ++struct hash { ++ size_t operator()(const libp2p::crypto::KeyPair& x) const; ++}; ++} // namespace std ++ ++#endif // LIBP2P_LIBP2P_CRYPTO_KEY_HPP +diff --git a/third_party/ipfs_client/include/libp2p/crypto/protobuf/protobuf_key.hpp b/third_party/ipfs_client/include/libp2p/crypto/protobuf/protobuf_key.hpp +new file mode 100644 +index 0000000000000..1a0d7ae7a2d4e +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/crypto/protobuf/protobuf_key.hpp +@@ -0,0 +1,29 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef KAGOME_PROTOBUF_KEY_HPP ++#define KAGOME_PROTOBUF_KEY_HPP ++ ++// #include ++ ++#include ++ ++#include ++ ++namespace libp2p::crypto { ++/** ++ * Strict type for key, which is encoded into Protobuf format ++ */ ++struct ProtobufKey { //: public boost::equality_comparable { ++ explicit ProtobufKey(std::vector key); ++ ~ProtobufKey() noexcept; ++ ++ std::vector key; ++ ++ bool operator==(const ProtobufKey& other) const { return key == other.key; } ++}; ++} // namespace libp2p::crypto ++ ++#endif // KAGOME_PROTOBUF_KEY_HPP +diff --git a/third_party/ipfs_client/include/libp2p/multi/multibase_codec.hpp b/third_party/ipfs_client/include/libp2p/multi/multibase_codec.hpp +new file mode 100644 +index 0000000000000..c7b9cbd1f7d40 +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/multi/multibase_codec.hpp +@@ -0,0 +1,65 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef LIBP2P_MULTIBASE_HPP ++#define LIBP2P_MULTIBASE_HPP ++ ++#include "vocab/expected.h" ++ ++#include ++#include ++#include ++ ++#include ++ ++namespace libp2p::multi { ++/** ++ * Allows to distinguish between different base-encoded binaries ++ * See more: https://github.com/multiformats/multibase ++ */ ++class MultibaseCodec { ++ public: ++ enum class Error { UNSUPPORTED_BASE = 1, INPUT_TOO_SHORT, BASE_CODEC_ERROR }; ++ ++ using ByteBuffer = common::ByteArray; ++ using FactoryResult = ipfs::expected; ++ ++ virtual ~MultibaseCodec() = default; ++ /** ++ * Encodings, supported by this Multibase ++ * @sa https://github.com/multiformats/multibase#multibase-table ++ */ ++ enum class Encoding : char { ++ BASE16_LOWER = 'f', ++ BASE16_UPPER = 'F', ++ BASE32_LOWER = 'b', ++ BASE32_UPPER = 'B', ++ BASE36 = 'k', ++ BASE58 = 'z', ++ BASE64 = 'm' ++ }; ++ ++ /** ++ * Encode the incoming bytes ++ * @param bytes to be encoded ++ * @param encoding - base of the desired encoding ++ * @return encoded string WITH an encoding prefix ++ */ ++ virtual std::string encode(const ByteBuffer& bytes, ++ Encoding encoding) const = 0; ++ ++ /** ++ * Decode the incoming string ++ * @param string to be decoded ++ * @return bytes, if decoding was successful, error otherwise ++ */ ++ virtual FactoryResult decode(std::string_view string) const = 0; ++}; ++ ++bool case_critical(MultibaseCodec::Encoding); ++ ++} // namespace libp2p::multi ++ ++#endif // LIBP2P_MULTIBASE_HPP +diff --git a/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base16.h b/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base16.h +new file mode 100644 +index 0000000000000..72a74237eb2ee +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base16.h +@@ -0,0 +1,24 @@ ++#ifndef IPFS_BASE32_H_ ++#define IPFS_BASE32_H_ ++ ++#include "base_error.hpp" ++ ++#include ++#include ++ ++#include ++ ++#include ++ ++namespace ipfs::base16 { ++std::string encodeLower(ByteView bytes); ++std::string encodeUpper(ByteView bytes); ++ ++using libp2p::common::ByteArray; ++using libp2p::multi::detail::BaseError; ++using Decoded = ipfs::expected; ++Decoded decode(std::string_view string); ++ ++} // namespace ipfs::base16 ++ ++#endif // IPFS_BASE32_H_ +diff --git a/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base32.hpp b/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base32.hpp +new file mode 100644 +index 0000000000000..c24dc59d54121 +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base32.hpp +@@ -0,0 +1,52 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef LIBP2P_BASE32_HPP ++#define LIBP2P_BASE32_HPP ++ ++#include "base_error.hpp" ++ ++#include ++#include ++ ++/** ++ * Encode/decode to/from base32 format ++ * Implementation is taken from ++ * https://github.com/mjg59/tpmtotp/blob/master/base32.c ++ */ ++namespace libp2p::multi::detail { ++ ++/** ++ * Encode bytes to base32 uppercase string ++ * @param bytes to be encoded ++ * @return encoded string ++ */ ++std::string encodeBase32Upper(ipfs::ByteView bytes); ++/** ++ * Encode bytes to base32 lowercase string ++ * @param bytes to be encoded ++ * @return encoded string ++ */ ++std::string encodeBase32Lower(ipfs::ByteView bytes); ++ ++/** ++ * Decode base32 uppercase to bytes ++ * @param string to be decoded ++ * @return decoded bytes in case of success ++ */ ++ipfs::expected decodeBase32Upper( ++ std::string_view string); ++ ++/** ++ * Decode base32 lowercase string to bytes ++ * @param string to be decoded ++ * @return decoded bytes in case of success ++ */ ++ipfs::expected decodeBase32Lower( ++ std::string_view string); ++ ++} // namespace libp2p::multi::detail ++ ++#endif // LIBP2P_BASE32_HPP +diff --git a/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base_error.hpp b/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base_error.hpp +new file mode 100644 +index 0000000000000..a0ab1b6c54be5 +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base_error.hpp +@@ -0,0 +1,24 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef LIBP2P_BASE_ERROR_HPP ++#define LIBP2P_BASE_ERROR_HPP ++ ++namespace libp2p::multi::detail { ++ ++enum class BaseError { ++ INVALID_BASE58_INPUT = 1, ++ INVALID_BASE64_INPUT, ++ INVALID_BASE32_INPUT, ++ INVALID_BASE36_INPUT, ++ NON_UPPERCASE_INPUT, ++ NON_LOWERCASE_INPUT, ++ UNIMPLEMENTED_MULTIBASE, ++ INVALID_BASE16_INPUT ++}; ++ ++} ++ ++#endif // LIBP2P_BASE_ERROR_HPP +diff --git a/third_party/ipfs_client/include/libp2p/multi/multicodec_type.hpp b/third_party/ipfs_client/include/libp2p/multi/multicodec_type.hpp +new file mode 100644 +index 0000000000000..bda027bb29567 +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/multi/multicodec_type.hpp +@@ -0,0 +1,78 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef LIBP2P_MULTICODECTYPE_HPP ++#define LIBP2P_MULTICODECTYPE_HPP ++ ++#include ++ ++namespace libp2p::multi { ++ ++/** ++ * LibP2P uses "protocol tables" to agree upon the mapping from one multicodec ++ * code. These tables can be application specific, though, like with other ++ * multiformats, there is a globally agreed upon table with common protocols ++ * and formats. ++ */ ++class MulticodecType { ++ public: ++ enum class Code { ++ IDENTITY = 0x00, ++ SHA1 = 0x11, ++ SHA2_256 = 0x12, ++ SHA2_512 = 0x13, ++ SHA3_512 = 0x14, ++ SHA3_384 = 0x15, ++ SHA3_256 = 0x16, ++ SHA3_224 = 0x17, ++ RAW = 0x55, ++ DAG_PB = 0x70, ++ DAG_CBOR = 0x71, ++ LIBP2P_KEY = 0x72, ++ DAG_JSON = 0x0129, ++ FILECOIN_COMMITMENT_UNSEALED = 0xf101, ++ FILECOIN_COMMITMENT_SEALED = 0xf102, ++ }; ++ ++ constexpr static std::string_view getName(Code code) { ++ switch (code) { ++ case Code::IDENTITY: ++ return "identity"; ++ case Code::SHA1: ++ return "sha1"; ++ case Code::SHA2_256: ++ return "sha2-256"; ++ case Code::SHA2_512: ++ return "sha2-512"; ++ case Code::SHA3_224: ++ return "sha3-224"; ++ case Code::SHA3_256: ++ return "sha3-256"; ++ case Code::SHA3_384: ++ return "sha3-384"; ++ case Code::SHA3_512: ++ return "sha3-512"; ++ case Code::RAW: ++ return "raw"; ++ case Code::DAG_PB: ++ return "dag-pb"; ++ case Code::DAG_CBOR: ++ return "dag-cbor"; ++ case Code::DAG_JSON: ++ return "dag-json"; ++ case Code::LIBP2P_KEY: ++ return "libp2p-key"; ++ case Code::FILECOIN_COMMITMENT_UNSEALED: ++ return "fil-commitment-unsealed"; ++ case Code::FILECOIN_COMMITMENT_SEALED: ++ return "fil-commitment-sealed"; ++ } ++ return "unknown"; ++ } ++}; ++ ++} // namespace libp2p::multi ++ ++#endif // LIBP2P_MULTICODECTYPE_HPP +diff --git a/third_party/ipfs_client/include/libp2p/multi/uvarint.hpp b/third_party/ipfs_client/include/libp2p/multi/uvarint.hpp +new file mode 100644 +index 0000000000000..4dd452abffba4 +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/multi/uvarint.hpp +@@ -0,0 +1,98 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef LIBP2P_VARINT_HPP ++#define LIBP2P_VARINT_HPP ++ ++#include "vocab/byte_view.h" ++ ++#include ++ ++#include ++#include ++#include ++ ++namespace libp2p::multi { ++ ++/** ++ * @class Encodes and decodes unsigned integers into and from ++ * variable-length byte arrays using LEB128 algorithm. ++ */ ++class UVarint { ++ public: ++ /** ++ * Constructs a varint from an unsigned integer 'number' ++ * @param number ++ */ ++ explicit UVarint(uint64_t number); ++ ++ /** ++ * Constructs a varint from an array of raw bytes, which are ++ * meant to be an already encoded unsigned varint ++ * @param varint_bytes an array of bytes representing an unsigned varint ++ */ ++ explicit UVarint(ipfs::ByteView varint_bytes); ++ ++ /** ++ * Constructs a varint from an array of raw bytes, which beginning may or ++ * may not be an encoded varint ++ * @param varint_bytes an array of bytes, possibly representing an unsigned ++ * varint ++ */ ++ static std::optional create(ipfs::ByteView varint_bytes); ++ ++ /** ++ * Converts a varint back to a usual unsigned integer. ++ * @return an integer previously encoded to the varint ++ */ ++ uint64_t toUInt64() const; ++ ++ /** ++ * @return an array view to raw bytes of the stored varint ++ */ ++ ipfs::ByteView toBytes() const; ++ ++ std::vector const& toVector() const; ++ ++ std::string toHex() const; ++ ++ /** ++ * Assigns the varint to an unsigned integer, encoding the latter ++ * @param n the integer to encode and store ++ * @return this varint ++ */ ++ UVarint& operator=(uint64_t n); ++ ++ bool operator==(const UVarint& r) const; ++ bool operator!=(const UVarint& r) const; ++ bool operator<(const UVarint& r) const; ++ ++ /** ++ * @return the number of bytes currently stored in a varint ++ */ ++ size_t size() const; ++ ++ /** ++ * @param varint_bytes an array with a raw byte representation of a varint ++ * @return the size of the varint stored in the array, if its content is a ++ * valid varint. Otherwise, the result is undefined ++ */ ++ static size_t calculateSize(ipfs::ByteView varint_bytes); ++ ++ UVarint() = delete; ++ UVarint(UVarint const&); ++ UVarint& operator=(UVarint const&); ++ ~UVarint() noexcept; ++ ++ private: ++ /// private ctor for unsafe creation ++ UVarint(ipfs::ByteView varint_bytes, size_t varint_size); ++ ++ std::vector bytes_{}; ++}; ++ ++} // namespace libp2p::multi ++ ++#endif // LIBP2P_VARINT_HPP +diff --git a/third_party/ipfs_client/include/multibase/algorithm.h b/third_party/ipfs_client/include/multibase/algorithm.h +new file mode 100644 +index 0000000000000..2cea1cabd296e +--- /dev/null ++++ b/third_party/ipfs_client/include/multibase/algorithm.h +@@ -0,0 +1,27 @@ ++#pragma once ++ ++#include ++ ++namespace multibase { ++ ++class algorithm { ++ public: ++ /** Tag identifying algorithms which operate on blocks */ ++ class block_tag {}; ++ ++ /** Tag identifying algorithms which operate on continuous data */ ++ class stream_tag {}; ++ ++ virtual ~algorithm() = default; ++ ++ /** Returns the input size required to decode a single block */ ++ virtual std::size_t block_size() { return 0; } ++ ++ /** Returns the size of a processed block */ ++ virtual std::size_t output_size() { return 0; } ++ ++ /** Processes an input block returning any intermediate result */ ++ virtual std::string process(std::string_view input) = 0; ++}; ++ ++} // namespace multibase +diff --git a/third_party/ipfs_client/include/multibase/basic_algorithm.h b/third_party/ipfs_client/include/multibase/basic_algorithm.h +new file mode 100644 +index 0000000000000..5da225c885fd4 +--- /dev/null ++++ b/third_party/ipfs_client/include/multibase/basic_algorithm.h +@@ -0,0 +1,322 @@ ++/* From: https://github.com/lockblox/multibase ++ * Copyright (c) 2018 markovchainy ++ * MIT License ++ */ ++#pragma once ++ ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++ ++namespace multibase { ++ ++template ++struct traits { ++ static const std::array charset; ++ static const char name[]; ++ static const char padding = 0; ++ using execution_style = algorithm::block_tag; ++}; ++ ++/** Template implementation of base encoding which computes a lookup table at ++ * compile time and avoids the virtual algorithm lookup penalty */ ++template > ++class basic_algorithm { ++ public: ++ class encoder : public algorithm { ++ public: ++ size_t output_size() override; ++ size_t block_size() override; ++ std::string process(std::string_view input) override; ++ ++ private: ++ constexpr size_t input_size() { return ratio.den; } ++ }; ++ ++ class decoder : public algorithm { ++ public: ++ size_t output_size() override; ++ size_t block_size() override; ++ std::string process(std::string_view input) override; ++ ++ private: ++ constexpr size_t input_size() { return ratio.num; } ++ }; ++ ++ private: ++ constexpr static auto first = Traits::charset.cbegin(); ++ constexpr static auto last = Traits::charset.cend(); ++ using CharsetT = decltype(Traits::charset); ++ using value_type = typename CharsetT::value_type; ++ using iterator = typename CharsetT::const_iterator; ++ ++ /** Find a value at compile time */ ++ constexpr static iterator find(iterator b, iterator e, ++ value_type const& v) noexcept { ++ return (b != e && *b != v) ? find(++b, e, v) : b; ++ } ++ ++ /** Determine the character encoding for a given value ++ @return character encoding, or xFF if none such encoding exists */ ++ constexpr static unsigned char getval(unsigned char p) noexcept { ++ return find(first, last, p) == last ++ ? static_cast(255) ++ : static_cast( ++ std::distance(first, find(first, last, p))); ++ } ++ ++ /** Compute base-2 logarithm */ ++ constexpr static std::intmax_t log2(std::intmax_t n) noexcept { ++ return (n == 1) ? 0 : ((n < 2) ? 1 : 1 + log2(n / 2)); ++ } ++ ++ /** encoding as determined by size of character set */ ++ constexpr static auto radix = sizeof(Traits::charset) / sizeof(value_type); ++ /** Ratio of encoded characters per byte */ ++ constexpr static auto ratio = std::ratio{}; ++ /** Map from value to corresponding character in base encoding */ ++ static const std::array valset; ++ ++ constexpr static auto base = T; ++}; ++ ++template ++const std::array basic_algorithm::valset = { ++ getval(0), getval(1), getval(2), getval(3), getval(4), ++ getval(5), getval(6), getval(7), getval(8), getval(9), ++ getval(10), getval(11), getval(12), getval(13), getval(14), ++ getval(15), getval(16), getval(17), getval(18), getval(19), ++ getval(20), getval(21), getval(22), getval(23), getval(24), ++ getval(25), getval(26), getval(27), getval(28), getval(29), ++ getval(30), getval(31), getval(32), getval(33), getval(34), ++ getval(35), getval(36), getval(37), getval(38), getval(39), ++ getval(40), getval(41), getval(42), getval(43), getval(44), ++ getval(45), getval(46), getval(47), getval(48), getval(49), ++ getval(50), getval(51), getval(52), getval(53), getval(54), ++ getval(55), getval(56), getval(57), getval(58), getval(59), ++ getval(60), getval(61), getval(62), getval(63), getval(64), ++ getval(65), getval(66), getval(67), getval(68), getval(69), ++ getval(70), getval(71), getval(72), getval(73), getval(74), ++ getval(75), getval(76), getval(77), getval(78), getval(79), ++ getval(80), getval(81), getval(82), getval(83), getval(84), ++ getval(85), getval(86), getval(87), getval(88), getval(89), ++ getval(90), getval(91), getval(92), getval(93), getval(94), ++ getval(95), getval(96), getval(97), getval(98), getval(99), ++ getval(100), getval(101), getval(102), getval(103), getval(104), ++ getval(105), getval(106), getval(107), getval(108), getval(109), ++ getval(110), getval(111), getval(112), getval(113), getval(114), ++ getval(115), getval(116), getval(117), getval(118), getval(119), ++ getval(120), getval(121), getval(122), getval(123), getval(124), ++ getval(125), getval(126), getval(127), getval(128), getval(129), ++ getval(130), getval(131), getval(132), getval(133), getval(134), ++ getval(135), getval(136), getval(137), getval(138), getval(139), ++ getval(140), getval(141), getval(142), getval(143), getval(144), ++ getval(145), getval(146), getval(147), getval(148), getval(149), ++ getval(150), getval(151), getval(152), getval(153), getval(154), ++ getval(155), getval(156), getval(157), getval(158), getval(159), ++ getval(160), getval(161), getval(162), getval(163), getval(164), ++ getval(165), getval(166), getval(167), getval(168), getval(169), ++ getval(170), getval(171), getval(172), getval(173), getval(174), ++ getval(175), getval(176), getval(177), getval(178), getval(179), ++ getval(180), getval(181), getval(182), getval(183), getval(184), ++ getval(185), getval(186), getval(187), getval(188), getval(189), ++ getval(190), getval(191), getval(192), getval(193), getval(194), ++ getval(195), getval(196), getval(197), getval(198), getval(199), ++ getval(200), getval(201), getval(202), getval(203), getval(204), ++ getval(205), getval(206), getval(207), getval(208), getval(209), ++ getval(210), getval(211), getval(212), getval(213), getval(214), ++ getval(215), getval(216), getval(217), getval(218), getval(219), ++ getval(220), getval(221), getval(222), getval(223), getval(224), ++ getval(225), getval(226), getval(227), getval(228), getval(229), ++ getval(230), getval(231), getval(232), getval(233), getval(234), ++ getval(235), getval(236), getval(237), getval(238), getval(239), ++ getval(240), getval(241), getval(242), getval(243), getval(244), ++ getval(245), getval(246), getval(247), getval(248), getval(249), ++ getval(250), getval(251), getval(252), getval(253), getval(254), ++ getval(255)}; ++ ++template ++std::string basic_algorithm::encoder::process( ++ std::string_view input) { ++ std::string output; ++ std::size_t isize = input.size(); ++ auto partial_blocks = static_cast(input.size()) / input_size(); ++ auto num_blocks = static_cast(partial_blocks); ++ auto osize = static_cast(std::ceil(partial_blocks * output_size())); ++ if constexpr (std::is_same_v) { ++ num_blocks = static_cast(std::ceil(partial_blocks)); ++ isize = input_size() * num_blocks; ++ } ++ output.resize(std::max(osize, (output_size() * num_blocks))); ++ auto input_it = std::begin(input); ++ int length = 0; ++ for (std::size_t i = 0; i < isize; ++i, ++input_it) { ++ int carry = i >= input.size() ? 0 : static_cast(*input_it); ++ int j = 0; ++ for (auto oi = output.rbegin(); ++ (oi != output.rend()) && (carry != 0 || j < length); ++oi, ++j) { ++ carry += 256 * (*oi); ++ auto byte = (unsigned char*)(&(*oi)); ++ *byte = carry % radix; ++ carry /= radix; ++ } ++ length = j; ++ } ++ std::transform(output.rbegin(), output.rend(), output.rbegin(), ++ [](auto c) { return Traits::charset[c]; }); ++ if constexpr (Traits::padding == 0) { ++ output.resize(osize); ++ } else { ++ auto pad_size = output.size() - osize; ++ output.replace(osize, pad_size, pad_size, Traits::padding); ++ } ++ if constexpr (std::is_same_v) { ++ output.erase(0, output.size() % output_size() ? output.size() - length : 0); ++ } ++ return output; ++} ++ ++template ++std::size_t basic_algorithm::encoder::block_size() { ++ return std::is_same_v ++ ? input_size() ++ : 0; ++} ++ ++template ++std::size_t basic_algorithm::encoder::output_size() { ++ return ratio.num; ++} ++ ++template ++std::size_t basic_algorithm::decoder::block_size() { ++ return std::is_same_v ++ ? input_size() ++ : 0; ++} ++ ++template ++std::size_t basic_algorithm::decoder::output_size() { ++ return ratio.den; ++} ++ ++template ++std::string basic_algorithm::decoder::process( ++ std::string_view input) { ++ std::string output; ++ auto end = std::find(input.begin(), input.end(), Traits::padding); ++ size_t input_size = std::distance(input.begin(), end); ++ auto partial_blocks = static_cast(input_size) / this->input_size(); ++ auto output_size = static_cast(this->output_size() * partial_blocks); ++ if constexpr (std::is_same_v) { ++ std::size_t num_blocks = 0; ++ auto input_size_float = static_cast(input.size()); ++ num_blocks = ++ static_cast(std::ceil(input_size_float / this->input_size())); ++ output.resize(this->output_size() * num_blocks); ++ input_size = this->input_size() * num_blocks; ++ } else { ++ output.resize(output_size); ++ } ++ auto input_it = input.begin(); ++ for (size_t i = 0; i < input_size; ++i, ++input_it) { ++ int carry = i > input.size() || *input_it == Traits::padding ++ ? 0 ++ : valset[(unsigned char)(*input_it)]; ++ if (carry == 255) { ++ // throw std::invalid_argument(std::string{"Invalid input character ++ // "} + *input_it); ++ return {}; ++ } ++ auto j = output.size(); ++ while (carry != 0 || j > 0) { ++ auto index = j - 1; ++ carry += radix * static_cast(output[index]); ++ output[index] = static_cast(carry % 256); ++ carry /= 256; ++ if (carry > 0 && index == 0) { ++ output.insert(0, 1, 0); ++ } else { ++ j = index; ++ } ++ } ++ } ++ if constexpr (std::is_same_v) { ++ output.erase(output_size, output.size()); ++ } ++ return output; ++} ++ ++template <> ++struct traits { ++ constexpr static const std::array charset = { ++ '0', '1', '2', '3', '4', '5', '6', '7', ++ '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; ++ constexpr static const char name[] = "base_16"; ++ using execution_style = algorithm::block_tag; ++ constexpr static const char padding = 0; ++}; ++using base_16 = basic_algorithm; ++ ++template <> ++struct traits { ++ constexpr static const std::array charset = { ++ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ++ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', ++ 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', ++ 'u', 'v', 'w', 'x', 'y', 'z'}; ++ constexpr static const char name[] = "base_36"; ++ using execution_style = algorithm::stream_tag; ++ constexpr static const char padding = 0; ++}; ++using base_36_btc = basic_algorithm; ++ ++ ++template <> ++struct traits { ++ constexpr static const std::array charset = { ++ '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', ++ 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', ++ 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', ++ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; ++ constexpr static const char name[] = "base_58_btc"; ++ using execution_style = algorithm::stream_tag; ++ constexpr static const char padding = 0; ++}; ++using base_58_btc = basic_algorithm; ++ ++template <> ++struct traits { ++ constexpr static const std::array charset = { ++ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', ++ 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ++ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', ++ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ++ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}; ++ constexpr static const char name[] = "base_64_pad"; ++ using execution_style = algorithm::block_tag; ++ constexpr static const char padding = '='; ++}; ++using base_64_pad = basic_algorithm; ++ ++template <> ++struct traits { ++ constexpr static const std::array charset = { ++ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', ++ 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ++ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', ++ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ++ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}; ++ constexpr static const char name[] = "base_64"; ++ using base_64 = basic_algorithm; ++ using execution_style = algorithm::block_tag; ++ constexpr static const char padding = 0; ++}; ++using base_64 = basic_algorithm; ++ ++} // namespace multibase +diff --git a/third_party/ipfs_client/include/multibase/encoding.h b/third_party/ipfs_client/include/multibase/encoding.h +new file mode 100644 +index 0000000000000..7675ca6e8445a +--- /dev/null ++++ b/third_party/ipfs_client/include/multibase/encoding.h +@@ -0,0 +1,21 @@ ++#pragma once ++#include ++#include ++ ++namespace multibase { ++ ++enum class encoding : unsigned char { ++ base_unknown = '?', ++ base_256 = 0, ++ base_16 = 'f', ++ base_16_upper = 'F', ++ base_32 = 'b', ++ base_32_upper = 'B', ++ base_36 = 'k', ++ base_58_btc = 'Z', ++ base_64 = 'm', ++ base_64_pad = 'M' ++ ++}; ++ ++} // namespace multibase +diff --git a/third_party/ipfs_client/include/smhasher/MurmurHash3.h b/third_party/ipfs_client/include/smhasher/MurmurHash3.h +new file mode 100644 +index 0000000000000..e1c6d34976c6a +--- /dev/null ++++ b/third_party/ipfs_client/include/smhasher/MurmurHash3.h +@@ -0,0 +1,37 @@ ++//----------------------------------------------------------------------------- ++// MurmurHash3 was written by Austin Appleby, and is placed in the public ++// domain. The author hereby disclaims copyright to this source code. ++ ++#ifndef _MURMURHASH3_H_ ++#define _MURMURHASH3_H_ ++ ++//----------------------------------------------------------------------------- ++// Platform-specific functions and macros ++ ++// Microsoft Visual Studio ++ ++#if defined(_MSC_VER) && (_MSC_VER < 1600) ++ ++typedef unsigned char uint8_t; ++typedef unsigned int uint32_t; ++typedef unsigned __int64 uint64_t; ++ ++// Other compilers ++ ++#else // defined(_MSC_VER) ++ ++#include ++ ++#endif // !defined(_MSC_VER) ++ ++//----------------------------------------------------------------------------- ++ ++void MurmurHash3_x86_32 ( const void * key, int len, uint32_t seed, void * out ); ++ ++void MurmurHash3_x86_128 ( const void * key, int len, uint32_t seed, void * out ); ++ ++void MurmurHash3_x64_128 ( const void * key, int len, uint32_t seed, void * out ); ++ ++//----------------------------------------------------------------------------- ++ ++#endif // _MURMURHASH3_H_ +diff --git a/third_party/ipfs_client/include/vocab/byte.h b/third_party/ipfs_client/include/vocab/byte.h +new file mode 100644 +index 0000000000000..17477c11c2a6f +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/byte.h +@@ -0,0 +1,43 @@ ++#ifndef IPFS_BYTE_H_ ++#define IPFS_BYTE_H_ ++ ++#include ++#include ++ ++#include ++#include ++#include ++ ++#ifdef __cpp_lib_byte ++ ++namespace ipfs { ++using Byte = std::byte; ++} // namespace ipfs ++ ++#else ++namespace ipfs { ++enum class Byte : std::uint_least8_t {}; ++} // namespace ipfs ++#endif ++ ++namespace { ++[[maybe_unused]] std::ostream& operator<<(std::ostream& str, ipfs::Byte b) { ++ return str << std::hex << std::setw(2) << std::setfill('0') ++ << static_cast(b); ++} ++} // namespace ++ ++namespace { ++// libc++ provides this, but for some reason libstdc++ does not ++[[maybe_unused]] std::uint8_t to_integer(ipfs::Byte b) { ++ return static_cast(b); ++} ++} // namespace ++ ++namespace ipfs { ++inline bool operator==(Byte a, Byte b) { ++ return to_integer(a) == to_integer(b); ++} ++} // namespace ipfs ++ ++#endif // IPFS_BYTE_H_ +diff --git a/third_party/ipfs_client/include/vocab/byte_view.h b/third_party/ipfs_client/include/vocab/byte_view.h +new file mode 100644 +index 0000000000000..69858d1972a30 +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/byte_view.h +@@ -0,0 +1,24 @@ ++#ifndef CHROMIUM_IPFS_BYTE_VIEW_H ++#define CHROMIUM_IPFS_BYTE_VIEW_H ++ ++#include "byte.h" ++#include "span.h" ++ ++#include ++ ++namespace ipfs { ++using ByteView = span; ++ ++// ByteView is a view over arbitrary opaque byte ++// Cast it to a view over 8-bit unsigned integers for inspection ++inline span as_octets(ByteView bytes) { ++ return {reinterpret_cast(bytes.data()), bytes.size()}; ++} ++template ++inline ByteView as_bytes(ContiguousBytes const& b) { ++ auto p = reinterpret_cast(b.data()); ++ return ByteView{p, b.size()}; ++} ++} // namespace ipfs ++ ++#endif // CHROMIUM_IPFS_BYTE_VIEW_H +diff --git a/third_party/ipfs_client/include/vocab/endian.h b/third_party/ipfs_client/include/vocab/endian.h +new file mode 100644 +index 0000000000000..2423006c7c02b +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/endian.h +@@ -0,0 +1,21 @@ ++#ifndef IPFS_ENDIAN_H_ ++#define IPFS_ENDIAN_H_ ++ ++#if __has_include() ++#include ++#endif ++#if __has_include() ++#include ++#endif ++ ++#ifdef htobe64 ++// Good ++#elif __has_include() ++#include ++#define htobe64 absl::ghtonll ++#elif __has_include() ++#include ++#define htobe64 native_to_big ++#endif ++ ++#endif // IPFS_ENDIAN_H_ +diff --git a/third_party/ipfs_client/include/vocab/expected.h b/third_party/ipfs_client/include/vocab/expected.h +new file mode 100644 +index 0000000000000..2006f2bf01397 +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/expected.h +@@ -0,0 +1,44 @@ ++#ifndef IPFS_EXPECTED_H_ ++#define IPFS_EXPECTED_H_ ++ ++// std::expected isn't available until C++23 and we need to support C++17 ++// boost::outcome isn't available inside the Chromium tree ++// absl::StatusOr doesn't allow templating or extending the error type, and ++// translating the specific error codes into generic ones isn't great. ++ ++#if __has_include("base/types/expected.h") ++#include "base/types/expected.h" ++namespace ipfs { ++template ++using expected = base::expected; ++template ++using unexpected = base::unexpected; ++} // namespace ipfs ++#elif __has_cpp_attribute(__cpp_lib_expected) ++ ++#include ++namespace ipfs { ++template ++using expected = std::expected; ++template ++using unexpected = std::unexpected; ++} // namespace ipfs ++ ++#elif __has_include() ++ ++// If the API differences between std::expected and boost::outcome::checked ++// become a problem, consider wrapping as proposed in the FAQ: ++// https://www.boost.org/doc/libs/master/libs/outcome/doc/html/faq.html#how-far-away-from-the-proposed-std-expected-t-e-is-outcome-s-checked-t-e ++#include ++namespace ipfs { ++template ++using expected = boost::outcome_v2::checked; ++template ++using unexpected = Error; ++} // namespace ipfs ++ ++#else ++#error Get an expected implementation ++#endif ++ ++#endif // IPFS_EXPECTED_H_ +diff --git a/third_party/ipfs_client/include/vocab/flat_mapset.h b/third_party/ipfs_client/include/vocab/flat_mapset.h +new file mode 100644 +index 0000000000000..1630e3f9ca358 +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/flat_mapset.h +@@ -0,0 +1,39 @@ ++#ifndef CHROMIUM_IPFS_VOCAB_MAP_SET_H_ ++#define CHROMIUM_IPFS_VOCAB_MAP_SET_H_ ++ ++#if __has_include("base/containers/flat_map.h") // Chromium ++ ++#include "base/containers/flat_map.h" ++#include "base/containers/flat_set.h" ++#include "base/debug/debugging_buildflags.h" ++namespace ipfs { ++using base::flat_map; ++using base::flat_set; ++} // namespace ipfs ++ ++#elif __has_cpp_attribute(__cpp_lib_flat_map) && \ ++ __has_cpp_attribute(__cpp_lib_flat_set) ++ ++#include ++#include ++namespace ipfs { ++using std::flat_map; ++using std::flat_set; ++} // namespace ipfs ++ ++#elif __has_include() //Boost ++#include ++#include ++namespace ipfs { ++using boost::container::flat_map; ++using boost::container::flat_set; ++} // namespace ipfs ++ ++#else ++ ++#error \ ++ "Provide an implementation for flat_map and flat_set, or install boost or have a Chromium tree or use a newer C++ version." ++ ++#endif ++ ++#endif // CHROMIUM_IPFS_VOCAB_MAP_SET_H_ +diff --git a/third_party/ipfs_client/include/vocab/html_escape.h b/third_party/ipfs_client/include/vocab/html_escape.h +new file mode 100644 +index 0000000000000..60339ad7d45bd +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/html_escape.h +@@ -0,0 +1,23 @@ ++#ifndef IPFS_HTML_ESCAPE_H_ ++#define IPFS_HTML_ESCAPE_H_ ++ ++#include ++ ++constexpr inline std::string_view html_escape(char& c) { ++ switch (c) { ++ case '"': ++ return """; ++ case '\'': ++ return "'"; ++ case '<': ++ return "<"; ++ case '>': ++ return ">"; ++ case '&': ++ return "&"; ++ default: ++ return {&c, 1UL}; ++ } ++} ++ ++#endif // IPFS_HTML_ESCAPE_H_ +diff --git a/third_party/ipfs_client/include/vocab/i128.h b/third_party/ipfs_client/include/vocab/i128.h +new file mode 100644 +index 0000000000000..4aa36cc09877f +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/i128.h +@@ -0,0 +1,16 @@ ++#ifndef IPFS_I128_H_ ++#define IPFS_I128_H_ ++ ++#if __has_include() ++#include ++namespace ipfs { ++using Int_128 = absl::int128; ++} ++#else ++namespace ipfs { ++// TODO Check if available, if not use boost multiprecision ++using Int_128 = __int128; ++} // namespace ipfs ++#endif ++ ++#endif // IPFS_I128_H_ +diff --git a/third_party/ipfs_client/include/vocab/raw_ptr.h b/third_party/ipfs_client/include/vocab/raw_ptr.h +new file mode 100644 +index 0000000000000..25405d3ea30ba +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/raw_ptr.h +@@ -0,0 +1,64 @@ ++#ifndef IPFS_OBSERVER_PTR_H_ ++#define IPFS_OBSERVER_PTR_H_ ++ ++#if __has_include("base/memory/raw_ptr.h") ++#include "base/memory/raw_ptr.h" ++ ++namespace ipfs { ++template ++using raw_ptr = base::raw_ptr; ++} ++ ++#elif defined(__has_cpp_attribute) && \ ++ __has_cpp_attribute(__cpp_lib_experimental_observer_ptr) ++#include ++ ++namespace ipfs { ++template ++using raw_ptr = std::experimental::observer_ptr; ++} ++ ++#else ++ ++#include ++ ++namespace ipfs { ++ ++/*! ++ * \brief Just an observing (non-owning) pointer. ++ */ ++template ++class raw_ptr { ++ T* ptr_; ++ ++ public: ++ // Chromium's raw_ptr has a default ctor whose semantics depend on build ++ // config. For components/ipfs purposes, there is no reason to ever default ++ // construct. Set it to nullptr. We have time needed to read_start a word. ++ raw_ptr() = delete; ++ ++ raw_ptr(T* p) : ptr_{p} {} ++ raw_ptr(raw_ptr&&) = default; ++ raw_ptr(raw_ptr const&) = default; ++ ++ raw_ptr& operator=(raw_ptr const&) = default; ++ ++ T* get() { return ptr_; } ++ T const* get() const { return ptr_; } ++ explicit operator bool() const { return !!ptr_; } ++ T* operator->() { return ptr_; } ++ T const* operator->() const { return ptr_; } ++ raw_ptr& operator=(T* p) { ++ ptr_ = p; ++ return *this; ++ } ++ T& operator*() { ++ assert(ptr_); ++ return *ptr_; ++ } ++}; ++} // namespace ipfs ++ ++#endif ++ ++#endif // IPFS_OBSERVER_PTR_H_ +diff --git a/third_party/ipfs_client/include/vocab/slash_delimited.h b/third_party/ipfs_client/include/vocab/slash_delimited.h +new file mode 100644 +index 0000000000000..53fd142465028 +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/slash_delimited.h +@@ -0,0 +1,35 @@ ++#ifndef IPFS_SLASH_DELIMITED_H_ ++#define IPFS_SLASH_DELIMITED_H_ ++ ++#include ++#include ++#include ++ ++namespace google::protobuf::internal { ++class LogMessage; ++} ++ ++namespace ipfs { ++struct SlashDelimited { ++ std::string_view remainder_; ++ ++ public: ++ SlashDelimited() : remainder_{""} {} ++ explicit SlashDelimited(std::string_view unowned); ++ explicit operator bool() const; ++ std::string_view pop(); ++ std::string_view pop_all(); ++ std::string_view pop_n(std::size_t); ++ std::string_view peek_back() const; ++ std::string pop_back(); ++ std::string to_string() const { return std::string{remainder_}; } ++ std::string_view to_view() const { return remainder_; } ++}; ++} // namespace ipfs ++ ++std::ostream& operator<<(std::ostream&, ipfs::SlashDelimited const&); ++google::protobuf::internal::LogMessage& operator<<( ++ google::protobuf::internal::LogMessage&, ++ ipfs::SlashDelimited const&); ++ ++#endif // IPFS_SLASH_DELIMITED_H_ +diff --git a/third_party/ipfs_client/include/vocab/span.h b/third_party/ipfs_client/include/vocab/span.h +new file mode 100644 +index 0000000000000..f9c05d2a7dd61 +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/span.h +@@ -0,0 +1,65 @@ ++#ifndef IPFS_SPAN_H_ ++#define IPFS_SPAN_H_ ++ ++#if __cpp_lib_span ++#include ++ ++namespace ipfs { ++template ++using span = std::span; ++} // namespace ipfs ++ ++#elif __has_include("base/containers/span.h") ++ ++#include "base/containers/span.h" ++namespace ipfs { ++template ++using span = base::span; ++} // namespace ipfs ++ ++#elif __has_include() ++ ++#include ++namespace ipfs { ++template ++using span = absl::Span; ++} // namespace ipfs ++ ++#elif __has_include() ++ ++#include ++namespace ipfs { ++template ++using span = boost::span; ++} // namespace ipfs ++ ++#elif __has_include() ++ ++// Prior to Boost 1.78, span did not exist in core yet ++#include ++#include ++namespace ipfs { ++template ++class span : public boost::beast::span { ++ public: ++ span(Value* d, std::size_t n) : boost::beast::span{d, n} {} ++ ++ template ++ span(std::vector const& v) ++ : boost::beast::span{v.data(), v.size()} {} ++ ++ span subspan(std::size_t off) const { ++ return span{this->data() + off, this->size() - off}; ++ } ++ Value& operator[](std::size_t i) { return this->data()[i]; } ++}; ++} // namespace ipfs ++ ++#else ++ ++#error \ ++ "No good implementation of span available. Implement one, move to a newer C++, or provide Boost or Abseil." ++ ++#endif ++ ++#endif // IPFS_SPAN_H_ +diff --git a/third_party/ipfs_client/include/vocab/stringify.h b/third_party/ipfs_client/include/vocab/stringify.h +new file mode 100644 +index 0000000000000..8572ebfef7165 +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/stringify.h +@@ -0,0 +1,17 @@ ++#ifndef IPFS_STRINGIFY_H_ ++#define IPFS_STRINGIFY_H_ ++ ++#include ++ ++namespace ipfs { ++namespace { ++template ++std::string Stringify(T const& t) { ++ std::ostringstream oss; ++ oss << t; ++ return oss.str(); ++} ++} // namespace ++} // namespace ipfs ++ ++#endif // IPFS_STRINGIFY_H_ +diff --git a/third_party/ipfs_client/ipns_record.proto b/third_party/ipfs_client/ipns_record.proto +new file mode 100644 +index 0000000000000..6018931b7466f +--- /dev/null ++++ b/third_party/ipfs_client/ipns_record.proto +@@ -0,0 +1,38 @@ ++syntax = "proto2"; ++option optimize_for = LITE_RUNTIME; ++package ipfs.ipns; ++ ++message IpnsEntry { ++ enum ValidityType { ++ // setting an EOL says "this record is valid until..." ++ EOL = 0; ++ } ++ ++ // deserialized copy of data[value] ++ optional bytes value = 1; ++ ++ // legacy field, verify 'signatureV2' instead ++ optional bytes signatureV1 = 2; ++ ++ // deserialized copies of data[validityType] and data[validity] ++ optional ValidityType validityType = 3; ++ optional bytes validity = 4; ++ ++ // deserialized copy of data[sequence] ++ optional uint64 sequence = 5; ++ ++ // record TTL in nanoseconds, a deserialized copy of data[ttl] ++ optional uint64 ttl = 6; ++ ++ // in order for nodes to properly validate a record upon receipt, they need the public ++ // key associated with it. For old RSA keys, its easiest if we just send this as part of ++ // the record itself. For newer Ed25519 keys, the public key can be embedded in the ++ // IPNS Name itself, making this field unnecessary. ++ optional bytes pubKey = 7; ++ ++ // the signature of the IPNS record ++ optional bytes signatureV2 = 8; ++ ++ // extensible record data in DAG-CBOR format ++ optional bytes data = 9; ++} +diff --git a/third_party/ipfs_client/keys.proto b/third_party/ipfs_client/keys.proto +new file mode 100644 +index 0000000000000..a6f4f75ddba93 +--- /dev/null ++++ b/third_party/ipfs_client/keys.proto +@@ -0,0 +1,22 @@ ++syntax = "proto2"; ++option optimize_for = LITE_RUNTIME; ++package ipfs.ipns; ++ ++enum KeyType { ++ RSA = 0; ++ Ed25519 = 1; ++ Secp256k1 = 2; ++ ECDSA = 3; ++} ++ ++// PublicKey ++message PublicKey { ++ required KeyType Type = 1; ++ required bytes Data = 2; ++} ++ ++// PrivateKey ++message PrivateKey { ++ required KeyType Type = 1; ++ required bytes Data = 2; ++} +diff --git a/third_party/ipfs_client/pb_dag.proto b/third_party/ipfs_client/pb_dag.proto +new file mode 100644 +index 0000000000000..5cd027631c6de +--- /dev/null ++++ b/third_party/ipfs_client/pb_dag.proto +@@ -0,0 +1,23 @@ ++syntax = "proto2"; ++option optimize_for = LITE_RUNTIME; ++package ipfs.pb_dag; ++ ++message PBLink { ++ // binary CID (with no multibase prefix) of the target object ++ optional bytes Hash = 1; ++ ++ // UTF-8 string name ++ optional string Name = 2; ++ ++ // cumulative size of target object ++ optional uint64 Tsize = 3; ++} ++ ++message PBNode { ++ // refs to other objects ++ repeated PBLink Links = 2; ++ ++ // opaque user data ++ optional bytes Data = 1; ++} ++ +diff --git a/third_party/ipfs_client/src/ipfs_client/bases/b16_upper.h b/third_party/ipfs_client/src/ipfs_client/bases/b16_upper.h +new file mode 100644 +index 0000000000000..9d0056ecb98b9 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/bases/b16_upper.h +@@ -0,0 +1,23 @@ ++#ifndef IPFS_B16_UPPER_H_ ++#define IPFS_B16_UPPER_H_ ++ ++#include ++ ++namespace multibase { ++template <> ++struct traits<::multibase::encoding::base_16_upper> { ++ constexpr static const std::array charset = { ++ '0', '1', '2', '3', '4', '5', '6', '7', ++ '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; ++ constexpr static const char name[] = "BASE_16"; ++ using execution_style = multibase::algorithm::block_tag; ++ constexpr static const char padding = 0; ++}; ++} // namespace multibase ++ ++namespace ipfs::mb { ++using base_16_upper = ++ multibase::basic_algorithm; ++} // namespace ipfs::mb ++ ++#endif // IPFS_B16_UPPER_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/bases/b32.h b/third_party/ipfs_client/src/ipfs_client/bases/b32.h +new file mode 100644 +index 0000000000000..9dac14db53ac3 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/bases/b32.h +@@ -0,0 +1,35 @@ ++#ifndef IPFS_B32_UPPER_H_ ++#define IPFS_B32_UPPER_H_ ++ ++#include ++ ++namespace multibase { ++template <> ++struct traits<::multibase::encoding::base_32> { ++ constexpr static const std::array charset = { ++ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', ++ 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', ++ 'w', 'x', 'y', 'z', '2', '3', '4', '5', '6', '7'}; ++ constexpr static const char name[] = "base_32"; ++ using execution_style = multibase::algorithm::block_tag; ++ constexpr static const char padding = 0; ++}; ++template <> ++struct traits<::multibase::encoding::base_32_upper> { ++ constexpr static const std::array charset = { ++ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', ++ 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', ++ 'W', 'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7'}; ++ constexpr static const char name[] = "base_32_upper"; ++ using execution_style = multibase::algorithm::block_tag; ++ constexpr static const char padding = 0; ++}; ++} // namespace multibase ++ ++namespace ipfs::mb { ++using base_32 = multibase::basic_algorithm; ++using base_32_upper = ++ multibase::basic_algorithm; ++} // namespace ipfs::mb ++ ++#endif // IPFS_B32_UPPER_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/block_requestor.cc b/third_party/ipfs_client/src/ipfs_client/block_requestor.cc +new file mode 100644 +index 0000000000000..8a63e6f7ae0cc +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/block_requestor.cc +@@ -0,0 +1 @@ ++#include +diff --git a/third_party/ipfs_client/src/ipfs_client/car.cc b/third_party/ipfs_client/src/ipfs_client/car.cc +new file mode 100644 +index 0000000000000..e36442347415f +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/car.cc +@@ -0,0 +1,132 @@ ++#include "car.h" ++ ++#include ++ ++#include ++ ++#include "log_macros.h" ++ ++#include ++ ++using Self = ipfs::Car; ++using Byte = ipfs::Byte; ++using ByteView = ipfs::ByteView; ++using VarInt = libp2p::multi::UVarint; ++ ++namespace { ++short ReadHeader(ByteView&, ipfs::ContextApi const&); ++std::pair GetV1PayloadPos(ByteView); ++} // namespace ++ ++Self::Car(ByteView bytes, ContextApi const& api) { ++ auto after_header = bytes; ++ auto version = ReadHeader(after_header, api); ++ switch (version) { ++ case 0: ++ LOG(ERROR) << "Problem parsing CAR header."; ++ break; ++ case 1: ++ LOG(INFO) << "Reading CARv1"; ++ data_ = after_header; ++ break; ++ case 2: { ++ auto [off, siz] = GetV1PayloadPos(after_header); ++ LOG(INFO) << "CARv2 carries a payload of " << siz << "B @ " << off; ++ // TODO validate off and siz are sane, e.g. not pointing back into pragma ++ // or whatever ++ data_ = bytes.subspan(off, siz); ++ ReadHeader(data_, api); ++ break; ++ } ++ default: ++ LOG(ERROR) << "Unsupported CAR format version " << version; ++ } ++} ++auto Self::NextBlock() -> std::optional { ++ auto len = VarInt::create(data_); ++ if (!len) { ++ return std::nullopt; ++ } ++ data_ = data_.subspan(len->size()); ++ if (len->toUInt64() > data_.size()) { ++ LOG(ERROR) << "Length prefix claims cid+block is " << len->toUInt64() ++ << " bytes, but I only have " << data_.size() ++ << " bytes left in the CAR payload."; ++ data_ = {}; ++ return std::nullopt; ++ } ++ Block rv; ++ rv.bytes = data_.subspan(0U, len->toUInt64()); ++ data_ = data_.subspan(len->toUInt64()); ++ if (rv.cid.ReadStart(rv.bytes)) { ++ // TODO : check hash ++ return rv; ++ } ++ return std::nullopt; ++} ++ ++namespace { ++// https://ipld.io/specs/transport/car/carv2/ ++short ReadHeader(ByteView& bytes, ipfs::ContextApi const& api) { ++ auto header_len = VarInt::create(bytes); ++ if (!header_len || ++ header_len->toUInt64() + header_len->size() > bytes.size()) { ++ return 0; ++ } ++ bytes = bytes.subspan(header_len->size()); ++ auto header_bytes = bytes.subspan(0UL, header_len->toUInt64()); ++ auto header = api.ParseCbor(header_bytes); ++ if (!header) { ++ return 0; ++ } ++ auto version_node = header->at("version"); ++ if (!version_node) { ++ return 0; ++ } ++ auto version = version_node->as_unsigned(); ++ if (version) { ++ bytes = bytes.subspan(header_len->toUInt64()); ++ return version.value(); ++ } ++ return 0; ++} ++std::uint64_t read_le_u64(ByteView bytes, unsigned& off) { ++ auto b = bytes.subspan(off, off + 8); ++ off += 8U; ++ auto shift_in = [](std::uint64_t i, Byte y) { ++ return (i << 8) | static_cast(y); ++ }; ++ return std::accumulate(b.rbegin(), b.rend(), 0UL, shift_in); ++} ++std::pair GetV1PayloadPos(ByteView bytes) { ++ // Following the 11 byte pragma, the CARv2 [header] is a fixed-length sequence ++ // of 40 bytes, broken into the following sections: ++ if (bytes.size() < 40) { ++ return {}; ++ } ++ ++ // Characteristics: A 128-bit (16-byte) bitfield used to describe certain ++ // features of the enclosed data. ++ auto reading_off = 16U; ++ ++ // Data offset: A 64-bit (8-byte) unsigned ++ // little-endian integer indicating the byte-offset from the beginning of the ++ // CARv2 [pragma] to the first byte of the CARv1 data payload. ++ auto data_offset = read_le_u64(bytes, reading_off); ++ ++ // Data size: A 64-bit ++ // (8-byte) unsigned little-endian integer indicating the byte-length of the ++ // CARv1 data payload. ++ auto data_size = read_le_u64(bytes, reading_off); ++ ++ // Index offset: A 64-bit (8-byte) unsigned little-endian ++ // integer indicating the byte-offset from the beginning of the CARv2 to the ++ // first byte of the index payload. This value may be 0 to indicate the ++ // absence of index data. ++ reading_off += 8; // Ignoring index and therefore index offset ++ ++ assert(reading_off == 40UL); ++ ++ return {data_offset, data_size}; ++} ++} // namespace +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/car.h b/third_party/ipfs_client/src/ipfs_client/car.h +new file mode 100644 +index 0000000000000..619ac48ed8cd3 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/car.h +@@ -0,0 +1,26 @@ ++#ifndef IPFS_CAR_H_ ++#define IPFS_CAR_H_ ++ ++#include ++#include ++ ++#include ++#include ++ ++namespace ipfs { ++class ContextApi; ++class Car { ++ public: ++ Car(ByteView, ContextApi const&); ++ struct Block { ++ Cid cid; ++ ByteView bytes; ++ }; ++ std::optional NextBlock(); ++ ++ private: ++ ByteView data_; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_CAR_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/cid.cc b/third_party/ipfs_client/src/ipfs_client/cid.cc +new file mode 100644 +index 0000000000000..b20686086bca6 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/cid.cc +@@ -0,0 +1,86 @@ ++#include ++ ++#include ++#include ++ ++#include "log_macros.h" ++ ++using Self = ipfs::Cid; ++using VarInt = libp2p::multi::UVarint; ++ ++Self::Cid(ipfs::MultiCodec cdc, ipfs::MultiHash hsh) ++ : codec_{cdc}, hash_{hsh} {} ++ ++Self::Cid(ipfs::ByteView bytes) { ++ ReadStart(bytes); ++} ++ ++Self::Cid(std::string_view s) { ++ if (s.size() == 46 && s[0] == 'Q' && s[1] == 'm') { ++ auto bytes = mb::Codec::Get(mb::Code::BASE58_BTC)->decode(s); ++ auto view = ByteView{bytes}; ++ ReadStart(view); ++ } else if (auto bytes = mb::decode(s)) { ++ if (bytes->size() > 4) { ++ auto view = ByteView{bytes.value()}; ++ ReadStart(view); ++ } ++ } else { ++ LOG(WARNING) << "Failed to decode the multibase for a CID: " << s; ++ } ++} ++ ++bool Self::ReadStart(ByteView& bytes) { ++ if (bytes.size() >= 34 && bytes[0] == ipfs::Byte{0x12} && ++ bytes[1] == ipfs::Byte{0x20}) { ++ hash_ = MultiHash{bytes}; ++ codec_ = hash_.valid() ? MultiCodec::DAG_PB : MultiCodec::INVALID; ++ bytes = bytes.subspan(34); ++ return true; ++ } ++ auto version = VarInt::create(bytes); ++ if (!version) { ++ return false; ++ } ++ if (version->toUInt64() != 1U) { ++ LOG(ERROR) << "CID version " << version->toUInt64() << " not supported."; ++ return false; ++ } ++ bytes = bytes.subspan(version->size()); ++ auto codec = VarInt::create(bytes); ++ if (!codec) { ++ return false; ++ } ++ auto cdc = static_cast(codec->toUInt64()); ++ codec_ = Validate(cdc); ++ bytes = bytes.subspan(codec->size()); ++ return hash_.ReadPrefix(bytes); ++} ++ ++bool Self::valid() const { ++ return codec_ != MultiCodec::INVALID && hash_.valid(); ++} ++ ++auto Self::hash() const -> ByteView { ++ return hash_.digest(); ++} ++auto Self::hash_type() const -> HashType { ++ return multi_hash().type(); ++} ++ ++std::string Self::to_string() const { ++ std::vector binary; ++ auto append_varint = [&binary](auto x) { ++ auto i = static_cast(x); ++ VarInt v{i}; ++ auto b = v.toBytes(); ++ binary.insert(binary.end(), b.begin(), b.end()); ++ }; ++ append_varint(1); // CID version 1 ++ append_varint(codec()); ++ append_varint(hash_type()); ++ append_varint(hash().size()); ++ auto h = hash(); ++ binary.insert(binary.end(), h.begin(), h.end()); ++ return mb::encode(mb::Code::BASE32_LOWER, binary); ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/context_api.cc b/third_party/ipfs_client/src/ipfs_client/context_api.cc +new file mode 100644 +index 0000000000000..f58a062d780ab +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/context_api.cc +@@ -0,0 +1,26 @@ ++#include ++ ++#include "crypto/openssl_sha2_256.h" ++ ++using Self = ipfs::ContextApi; ++ ++Self::ContextApi() { ++#if HAS_OPENSSL_SHA ++ hashers_.emplace(HashType::SHA2_256, ++ std::make_unique()); ++#endif ++} ++ ++auto Self::Hash(HashType ht, ByteView data) ++ -> std::optional> { ++ auto it = hashers_.find(ht); ++ if (hashers_.end() == it || !(it->second)) { ++ return std::nullopt; ++ } ++ return it->second->hash(data); ++} ++ ++unsigned int Self::GetGatewayRate(std::string_view) { ++ return 120; ++} ++void Self::SetGatewayRate(std::string_view, unsigned int) {} +diff --git a/third_party/ipfs_client/src/ipfs_client/crypto/openssl_sha2_256.cc b/third_party/ipfs_client/src/ipfs_client/crypto/openssl_sha2_256.cc +new file mode 100644 +index 0000000000000..ff5c7a24d23bb +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/crypto/openssl_sha2_256.cc +@@ -0,0 +1,32 @@ ++#include "openssl_sha2_256.h" ++ ++using Self = ipfs::crypto::OpensslSha2_256; ++ ++#include "log_macros.h" ++ ++#if HAS_OPENSSL_SHA ++ ++#include ++ ++Self::~OpensslSha2_256() {} ++auto Self::hash(ipfs::ByteView data) -> std::optional> { ++ SHA256_CTX ctx; ++ if (1 != SHA256_Init(&ctx)) { ++ LOG(ERROR) << "Failed to initialize SHA256"; ++ return std::nullopt; ++ } ++ if (1 != SHA256_Update(&ctx, data.data(), data.size())) { ++ LOG(ERROR) << "Failure injesting data into SHA256."; ++ return {}; ++ } ++ std::vector rv(SHA256_DIGEST_LENGTH, Byte{}); ++ auto p = reinterpret_cast(rv.data()); ++ if (1 == SHA256_Final(p, &ctx)) { ++ return rv; ++ } else { ++ LOG(ERROR) << "Error calculating sha2-256 hash."; ++ return std::nullopt; ++ } ++} ++ ++#endif +diff --git a/third_party/ipfs_client/src/ipfs_client/crypto/openssl_sha2_256.h b/third_party/ipfs_client/src/ipfs_client/crypto/openssl_sha2_256.h +new file mode 100644 +index 0000000000000..c4e7bb975d366 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/crypto/openssl_sha2_256.h +@@ -0,0 +1,18 @@ ++#ifndef IPFS_OPENSSL_SHA2_256_H_ ++#define IPFS_OPENSSL_SHA2_256_H_ ++ ++#if __has_include() ++#define HAS_OPENSSL_SHA 1 ++#endif ++ ++#include ++ ++namespace ipfs::crypto { ++class OpensslSha2_256 final : public Hasher { ++ public: ++ ~OpensslSha2_256() noexcept override; ++ std::optional> hash(ByteView) override; ++}; ++} // namespace ipfs::crypto ++ ++#endif // IPFS_OPENSSL_SHA2_256_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/dag_cbor_value.cc b/third_party/ipfs_client/src/ipfs_client/dag_cbor_value.cc +new file mode 100644 +index 0000000000000..20a6fc713ad4e +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/dag_cbor_value.cc +@@ -0,0 +1,69 @@ ++#include ++ ++#include ++ ++#include "log_macros.h" ++ ++#include ++ ++using Self = ipfs::DagCborValue; ++ ++void Self::html(std::ostream& str) const { ++ if (auto u = as_unsigned()) { ++ str << "" << *u << "\n"; ++ } else if (auto si = as_signed()) { ++ str << "" << *si << "\n"; ++ } else if (auto fl = as_float()) { ++ str << "" << *si << "\n"; ++ } else if (auto s = as_string()) { ++ str << "

""; ++ for (auto c : *s) { ++ str << html_escape(c); ++ } ++ str << ""

\n"; ++ } else if (auto cid = as_link()) { ++ auto cs = cid.value().to_string(); ++ if (cs.size()) { ++ str << "" << cs ++ << "\n"; ++ } else { ++ str << "\n"; ++ } ++ } else if (auto bin = as_bytes()) { ++ str << "

0x"; ++ for (auto b : *bin) { ++ str << ' ' << std::hex << std::setw(2) << std::setfill('0') ++ << static_cast(b); ++ } ++ str << "

\n"; ++ } else if (is_array()) { ++ str << "
    \n"; ++ iterate_array([&str](auto& v) { ++ str << "
  1. \n"; ++ v.html(str); ++ str << "
  2. \n"; ++ }); ++ str << "
\n"; ++ } else if (is_map()) { ++ str << "\n"; ++ iterate_map([&str](auto k, auto& v) { ++ str << " \n"; ++ }); ++ str << "
" << k << "\n"; ++ v.html(str); ++ str << "
\n"; ++ } else if (auto bul = as_bool()) { ++ auto val = (bul.value() ? "True" : "False"); ++ str << " " << val << "\n"; ++ } else { ++ str << "\n"; ++ } ++} ++ ++std::string Self::html() const { ++ std::ostringstream oss; ++ oss << "DAG-CBOR Preview\n"; ++ html(oss); ++ oss << ""; ++ return oss.str(); ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/dag_json_value.cc b/third_party/ipfs_client/src/ipfs_client/dag_json_value.cc +new file mode 100644 +index 0000000000000..12a493cbd92cb +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/dag_json_value.cc +@@ -0,0 +1,22 @@ ++#include ++ ++#include ++ ++using Self = ipfs::DagJsonValue; ++ ++Self::~DagJsonValue() noexcept {} ++auto Self::get_if_link() const -> std::optional { ++ auto slash = (*this)["/"]; ++ if (!slash) { ++ return std::nullopt; ++ } ++ auto str = slash->get_if_string(); ++ if (!str) { ++ return std::nullopt; ++ } ++ auto cid = Cid(*str); ++ if (cid.valid()) { ++ return cid; ++ } ++ return std::nullopt; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/gateways.cc b/third_party/ipfs_client/src/ipfs_client/gateways.cc +new file mode 100644 +index 0000000000000..a4c3813412897 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gateways.cc +@@ -0,0 +1,121 @@ ++#include ++ ++#include ++ ++#include "log_macros.h" ++ ++#include ++#include ++#include ++ ++using namespace std::string_literals; ++ ++ipfs::Gateways::Gateways() ++ : random_engine_{std::random_device{}()}, dist_{0.01} { ++ auto gws = DefaultGateways(); ++ for (auto [k, v] : gws) { ++ known_gateways_[k] = v; ++ } ++} ++ipfs::Gateways::~Gateways() {} ++ ++auto ipfs::Gateways::GenerateList() -> GatewayList { ++ GatewayList result; ++ for (auto [k, v] : known_gateways_) { ++ result.push_back({k, v + dist_(random_engine_)}); ++ } ++ std::sort(result.begin(), result.end()); ++ return result; ++} ++ ++void ipfs::Gateways::promote(std::string const& key) { ++ auto it = known_gateways_.find(key); ++ if (known_gateways_.end() == it) { ++ LOG(ERROR) << "Can't promote (" << key ++ << ") because I don't know that one."; ++ } else { ++ auto l = known_gateways_.at(key)++; ++ if (l % (++up_log_ / 2) <= 9) { ++ LOG(INFO) << "Promote(" << key << ")"; ++ } ++ } ++} ++void ipfs::Gateways::demote(std::string const& key) { ++ auto it = known_gateways_.find(key); ++ if (known_gateways_.end() == it) { ++ VLOG(2) << "Can't demote " << key << " as I don't have that gateway."; ++ } else if (it->second) { ++ if (it->second-- % 3 == 0) { ++ LOG(INFO) << "Demote(" << key << ") to " << it->second; ++ } ++ } else { ++ LOG(INFO) << "Demoted(" << key << ") for the last time - dropping."; ++ known_gateways_.erase(it); ++ } ++} ++ ++void ipfs::Gateways::AddGateways(std::vector v) { ++ LOG(INFO) << "AddGateways(" << v.size() << ')'; ++ for (auto& ip : v) { ++ if (ip.empty()) { ++ LOG(ERROR) << "ERROR: Attempted to add empty string as gateway!"; ++ continue; ++ } ++ std::string prefix; ++ if (ip.find("://") == std::string::npos) { ++ prefix = "http://"; ++ prefix.append(ip); ++ } else { ++ prefix = ip; ++ } ++ if (prefix.back() != '/') { ++ prefix.push_back('/'); ++ } ++ if (known_gateways_.insert({prefix, 99}).second) { ++ VLOG(1) << "Adding discovered gateway " << prefix; ++ } ++ } ++} ++ ++auto ipfs::Gateways::DefaultGateways() -> GatewayList { ++ auto* ovr = std::getenv("IPFS_GATEWAY"); ++ if (ovr && *ovr) { ++ std::istringstream user_override{ovr}; ++ GatewayList result; ++ std::string gw; ++ while (user_override >> gw) { ++ if ( gw.empty() ) { ++ continue; ++ } ++ if ( gw.back() != '/' ) { ++ gw.push_back('/'); ++ } ++ result.push_back( {gw, 0} ); ++ } ++ auto N = static_cast(result.size()); ++ for (auto i = 0; i < N; ++i) { ++ auto& r = result[i]; ++ r.rate = N - i; ++ LOG(INFO) << "User-specified gateway: " << r.prefix << '=' << r.rate; ++ } ++ return result; ++ } ++ return {{"http://localhost:8080/"s, 929}, ++ {"https://jcsl.hopto.org/"s, 863}, ++ {"https://human.mypinata.cloud/"s, 798}, ++ {"https://ipfs.io/"s, 753}, ++ {"https://gateway.ipfs.io/"s, 678}, ++ {"https://dweb.link/"s, 598}, ++ {"https://gateway.pinata.cloud/"s, 519}, ++ {"https://ipfs.joaoleitao.org/"s, 434}, ++ {"https://ipfs.runfission.com/"s, 371}, ++ {"https://nftstorage.link/"s, 307}, ++ {"https://w3s.link/"s, 243}, ++ {"https://ipfs.fleek.co/"s, 203}, ++ {"https://ipfs.jpu.jp/"s, 162}, ++ {"https://permaweb.eu.org/"s, 121}, ++ {"https://jorropo.net/"s, 76}, ++ {"https://hardbin.com/"s, 39}, ++ {"https://ipfs.soul-network.com/"s, 1}, ++ {"https://storry.tv/"s, 0}}; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/generated_directory_listing.cc b/third_party/ipfs_client/src/ipfs_client/generated_directory_listing.cc +new file mode 100644 +index 0000000000000..5a7c4fce733d6 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/generated_directory_listing.cc +@@ -0,0 +1,45 @@ ++#include "generated_directory_listing.h" ++ ++#include "log_macros.h" ++ ++ipfs::GeneratedDirectoryListing::GeneratedDirectoryListing( ++ std::string_view base_path) ++ : html_("\n "), base_path_(base_path) { ++ if (base_path.empty() || base_path[0] != '/') { ++ base_path_.insert(0UL, 1UL, '/'); ++ } ++ if (base_path_.back() != '/') { ++ base_path_.push_back('/'); ++ } ++ html_.append(base_path_) ++ .append(" (directory listing)\n") ++ .append(" \n") ++ .append("
    \n"); ++ if (base_path.find_first_not_of("/") < base_path.size()) { ++ std::string_view dotdotpath{base_path_}; ++ dotdotpath.remove_suffix(1); // Remove that trailing / ++ auto last_slash = dotdotpath.find_last_of("/"); ++ dotdotpath = dotdotpath.substr(0, last_slash + 1UL); ++ AddLink("..", dotdotpath); ++ } ++} ++ ++void ipfs::GeneratedDirectoryListing::AddEntry(std::string_view name) { ++ auto path = base_path_; ++ path.append(name); ++ AddLink(name, path); ++} ++void ipfs::GeneratedDirectoryListing::AddLink(std::string_view name, ++ std::string_view path) { ++ html_.append("
  • \n") ++ .append(" ") ++ .append(name) ++ .append("\n") ++ .append("
  • \n"); ++} ++ ++std::string const& ipfs::GeneratedDirectoryListing::Finish() { ++ return html_.append("
\n").append(" \n").append("\n"); ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/generated_directory_listing.h b/third_party/ipfs_client/src/ipfs_client/generated_directory_listing.h +new file mode 100644 +index 0000000000000..8daa0ec01cb9e +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/generated_directory_listing.h +@@ -0,0 +1,41 @@ ++#ifndef IPFS_GENERATED_DIRECTORY_LISTING_H_ ++#define IPFS_GENERATED_DIRECTORY_LISTING_H_ ++ ++#include ++#include ++ ++namespace ipfs { ++ ++/*! ++ * \brief An index.html listing out a directory node's content ++ */ ++class GeneratedDirectoryListing { ++ public: ++ ++ /*! ++ * \brief Get the HTML preamble going ++ * \param base_path - The path _to_ this directory ++ */ ++ GeneratedDirectoryListing(std::string_view base_path); ++ ++ /*! ++ * \brief Add an entry to the list ++ * \param name - The directory's way of referring to that CID ++ */ ++ void AddEntry(std::string_view name); ++ ++ /*! ++ * \brief Finish up all the HTML stuff at the end. ++ * \return The generated HTML ++ */ ++ std::string const& Finish(); ++ ++ private: ++ std::string html_; ++ std::string base_path_; ++ ++ void AddLink(std::string_view name, std::string_view path); ++}; ++} // namespace ipfs ++ ++#endif // IPFS_GENERATED_DIRECTORY_LISTING_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/block_request_splitter.cc b/third_party/ipfs_client/src/ipfs_client/gw/block_request_splitter.cc +new file mode 100644 +index 0000000000000..3ded788f6bdf1 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/block_request_splitter.cc +@@ -0,0 +1,30 @@ ++#include ++ ++#include ++ ++using Self = ipfs::gw::BlockRequestSplitter; ++ ++std::string_view Self::name() const { ++ return "BlockRequestSplitter"; ++} ++auto Self::handle(ipfs::gw::RequestPtr r) -> HandleOutcome { ++ if (r->type != Type::Car) { ++ return HandleOutcome::NOT_HANDLED; ++ } ++ { ++ auto br = std::make_shared(*r); ++ br->type = Type::Block; ++ br->path.clear(); ++ forward(br); ++ } ++ /* ++ { ++ auto pr = std::make_shared(*r); ++ pr->type = Type::Providers; ++ pr->path.clear(); ++ pr->affinity.clear(); ++ forward(pr); ++ } ++ */ ++ return HandleOutcome::NOT_HANDLED; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/default_requestor.cc b/third_party/ipfs_client/src/ipfs_client/gw/default_requestor.cc +new file mode 100644 +index 0000000000000..974cf5b8539e4 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/default_requestor.cc +@@ -0,0 +1,30 @@ ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++auto ipfs::gw::default_requestor(ipfs::GatewayList /* gws */, ++ std::shared_ptr early, ++ std::shared_ptr api) ++ -> std::shared_ptr { ++ auto result = std::make_shared(); ++ result->or_else(std::make_shared()); ++ if (early) { ++ result->or_else(early); ++ early->api(api); ++ } ++ // auto pool = std::make_shared(); ++ result->or_else(std::make_shared(api)) ++ .or_else(std::make_shared()) ++ .or_else(std::make_shared()); ++ // for (auto& gw : gws) { ++ // auto gwr = std::make_shared(gw.prefix, gw.rate, ++ // api); pool->add(gwr); ++ // } ++ return result; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/dnslink_requestor.cc b/third_party/ipfs_client/src/ipfs_client/gw/dnslink_requestor.cc +new file mode 100644 +index 0000000000000..731b750ffd43a +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/dnslink_requestor.cc +@@ -0,0 +1,69 @@ ++#include ++ ++#include "ipfs_client/ipld/ipns_name.h" ++ ++#include ++#include ++ ++#include ++#include ++#include ++ ++#include "log_macros.h" ++ ++#include ++ ++using Self = ipfs::gw::DnsLinkRequestor; ++using namespace std::literals; ++ ++Self::DnsLinkRequestor(std::shared_ptr api) { ++ api_ = api; ++} ++std::string_view Self::name() const { ++ return "DNSLink requestor"; ++} ++namespace { ++bool parse_results(ipfs::gw::RequestPtr req, ++ std::vector const& results, ++ std::shared_ptr const&); ++} ++auto Self::handle(ipfs::gw::RequestPtr req) -> HandleOutcome { ++ if (req->type != Type::DnsLink) { ++ return HandleOutcome::NOT_HANDLED; ++ } ++ // std::function requires target be copy-constructible ++ auto success = std::make_shared(); ++ *success = false; ++ auto a = api_; ++ auto res = [req, success, a](std::vector const& results) { ++ *success = *success || parse_results(req, results, a); ++ }; ++ auto don = [success, req]() { ++ LOG(INFO) << "DNSLink request completed for " << req->main_param ++ << " success=" << *success; ++ if (!*success) { ++ req->dependent->finish(ipfs::Response::HOST_NOT_FOUND); ++ } ++ }; ++ api_->SendDnsTextRequest("_dnslink." + req->main_param, res, std::move(don)); ++ return HandleOutcome::PENDING; ++} ++namespace { ++bool parse_results(ipfs::gw::RequestPtr req, ++ std::vector const& results, ++ std::shared_ptr const& api) { ++ constexpr auto prefix = "dnslink="sv; ++ LOG(INFO) << "Scanning " << results.size() << " DNS TXT records for " ++ << req->main_param << " looking for dnslink..."; ++ for (auto& result : results) { ++ if (starts_with(result, prefix)) { ++ LOG(INFO) << "DNSLink result=" << result; ++ req->RespondSuccessfully(result.substr(prefix.size()), api); ++ return true; ++ } else { ++ LOG(INFO) << "Irrelevant TXT result, ignored: " << result; ++ } ++ } ++ return false; ++} ++} // namespace +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/gateway_http_requestor.cc b/third_party/ipfs_client/src/ipfs_client/gw/gateway_http_requestor.cc +new file mode 100644 +index 0000000000000..5fd1d91ec2b9d +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/gateway_http_requestor.cc +@@ -0,0 +1,136 @@ ++#include "gateway_http_requestor.h" ++ ++#include ++#include ++ ++#include ++#include ++#include ++ ++#include "log_macros.h" ++ ++using Self = ipfs::gw::GatewayHttpRequestor; ++using ReqTyp = ipfs::gw::Type; ++ ++std::string_view Self::name() const { ++ return "simplistic HTTP requestor"; ++} ++auto Self::handle(ipfs::gw::RequestPtr r) -> HandleOutcome { ++ DCHECK(r); ++ DCHECK(r->dependent); ++ DCHECK_GT(prefix_.size(), 0UL); ++ if (!r->is_http()) { ++ LOG(ERROR) << name() << " only handles HTTP requests"; ++ return HandleOutcome::NOT_HANDLED; ++ } ++ auto req_key = r->url_suffix().append(r->accept()); ++ if (seen_[req_key] > 0xFD) { ++ return HandleOutcome::NOT_HANDLED; ++ } ++ if (target(*r) <= r->parallel + pending_ + seen_[req_key]) { ++ return HandleOutcome::MAYBE_LATER; ++ } ++ auto desc = r->describe_http(prefix_); ++ if (!desc.has_value() || desc.value().url.empty()) { ++ LOG(ERROR) ++ << r->debug_string() ++ << " is HTTP but can't describe the HTTP request that would happen?"; ++ return HandleOutcome::NOT_HANDLED; ++ } ++ desc.value().timeout_seconds += extra_seconds_; ++ auto cb = [this, r, desc, req_key](std::int16_t status, std::string_view body, ++ ContextApi::HeaderAccess ha) { ++ if (r->parallel) { ++ r->parallel--; ++ } ++ if (pending_) { ++ pending_--; ++ } ++ if (r->type == Type::Zombie) { ++ return; ++ } else if (status == 408 || status == 504) { ++ // Timeouts ++ extra_seconds_++; ++ forward(r); ++ return; ++ } else if (status / 100 == 2) { ++ auto ct = ha("content-type"); ++ std::transform(ct.begin(), ct.end(), ct.begin(), ::tolower); ++ if (ct.empty()) { ++ LOG(ERROR) << "No content-type header?"; ++ } ++ if (ct.size() && desc->accept.size() && ++ ct.find(desc->accept) == std::string::npos) { ++ LOG(WARNING) << "Requested with Accept: " << desc->accept ++ << " but received response with content-type: " << ct; ++ LOG(INFO) << "Demote(" << prefix_ << ')'; ++ } else if (!r->RespondSuccessfully(body, api_)) { ++ LOG(ERROR) << "Got an unuseful response from " << prefix_ ++ << " forwarding request " << r->debug_string() ++ << " to next requestor."; ++ } else { ++ // Good cases ++ if (typ_good_.insert(r->type).second) { ++ VLOG(1) << prefix_ << " OK with requests of type " ++ << static_cast(r->type); ++ } else if (typ_bad_.erase(r->type)) { ++ VLOG(1) << prefix_ << " truly OK with requests of type " ++ << static_cast(r->type); ++ } ++ if (aff_good_.insert(r->affinity).second) { ++ VLOG(1) << prefix_ << " likes requests in the neighborhood of " ++ << r->affinity; ++ } else if (aff_bad_.erase(r->affinity)) { ++ VLOG(1) << prefix_ << " truly OK with affinity " << r->affinity; ++ } ++ VLOG(2) << prefix_ << " had a success on " << r->debug_string(); ++ LOG(INFO) << "Promote(" << prefix_ << ')'; ++ ++strength_; ++ return; ++ } ++ } else if (status / 100 == 4) { ++ seen_[req_key] += 9; ++ } ++ seen_[req_key] += 9; ++ LOG(INFO) << "Demote(" << prefix_ << ')'; ++ if (strength_ > 0) { ++ --strength_; ++ } ++ aff_bad_.insert(r->affinity); ++ typ_bad_.insert(r->type); ++ forward(r); ++ }; ++ DCHECK(api_); ++ api_->SendHttpRequest(desc.value(), cb); ++ seen_[req_key]++; ++ pending_++; ++ return HandleOutcome::PENDING; ++} ++ ++Self::GatewayHttpRequestor(std::string gateway_prefix, ++ int strength, ++ std::shared_ptr api) ++ : prefix_{gateway_prefix}, strength_{strength} { ++ api_ = api; ++} ++Self::~GatewayHttpRequestor() {} ++ ++int Self::target(GatewayRequest const& r) const { ++ int result = (strength_ - pending_) / 2; ++ if (!pending_) { ++ ++result; ++ } ++ if (typ_good_.count(r.type)) { ++ result += 3; ++ } ++ if (!typ_bad_.count(r.type)) { ++ result += 2; ++ } ++ if (aff_good_.count(r.affinity)) { ++ result += 5; ++ } ++ if (aff_bad_.count(r.affinity) == 0UL) { ++ result += 4; ++ } ++ return result; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/gateway_http_requestor.h b/third_party/ipfs_client/src/ipfs_client/gw/gateway_http_requestor.h +new file mode 100644 +index 0000000000000..8c61bef879db8 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/gateway_http_requestor.h +@@ -0,0 +1,34 @@ ++#ifndef IPFS_GATEWAY_HTTP_REQUESTOR_H_ ++#define IPFS_GATEWAY_HTTP_REQUESTOR_H_ ++ ++#include ++#include ++#include ++ ++#include ++#include ++#include ++ ++namespace ipfs::gw { ++class GatewayHttpRequestor final : public Requestor { ++ std::string prefix_; ++ int strength_; ++ std::unordered_map seen_; ++ std::set aff_good_, aff_bad_; ++ std::set typ_good_, typ_bad_; ++ int pending_ = 0; ++ int extra_seconds_ = 0; ++ ++ HandleOutcome handle(RequestPtr) override; ++ std::string_view name() const override; ++ int target(GatewayRequest const&) const; ++ ++ public: ++ GatewayHttpRequestor(std::string gateway_prefix, ++ int strength, ++ std::shared_ptr); ++ ~GatewayHttpRequestor() noexcept override; ++}; ++} // namespace ipfs::gw ++ ++#endif // IPFS_GATEWAY_HTTP_REQUESTOR_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/gateway_request.cc b/third_party/ipfs_client/src/ipfs_client/gw/gateway_request.cc +new file mode 100644 +index 0000000000000..0847ff7aca3eb +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/gateway_request.cc +@@ -0,0 +1,324 @@ ++#include ++ ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "log_macros.h" ++ ++#include ++#include ++ ++using namespace std::literals; ++ ++using Self = ipfs::gw::GatewayRequest; ++ ++std::shared_ptr Self::fromIpfsPath(ipfs::SlashDelimited p) { ++ auto name_space = p.pop(); ++ auto r = std::make_shared(); ++ r->main_param = p.pop(); ++ Cid cid(r->main_param); ++ if (cid.valid()) { ++ r->cid = std::move(cid); ++ } else { ++ r->cid = std::nullopt; ++ } ++ if (name_space == "ipfs") { ++ if (!r->cid.has_value()) { ++ LOG(ERROR) << "IPFS request with invalid/unsupported CID " ++ << r->main_param; ++ return {}; ++ } ++ if (r->cid.value().hash_type() == HashType::IDENTITY) { ++ r->type = Type::Identity; ++ } else { ++ r->path = p.pop_all(); ++ r->type = r->path.empty() ? Type::Block : Type::Car; ++ } ++ } else if (name_space == "ipns") { ++ r->path = p.pop_all(); ++ if (Cid(r->main_param).valid()) { ++ r->type = Type::Ipns; ++ } else { ++ r->type = Type::DnsLink; ++ } ++ } else { ++ LOG(FATAL) << "Unsupported namespace in ipfs path: /" << name_space << '/' ++ << p.pop_all(); ++ } ++ return r; ++} ++ ++std::string Self::url_suffix() const { ++ switch (type) { ++ case Type::Block: ++ return "/ipfs/" + main_param; ++ case Type::Car: ++ return "/ipfs/" + main_param + "/" + path + "?dag-scope=entity"; ++ case Type::Ipns: ++ return "/ipns/" + main_param; ++ case Type::Providers: ++ return "/routing/v1/providers/" + main_param; ++ case Type::DnsLink: ++ LOG(FATAL) << "Don't try to use HTTP(s) for DNS TXT records."; ++ return {}; ++ case Type::Identity: ++ case Type::Zombie: ++ return {}; ++ } ++ LOG(FATAL) << "Unhandled gateway request type: " << static_cast(type); ++ return {}; ++} ++std::string_view Self::accept() const { ++ switch (type) { ++ case Type::Block: ++ return "application/vnd.ipld.raw"sv; ++ case Type::Ipns: ++ return "application/vnd.ipfs.ipns-record"sv; ++ case Type::Car: ++ return "application/vnd.ipld.car"sv; ++ case Type::Providers: ++ return "application/json"sv; ++ case Type::DnsLink: ++ // TODO : not sure this advice is 100% good, actually. ++ // If the user's system setup allows for text records to actually work, ++ // it would be good to respect their autonomy and try to follow the ++ // system's DNS setup. However, it's extremely easy to get yourself in a ++ // situation where Chromium _cannot_ access text records. If you're in ++ // that scenario, it might be better to try to use an IPFS gateway with ++ // DNSLink capability. ++ LOG(FATAL) << "Don't try to use HTTP(s) for DNS TXT records."; ++ return {}; ++ case Type::Identity: ++ case Type::Zombie: ++ return {}; ++ } ++ LOG(FATAL) << "Invalid gateway request type: " << static_cast(type); ++ return {}; ++} ++short Self::timeout_seconds() const { ++ switch (type) { ++ case Type::DnsLink: ++ return 16; ++ case Type::Block: ++ return 39; ++ case Type::Providers: ++ return 64; ++ case Type::Car: ++ return 128; ++ case Type::Ipns: ++ return 256; ++ case Type::Identity: ++ case Type::Zombie: ++ return 0; ++ } ++ LOG(FATAL) << "timeout_seconds() called for unsupported gateway request type " ++ << static_cast(type); ++ return 0; ++} ++ ++auto Self::identity_data() const -> std::string_view { ++ if (type != Type::Identity) { ++ return ""; ++ } ++ auto hash = cid.value().hash(); ++ auto d = reinterpret_cast(hash.data()); ++ return std::string_view{d, hash.size()}; ++} ++ ++bool Self::is_http() const { ++ switch (type) { ++ case Type::Ipns: ++ case Type::Car: ++ case Type::Block: ++ case Type::Providers: ++ return true; ++ case Type::Identity: ++ case Type::DnsLink: ++ case Type::Zombie: ++ return false; ++ } ++ return true; ++} ++auto Self::describe_http(std::string_view prefix) const ++ -> std::optional { ++ if (!is_http()) { ++ return {}; ++ } ++ DCHECK(!prefix.empty()); ++ auto url = url_suffix(); ++ if (url.front() == '/' && prefix.back() == '/') { ++ prefix.remove_suffix(1UL); ++ } else if (url.front() != '/' && prefix.back() != '/') { ++ url.insert(0UL, 1UL, '/'); ++ } ++ url.insert(0UL, prefix); ++ return HttpRequestDescription{url, timeout_seconds(), std::string{accept()}, max_response_size()}; ++} ++std::optional Self::max_response_size() const { ++ switch (type) { ++ case Type::Identity: ++ return 0; ++ case Type::DnsLink: ++ return std::nullopt; ++ case Type::Ipns: ++ return MAX_IPNS_PB_SERIALIZED_SIZE; ++ case Type::Block: ++ return BLOCK_RESPONSE_BUFFER_SIZE; ++ case Type::Car: { ++ // There could be an unlimited number of blocks in the CAR ++ // The _floor_ is the number of path components. ++ // But one path component could be a HAMT sharded directory that we may ++ // need to pass through several layers on. ++ // And the final path component could be a UnixFS file with an unlimited ++ // number of blocks in it. ++ return std::nullopt; ++ } ++ case Type::Zombie: ++ return 0; ++ case Type::Providers: ++ // This one's tricky. ++ // One could easily guess a pracitical limit to the size of a Peer, ++ // and the spec says it SHOULD be limited to 100 peers. ++ // But there's no guaranteed limits. A peer could have an unlimited ++ // number of multiaddrs. And they're allowed to throw in arbitrary ++ // fields I'm supposed to ignore. So in theory it could be infinitely ++ // large. ++ return std::nullopt; ++ } ++ LOG(ERROR) << "Invalid gateway request type " << static_cast(type); ++ return std::nullopt; ++} ++std::string_view ipfs::gw::name(ipfs::gw::Type t) { ++ using ipfs::gw::Type; ++ switch (t) { ++ case Type::Block: ++ return "Block"; ++ case Type::Car: ++ return "Car"; ++ case Type::Ipns: ++ return "Ipns"; ++ case Type::DnsLink: ++ return "DnsLink"; ++ case Type::Providers: ++ return "Providers"; ++ case Type::Identity: ++ return "Identity"; ++ case Type::Zombie: ++ return "CompletedRequest"; ++ } ++ static std::array buf; ++ std::sprintf(buf.data(), "InvalidType %d", static_cast(t)); ++ return buf.data(); ++} ++std::string Self::debug_string() const { ++ std::ostringstream oss; ++ oss << "Request{Type=" << type << ' ' << main_param; ++ if (!path.empty()) { ++ oss << ' ' << path; ++ } ++ if (dependent) { ++ oss << " for=" << dependent->path().to_string(); ++ } ++ oss << " plel=" << parallel << '}'; ++ return oss.str(); ++} ++bool Self::RespondSuccessfully(std::string_view bytes, ++ std::shared_ptr const& api) { ++ using namespace ipfs::ipld; ++ bool success = false; ++ switch (type) { ++ case Type::Block: { ++ DCHECK(cid.has_value()); ++ if (!cid.has_value()) { ++ LOG(ERROR) << "Your CID doesn't even have a value!"; ++ return false; ++ } ++ DCHECK(api); ++ auto node = DagNode::fromBytes(api, cid.value(), bytes); ++ success = orchestrator_->add_node(main_param, node); ++ } break; ++ case Type::Identity: ++ success = orchestrator_->add_node( ++ main_param, std::make_shared(std::string{bytes})); ++ break; ++ case Type::Ipns: ++ if (cid.has_value()) { ++ DCHECK(api); ++ auto byte_ptr = reinterpret_cast(bytes.data()); ++ auto rec = ipfs::ValidateIpnsRecord({byte_ptr, bytes.size()}, ++ cid.value(), *api); ++ if (rec.has_value()) { ++ auto node = DagNode::fromIpnsRecord(rec.value()); ++ success = orchestrator_->add_node(main_param, node); ++ } else { ++ LOG(ERROR) << "IPNS record failed to validate!"; ++ return false; ++ } ++ } ++ break; ++ case Type::DnsLink: ++ LOG(INFO) << "Resolved " << debug_string() << " to " << bytes; ++ if (orchestrator_) { ++ success = orchestrator_->add_node( ++ main_param, std::make_shared(bytes)); ++ } else { ++ LOG(FATAL) << "I have no orchestrator!!"; ++ } ++ break; ++ case Type::Car: { ++ DCHECK(api); ++ Car car(as_bytes(bytes), *api); ++ auto added = 0; ++ while (auto block = car.NextBlock()) { ++ auto cid_s = block->cid.to_string(); ++ auto n = DagNode::fromBytes(api, block->cid, block->bytes); ++ if (!n) { ++ LOG(ERROR) << "Unable to handle block from CAR: " << cid_s; ++ } else if (orchestrator_->add_node(cid_s, n)) { ++ ++added; ++ } else { ++ LOG(INFO) << "Did not add node from CAR: " << cid_s; ++ } ++ } ++ LOG(INFO) << "Added " << added << " nodes from a CAR."; ++ success = added > 0; ++ break; ++ } ++ case Type::Providers: ++ LOG(WARNING) << "TODO - handle responses to providers requests."; ++ break; ++ case Type::Zombie: ++ LOG(WARNING) << "Responding to a zombie is ill-advised."; ++ break; ++ default: ++ LOG(ERROR) << "TODO " << static_cast(type); ++ } ++ if (success) { ++ for (auto& hook : bytes_received_hooks) { ++ hook(bytes); ++ } ++ bytes_received_hooks.clear(); ++ orchestrator_->build_response(dependent); ++ } ++ return success; ++} ++void Self::Hook(std::function f) { ++ bytes_received_hooks.push_back(f); ++} ++void Self::orchestrator(std::shared_ptr const& orc) { ++ orchestrator_ = orc; ++} ++bool Self::PartiallyRedundant() const { ++ if (!orchestrator_) { ++ return false; ++ } ++ return orchestrator_->has_key(main_param); ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/inline_request_handler.cc b/third_party/ipfs_client/src/ipfs_client/gw/inline_request_handler.cc +new file mode 100644 +index 0000000000000..435142a1a74ee +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/inline_request_handler.cc +@@ -0,0 +1,23 @@ ++#include ++ ++#include ++#include ++#include ++ ++#include "log_macros.h" ++ ++using Self = ipfs::gw::InlineRequestHandler; ++ ++std::string_view Self::name() const { ++ return "InlineRequestHandler"; ++} ++auto Self::handle(ipfs::gw::RequestPtr req) -> HandleOutcome { ++ if (req->type != gw::Type::Identity) { ++ VLOG(2) << ipfs::gw::name(req->type); ++ return HandleOutcome::NOT_HANDLED; ++ } ++ std::string data{req->identity_data()}; ++ LOG(INFO) << "Responding to inline CID without using network."; ++ req->RespondSuccessfully(data, api_); ++ return HandleOutcome::DONE; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/requestor.cc b/third_party/ipfs_client/src/ipfs_client/gw/requestor.cc +new file mode 100644 +index 0000000000000..72e6746c7a807 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/requestor.cc +@@ -0,0 +1,70 @@ ++#include ++ ++#include ++#include ++ ++#include ++#include ++#include ++#include ++ ++#include "log_macros.h" ++ ++using Self = ipfs::gw::Requestor; ++using ReqPtr = std::shared_ptr; ++ ++Self& Self::or_else(std::shared_ptr p) { ++ if (next_) { ++ next_->or_else(p); ++ } else { ++ VLOG(2) << name() << " is followed by " << p->name(); ++ next_ = p; ++ } ++ if (api_ && !p->api_) { ++ VLOG(1) << name() << " granting context to " << p->name(); ++ p->api_ = api_; ++ } ++ return *this; ++} ++ ++void Self::request(ReqPtr req) { ++ if (!req || req->type == Type::Zombie) { ++ return; ++ } ++ switch (handle(req)) { ++ case HandleOutcome::MAYBE_LATER: ++ // TODO ++ forward(req); ++ break; ++ case HandleOutcome::PARALLEL: ++ case HandleOutcome::NOT_HANDLED: ++ if (next_) { ++ next_->request(req); ++ } else { ++ LOG(ERROR) << "Ran out of Requestors in the chain while looking for " ++ "one that can handle " ++ << req->debug_string(); ++ definitive_failure(req); ++ } ++ break; ++ case HandleOutcome::PENDING: ++ break; ++ case HandleOutcome::DONE: ++ VLOG(2) << req->debug_string() << " finished synchronously: " << name(); ++ break; ++ } ++} ++void Self::definitive_failure(ipfs::gw::RequestPtr r) const { ++ DCHECK(r); ++ DCHECK(r->dependent); ++ r->dependent->finish(Response::PLAIN_NOT_FOUND); ++} ++ ++void Self::forward(ipfs::gw::RequestPtr req) const { ++ if (next_) { ++ next_->request(req); ++ } ++} ++void Self::api(std::shared_ptr a) { ++ api_ = a; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/requestor_pool.cc b/third_party/ipfs_client/src/ipfs_client/gw/requestor_pool.cc +new file mode 100644 +index 0000000000000..b337dda1b1529 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/requestor_pool.cc +@@ -0,0 +1,73 @@ ++#include "requestor_pool.h" ++ ++#include ++ ++#include "log_macros.h" ++ ++using Self = ipfs::gw::RequestorPool; ++ ++std::string_view Self::name() const { ++ return "requestor pool"; ++} ++Self& Self::add(std::shared_ptr r) { ++ if (api_ && !(r->api_)) { ++ r->api_ = api_; ++ } ++ pool_.push_back(r); ++ r->or_else(shared_from_this()); ++ return *this; ++} ++auto Self::handle(ipfs::gw::RequestPtr req) -> HandleOutcome { ++ auto now = std::time(nullptr); ++ for (auto i = 0UL; i * 2 < waiting_.size(); ++i) { ++ auto& t = waiting_.front().when; ++ if (t != now) { ++ auto to_pop = waiting_.front(); ++ waiting_.pop(); ++ check(to_pop); ++ } ++ } ++ return check({req, 0UL, 0L}); ++} ++auto Self::check(Waiting w) -> HandleOutcome { ++ using O = HandleOutcome; ++ auto next_retry = pool_.size(); ++ auto req = w.req; ++ if (req->PartiallyRedundant()) { ++ return O::DONE; ++ } ++ for (auto i = w.at_idx; i < pool_.size(); ++i) { ++ if (req->type == Type::Zombie) { ++ return O::DONE; ++ } ++ auto& tor = pool_[i]; ++ switch (tor->handle(req)) { ++ case O::DONE: ++ LOG(INFO) << "RequestorPool::handle returning DONE because a member of " ++ "the pool's handle returned DONE."; ++ return O::DONE; ++ case O::PENDING: ++ case O::PARALLEL: ++ req->parallel++; ++ break; ++ case O::MAYBE_LATER: ++ if (next_retry == pool_.size()) { ++ next_retry = i; ++ } ++ break; ++ case O::NOT_HANDLED: ++ break; ++ } ++ } ++ if (req->parallel > 0) { ++ return O::PENDING; ++ } ++ if (next_retry < pool_.size()) { ++ w.when = std::time(nullptr); ++ waiting_.emplace(w); ++ return O::PENDING; ++ } ++ VLOG(1) << "Have exhausted all requestors in pool looking for " ++ << req->debug_string(); ++ return O::NOT_HANDLED; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/requestor_pool.h b/third_party/ipfs_client/src/ipfs_client/gw/requestor_pool.h +new file mode 100644 +index 0000000000000..86104319a32eb +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/requestor_pool.h +@@ -0,0 +1,32 @@ ++#ifndef IPFS_REQUESTOR_POOL_H_ ++#define IPFS_REQUESTOR_POOL_H_ ++ ++#include ++ ++#include ++ ++#include ++#include ++#include ++ ++namespace ipfs::gw { ++class RequestorPool : public Requestor { ++ std::string_view name() const override; ++ HandleOutcome handle(RequestPtr) override; ++ ++ std::vector> pool_; ++ struct Waiting { ++ RequestPtr req; ++ std::size_t at_idx; ++ std::time_t when; ++ }; ++ std::queue waiting_; ++ ++ HandleOutcome check(Waiting); ++ ++ public: ++ RequestorPool& add(std::shared_ptr); ++}; ++} // namespace ipfs::gw ++ ++#endif // IPFS_REQUESTOR_POOL_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/terminating_requestor.cc b/third_party/ipfs_client/src/ipfs_client/gw/terminating_requestor.cc +new file mode 100644 +index 0000000000000..791ffd849ddd4 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/terminating_requestor.cc +@@ -0,0 +1,23 @@ ++#include "ipfs_client/gw/terminating_requestor.h" ++ ++#include ++ ++#include "log_macros.h" ++ ++using Self = ipfs::gw::TerminatingRequestor; ++ ++std::string_view Self::name() const { ++ return "Terminating requestor"; ++} ++auto Self::handle(ipfs::gw::RequestPtr r) -> HandleOutcome { ++ if (r->type == Type::Zombie) { ++ return HandleOutcome::DONE; ++ } else if (r->parallel) { ++ return HandleOutcome::PENDING; ++ } else { ++ VLOG(2) << "Out of options, giving up on gateway request " ++ << r->debug_string(); ++ definitive_failure(r); ++ return HandleOutcome::DONE; ++ } ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/http_request_description.cc b/third_party/ipfs_client/src/ipfs_client/http_request_description.cc +new file mode 100644 +index 0000000000000..19b29d0ccde51 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/http_request_description.cc +@@ -0,0 +1,12 @@ ++#include ++ ++using Self = ipfs::HttpRequestDescription; ++ ++bool Self::operator==(HttpRequestDescription const& r) const { ++ // The concept of identity does NOT involve feedback-looping timeout fudge ++ // Nor is the acceptable size of a response necessary to distinguish. ++ return url == r.url && accept == r.accept; ++} ++bool Self::operator<(HttpRequestDescription const& r) const { ++ return url == r.url ? accept < r.accept : url < r.url; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/identity_cid.cc b/third_party/ipfs_client/src/ipfs_client/identity_cid.cc +new file mode 100644 +index 0000000000000..9ea2421d5cdf3 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/identity_cid.cc +@@ -0,0 +1,19 @@ ++#include ++ ++#include ++ ++namespace Self = ipfs::id_cid; ++ ++auto Self::forText(std::string_view txt) -> Cid { ++ txt = txt.substr(0UL, MaximumHashLength); ++ auto p = reinterpret_cast(txt.data()); ++ auto b = ByteView{p, txt.size()}; ++ MultiHash mh(HashType::IDENTITY, b); ++ if (mh.valid()) { ++ return Cid{MultiCodec::RAW, mh}; ++ } else { ++ LOG(FATAL) ++ << "We really shouldn't be able to fail to 'hash' using identity."; ++ return forText("Unreachable"); ++ } ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/ipfs_request.cc b/third_party/ipfs_client/src/ipfs_client/ipfs_request.cc +new file mode 100644 +index 0000000000000..e4f3e1e4b47e3 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipfs_request.cc +@@ -0,0 +1,43 @@ ++#include ++ ++#include ++ ++#include "log_macros.h" ++ ++#include ++ ++using Self = ipfs::IpfsRequest; ++ ++// Self::IpfsRequest(std::string path_p) ++// : path_{path_p}, callback_([](auto&, auto&) {}) {} ++Self::IpfsRequest(std::string path_p, Finisher f) ++ : path_{path_p}, callback_{f} {} ++ ++std::shared_ptr Self::fromUrl(std::string url, ipfs::IpfsRequest::Finisher f) { ++ url.erase(4UL, 2UL ); ++ url.insert(0UL, 1UL, '/'); ++ return std::make_shared(std::move(url), std::move(f)); ++} ++ ++void Self::till_next(std::size_t w) { ++ waiting_ = w; ++} ++void Self::finish(ipfs::Response& r) { ++ VLOG(2) << "IpfsRequest::finish(" << waiting_ << ',' << r.status_ << ");"; ++ if (waiting_) { ++ if (--waiting_) { ++ return; ++ } ++ } ++ callback_(*this, r); ++ // TODO - cancel other gw req pointing into this ++ callback_ = [](auto& q, auto&) { ++ VLOG(2) << "IPFS request " << q.path().pop_all() << " satisfied multiply"; ++ }; ++} ++bool Self::ready_after() { ++ return waiting_ == 0 || 0 == --waiting_; ++} ++void Self::new_path(std::string_view sv) { ++ path_.assign(sv); ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/chunk.cc b/third_party/ipfs_client/src/ipfs_client/ipld/chunk.cc +new file mode 100644 +index 0000000000000..e5540b080e4b3 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/chunk.cc +@@ -0,0 +1,19 @@ ++#include "chunk.h" ++ ++#include "log_macros.h" ++ ++using Chunk = ipfs::ipld::Chunk; ++ ++Chunk::Chunk(std::string data) : data_{data} {} ++Chunk::~Chunk() {} ++ ++auto Chunk::resolve(ResolutionState& params) -> ResolveResult { ++ if (params.IsFinalComponent()) { ++ return Response{"", 200, data_, params.MyPath().to_string()}; ++ } else { ++ LOG(ERROR) << "Can't resolve a path (" << params.MyPath() ++ << ") inside of a file chunk!"; ++ return ProvenAbsent{}; ++ } ++} ++ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/chunk.h b/third_party/ipfs_client/src/ipfs_client/ipld/chunk.h +new file mode 100644 +index 0000000000000..b846cc5379171 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/chunk.h +@@ -0,0 +1,18 @@ ++#ifndef IPFS_CHUNK_H_ ++#define IPFS_CHUNK_H_ ++ ++#include ++ ++namespace ipfs::ipld { ++class Chunk : public DagNode { ++ std::string const data_; ++ ++ ResolveResult resolve(ResolutionState&) override; ++ ++ public: ++ explicit Chunk(std::string); ++ virtual ~Chunk() noexcept; ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_CHUNK_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/dag_cbor_node.cc b/third_party/ipfs_client/src/ipfs_client/ipld/dag_cbor_node.cc +new file mode 100644 +index 0000000000000..064a89373f743 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/dag_cbor_node.cc +@@ -0,0 +1,25 @@ ++#include "dag_cbor_node.h" ++ ++#include "log_macros.h" ++ ++using Self = ipfs::ipld::DagCborNode; ++ ++auto Self::resolve(ResolutionState& params) -> ResolveResult { ++ if (auto cid = doc_->as_link()) { ++ auto cid_str = cid.value().to_string(); ++ return CallChild(params, "", cid_str); ++ } ++ if (params.IsFinalComponent()) { ++ return Response{"text/html", 200, doc_->html(), ++ params.PathToResolve().to_string()}; ++ } ++ return CallChild(params, [this](std::string_view element_name) -> NodePtr { ++ if (auto child = doc_->at(element_name)) { ++ return std::make_shared(std::move(child)); ++ } ++ return {}; ++ }); ++} ++ ++Self::DagCborNode(std::unique_ptr p) : doc_{std::move(p)} {} ++Self::~DagCborNode() {} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/dag_cbor_node.h b/third_party/ipfs_client/src/ipfs_client/ipld/dag_cbor_node.h +new file mode 100644 +index 0000000000000..c9ba53331674a +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/dag_cbor_node.h +@@ -0,0 +1,22 @@ ++#ifndef IPFS_DAG_CBOR_NODE_H_ ++#define IPFS_DAG_CBOR_NODE_H_ ++ ++#include ++ ++#include ++ ++namespace ipfs::ipld { ++class DagCborNode final : public DagNode { ++ ResolveResult resolve(ResolutionState&) override; ++ ++ public: ++ using Data = DagCborValue; ++ explicit DagCborNode(std::unique_ptr); ++ ~DagCborNode() noexcept override; ++ ++ private: ++ std::unique_ptr doc_; ++}; ++} ++ ++#endif // IPFS_DAG_CBOR_NODE_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/dag_json_node.cc b/third_party/ipfs_client/src/ipfs_client/ipld/dag_json_node.cc +new file mode 100644 +index 0000000000000..dfb3f38b0a0e1 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/dag_json_node.cc +@@ -0,0 +1,95 @@ ++#include "dag_json_node.h" ++ ++#include ++ ++#include ++ ++using Self = ipfs::ipld::DagJsonNode; ++ ++Self::DagJsonNode(std::unique_ptr j) : data_(std::move(j)) { ++ auto cid = data_->get_if_link(); ++ if (!cid) { ++ return; ++ } ++ auto cid_str = cid->to_string(); ++ if (cid_str.size()) { ++ links_.emplace_back("", Link(cid_str)); ++ } ++} ++Self::~DagJsonNode() noexcept {} ++ ++auto Self::resolve(ResolutionState& params) -> ResolveResult { ++ auto respond_as_link = CallChild(params, ""); ++ if (!std::get_if(&respond_as_link)) { ++ return respond_as_link; ++ } ++ if (params.IsFinalComponent()) { ++ return Response{"text/html", 200, html(), params.MyPath().to_string()}; ++ } ++ return CallChild(params, [this](std::string_view name) -> NodePtr { ++ auto child_data = (*data_)[name]; ++ if (child_data) { ++ return std::make_shared(std::move(child_data)); ++ } ++ return {}; ++ }); ++} ++ ++auto Self::is_link() -> Link* { ++ if (links_.size() == 1UL && links_.front().first.empty()) { ++ return &links_.front().second; ++ } else { ++ return nullptr; ++ } ++} ++namespace { ++void write_body(std::ostream& str, ipfs::DagJsonValue const& val) { ++ if (auto link = val.get_if_link()) { ++ auto cid_str = link.value().to_string(); ++ str << "" << cid_str << "\n"; ++ } else if (auto keys = val.object_keys()) { ++ str << "{\n"; ++ for (auto& key : keys.value()) { ++ str << " \n \n" ++ << " \n \n \n"; ++ } ++ str << "
  ""; ++ for (auto c : key) { ++ str << html_escape(c); ++ } ++ str << "":\n"; ++ auto child = val[key]; ++ write_body(str, *child); ++ str << ",
}\n"; ++ } else if (val.iterate_list([](auto&) {})) { ++ str << "[\n"; ++ val.iterate_list([&str](auto& child) { ++ str << " \n \n \n \n"; ++ }); ++ str << "
  \n"; ++ write_body(str, child); ++ str << "
]\n"; ++ } else { ++ auto plain = val.pretty_print(); ++ // str << "

"; ++ for (auto c : plain) { ++ if (c == '\n') { ++ str << "
\n"; ++ } else { ++ str << html_escape(c); ++ } ++ } ++ // str << "

\n"; ++ } ++} ++} // namespace ++std::string const& Self::html() { ++ if (html_.empty()) { ++ std::ostringstream html; ++ html << "Preview of DAG-JSON\n"; ++ write_body(html, *data_); ++ html << "\n"; ++ html_ = html.str(); ++ } ++ return html_; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/dag_json_node.h b/third_party/ipfs_client/src/ipfs_client/ipld/dag_json_node.h +new file mode 100644 +index 0000000000000..1d8012a007487 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/dag_json_node.h +@@ -0,0 +1,22 @@ ++#ifndef IPFS_DAG_JSON_NODE_H_ ++#define IPFS_DAG_JSON_NODE_H_ ++ ++#include ++#include ++ ++namespace ipfs::ipld { ++class DagJsonNode final : public DagNode { ++ std::unique_ptr data_; ++ std::string html_; ++ ResolveResult resolve(ResolutionState& params) override; ++ Link* is_link(); ++ std::string const& html(); ++ ++ public: ++ DagJsonNode(std::unique_ptr); ++ ~DagJsonNode() noexcept override; ++}; ++ ++} // namespace ipfs::ipld ++ ++#endif // IPFS_DAG_JSON_NODE_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/dag_node.cc b/third_party/ipfs_client/src/ipfs_client/ipld/dag_node.cc +new file mode 100644 +index 0000000000000..4d11f6f23bc78 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/dag_node.cc +@@ -0,0 +1,248 @@ ++#include ++ ++#include "chunk.h" ++#include "dag_cbor_node.h" ++#include "dag_json_node.h" ++#include "directory_shard.h" ++#include "ipns_name.h" ++#include "root.h" ++#include "small_directory.h" ++#include "symlink.h" ++#include "unixfs_file.h" ++ ++#include ++#include ++#include ++ ++#include "log_macros.h" ++ ++#include ++#include ++ ++using Node = ipfs::ipld::DagNode; ++ ++std::shared_ptr Node::fromBytes(std::shared_ptr const& api, ++ Cid const& cid, ++ std::string_view bytes) { ++ return fromBytes(api, cid, as_bytes(bytes)); ++} ++auto Node::fromBytes(std::shared_ptr const& api, ++ ipfs::Cid const& cid, ++ ipfs::ByteView bytes) -> NodePtr { ++ std::shared_ptr result = nullptr; ++ auto hash = api->Hash(cid.hash_type(), bytes); ++ if (!hash.has_value()) { ++ LOG(ERROR) << "Could not hash response for " << cid.to_string(); ++ return {}; ++ } ++ if (hash.value().size() != cid.hash().size()) { ++ return {}; ++ } ++ for (auto i = 0U; i < hash.value().size(); ++i) { ++ auto e = cid.hash()[i]; ++ auto a = hash.value().at(i); ++ if (e != a) { ++ return {}; ++ } ++ } ++ auto required = cid.hash(); ++ auto calculated = hash.value(); ++ if (!std::equal(required.begin(), required.end(), calculated.begin(), ++ calculated.end())) { ++ LOG(ERROR) << "Hash of response did not match the one in the CID " ++ << cid.to_string(); ++ return {}; ++ } ++ switch (cid.codec()) { ++ case MultiCodec::DAG_CBOR: { ++ auto p = reinterpret_cast(bytes.data()); ++ auto cbor = api->ParseCbor({p, bytes.size()}); ++ if (cbor) { ++ result = std::make_shared(std::move(cbor)); ++ } else { ++ LOG(ERROR) << "CBOR node " << cid.to_string() ++ << " does not parse as CBOR."; ++ } ++ } break; ++ case MultiCodec::DAG_JSON: { ++ auto p = reinterpret_cast(bytes.data()); ++ auto json = api->ParseJson({p, bytes.size()}); ++ if (json) { ++ result = std::make_shared(std::move(json)); ++ } else { ++ LOG(ERROR) << "JSON node " << cid.to_string() ++ << " does not parse as JSON."; ++ } ++ } break; ++ case MultiCodec::RAW: ++ case MultiCodec::DAG_PB: { ++ ipfs::PbDag b{cid, bytes}; ++ if (b.valid()) { ++ result = fromBlock(b); ++ } else { ++ std::ostringstream hex; ++ for (auto byt : bytes) { ++ hex << ' ' << std::hex ++ << static_cast(static_cast(byt)); ++ } ++ LOG(ERROR) ++ << "Have a response that did not parse as a valid block, cid: " ++ << cid.to_string() << " contents: " << bytes.size() ++ << " bytes = " << hex.str(); ++ } ++ } break; ++ case MultiCodec::INVALID: ++ case MultiCodec::IDENTITY: ++ case MultiCodec::LIBP2P_KEY: ++ default: ++ LOG(ERROR) << "Response for unhandled CID Codec: " ++ << GetName(cid.codec()); ++ } ++ if (result) { ++ result->set_api(api); ++ } ++ return result; ++} ++std::shared_ptr Node::fromBlock(ipfs::PbDag const& block) { ++ std::shared_ptr result; ++ switch (block.type()) { ++ case PbDag::Type::FileChunk: ++ return std::make_shared(block.chunk_data()); ++ case PbDag::Type::NonFs: ++ return std::make_shared(block.unparsed()); ++ case PbDag::Type::Symlink: ++ return std::make_shared(block.chunk_data()); ++ case PbDag::Type::Directory: ++ result = std::make_shared(); ++ break; ++ case PbDag::Type::File: ++ case PbDag::Type::Raw: ++ result = std::make_shared(); ++ break; ++ case PbDag::Type::HAMTShard: ++ if (block.fsdata().has_fanout()) { ++ result = std::make_shared(block.fsdata().fanout()); ++ } else { ++ result = std::make_shared(); ++ } ++ break; ++ case PbDag::Type::Metadata: ++ LOG(ERROR) << "Metadata blocks unhandled."; ++ return result; ++ case PbDag::Type::Invalid: ++ LOG(ERROR) << "Invalid block."; ++ return result; ++ default: ++ LOG(FATAL) << "TODO " << static_cast(block.type()); ++ } ++ auto add_link = [&result](auto& n, auto c) { ++ result->links_.emplace_back(n, c); ++ return true; ++ }; ++ block.List(add_link); ++ return result; ++} ++ ++auto Node::fromIpnsRecord(ipfs::ValidatedIpns const& v) -> NodePtr { ++ return std::make_shared(v.value); ++} ++ ++std::shared_ptr Node::deroot() { ++ return shared_from_this(); ++} ++std::shared_ptr Node::rooted() { ++ return std::make_shared(shared_from_this()); ++} ++auto Node::as_hamt() -> DirShard* { ++ return nullptr; ++} ++void Node::set_api(std::shared_ptr api) { ++ api_ = api; ++} ++auto Node::resolve(SlashDelimited initial_path, BlockLookup blu) ++ -> ResolveResult { ++ ResolutionState state; ++ state.resolved_path_components = ""; ++ state.unresolved_path = initial_path; ++ state.get_available_block = blu; ++ return resolve(state); ++} ++auto Node::CallChild(ipfs::ipld::ResolutionState& state) -> ResolveResult { ++ return CallChild(state, state.NextComponent(api_.get())); ++} ++auto Node::CallChild(ipfs::ipld::ResolutionState& state, ++ std::string_view link_key, ++ std::string_view block_key) -> ResolveResult { ++ auto child = FindChild(link_key); ++ if (!child) { ++ links_.emplace_back(link_key, Link{std::string{block_key}, {}}); ++ } ++ return CallChild(state, link_key); ++} ++auto Node::CallChild(ResolutionState& state, std::string_view link_key) ++ -> ResolveResult { ++ auto* child = FindChild(link_key); ++ if (!child) { ++ return ProvenAbsent{}; ++ } ++ auto& node = child->node; ++ if (!node) { ++ node = state.GetBlock(child->cid); ++ } ++ if (node) { ++ Descend(state); ++ return node->resolve(state); ++ } else { ++ std::string needed{"/ipfs/"}; ++ needed.append(child->cid); ++ auto more = state.unresolved_path.to_view(); ++ if (more.size()) { ++ if (more.front() != '/') { ++ needed.push_back('/'); ++ } ++ needed.append(more); ++ } ++ return MoreDataNeeded{needed}; ++ } ++} ++auto Node::CallChild(ResolutionState& state, ++ std::function gen_child) ++ -> ResolveResult { ++ auto link_key = state.NextComponent(api_.get()); ++ auto child = FindChild(link_key); ++ if (!child) { ++ links_.emplace_back(link_key, Link{{}, {}}); ++ child = &links_.back().second; ++ } ++ auto& node = child->node; ++ if (!node) { ++ node = gen_child(link_key); ++ if (!node) { ++ return ProvenAbsent{}; ++ } ++ } ++ Descend(state); ++ return node->resolve(state); ++} ++auto Node::FindChild(std::string_view link_key) -> Link* { ++ for (auto& [name, link] : links_) { ++ if (name == link_key) { ++ return &link; ++ } ++ } ++ return nullptr; ++} ++void Node::Descend(ResolutionState& state) { ++ auto next = state.unresolved_path.pop(); ++ if (next.empty()) { ++ return; ++ } ++ if (!state.resolved_path_components.ends_with('/')) { ++ state.resolved_path_components.push_back('/'); ++ } ++ state.resolved_path_components.append(next); ++} ++ ++std::ostream& operator<<(std::ostream& s, ipfs::ipld::PathChange const& c) { ++ return s << "PathChange{" << c.new_path << '}'; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/directory_shard.cc b/third_party/ipfs_client/src/ipfs_client/ipld/directory_shard.cc +new file mode 100644 +index 0000000000000..7839e62b66247 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/directory_shard.cc +@@ -0,0 +1,101 @@ ++#include "directory_shard.h" ++ ++#include "log_macros.h" ++ ++#include ++#include ++ ++#include ++ ++#include ++#include ++#include ++ ++using namespace std::literals; ++ ++using Self = ipfs::ipld::DirShard; ++ ++auto Self::resolve(ResolutionState& parms) -> ResolveResult { ++ if (parms.IsFinalComponent()) { ++ auto index_parm = parms.WithPath("index.html"sv); ++ auto result = resolve(index_parm); ++ // TODO generate index.html if not present ++ auto resp = std::get_if(&result); ++ if (resp) { ++ resp->mime_ = "text/html"; ++ } ++ return result; ++ } ++ std::string name{parms.NextComponent(api_.get())}; ++ auto hash = hexhash(name); ++ return resolve_internal(hash.begin(), hash.end(), name, parms); ++} ++auto Self::resolve_internal(ipfs::ipld::DirShard::HashIter hash_b, ++ ipfs::ipld::DirShard::HashIter hash_e, ++ std::string_view human_name, ++ ResolutionState& parms) -> ResolveResult { ++ auto hash_chunk = hash_b == hash_e ? std::string{} : *hash_b; ++ for (auto& [name, link] : links_) { ++ if (!starts_with(name, hash_chunk)) { ++ continue; ++ } ++ if (ends_with(name, human_name)) { ++ VLOG(2) << "Found " << human_name << ", leaving HAMT sharded directory " ++ << name << "->" << link.cid; ++ return CallChild(parms, name); ++ } ++ auto node = parms.GetBlock(link.cid); ++ if (!node) { ++ // Unfortunately we can't really append more path and do a full Car ++ // request ++ // The gateway would hash whatever we gave it and compare it to a ++ // partially-consumed hash ++ return MoreDataNeeded{{"/ipfs/" + link.cid}}; ++ } ++ auto downcast = node->as_hamt(); ++ if (downcast) { ++ if (hash_b == hash_e) { ++ LOG(ERROR) << "Ran out of hash bits."; ++ return ProvenAbsent{}; ++ } ++ VLOG(2) << "Found hash chunk, continuing to next level of HAMT sharded " ++ "directory " ++ << name << "->" << link.cid; ++ return downcast->resolve_internal(std::next(hash_b), hash_e, human_name, ++ parms); ++ } else { ++ return ProvenAbsent{}; ++ } ++ } ++ return ProvenAbsent{}; ++} ++std::vector Self::hexhash(std::string_view path_element) const { ++ auto hex_width = 0U; ++ for (auto x = fanout_; (x >>= 4); ++hex_width) ++ ; ++ std::array digest = {0U, 0U}; ++ MurmurHash3_x64_128(path_element.data(), path_element.size(), 0, ++ digest.data()); ++ std::vector result; ++ for (auto d : digest) { ++ auto hash_bits = htobe64(d); ++ while (hash_bits) { ++ // 2. Pop the log2(fanout_) lowest bits from the path component hash ++ // digest,... ++ auto popped = hash_bits % fanout_; ++ hash_bits /= fanout_; ++ std::ostringstream oss; ++ // ... then hex encode (using 0-F) using little endian those bits ... ++ oss << std::setfill('0') << std::setw(hex_width) << std::uppercase ++ << std::hex << popped; ++ result.push_back(oss.str()); ++ } ++ } ++ return result; ++} ++ ++Self::DirShard(std::uint64_t fanout) : fanout_{fanout} {} ++Self::~DirShard() {} ++Self* Self::as_hamt() { ++ return this; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/directory_shard.h b/third_party/ipfs_client/src/ipfs_client/ipld/directory_shard.h +new file mode 100644 +index 0000000000000..c1cb811355922 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/directory_shard.h +@@ -0,0 +1,26 @@ ++#ifndef IPFS_DIRECTORY_SHARD_H_ ++#define IPFS_DIRECTORY_SHARD_H_ 1 ++ ++#include ++ ++namespace ipfs::ipld { ++class DirShard : public DagNode { ++ std::uint64_t const fanout_; ++ ++ ResolveResult resolve(ResolutionState&) override; ++ DirShard* as_hamt() override; ++ ++ std::vector hexhash(std::string_view path_element) const; ++ using HashIter = std::vector::const_iterator; ++ ResolveResult resolve_internal(HashIter, ++ HashIter, ++ std::string_view, ++ ResolutionState&); ++ ++ public: ++ explicit DirShard(std::uint64_t fanout = 256UL); ++ virtual ~DirShard() noexcept; ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_DIRECTORY_SHARD_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/ipns_name.cc b/third_party/ipfs_client/src/ipfs_client/ipld/ipns_name.cc +new file mode 100644 +index 0000000000000..f38200923f49f +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/ipns_name.cc +@@ -0,0 +1,25 @@ ++#include "ipns_name.h" ++ ++#include "log_macros.h" ++ ++using Self = ipfs::ipld::IpnsName; ++ ++Self::IpnsName(std::string_view target_abs_path) ++ : target_path_{target_abs_path} {} ++ ++auto Self::resolve(ResolutionState& params) -> ResolveResult { ++ // Can't use PathChange, as the target is truly absolute (rootless) ++ SlashDelimited t{target_path_}; ++ t.pop(); // Discard namespace, though realistically it's going to be ipfs ++ // basically all the time ++ auto name = t.pop(); ++ if (t) { ++ LOG(WARNING) << "Odd case: name points at /ns/root/MORE/PATH (" ++ << target_path_ << "): " << params.MyPath(); ++ auto path = t.to_string() + "/" + params.PathToResolve().to_string(); ++ auto altered = params.WithPath(path); ++ return CallChild(altered, "", name); ++ } else { ++ return CallChild(params, "", name); ++ } ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/ipns_name.h b/third_party/ipfs_client/src/ipfs_client/ipld/ipns_name.h +new file mode 100644 +index 0000000000000..8b50d6e86e397 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/ipns_name.h +@@ -0,0 +1,18 @@ ++#ifndef IPFS_IPLD_IPNS_NAME_H_ ++#define IPFS_IPLD_IPNS_NAME_H_ ++ ++#include "ipfs_client/ipld/dag_node.h" ++ ++namespace ipfs::ipld { ++class IpnsName : public DagNode { ++ std::string const target_path_; ++ ++ ResolveResult resolve(ResolutionState& params) override; ++ ++ public: ++ IpnsName(std::string_view target_abs_path); ++ virtual ~IpnsName() noexcept {} ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_IPLD_IPNS_NAME_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/link.cc b/third_party/ipfs_client/src/ipfs_client/ipld/link.cc +new file mode 100644 +index 0000000000000..f9dad98e58840 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/link.cc +@@ -0,0 +1,6 @@ ++#include "ipfs_client/ipld/link.h" ++ ++using Self = ipfs::ipld::Link; ++ ++Self::Link(std::string cid_s) : cid{cid_s} {} ++Self::Link(std::string s, std::shared_ptr n) : cid{s}, node{n} {} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/resolution_state.cc b/third_party/ipfs_client/src/ipfs_client/ipld/resolution_state.cc +new file mode 100644 +index 0000000000000..7b51513d83d6f +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/resolution_state.cc +@@ -0,0 +1,36 @@ ++#include ++ ++#include ++ ++using Self = ipfs::ipld::ResolutionState; ++ ++bool Self::IsFinalComponent() const { ++ return !unresolved_path; ++} ++auto Self::PathToResolve() const -> SlashDelimited { ++ return unresolved_path; ++} ++auto Self::MyPath() const -> SlashDelimited { ++ return SlashDelimited{resolved_path_components}; ++} ++std::string Self::NextComponent(ContextApi const* api) const { ++ auto copy = unresolved_path; ++ if (api) { ++ return api->UnescapeUrlComponent(copy.pop()); ++ } else { ++ return std::string{copy.pop()}; ++ } ++} ++auto Self::GetBlock(std::string const& block_key) const -> NodePtr { ++ return get_available_block(block_key); ++} ++Self Self::WithPath(std::string_view p) const { ++ auto rv = *this; ++ rv.unresolved_path = SlashDelimited{p}; ++ return rv; ++} ++auto Self::RestartResolvedPath() const -> ResolutionState { ++ auto rv = *this; ++ rv.resolved_path_components.clear(); ++ return rv; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/root.cc b/third_party/ipfs_client/src/ipfs_client/ipld/root.cc +new file mode 100644 +index 0000000000000..fd5af1b2891b2 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/root.cc +@@ -0,0 +1,90 @@ ++#include "root.h" ++ ++#include "log_macros.h" ++ ++using namespace std::literals; ++ ++using Self = ipfs::ipld::Root; ++using Ptr = std::shared_ptr; ++ ++Self::Root(std::shared_ptr under) { ++ links_.push_back({{}, Link{{}, under}}); ++} ++Self::~Root() {} ++ ++Ptr Self::deroot() { ++ return links_.at(0).second.node; ++} ++Ptr Self::rooted() { ++ return shared_from_this(); ++} ++ ++auto Self::resolve(ResolutionState& params) -> ResolveResult { ++ auto location = params.PathToResolve().to_string(); ++ auto result = deroot()->resolve(params); ++ if (auto pc = std::get_if(&result)) { ++ auto lower = params.WithPath(pc->new_path); ++ result = resolve(lower); ++ location.assign(lower.MyPath().to_view()); ++ } else if (std::get_if(&result)) { ++ if (params.NextComponent(api_.get()) == "_redirects") { ++ return result; ++ } ++ if (!redirects_.has_value()) { ++ auto redirects_path = params.WithPath("_redirects"); ++ result = resolve(redirects_path); ++ auto redirect_resp = std::get_if(&result); ++ if (redirect_resp && redirect_resp->status_ == 200) { ++ redirects_ = redirects::File(redirect_resp->body_); ++ } else { ++ // Either this is ProvenAbsent, in which case this will be interpreted ++ // as the original ProvenAbsent Or it's MoreDataNeeded but for ++ // _redirects, which is what we need now ++ return result; ++ } ++ } ++ if (redirects_.has_value() && redirects_.value().valid()) { ++ Response* resp = nullptr; ++ auto status = redirects_.value().rewrite(location); ++ if (location.find("://") < location.size()) { ++ LOG(INFO) << "_redirects file sent us to a whole URL, scheme-and-all: " ++ << location << " status=" << status; ++ return Response{"", status, "", location}; ++ } ++ auto lower_parm = params.WithPath(location).RestartResolvedPath(); ++ switch (status / 100) { ++ case 0: // no rewrites available ++ break; ++ case 2: ++ result = deroot()->resolve(lower_parm); ++ location.assign(lower_parm.MyPath().to_view()); ++ break; ++ case 3: ++ // Let the redirect happen ++ return Response{"", status, "", location}; ++ case 4: { ++ result = deroot()->resolve(lower_parm); ++ location.assign(lower_parm.MyPath().to_view()); ++ if (std::get_if(&result)) { ++ return Response{"", 500, "", location}; ++ } ++ resp = std::get_if(&result); ++ if (resp) { ++ resp->status_ = status; ++ return *resp; ++ } ++ break; // MoreDataNeeded to fetch e.g. custom 404 page ++ } ++ default: ++ LOG(ERROR) << "Unsupported status came back from _redirects file: " ++ << status; ++ return ProvenAbsent{}; ++ } ++ } ++ } ++ auto resp = std::get_if(&result); ++ if (resp && resp->location_.empty()) { ++ resp->location_ = location; ++ } ++ return result; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/root.h b/third_party/ipfs_client/src/ipfs_client/ipld/root.h +new file mode 100644 +index 0000000000000..b57951b42f7f1 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/root.h +@@ -0,0 +1,23 @@ ++#ifndef IPFS_ROOT_H_ ++#define IPFS_ROOT_H_ ++ ++#include ++#include ++ ++#include ++ ++namespace ipfs::ipld { ++class Root : public DagNode { ++ std::optional redirects_; ++ ++ ResolveResult resolve(ResolutionState& params) override; ++ std::shared_ptr rooted() override; ++ std::shared_ptr deroot() override; ++ ++ public: ++ Root(std::shared_ptr); ++ virtual ~Root() noexcept; ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_ROOT_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/small_directory.cc b/third_party/ipfs_client/src/ipfs_client/ipld/small_directory.cc +new file mode 100644 +index 0000000000000..b8613663932ca +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/small_directory.cc +@@ -0,0 +1,35 @@ ++#include "small_directory.h" ++ ++#include ++#include "ipfs_client/generated_directory_listing.h" ++#include "ipfs_client/path2url.h" ++ ++#include "log_macros.h" ++ ++#include ++ ++using namespace std::literals; ++ ++using Self = ipfs::ipld::SmallDirectory; ++ ++auto Self::resolve(ResolutionState& params) -> ResolveResult { ++ if (params.IsFinalComponent()) { ++ LOG(INFO) << "Directory listing requested for " << params.MyPath(); ++ auto result = CallChild(params, "index.html"); ++ if (auto resp = std::get_if(&result)) { ++ resp->mime_ = "text/html"; ++ } ++ if (!std::get_if(&result)) { ++ return result; ++ } ++ auto dir_path = params.MyPath().to_view(); ++ GeneratedDirectoryListing index_html{dir_path}; ++ for (auto& [name, link] : links_) { ++ index_html.AddEntry(name); ++ } ++ return Response{"text/html", 200, index_html.Finish(), ""}; ++ } ++ return CallChild(params); ++} ++ ++Self::~SmallDirectory() {} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/small_directory.h b/third_party/ipfs_client/src/ipfs_client/ipld/small_directory.h +new file mode 100644 +index 0000000000000..a076122c5041f +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/small_directory.h +@@ -0,0 +1,17 @@ ++#ifndef IPFS_UNIXFS_DIRECTORY_H_ ++#define IPFS_UNIXFS_DIRECTORY_H_ ++ ++#include "ipfs_client/ipld/link.h" ++ ++#include ++ ++namespace ipfs::ipld { ++class SmallDirectory : public DagNode { ++ ResolveResult resolve(ResolutionState&) override; ++ ++ public: ++ virtual ~SmallDirectory() noexcept; ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_UNIXFS_DIRECTORY_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/symlink.cc b/third_party/ipfs_client/src/ipfs_client/ipld/symlink.cc +new file mode 100644 +index 0000000000000..b35725ad7f703 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/symlink.cc +@@ -0,0 +1,37 @@ ++#include "symlink.h" ++ ++#include "log_macros.h" ++ ++using Self = ipfs::ipld::Symlink; ++ ++Self::Symlink(std::string target) : target_{target} {} ++ ++Self::~Symlink() {} ++ ++auto Self::resolve(ResolutionState& params) -> ResolveResult { ++ std::string result; ++ if (!is_absolute()) { ++ auto left_path = params.MyPath(); ++ left_path.pop_n(2); // Returning a path relative to content root. ++ left_path.pop_back(); // Because the final component refers to this ++ // symlink, which is getting replaced with target ++ result.assign(left_path.to_view()); ++ } ++ result.append("/").append(target_); ++ if (!params.IsFinalComponent()) { ++ result.append("/").append(params.PathToResolve().to_string()); ++ } ++ std::size_t i; ++ while ((i = result.find("//")) != std::string::npos) { ++ result.erase(i, 1); ++ } ++ if (result.ends_with('/')) { ++ result.resize(result.size() - 1); ++ } ++ LOG(INFO) << "symlink: '" << params.MyPath() << "' -> '" << result << "'."; ++ return PathChange{result}; ++} ++ ++bool Self::is_absolute() const { ++ return target_.at(0) == '/'; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/symlink.h b/third_party/ipfs_client/src/ipfs_client/ipld/symlink.h +new file mode 100644 +index 0000000000000..937f0d248c25d +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/symlink.h +@@ -0,0 +1,20 @@ ++#ifndef IPFS_SYMLINK_H_ ++#define IPFS_SYMLINK_H_ ++ ++#include ++ ++namespace ipfs::ipld { ++class Symlink : public DagNode { ++ std::string const target_; ++ ++ ResolveResult resolve(ResolutionState& params) override; ++ ++ bool is_absolute() const; ++ ++ public: ++ Symlink(std::string target); ++ ~Symlink() noexcept override; ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_SYMLINK_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/unixfs_file.cc b/third_party/ipfs_client/src/ipfs_client/ipld/unixfs_file.cc +new file mode 100644 +index 0000000000000..784a1df367152 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/unixfs_file.cc +@@ -0,0 +1,50 @@ ++#include "unixfs_file.h" ++ ++#include "log_macros.h" ++ ++using namespace std::literals; ++ ++using Self = ipfs::ipld::UnixfsFile; ++ ++auto Self::resolve(ResolutionState& params) -> ResolveResult { ++ if (!params.IsFinalComponent()) { ++ LOG(ERROR) << "Can't path through a file, (at " << params.MyPath() ++ << ") but given the path " << params.PathToResolve(); ++ return ProvenAbsent{}; ++ } ++ std::vector missing; ++ std::string body; ++ for (auto& child : links_) { ++ auto& link = child.second; ++ if (!link.node) { ++ link.node = params.GetBlock(link.cid); ++ } ++ if (link.node) { ++ auto recurse = link.node->resolve(params); ++ auto mdn = std::get_if(&recurse); ++ if (mdn) { ++ missing.insert(missing.end(), mdn->ipfs_abs_paths_.begin(), ++ mdn->ipfs_abs_paths_.end()); ++ continue; ++ } ++ if (missing.empty()) { ++ body.append(std::get(recurse).body_); ++ } ++ } else { ++ missing.push_back("/ipfs/" + link.cid); ++ } ++ } ++ if (missing.empty()) { ++ return Response{ ++ "", ++ 200, ++ body, ++ params.MyPath().to_string(), ++ }; ++ } ++ auto result = MoreDataNeeded{missing}; ++ result.insist_on_car = true; ++ return result; ++} ++ ++Self::~UnixfsFile() {} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/unixfs_file.h b/third_party/ipfs_client/src/ipfs_client/ipld/unixfs_file.h +new file mode 100644 +index 0000000000000..3447e949d330e +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/unixfs_file.h +@@ -0,0 +1,15 @@ ++#ifndef IPFS_UNIXFS_FILE_H_ ++#define IPFS_UNIXFS_FILE_H_ ++ ++#include ++ ++namespace ipfs::ipld { ++class UnixfsFile : public DagNode { ++ ResolveResult resolve(ResolutionState&) override; ++ ++ public: ++ virtual ~UnixfsFile() noexcept; ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_UNIXFS_FILE_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipns_names.cc b/third_party/ipfs_client/src/ipfs_client/ipns_names.cc +new file mode 100644 +index 0000000000000..6eccfaa7dc51b +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipns_names.cc +@@ -0,0 +1,101 @@ ++#include ++ ++#include ++ ++#include "log_macros.h" ++ ++using Self = ipfs::IpnsNames; ++ ++void Self::NoSuchName(std::string const& name) { ++ names_[name]; // If it already exists, leave it. ++} ++void Self::AssignName(std::string const& name, ValidatedIpns entry) { ++ auto& res = entry.value; ++ if (res.size() && res.front() == '/') { ++ res.erase(0, 1); ++ } ++ auto endofcid = res.find_first_of("/?#", 6); ++ using namespace libp2p::multi; ++ auto cid_str = res.substr(5, endofcid); ++ LOG(INFO) << "IPNS points to CID " << cid_str; ++ auto cid = Cid(cid_str); ++ if (cid.valid()) { ++ auto desensitized = res.substr(0, 5); ++ desensitized.append(cid_str); ++ if (endofcid < res.size()) { ++ auto extra = res.substr(endofcid); ++ LOG(INFO) << name << " resolution contains oddity '" << extra; ++ desensitized.append(extra); ++ } ++ LOG(INFO) << name << " now resolves to (desensitized)" << desensitized; ++ entry.value = desensitized; ++ } else { ++ LOG(INFO) << name << " now resolves to (extra level)" << res; ++ } ++ auto it = names_.find(name); ++ if (it == names_.end()) { ++ names_.emplace(name, std::move(entry)); ++ } else if (it->second.sequence < entry.sequence) { ++ LOG(INFO) << "Updating IPNS record for " << name << " from sequence " ++ << it->second.sequence << " where it pointed to " ++ << it->second.value << " to sequence " << entry.sequence ++ << " where it points to " << entry.value; ++ it->second = std::move(entry); ++ } else { ++ LOG(INFO) << "Discarding redundant IPNS record for " << name; ++ } ++} ++void Self::AssignDnsLink(std::string const& name, std::string_view target) { ++ ValidatedIpns v; ++ v.value.assign(target); ++ auto t = std::time(nullptr); ++ v.use_until = v.cache_until = t + 300; ++ AssignName(name, std::move(v)); ++} ++ ++std::string_view Self::NameResolvedTo(std::string_view original_name) const { ++ std::string name{original_name}; ++ std::string_view prev = ""; ++ auto trailer = names_.end(); ++ auto trail_step = false; ++ auto now = std::time(nullptr); ++ while (true) { ++ auto it = names_.find(name); ++ if (names_.end() == it) { ++ LOG(INFO) << "Host not in immediate access map: " << name << " (" ++ << std::string{original_name} << ')'; ++ return prev; ++ } else if (it == trailer) { ++ LOG(ERROR) << "Host cycle found in IPNS: " << std::string{original_name} ++ << ' ' << name; ++ return ""; ++ } ++ auto& target = it->second.value; ++ if (target.empty()) { ++ return kNoSuchName; ++ } ++ if (target.at(2) == 'f') { ++ return target; ++ } ++ if (it->second.use_until < now) { ++ return prev; ++ } ++ if (trail_step) { ++ if (trailer == names_.end()) { ++ trailer = names_.find(name); ++ } else { ++ trailer = names_.find(trailer->second.value.substr(5)); ++ } ++ } ++ trail_step = !trail_step; ++ prev = it->second.value; ++ name.assign(prev, 5); ++ } ++} ++auto Self::Entry(std::string const& name) -> ValidatedIpns const* { ++ auto it = names_.find(name); ++ return it == names_.end() ? nullptr : &(it->second); ++} ++ ++Self::IpnsNames() {} ++Self::~IpnsNames() {} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipns_record.cc b/third_party/ipfs_client/src/ipfs_client/ipns_record.cc +new file mode 100644 +index 0000000000000..86be780f9066f +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipns_record.cc +@@ -0,0 +1,244 @@ ++#include ++ ++#include ++#include ++#include ++ ++#include "log_macros.h" ++ ++#include ++#include ++ ++#if __has_include() ++#include ++#else ++#include "ipfs_client/ipns_record.pb.h" ++#endif ++ ++namespace { ++bool matches(ipfs::MultiHash const& hash, ++ ipfs::ByteView pubkey_bytes, ++ ipfs::ContextApi& api) { ++ auto result = api.Hash(hash.type(), pubkey_bytes); ++ if (!result.has_value()) { ++ return false; ++ } ++ return std::equal(result->begin(), result->end(), hash.digest().begin(), ++ hash.digest().end()); ++} ++} // namespace ++ ++namespace { ++void assign(std::string& out, ++ ipfs::DagCborValue& top, ++ std::string_view key) { ++ auto p = top.at(key); ++ if (!p) { ++ out.assign("Key '").append(key).append("' not present in IPNS CBOR!"); ++ } else { ++ // YEP! as_bytes() . There are only 2 string values here, they are logically ++ // text, but they are defined in the spec to be bytes. ++ auto o = p->as_bytes(); ++ if (o.has_value()) { ++ auto chars = reinterpret_cast(o.value().data()); ++ out.assign(chars, o.value().size()); ++ } else { ++ out.assign("Key '").append(key).append( ++ "' was not a string in IPNS CBOR!"); ++ } ++ } ++} ++void assign(std::uint64_t& out, ++ ipfs::DagCborValue& top, ++ std::string_view key) { ++ auto p = top.at(key); ++ if (!p) { ++ LOG(ERROR) << "Key '" << key << "' is not present in IPNS CBOR!"; ++ out = std::numeric_limits::max(); ++ } else { ++ auto o = p->as_unsigned(); ++ if (o.has_value()) { ++ out = o.value(); ++ } else { ++ LOG(ERROR) << "Key '" << key ++ << "' is not an unsigned integer in IPNS CBOR!"; ++ out = std::numeric_limits::max(); ++ } ++ } ++} ++} // namespace ++ ++auto ipfs::ValidateIpnsRecord(ipfs::ByteView top_level_bytes, ++ Cid const& name, ++ ContextApi& api) -> std::optional { ++ DCHECK_EQ(name.codec(), MultiCodec::LIBP2P_KEY); ++ if (name.codec() != MultiCodec::LIBP2P_KEY) { ++ return {}; ++ } ++ // https://github.com/ipfs/specs/blob/main/ipns/IPNS.md#record-verification ++ ++ // Before parsing the protobuf, confirm that the serialized IpnsEntry bytes ++ // sum to less than or equal to the size limit. ++ if (top_level_bytes.size() > MAX_IPNS_PB_SERIALIZED_SIZE) { ++ LOG(ERROR) << "IPNS record too large: " << top_level_bytes.size(); ++ return {}; ++ } ++ ++ ipfs::ipns::IpnsEntry entry; ++ if (!entry.ParseFromArray(top_level_bytes.data(), top_level_bytes.size())) { ++ LOG(ERROR) << "Failed to parse top-level bytes as a protobuf"; ++ return {}; ++ } ++ ++ // Confirm IpnsEntry.signatureV2 and IpnsEntry.data are present and are not ++ // empty ++ if (!entry.has_signaturev2()) { ++ LOG(ERROR) << "IPNS record contains no .signatureV2!"; ++ return {}; ++ } ++ if (!entry.has_data() || entry.data().empty()) { ++ LOG(ERROR) << "IPNS record has no .data"; ++ return {}; ++ } ++ ++ // The only supported value is 0, which indicates the validity field contains ++ // the expiration date after which the IPNS record becomes invalid. ++ DCHECK_EQ(entry.validitytype(), 0); ++ ++ auto parsed = ++ api.ParseCbor({reinterpret_cast(entry.data().data()), ++ entry.data().size()}); ++ if (!parsed) { ++ LOG(ERROR) << "CBOR parsing failed."; ++ return {}; ++ } ++ IpnsCborEntry result; ++ assign(result.value, *parsed, "Value"); ++ if (entry.has_value() && result.value != entry.value()) { ++ LOG(ERROR) << "Mismatch on Value field in IPNS record... CBOR(v2): '" ++ << result.value << "' but PB(v1): '" << entry.value() ++ << "' : " << parsed->html(); ++ return {}; ++ } ++ ipfs::ByteView public_key; ++ if (entry.has_pubkey()) { ++ public_key = ipfs::ByteView{ ++ reinterpret_cast(entry.pubkey().data()), ++ entry.pubkey().size()}; ++ if (!matches(name.multi_hash(), public_key, api)) { ++ LOG(ERROR) << "Given IPNS record contains a pubkey that does not match " ++ "the hash from the IPNS name that fetched it!"; ++ return {}; ++ } ++ } else if (name.hash_type() == HashType::IDENTITY) { ++ public_key = name.hash(); ++ } else { ++ LOG(ERROR) << "IPNS record contains no public key, and the IPNS name " ++ << name.to_string() ++ << " is a true hash, not identity. Validation impossible."; ++ return {}; ++ } ++ ipfs::ipns::PublicKey pk; ++ auto* pkbp = reinterpret_cast(public_key.data()); ++ if (!pk.ParseFromArray(pkbp, public_key.size())) { ++ LOG(ERROR) << "Failed to parse public key bytes"; ++ return {}; ++ } ++ LOG(INFO) << "Record contains a public key of type " << pk.type() ++ << " and points to " << entry.value(); ++ auto& signature_str = entry.signaturev2(); ++ ByteView signature{reinterpret_cast(signature_str.data()), ++ signature_str.size()}; ++ // https://specs.ipfs.tech/ipns/ipns-record/#record-verification ++ // Create bytes for signature verification by concatenating ++ // ipto_hex(ns-signature:// prefix (bytes in hex: ++ // 69706e732d7369676e61747572653a) with raw CBOR bytes from IpnsEntry.data ++ auto bytes_str = entry.data(); ++ bytes_str.insert( ++ 0, "\x69\x70\x6e\x73\x2d\x73\x69\x67\x6e\x61\x74\x75\x72\x65\x3a"); ++ ByteView bytes{reinterpret_cast(bytes_str.data()), ++ bytes_str.size()}; ++ ByteView key_bytes{reinterpret_cast(pk.data().data()), ++ pk.data().size()}; ++ if (!api.VerifyKeySignature(static_cast(pk.type()), signature, ++ bytes, key_bytes)) { ++ LOG(ERROR) << "Verification failed!!"; ++ return {}; ++ } ++ // TODO check expiration date ++ if (entry.has_value() && entry.value() != result.value) { ++ LOG(ERROR) << "IPNS " << name.to_string() << " has different values for V1(" ++ << entry.value() << ") and V2(" << result.value << ')'; ++ return {}; ++ } ++ assign(result.validity, *parsed, "Validity"); ++ if (entry.has_validity() && entry.validity() != result.validity) { ++ LOG(ERROR) << "IPNS " << name.to_string() ++ << " has different validity for V1(" << entry.validity() ++ << ") and V2(" << result.validity << ')'; ++ return {}; ++ } ++ assign(result.validityType, *parsed, "ValidityType"); ++ if (entry.has_validitytype() && ++ entry.validitytype() != static_cast(result.validityType)) { ++ LOG(ERROR) << "IPNS " << name.to_string() ++ << " has different validity types for V1(" ++ << entry.validitytype() << ") and V2(" << result.validityType ++ << ')'; ++ return {}; ++ } ++ assign(result.sequence, *parsed, "Sequence"); ++ if (entry.has_sequence() && entry.sequence() != result.sequence) { ++ LOG(ERROR) << "IPNS " << name.to_string() ++ << " has different validity types for V1(" << entry.sequence() ++ << ") and V2(" << result.sequence << ')'; ++ return {}; ++ } ++ assign(result.ttl, *parsed, "TTL"); ++ if (entry.has_ttl() && entry.ttl() != result.ttl) { ++ LOG(ERROR) << "IPNS " << name.to_string() ++ << " has different validity types for V1(" << entry.ttl() ++ << ") and V2(" << result.ttl << ')'; ++ return {}; ++ } ++ LOG(INFO) << "IPNS record verification passes for " << name.to_string() ++ << " sequence: " << result.sequence << " points at " ++ << result.value; ++ return result; ++} ++ ++ipfs::ValidatedIpns::ValidatedIpns() = default; ++ipfs::ValidatedIpns::ValidatedIpns(ValidatedIpns&&) = default; ++ipfs::ValidatedIpns::ValidatedIpns(ValidatedIpns const&) = default; ++auto ipfs::ValidatedIpns::operator=(ValidatedIpns const&) ++ -> ValidatedIpns& = default; ++ipfs::ValidatedIpns::ValidatedIpns(IpnsCborEntry const& e) ++ : value{e.value}, sequence{e.sequence} { ++ std::istringstream ss{e.validity}; ++ std::tm t = {}; ++ ss >> std::get_time(&t, "%Y-%m-%dT%H:%M:%S"); ++ long ttl = (e.ttl / 1'000'000'000UL) + 1; ++#ifdef _MSC_VER ++ use_until = _mkgmtime(&t); ++#else ++ use_until = timegm(&t); ++#endif ++ cache_until = std::time(nullptr) + ttl; ++} ++ ++std::string ipfs::ValidatedIpns::Serialize() const { ++ DCHECK_EQ(value.find(' '), std::string::npos); ++ DCHECK_EQ(gateway_source.find(' '), std::string::npos); ++ std::ostringstream ss; ++ ss << std::hex << sequence << ' ' << use_until << ' ' << cache_until << ' ' ++ << fetch_time << ' ' << resolution_ms << ' ' << value << ' ' ++ << gateway_source; ++ return ss.str(); ++} ++auto ipfs::ValidatedIpns::Deserialize(std::string s) -> ValidatedIpns { ++ std::istringstream ss(s); ++ ValidatedIpns e; ++ ss >> std::hex >> e.sequence >> e.use_until >> e.cache_until >> ++ e.fetch_time >> e.resolution_ms >> e.value >> e.gateway_source; ++ return e; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/logger.cc b/third_party/ipfs_client/src/ipfs_client/logger.cc +new file mode 100644 +index 0000000000000..8c093bfca89aa +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/logger.cc +@@ -0,0 +1,76 @@ ++#include ++ ++#include ++ ++#include ++ ++namespace lg = ipfs::log; ++ ++namespace { ++lg::Level current_level = lg::Level::WARN; ++lg::Handler current_handler = nullptr; ++ ++void CheckLevel(google::protobuf::LogLevel lv, ++ char const* f, ++ int l, ++ std::string const& m) { ++ auto lev = static_cast(lv); ++ if (lev < static_cast(current_level)) { ++ return; ++ } ++ if (!current_handler) { ++ return; ++ } ++ current_handler(m, f, l, static_cast(lev)); ++} ++} // namespace ++ ++void lg::SetLevel(Level lev) { ++ IsInitialized(); ++ current_level = lev; ++} ++ ++void lg::SetHandler(Handler h) { ++ current_handler = h; ++ google::protobuf::SetLogHandler(&CheckLevel); ++} ++ ++void lg::DefaultHandler(std::string const& message, ++ char const* source_file, ++ int source_line, ++ Level lev) { ++ std::clog << source_file << ':' << source_line << ": " << LevelDescriptor(lev) ++ << ": " << message << '\n'; ++ if (lev == Level::FATAL) { ++ std::abort(); ++ } ++} ++ ++std::string_view lg::LevelDescriptor(Level l) { ++ switch (l) { ++ case Level::TRACE: ++ return "trace"; ++ case Level::DEBUG: ++ return "debug"; ++ case Level::INFO: ++ return "note"; // The next 3 are gcc- & clang-inspired ++ case Level::WARN: ++ return "warning"; ++ case Level::ERROR: ++ return "error"; ++ case Level::FATAL: ++ return " ### FATAL ERROR ### "; ++ case Level::OFF: ++ return "off"; ++ default: ++ return "Unknown log level used: possible corruption?"; ++ } ++} ++ ++bool lg::IsInitialized() { ++ if (current_handler) { ++ return true; ++ } ++ SetHandler(&DefaultHandler); ++ return false; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/multi_base.cc b/third_party/ipfs_client/src/ipfs_client/multi_base.cc +new file mode 100644 +index 0000000000000..58c9a0f18d100 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/multi_base.cc +@@ -0,0 +1,133 @@ ++#include "ipfs_client/multi_base.h" ++ ++#include "bases/b16_upper.h" ++#include "bases/b32.h" ++ ++#include ++ ++#include "log_macros.h" ++ ++using namespace std::literals; ++ ++namespace imb = ipfs::mb; ++namespace { ++constexpr std::string_view UnsupportedMultibase = "unsupported-multibase"; ++ ++template ++std::string encode_adapt(ipfs::ByteView bytes) { ++ auto p = reinterpret_cast(bytes.data()); ++ typename Target::encoder target; ++ return target.process({p, bytes.size()}); ++} ++enum class EncodedCase { lower, UPPER, Sensitive }; ++template ++std::vector decode_adapt(std::string_view encoded_sv) { ++ typename Target::decoder target; ++ std::string encoded_s{encoded_sv}; ++ switch (ec) { ++ case EncodedCase::lower: ++ for (auto& c : encoded_s) { ++ if (c >= 'A' && c <= 'Z') { ++ c = c - 'A' + 'a'; ++ } ++ } ++ break; ++ case EncodedCase::UPPER: ++ for (auto& c : encoded_s) { ++ if (c >= 'a' && c <= 'z') { ++ c = c - 'a' + 'A'; ++ } ++ } ++ break; ++ case EncodedCase::Sensitive: ++ break; ++ } ++ auto s = target.process(encoded_s); ++ auto b = reinterpret_cast(s.data()); ++ auto e = b + s.size(); ++ return std::vector(b, e); ++} ++template ++constexpr imb::Codec adapt(std::string_view name) { ++ return imb::Codec{&decode_adapt, ++ &encode_adapt, name}; ++} ++} // namespace ++ ++auto imb::Codec::Get(Code c) -> Codec const* { ++ switch (c) { ++ case Code::IDENTITY: ++ return nullptr; ++ case Code::UNSUPPORTED: ++ return nullptr; ++ case Code::BASE16_LOWER: { ++ static auto b16 = ++ adapt("base16"sv); ++ return &b16; ++ } ++ case Code::BASE16_UPPER: { ++ static auto b16u = ++ adapt("base16upper"sv); ++ return &b16u; ++ } ++ case Code::BASE32_LOWER: { ++ static auto b32 = adapt("base32"sv); ++ return &b32; ++ } ++ case Code::BASE32_UPPER: { ++ static auto b32u = ++ adapt("base32upper"sv); ++ return &b32u; ++ } ++ case Code::BASE36_LOWER: { ++ static auto b36 = ++ adapt("base36"sv); ++ return &b36; ++ } ++ case Code::BASE36_UPPER: ++ return nullptr; ++ case Code::BASE58_BTC: { ++ static auto b58 = ++ adapt("base58btc"sv); ++ return &b58; ++ } ++ case Code::BASE64: ++ return nullptr; ++ } ++ return nullptr; ++} ++std::string_view imb::GetName(Code c) { ++ if (auto codec = Codec::Get(c)) { ++ return codec->name; ++ } ++ return UnsupportedMultibase; ++} ++auto imb::CodeFromPrefix(char ch) -> Code { ++ auto c = static_cast(ch); ++ return Codec::Get(c) ? Code::UNSUPPORTED : c; ++} ++auto imb::decode(std::string_view mb_str) -> std::optional> { ++ if (mb_str.empty()) { ++ return std::nullopt; ++ } ++ if (auto* codec = Codec::Get(static_cast(mb_str[0]))) { ++ return codec->decode(mb_str.substr(1)); ++ } else { ++ return std::nullopt; ++ } ++} ++std::string imb::encode(Code c, ByteView bs) { ++ if (auto codec = Codec::Get(c)) { ++ auto rv = codec->encode(bs); ++ if (rv.size() >= bs.size()) { ++ rv.insert(0UL, 1UL, static_cast(c)); ++ return rv; ++ } else { ++ LOG(ERROR) << "Error encoding into base " << codec->name; ++ } ++ } else { ++ LOG(ERROR) << "Can't encode to multibase " << static_cast(c) ++ << " because I can't find a codec??"; ++ } ++ return {}; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/multi_hash.cc b/third_party/ipfs_client/src/ipfs_client/multi_hash.cc +new file mode 100644 +index 0000000000000..20cf5b19a16c8 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/multi_hash.cc +@@ -0,0 +1,56 @@ ++#include ++ ++#include ++ ++using Self = ipfs::MultiHash; ++using VarInt = libp2p::multi::UVarint; ++ ++Self::MultiHash(ipfs::HashType t, ipfs::ByteView digest) ++ : type_{t}, hash_(digest.begin(), digest.end()) {} ++ ++Self::MultiHash(ipfs::ByteView bytes) { ++ ReadPrefix(bytes); ++} ++bool Self::ReadPrefix(ipfs::ByteView& bytes) { ++ auto i = VarInt::create(bytes); ++ if (!i) { ++ return false; ++ } ++ bytes = bytes.subspan(i->size()); ++ auto type = Validate(static_cast(i->toUInt64())); ++ i = VarInt::create(bytes); ++ if (!i) { ++ return false; ++ } ++ auto length = i->toUInt64(); ++ if (length > bytes.size()) { ++ return false; ++ } ++ bytes = bytes.subspan(i->size()); ++ hash_.assign(bytes.begin(), std::next(bytes.begin(), length)); ++ bytes = bytes.subspan(length); ++ type_ = type; ++ return true; ++} ++bool Self::valid() const { ++ return type_ != HashType::INVALID && hash_.size() > 0UL; ++} ++namespace { ++constexpr std::string_view InvalidHashTypeName; ++} ++std::string_view ipfs::GetName(HashType t) { ++ switch (t) { ++ case HashType::INVALID: ++ return InvalidHashTypeName; ++ case HashType::IDENTITY: ++ return "identity"; ++ case HashType::SHA2_256: ++ return "sha2-256"; ++ } ++ // Don't use default: -> let it fall through. We want compiler warnings about ++ // unhandled cases. ++ return InvalidHashTypeName; ++} ++auto ipfs::Validate(HashType t) -> HashType { ++ return GetName(t) == InvalidHashTypeName ? HashType::INVALID : t; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/multicodec.cc b/third_party/ipfs_client/src/ipfs_client/multicodec.cc +new file mode 100644 +index 0000000000000..68cad03ea5862 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/multicodec.cc +@@ -0,0 +1,33 @@ ++#include ++ ++using Cdc = ipfs::MultiCodec; ++ ++namespace { ++constexpr std::string_view InvalidMulticodecLabel{"invalid-multicodec"}; ++} ++ ++std::string_view ipfs::GetName(Cdc c) { ++ switch (c) { ++ case Cdc::INVALID: ++ return InvalidMulticodecLabel; ++ case Cdc::IDENTITY: ++ return "identity"; ++ case Cdc::RAW: ++ return "raw"; ++ case Cdc::DAG_PB: ++ return "dag-pb"; ++ case Cdc::DAG_CBOR: ++ return "dag-cbor"; ++ case Cdc::LIBP2P_KEY: ++ return "libp2p-key"; ++ case Cdc::DAG_JSON: ++ return "dag-json"; ++ } ++ return InvalidMulticodecLabel; ++} ++Cdc ipfs::Validate(Cdc c) { ++ if (GetName(c) == InvalidMulticodecLabel) { ++ return Cdc::INVALID; ++ } ++ return c; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/orchestrator.cc b/third_party/ipfs_client/src/ipfs_client/orchestrator.cc +new file mode 100644 +index 0000000000000..3392ae1126e39 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/orchestrator.cc +@@ -0,0 +1,146 @@ ++#include "ipfs_client/orchestrator.h" ++ ++#include ++#include ++#include ++ ++#include "log_macros.h" ++#include "path2url.h" ++ ++using namespace std::literals; ++ ++using Self = ipfs::Orchestrator; ++ ++Self::Orchestrator(std::shared_ptr requestor, ++ std::shared_ptr api) ++ // : gw_requestor_{ga}, api_{api}, requestor_{requestor} { ++ : api_{api}, requestor_{requestor} { ++ DCHECK(requestor); ++} ++ ++void Self::build_response(std::shared_ptr req) { ++ if (!req || !req->ready_after()) { ++ return; ++ } ++ auto req_path = req->path(); ++ VLOG(2) << "build_response(" << req_path.to_string() << ')'; ++ req_path.pop(); // namespace ++ std::string affinity{req_path.pop()}; ++ auto it = dags_.find(affinity); ++ if (dags_.end() == it) { ++ if (gw_request(req, req->path(), affinity)) { ++ build_response(req); ++ } ++ } else { ++ VLOG(2) << "Requesting root " << affinity << " resolve path " ++ << req_path.to_string(); ++ auto root = it->second->rooted(); ++ if (root != it->second) { ++ it->second = root; ++ } ++ from_tree(req, root, req_path, affinity); ++ } ++} ++void Self::from_tree(std::shared_ptr req, ++ ipfs::ipld::NodePtr& node, ++ SlashDelimited relative_path, ++ std::string const& affinity) { ++ auto root = node->rooted(); ++ auto block_look_up = [this](auto& k) { ++ auto i = dags_.find(k); ++ return i == dags_.end() ? ipld::NodePtr{} : i->second; ++ }; ++ auto start = std::string{req->path().pop_n(2)}; ++ auto result = root->resolve(relative_path, block_look_up); ++ auto response = std::get_if(&result); ++ if (response) { ++ VLOG(2) << "Tree gave us a response: status=" << response->status_ ++ << " mime=" << response->mime_ ++ << " location=" << response->location_ << " body is " ++ << response->body_.size() << " bytes."; ++ if (response->mime_.empty() && !response->body_.empty()) { ++ if (response->location_.empty()) { ++ LOG(INFO) << "Request for " << req->path() ++ << " returned no location, so sniffing from request path and " ++ "body of " ++ << response->body_.size() << "B."; ++ response->mime_ = sniff(req->path(), response->body_); ++ } else { ++ std::string hit_path{req->path().pop_n(2)}; ++ if (!hit_path.ends_with('/') && ++ !(response->location_.starts_with('/'))) { ++ hit_path.push_back('/'); ++ } ++ hit_path.append(response->location_); ++ LOG(INFO) << "Request for " << req->path() << " returned a location of " ++ << response->location_ << " and a body of " ++ << response->body_.size() << " bytes, sniffing mime from " ++ << hit_path; ++ response->mime_ = sniff(SlashDelimited{hit_path}, response->body_); ++ } ++ } ++ req->finish(*response); ++ } else if (std::holds_alternative(result)) { ++ auto& np = std::get(result); ++ LOG(INFO) << "Symlink converts request to " << req->path().to_string() ++ << " into " << np.new_path ++ << ". TODO - check for infinite loops."; ++ req->new_path(np.new_path); ++ build_response(req); ++ } else if (std::get_if(&result)) { ++ req->finish(Response::IMMUTABLY_GONE); ++ } else { ++ auto& mps = std::get(result).ipfs_abs_paths_; ++ req->till_next(mps.size()); ++ if (std::any_of(mps.begin(), mps.end(), [this, &req, &affinity](auto& p) { ++ return gw_request(req, SlashDelimited{p}, affinity); ++ })) { ++ from_tree(req, node, relative_path, affinity); ++ } ++ } ++} ++bool Self::gw_request(std::shared_ptr ir, ++ ipfs::SlashDelimited path, ++ std::string const& aff) { ++ VLOG(1) << "Seeking " << path.to_string(); ++ auto req = gw::GatewayRequest::fromIpfsPath(path); ++ if (req) { ++ req->dependent = ir; ++ req->orchestrator(shared_from_this()); ++ req->affinity = aff; ++ requestor_->request(req); ++ } else { ++ LOG(ERROR) << "Failed to create a request for " << path.to_string(); ++ } ++ return false; ++} ++ ++bool Self::add_node(std::string key, ipfs::ipld::NodePtr p) { ++ if (p) { ++ if (dags_.insert({key, p}).second) { ++ p->set_api(api_); ++ } ++ return true; ++ } else { ++ LOG(INFO) << "NULL block attempted to be added for " << key; ++ } ++ return false; ++} ++ ++std::string Self::sniff(ipfs::SlashDelimited p, std::string const& body) const { ++ auto fake_url = path2url(p.to_string()); ++ auto file_name = p.peek_back(); ++ auto dot = file_name.find_last_of('.'); ++ std::string ext = ""; ++ if (dot < file_name.size()) { ++ ext.assign(file_name, dot + 1); ++ } ++ auto result = api_->MimeType(ext, body, fake_url); ++ LOG(INFO) << "Deduced mime from (ext=" << ext << " body of " << body.size() ++ << " bytes, 'url'=" << fake_url << ")=" << result; ++ return result; ++} ++ ++bool Self::has_key(std::string const& k) const { ++ return dags_.count(k); ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/path2url.cc b/third_party/ipfs_client/src/ipfs_client/path2url.cc +new file mode 100644 +index 0000000000000..0d7cf305a47b4 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/path2url.cc +@@ -0,0 +1,16 @@ ++#include "path2url.h" ++ ++#include "log_macros.h" ++ ++std::string ipfs::path2url(std::string p) { ++ while (!p.empty() && p[0] == '/') { ++ p.erase(0UL, 1UL); ++ } ++ DCHECK_EQ(p.at(0), 'i'); ++ DCHECK_EQ(p.at(1), 'p'); ++ DCHECK(p.at(2) == 'f' || p.at(2) == 'n'); ++ DCHECK_EQ(p.at(3), 's'); ++ DCHECK_EQ(p.at(4), '/'); ++ p.insert(4, ":/"); ++ return p; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/path2url.h b/third_party/ipfs_client/src/ipfs_client/path2url.h +new file mode 100644 +index 0000000000000..683e92d759b4e +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/path2url.h +@@ -0,0 +1,10 @@ ++#ifndef IPFS_PATH2URL_H_ ++#define IPFS_PATH2URL_H_ ++ ++#include ++ ++namespace ipfs { ++std::string path2url(std::string path_as_string); ++} ++ ++#endif // IPFS_PATH2URL_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/redirects.cc b/third_party/ipfs_client/src/ipfs_client/redirects.cc +new file mode 100644 +index 0000000000000..b2395dcae75c0 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/redirects.cc +@@ -0,0 +1,259 @@ ++#include "redirects.h" ++ ++#include "log_macros.h" ++ ++#include ++ ++#include ++#include ++ ++namespace r = ipfs::redirects; ++using namespace std::literals; ++ ++namespace { ++// 2.4.4 Max File Size ++// The file size must not exceed 64 KiB. ++constexpr std::size_t MAX_SIZE = 64UL * 1024UL * 1024UL; ++ ++// Not including \n which terminates lines ++constexpr std::string_view WHITESPACE = " \t\f\r\v\n"; ++ ++// https://specs.ipfs.tech/http-gateways/web-redirects-file/#status ++constexpr int DEFAULT_STATUS = 301; ++// https://specs.ipfs.tech/http-gateways/web-redirects-file/#error-handling ++constexpr int PARSE_ERROR_STATUS = 500; ++} // namespace ++ ++r::Directive::Directive(std::string_view from, std::string_view to, int status) ++ : to_{to}, status_{status} { ++ SlashDelimited comp_str_s{from}; ++ std::unordered_set placeholders; ++ while (comp_str_s) { ++ auto comp_str = comp_str_s.pop(); ++ if (comp_str.empty()) { ++ LOG(ERROR) << "Got empty slash-delimited component. Should not have."; ++ return; ++ } else if (comp_str == "*") { ++ components_.emplace_back(ComponentType::SPLAT, comp_str); ++ } else if (comp_str[0] == ':') { ++ if (placeholders.insert(comp_str).second) { ++ components_.emplace_back(ComponentType::PLACEHOLDER, comp_str); ++ } else { ++ to_.assign("ERROR: Duplicate placeholder ").append(comp_str); ++ return; ++ } ++ } else { ++ components_.emplace_back(ComponentType::LITERAL, comp_str); ++ } ++ } ++} ++std::uint16_t r::Directive::rewrite(std::string& path) const { ++ auto input = SlashDelimited{path}; ++ auto result = to_; ++ auto replace = [&result](std::string_view ph, std::string_view val) { ++ std::size_t pos; ++ while ((pos = result.find(ph)) < result.size()) { ++ result.replace(pos, ph.size(), val); ++ } ++ }; ++ for (auto [type, comp_str] : components_) { ++ if (!input) { ++ VLOG(2) << "Ran out of input in [" << path ++ << "] before running out of pattern components to match against " ++ "(was looking for [" ++ << comp_str << "]. Not a match."; ++ return 0; ++ } ++ if (type == ComponentType::LITERAL) { ++ if (comp_str != input.pop()) { ++ return 0; ++ } ++ } else if (type == ComponentType::PLACEHOLDER) { ++ replace(comp_str, input.pop()); ++ } else { ++ replace(":splat"sv, input.pop_all()); ++ } ++ } ++ if (input) { ++ return 0; ++ } else { ++ path = result; ++ return status_; ++ } ++} ++std::string r::Directive::error() const { ++ if (starts_with(to_, "ERROR: ")) { ++ return to_; ++ } ++ if (status_ < 200 || status_ > 451) { ++ return "UNSUPPORTED STATUS " + std::to_string(status_); ++ } ++ if (components_.empty()) { ++ return "Empty directive pattern"; ++ } ++ if (to_.empty()) { ++ return "Empty redirect target location"; ++ } ++ if (to_.at(0) != '/' && to_.find("://") == std::string::npos) { ++ return "Location must begin with / or be a URL"; ++ } ++ return {}; ++} ++ ++std::uint16_t r::File::rewrite(std::string& missing_path) const { ++ for (auto& directive : directives_) { ++ auto status = directive.rewrite(missing_path); ++ if (status) { ++ return status; ++ } ++ } ++ return 0; ++} ++r::File::File(std::string_view to_parse) { ++ if (to_parse.size() > MAX_SIZE) { ++ error_ = "INPUT FILE TOO LARGE " + std::to_string(to_parse.size()); ++ return; ++ } ++ for (auto line_number = 1; valid() && to_parse.size(); ++line_number) { ++ auto line_end = to_parse.find('\n'); ++ auto line = to_parse.substr(0UL, line_end); ++ if (!parse_line(line, line_number)) { ++ LOG(INFO) << "Line #" << line_number << " ignored: [" << line << ']'; ++ } else if (directives_.empty()) { ++ LOG(ERROR) << "Expected to have a directive after parsing line #" ++ << line_number << ": " << line; ++ } else if (directives_.back().valid()) { ++ VLOG(1) << "Line #" << line_number << " parsed. " << line; ++ } else { ++ error_ = "FAILURE PARSING LINE # " + std::to_string(line_number); ++ error_.append(": ") ++ .append(directives_.back().error()) ++ .append(" [") ++ .append(line) ++ .push_back(']'); ++ LOG(ERROR) << error_; ++ return; ++ } ++ if (line_end < to_parse.size()) { ++ to_parse.remove_prefix(line_end + 1); ++ } else { ++ break; ++ } ++ } ++ if (directives_.empty()) { ++ error_ = "No redirection directives in _redirects"; ++ LOG(ERROR) << error_; ++ } ++} ++ ++namespace { ++std::pair parse_status(std::string_view line, ++ std::size_t col); ++} ++bool r::File::parse_line(std::string_view line, int line_number) { ++ if (line.empty()) { ++ // empty line is not a directive ++ return false; ++ } ++ auto bpos = line.find_first_not_of(WHITESPACE); ++ if (bpos == std::string_view::npos) { ++ // effectively empty line ++ return false; ++ } else if (line[bpos] == '#') { ++ // https://specs.ipfs.tech/http-gateways/web-redirects-file/#comments ++ return false; ++ } ++ auto epos = line.find_first_of(WHITESPACE, bpos); ++ if (epos == std::string_view::npos) { ++ error_ = "Parsing _redirects file: line # " + std::to_string(line_number); ++ error_ ++ .append(" , expected at least 2 tokens (from and to) for directive: [") ++ .append(line) ++ .append("], but didn't even get whitespace to end from"); ++ return false; ++ } ++ auto from = line.substr(bpos, epos - bpos); ++ bpos = line.find_first_not_of(WHITESPACE, epos); ++ if (bpos == std::string_view::npos) { ++ error_ = "Parsing _redirects file: line # " + std::to_string(line_number); ++ error_ ++ .append(" , expected at least 2 tokens (from and to) for directive: [") ++ .append(line) ++ .append("], but didn't get a to"); ++ return false; ++ } ++ epos = line.find_first_of(WHITESPACE, bpos); ++ auto to = line.substr(bpos, epos - bpos); ++ auto [status, err] = parse_status(line, epos); ++ if (err.empty()) { ++ directives_.emplace_back(from, to, status); ++ return true; ++ } else { ++ error_ = err; ++ LOG(ERROR) << "Error parsing status on line #" << line_number << " [" ++ << line << "]."; ++ return false; ++ } ++} ++ ++namespace { ++ ++std::pair parse_status(std::string_view line, ++ std::size_t col) { ++ if (col >= line.size()) { ++ VLOG(2) << " No status specified, using default."; ++ return {DEFAULT_STATUS, ""}; ++ } ++ auto b = line.find_first_not_of(WHITESPACE, col); ++ if (b >= line.size()) { ++ VLOG(2) ++ << " No status specified (line ended in whitespace), using default."; ++ return {DEFAULT_STATUS, ""}; ++ } ++ auto status_str = line.substr(b); ++ if (status_str.size() < 3) { ++ return {PARSE_ERROR_STATUS, ++ " Not enough characters for a valid status string: [" + ++ std::string{status_str} + "]."}; ++ } ++ auto good = [](int i) { return std::make_pair(i, ""s); }; ++ auto unsupported = [status_str]() { ++ return std::make_pair( ++ PARSE_ERROR_STATUS, ++ "Unsupported status specified in directive:" + std::string{status_str}); ++ }; ++ /* ++ * 200 - OK treated as a rewrite, without changing the URL in the browser. ++ * 301 - Permanent Redirect (default) ++ * 302 - Found (commonly used for Temporary Redirect) ++ * 303 - See Other (replacing PUT and POST with GET) ++ * 307 - Temporary Redirect (explicitly preserving body and HTTP method) ++ * 308 - Permanent Redirect (preserving body & method of original request) ++ * 404 - Not Found (Useful for a pretty 404 page) ++ * 410 - Gone ++ * 451 - Unavailable For Legal Reasons ++ */ ++ switch (status_str[0]) { ++ case '2': ++ return status_str == "200" ? good(200) : unsupported(); ++ case '3': ++ if (status_str[1] != '0') { ++ return unsupported(); ++ } ++ return good(300 + status_str[2] - '0'); ++ case '4': ++ switch (status_str[1]) { ++ case '0': ++ return status_str[2] == '4' ? good(404) : unsupported(); ++ case '1': ++ return status_str[2] == '0' ? good(410) : unsupported(); ++ case '5': ++ return status_str[2] == '1' ? good(451) : unsupported(); ++ default: ++ return unsupported(); ++ } ++ default: ++ return unsupported(); ++ } ++} ++} // namespace +diff --git a/third_party/ipfs_client/src/ipfs_client/redirects.h b/third_party/ipfs_client/src/ipfs_client/redirects.h +new file mode 100644 +index 0000000000000..e0b333f1de2f1 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/redirects.h +@@ -0,0 +1,41 @@ ++#ifndef IPFS_REDIRECTS_H_ ++#define IPFS_REDIRECTS_H_ ++ ++#include ++ ++#include ++#include ++#include ++ ++namespace ipfs { ++namespace redirects { ++class Directive { ++ enum class ComponentType { LITERAL, PLACEHOLDER, SPLAT }; ++ std::vector> components_; ++ std::string to_; ++ int const status_; ++ ++ public: ++ Directive(std::string_view, std::string_view, int); ++ std::uint16_t rewrite(std::string&) const; ++ std::string error() const; ++ bool valid() const { return error().empty(); } ++}; ++class File { ++ std::vector directives_; ++ std::string error_; ++ ++ public: ++ File(std::string_view to_parse); ++ ++ bool valid() const { return error().empty(); } ++ std::string const& error() const { return error_; } ++ std::uint16_t rewrite(std::string& missing_path) const; ++ ++ private: ++ bool parse_line(std::string_view, int); ++}; ++} // namespace redirects ++} // namespace ipfs ++ ++#endif // IPFS_REDIRECTS_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/response.cc b/third_party/ipfs_client/src/ipfs_client/response.cc +new file mode 100644 +index 0000000000000..411d87d1354e4 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/response.cc +@@ -0,0 +1,16 @@ ++#include "ipfs_client/response.h" ++ ++using Self = ipfs::Response; ++ ++Self Self::PLAIN_NOT_FOUND{"text/html", static_cast(404), ++ std::string{}, std::string{}}; ++Self Self::IMMUTABLY_GONE{"text/plain", 410, ++ "Using immutable data it has been proven the " ++ "resource does not exist anywhere.", ++ std::string{}}; ++ ++Self Self::HOST_NOT_FOUND{ ++ "text/plain", Self::HOST_NOT_FOUND_STATUS, ++ "either a hostname didn't resolve a DNS TXT records for dnslink=, or we " ++ "can't find a gateway with the necessary IPNS record", ++ std::string{}}; +diff --git a/third_party/ipfs_client/src/ipfs_client/signing_key_type.cc b/third_party/ipfs_client/src/ipfs_client/signing_key_type.cc +new file mode 100644 +index 0000000000000..b6489a47b130f +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/signing_key_type.cc +@@ -0,0 +1,15 @@ ++#include ++ ++#include ++ ++using T = ipfs::SigningKeyType; ++namespace n = ipfs::ipns; ++ ++// It is critically important that these 2 enumerations remain in-synch. ++// However, some headers that reference SigningKeyType need to be able to ++// compile without access to protobuf. ++static_assert(static_cast(T::RSA) == n::RSA); ++static_assert(static_cast(T::Ed25519) == n::Ed25519); ++static_assert(static_cast(T::Secp256k1) == n::Secp256k1); ++static_assert(static_cast(T::ECDSA) == n::ECDSA); ++static_assert(static_cast(T::KeyTypeCount) == n::KeyType_ARRAYSIZE); +diff --git a/third_party/ipfs_client/src/libp2p/crypto/protobuf_key.hpp b/third_party/ipfs_client/src/libp2p/crypto/protobuf_key.hpp +new file mode 100644 +index 0000000000000..459426f8c58a2 +--- /dev/null ++++ b/third_party/ipfs_client/src/libp2p/crypto/protobuf_key.hpp +@@ -0,0 +1,29 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef KAGOME_PROTOBUF_KEY_HPP ++#define KAGOME_PROTOBUF_KEY_HPP ++ ++#include ++#include ++ ++#include ++ ++namespace libp2p::crypto { ++ /** ++ * Strict type for key, which is encoded into Protobuf format ++ */ ++ struct ProtobufKey : public boost::equality_comparable { ++ explicit ProtobufKey(std::vector key) : key{std::move(key)} {} ++ ++ std::vector key; ++ ++ bool operator==(const ProtobufKey &other) const { ++ return key == other.key; ++ } ++ }; ++} // namespace libp2p::crypto ++ ++#endif // KAGOME_PROTOBUF_KEY_HPP +diff --git a/third_party/ipfs_client/src/libp2p/multi/multibase_codec/codecs/base16.cc b/third_party/ipfs_client/src/libp2p/multi/multibase_codec/codecs/base16.cc +new file mode 100644 +index 0000000000000..b032cccbbdc1c +--- /dev/null ++++ b/third_party/ipfs_client/src/libp2p/multi/multibase_codec/codecs/base16.cc +@@ -0,0 +1,104 @@ ++#include ++ ++namespace b16 = ipfs::base16; ++ ++namespace { ++std::uint8_t to_i(char c); ++template ++char to_c(std::uint8_t n) { ++ if (n < 10) { ++ return n + '0'; ++ } else { ++ return n - 10 + a; ++ } ++} ++template ++std::string encode(ipfs::ByteView bytes) { ++ std::string result; ++ result.reserve(bytes.size() * 2); ++ for (auto b : bytes) { ++ auto i = to_integer(b); ++ result.push_back(to_c(i >> 4)); ++ result.push_back(to_c(i & 0xF)); ++ } ++ return result; ++} ++} // namespace ++ ++std::string b16::encodeLower(ByteView bytes) { ++ return encode<'a'>(bytes); ++} ++std::string b16::encodeUpper(ByteView bytes) { ++ return encode<'A'>(bytes); ++} ++auto b16::decode(std::string_view s) -> Decoded { ++ ByteArray result(s.size() / 2, ipfs::Byte{}); ++ for (auto i = 0U; i + 1U < s.size(); i += 2U) { ++ auto a = to_i(s[i]); ++ auto b = to_i(s[i + 1]); ++ if (a > 0xF || b > 0xF) { ++ return ipfs::unexpected{BaseError::INVALID_BASE16_INPUT}; ++ } ++ result[i / 2] = ipfs::Byte{static_cast((a << 4) | b)}; ++ } ++ if (s.size() % 2) { ++ auto a = to_i(s.back()); ++ if (a <= 0xF) { ++ result.push_back(ipfs::Byte{a}); ++ } ++ } ++ return result; ++} ++ ++namespace { ++std::uint8_t to_i(char c) { ++ switch (c) { ++ case '0': ++ return 0; ++ case '1': ++ return 1; ++ case '2': ++ return 2; ++ case '3': ++ return 3; ++ case '4': ++ return 4; ++ case '5': ++ return 5; ++ case '6': ++ return 6; ++ case '7': ++ return 7; ++ case '8': ++ return 8; ++ case '9': ++ return 9; ++ case 'a': ++ return 10; ++ case 'b': ++ return 11; ++ case 'c': ++ return 12; ++ case 'd': ++ return 13; ++ case 'e': ++ return 14; ++ case 'f': ++ return 15; ++ case 'A': ++ return 10; ++ case 'B': ++ return 11; ++ case 'C': ++ return 12; ++ case 'D': ++ return 13; ++ case 'E': ++ return 14; ++ case 'F': ++ return 15; ++ default: ++ return 0xFF; ++ } ++} ++} // namespace +diff --git a/third_party/ipfs_client/src/libp2p/multi/uvarint.cc b/third_party/ipfs_client/src/libp2p/multi/uvarint.cc +new file mode 100644 +index 0000000000000..2e6ed6eb0bada +--- /dev/null ++++ b/third_party/ipfs_client/src/libp2p/multi/uvarint.cc +@@ -0,0 +1,107 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#include ++ ++namespace libp2p::multi { ++ ++UVarint::UVarint(UVarint const& rhs) : bytes_(rhs.bytes_) {} ++UVarint::UVarint(uint64_t number) { ++ do { ++ auto byte = static_cast(number) & ipfs::Byte{0x7f}; ++ number >>= 7; ++ if (number != 0) { ++ byte |= ipfs::Byte{0x80}; ++ } ++ bytes_.push_back(byte); ++ } while (number != 0); ++} ++ ++UVarint::UVarint(ipfs::ByteView varint_bytes) { ++ auto size = calculateSize(varint_bytes); ++ if (size <= varint_bytes.size()) { ++ bytes_.assign(varint_bytes.begin(), varint_bytes.begin() + size); ++ } ++} ++ ++UVarint::UVarint(ipfs::ByteView varint_bytes, size_t varint_size) ++ : bytes_(varint_bytes.begin(), varint_bytes.begin() + varint_size) {} ++ ++std::optional UVarint::create(ipfs::ByteView varint_bytes) { ++ size_t size = calculateSize(varint_bytes); ++ if (size > 0 && size <= varint_bytes.size()) { ++ return UVarint{varint_bytes, size}; ++ } ++ return {}; ++} ++ ++uint64_t UVarint::toUInt64() const { ++ uint64_t res = 0; ++ size_t index = 0; ++ for (const auto& byte : bytes_) { ++ res += static_cast((byte & ipfs::Byte{0x7f})) << index; ++ index += 7; ++ } ++ return res; ++} ++ ++ipfs::ByteView UVarint::toBytes() const { ++ return ipfs::ByteView{bytes_.data(), bytes_.size()}; ++} ++ ++std::vector const& UVarint::toVector() const { ++ return bytes_; ++} ++ ++size_t UVarint::size() const { ++ return bytes_.size(); ++} ++ ++UVarint& UVarint::operator=(UVarint const& rhs) { ++ bytes_ = rhs.bytes_; // actually OK even if &rhs == this ++ return *this; ++} ++UVarint& UVarint::operator=(uint64_t n) { ++ *this = UVarint(n); ++ return *this; ++} ++ ++bool UVarint::operator==(const UVarint& r) const { ++ return std::equal(bytes_.begin(), bytes_.end(), r.bytes_.begin(), ++ r.bytes_.end()); ++} ++ ++bool UVarint::operator!=(const UVarint& r) const { ++ return !(*this == r); ++} ++ ++bool UVarint::operator<(const UVarint& r) const { ++ return toUInt64() < r.toUInt64(); ++} ++ ++size_t UVarint::calculateSize(ipfs::ByteView varint_bytes) { ++ size_t size = 0; ++ size_t shift = 0; ++ constexpr size_t capacity = sizeof(uint64_t) * 8; ++ bool last_byte_found = false; ++ for (const auto& byte : varint_bytes) { ++ ++size; ++ std::uint_least64_t slice = to_integer(byte) & 0x7f; ++ if (shift >= capacity || ((slice << shift) >> shift) != slice) { ++ size = 0; ++ break; ++ } ++ if ((byte & ipfs::Byte{0x80}) == ipfs::Byte{0}) { ++ last_byte_found = true; ++ break; ++ } ++ shift += 7; ++ } ++ return last_byte_found ? size : 0; ++} ++ ++UVarint::~UVarint() noexcept {} ++ ++} // namespace libp2p::multi +diff --git a/third_party/ipfs_client/src/log_macros.h b/third_party/ipfs_client/src/log_macros.h +new file mode 100644 +index 0000000000000..e406429d0f280 +--- /dev/null ++++ b/third_party/ipfs_client/src/log_macros.h +@@ -0,0 +1,53 @@ ++#ifndef IPFS_LOG_MACROS_H_ ++#define IPFS_LOG_MACROS_H_ ++ ++#include ++ ++#if __has_include("base/logging.h") //In Chromium ++ ++#include "base/logging.h" ++#include "base/check_op.h" ++ ++#else // Not in Chromium ++ ++#include ++ ++#include ++ ++#define DCHECK_EQ GOOGLE_DCHECK_EQ ++#define DCHECK_GT GOOGLE_DCHECK_GT ++#define DCHECK GOOGLE_DCHECK ++#define LOG GOOGLE_LOG ++ ++#define VLOG(X) \ ++ ::google::protobuf::internal::LogFinisher() = \ ++ ::google::protobuf::internal::LogMessage( \ ++ static_cast<::google::protobuf::LogLevel>( \ ++ ::google::protobuf::LOGLEVEL_INFO - X), \ ++ __FILE__, __LINE__) ++ ++#pragma GCC diagnostic push ++#pragma GCC diagnostic ignored "-Wunused-variable" ++namespace { ++static bool is_logging_initialized = ::ipfs::log::IsInitialized(); ++} ++#pragma GCC diagnostic pop ++ ++#endif //Chromium in-tree check ++ ++#define L_VAR(X) LOG(INFO) << "VAR " << #X << "='" << (X) << '\''; ++ ++inline bool starts_with(std::string_view full_text, std::string_view prefix) { ++ if (prefix.size() > full_text.size()) { ++ return false; ++ } ++ return full_text.substr(0UL, prefix.size()) == prefix; ++} ++inline bool ends_with(std::string_view full_text, std::string_view suffix) { ++ if (suffix.size() > full_text.size()) { ++ return false; ++ } ++ return full_text.substr(full_text.size() - suffix.size()) == suffix; ++} ++ ++#endif // IPFS_LOG_MACROS_H_ +diff --git a/third_party/ipfs_client/src/smhasher/MurmurHash3.cc b/third_party/ipfs_client/src/smhasher/MurmurHash3.cc +new file mode 100644 +index 0000000000000..677aedf1d7a55 +--- /dev/null ++++ b/third_party/ipfs_client/src/smhasher/MurmurHash3.cc +@@ -0,0 +1,424 @@ ++//----------------------------------------------------------------------------- ++// MurmurHash3 was written by Austin Appleby, and is placed in the public ++// domain. The author hereby disclaims copyright to this source code. ++ ++// Note - The x86 and x64 versions do _not_ produce the same results, as the ++// algorithms are optimized for their respective platforms. You can still ++// compile and run any of them on any platform, but your performance with the ++// non-native version will be less than optimal. ++ ++#include "smhasher/MurmurHash3.h" ++#ifdef __GNUG__ ++#pragma GCC diagnostic ignored "-Wimplicit-fallthrough" ++#endif ++ ++#ifdef __clang__ ++#pragma clang diagnostic ignored "-Wimplicit-fallthrough" ++#endif ++//----------------------------------------------------------------------------- ++// Platform-specific functions and macros ++ ++// Microsoft Visual Studio ++ ++#if defined(_MSC_VER) ++ ++#define FORCE_INLINE __forceinline ++ ++#include ++ ++#define ROTL32(x, y) _rotl(x, y) ++#define ROTL64(x, y) _rotl64(x, y) ++ ++#define BIG_CONSTANT(x) (x) ++ ++// Other compilers ++ ++#else // defined(_MSC_VER) ++ ++#define FORCE_INLINE inline __attribute__((always_inline)) ++ ++inline uint32_t rotl32(uint32_t x, int8_t r) { ++ return (x << r) | (x >> (32 - r)); ++} ++ ++inline uint64_t rotl64(uint64_t x, int8_t r) { ++ return (x << r) | (x >> (64 - r)); ++} ++ ++#define ROTL32(x, y) rotl32(x, y) ++#define ROTL64(x, y) rotl64(x, y) ++ ++#define BIG_CONSTANT(x) (x##LLU) ++ ++#endif // !defined(_MSC_VER) ++ ++//----------------------------------------------------------------------------- ++// Block read - if your platform needs to do endian-swapping or can only ++// handle aligned reads, do the conversion here ++ ++FORCE_INLINE uint32_t getblock32(const uint32_t* p, int i) { ++ return p[i]; ++} ++ ++FORCE_INLINE uint64_t getblock64(const uint64_t* p, int i) { ++ return p[i]; ++} ++ ++//----------------------------------------------------------------------------- ++// Finalization mix - force all bits of a hash block to avalanche ++ ++FORCE_INLINE uint32_t fmix32(uint32_t h) { ++ h ^= h >> 16; ++ h *= 0x85ebca6b; ++ h ^= h >> 13; ++ h *= 0xc2b2ae35; ++ h ^= h >> 16; ++ ++ return h; ++} ++ ++//---------- ++ ++FORCE_INLINE uint64_t fmix64(uint64_t k) { ++ k ^= k >> 33; ++ k *= BIG_CONSTANT(0xff51afd7ed558ccd); ++ k ^= k >> 33; ++ k *= BIG_CONSTANT(0xc4ceb9fe1a85ec53); ++ k ^= k >> 33; ++ ++ return k; ++} ++ ++//----------------------------------------------------------------------------- ++ ++void MurmurHash3_x86_32(const void* key, int len, uint32_t seed, void* out) { ++ const uint8_t* data = (const uint8_t*)key; ++ const int nblocks = len / 4; ++ ++ uint32_t h1 = seed; ++ ++ const uint32_t c1 = 0xcc9e2d51; ++ const uint32_t c2 = 0x1b873593; ++ ++ //---------- ++ // body ++ ++ const uint32_t* blocks = (const uint32_t*)(data + nblocks * 4); ++ ++ for (int i = -nblocks; i; i++) { ++ uint32_t k1 = getblock32(blocks, i); ++ ++ k1 *= c1; ++ k1 = ROTL32(k1, 15); ++ k1 *= c2; ++ ++ h1 ^= k1; ++ h1 = ROTL32(h1, 13); ++ h1 = h1 * 5 + 0xe6546b64; ++ } ++ ++ //---------- ++ // tail ++ ++ const uint8_t* tail = (const uint8_t*)(data + nblocks * 4); ++ ++ uint32_t k1 = 0; ++ ++ switch (len & 3) { ++ case 3: ++ k1 ^= tail[2] << 16; ++ case 2: ++ k1 ^= tail[1] << 8; ++ case 1: ++ k1 ^= tail[0]; ++ k1 *= c1; ++ k1 = ROTL32(k1, 15); ++ k1 *= c2; ++ h1 ^= k1; ++ }; ++ ++ //---------- ++ // finalization ++ ++ h1 ^= len; ++ ++ h1 = fmix32(h1); ++ ++ *(uint32_t*)out = h1; ++} ++ ++//----------------------------------------------------------------------------- ++ ++void MurmurHash3_x86_128(const void* key, ++ const int len, ++ uint32_t seed, ++ void* out) { ++ const uint8_t* data = (const uint8_t*)key; ++ const int nblocks = len / 16; ++ ++ uint32_t h1 = seed; ++ uint32_t h2 = seed; ++ uint32_t h3 = seed; ++ uint32_t h4 = seed; ++ ++ const uint32_t c1 = 0x239b961b; ++ const uint32_t c2 = 0xab0e9789; ++ const uint32_t c3 = 0x38b34ae5; ++ const uint32_t c4 = 0xa1e38b93; ++ ++ //---------- ++ // body ++ ++ const uint32_t* blocks = (const uint32_t*)(data + nblocks * 16); ++ ++ for (int i = -nblocks; i; i++) { ++ uint32_t k1 = getblock32(blocks, i * 4 + 0); ++ uint32_t k2 = getblock32(blocks, i * 4 + 1); ++ uint32_t k3 = getblock32(blocks, i * 4 + 2); ++ uint32_t k4 = getblock32(blocks, i * 4 + 3); ++ ++ k1 *= c1; ++ k1 = ROTL32(k1, 15); ++ k1 *= c2; ++ h1 ^= k1; ++ ++ h1 = ROTL32(h1, 19); ++ h1 += h2; ++ h1 = h1 * 5 + 0x561ccd1b; ++ ++ k2 *= c2; ++ k2 = ROTL32(k2, 16); ++ k2 *= c3; ++ h2 ^= k2; ++ ++ h2 = ROTL32(h2, 17); ++ h2 += h3; ++ h2 = h2 * 5 + 0x0bcaa747; ++ ++ k3 *= c3; ++ k3 = ROTL32(k3, 17); ++ k3 *= c4; ++ h3 ^= k3; ++ ++ h3 = ROTL32(h3, 15); ++ h3 += h4; ++ h3 = h3 * 5 + 0x96cd1c35; ++ ++ k4 *= c4; ++ k4 = ROTL32(k4, 18); ++ k4 *= c1; ++ h4 ^= k4; ++ ++ h4 = ROTL32(h4, 13); ++ h4 += h1; ++ h4 = h4 * 5 + 0x32ac3b17; ++ } ++ ++ //---------- ++ // tail ++ ++ const uint8_t* tail = (const uint8_t*)(data + nblocks * 16); ++ ++ uint32_t k1 = 0; ++ uint32_t k2 = 0; ++ uint32_t k3 = 0; ++ uint32_t k4 = 0; ++ ++ switch (len & 15) { ++ case 15: ++ k4 ^= tail[14] << 16; ++ case 14: ++ k4 ^= tail[13] << 8; ++ case 13: ++ k4 ^= tail[12] << 0; ++ k4 *= c4; ++ k4 = ROTL32(k4, 18); ++ k4 *= c1; ++ h4 ^= k4; ++ ++ case 12: ++ k3 ^= tail[11] << 24; ++ case 11: ++ k3 ^= tail[10] << 16; ++ case 10: ++ k3 ^= tail[9] << 8; ++ case 9: ++ k3 ^= tail[8] << 0; ++ k3 *= c3; ++ k3 = ROTL32(k3, 17); ++ k3 *= c4; ++ h3 ^= k3; ++ ++ case 8: ++ k2 ^= tail[7] << 24; ++ case 7: ++ k2 ^= tail[6] << 16; ++ case 6: ++ k2 ^= tail[5] << 8; ++ case 5: ++ k2 ^= tail[4] << 0; ++ k2 *= c2; ++ k2 = ROTL32(k2, 16); ++ k2 *= c3; ++ h2 ^= k2; ++ ++ case 4: ++ k1 ^= tail[3] << 24; ++ case 3: ++ k1 ^= tail[2] << 16; ++ case 2: ++ k1 ^= tail[1] << 8; ++ case 1: ++ k1 ^= tail[0] << 0; ++ k1 *= c1; ++ k1 = ROTL32(k1, 15); ++ k1 *= c2; ++ h1 ^= k1; ++ }; ++ ++ //---------- ++ // finalization ++ ++ h1 ^= len; ++ h2 ^= len; ++ h3 ^= len; ++ h4 ^= len; ++ ++ h1 += h2; ++ h1 += h3; ++ h1 += h4; ++ h2 += h1; ++ h3 += h1; ++ h4 += h1; ++ ++ h1 = fmix32(h1); ++ h2 = fmix32(h2); ++ h3 = fmix32(h3); ++ h4 = fmix32(h4); ++ ++ h1 += h2; ++ h1 += h3; ++ h1 += h4; ++ h2 += h1; ++ h3 += h1; ++ h4 += h1; ++ ++ ((uint32_t*)out)[0] = h1; ++ ((uint32_t*)out)[1] = h2; ++ ((uint32_t*)out)[2] = h3; ++ ((uint32_t*)out)[3] = h4; ++} ++ ++//----------------------------------------------------------------------------- ++ ++void MurmurHash3_x64_128(const void* key, ++ const int len, ++ const uint32_t seed, ++ void* out) { ++ const uint8_t* data = (const uint8_t*)key; ++ const int nblocks = len / 16; ++ ++ uint64_t h1 = seed; ++ uint64_t h2 = seed; ++ ++ const uint64_t c1 = BIG_CONSTANT(0x87c37b91114253d5); ++ const uint64_t c2 = BIG_CONSTANT(0x4cf5ad432745937f); ++ ++ //---------- ++ // body ++ ++ const uint64_t* blocks = (const uint64_t*)(data); ++ ++ for (int i = 0; i < nblocks; i++) { ++ uint64_t k1 = getblock64(blocks, i * 2 + 0); ++ uint64_t k2 = getblock64(blocks, i * 2 + 1); ++ ++ k1 *= c1; ++ k1 = ROTL64(k1, 31); ++ k1 *= c2; ++ h1 ^= k1; ++ ++ h1 = ROTL64(h1, 27); ++ h1 += h2; ++ h1 = h1 * 5 + 0x52dce729; ++ ++ k2 *= c2; ++ k2 = ROTL64(k2, 33); ++ k2 *= c1; ++ h2 ^= k2; ++ ++ h2 = ROTL64(h2, 31); ++ h2 += h1; ++ h2 = h2 * 5 + 0x38495ab5; ++ } ++ ++ //---------- ++ // tail ++ ++ const uint8_t* tail = (const uint8_t*)(data + nblocks * 16); ++ ++ uint64_t k1 = 0; ++ uint64_t k2 = 0; ++ ++ switch (len & 15) { ++ case 15: ++ k2 ^= ((uint64_t)tail[14]) << 48; ++ case 14: ++ k2 ^= ((uint64_t)tail[13]) << 40; ++ case 13: ++ k2 ^= ((uint64_t)tail[12]) << 32; ++ case 12: ++ k2 ^= ((uint64_t)tail[11]) << 24; ++ case 11: ++ k2 ^= ((uint64_t)tail[10]) << 16; ++ case 10: ++ k2 ^= ((uint64_t)tail[9]) << 8; ++ case 9: ++ k2 ^= ((uint64_t)tail[8]) << 0; ++ k2 *= c2; ++ k2 = ROTL64(k2, 33); ++ k2 *= c1; ++ h2 ^= k2; ++ ++ case 8: ++ k1 ^= ((uint64_t)tail[7]) << 56; ++ case 7: ++ k1 ^= ((uint64_t)tail[6]) << 48; ++ case 6: ++ k1 ^= ((uint64_t)tail[5]) << 40; ++ case 5: ++ k1 ^= ((uint64_t)tail[4]) << 32; ++ case 4: ++ k1 ^= ((uint64_t)tail[3]) << 24; ++ case 3: ++ k1 ^= ((uint64_t)tail[2]) << 16; ++ case 2: ++ k1 ^= ((uint64_t)tail[1]) << 8; ++ case 1: ++ k1 ^= ((uint64_t)tail[0]) << 0; ++ k1 *= c1; ++ k1 = ROTL64(k1, 31); ++ k1 *= c2; ++ h1 ^= k1; ++ }; ++ ++ //---------- ++ // finalization ++ ++ h1 ^= len; ++ h2 ^= len; ++ ++ h1 += h2; ++ h2 += h1; ++ ++ h1 = fmix64(h1); ++ h2 = fmix64(h2); ++ ++ h1 += h2; ++ h2 += h1; ++ ++ ((uint64_t*)out)[0] = h1; ++ ((uint64_t*)out)[1] = h2; ++} ++ ++//----------------------------------------------------------------------------- +diff --git a/third_party/ipfs_client/src/vocab/byte_view.cc b/third_party/ipfs_client/src/vocab/byte_view.cc +new file mode 100644 +index 0000000000000..f71dcaa0181f1 +--- /dev/null ++++ b/third_party/ipfs_client/src/vocab/byte_view.cc +@@ -0,0 +1,2 @@ ++#include "vocab/byte_view.h" ++ +diff --git a/third_party/ipfs_client/src/vocab/slash_delimited.cc b/third_party/ipfs_client/src/vocab/slash_delimited.cc +new file mode 100644 +index 0000000000000..c81ae5823c867 +--- /dev/null ++++ b/third_party/ipfs_client/src/vocab/slash_delimited.cc +@@ -0,0 +1,117 @@ ++#include ++ ++#include "log_macros.h" ++ ++#include ++ ++#if __has_include() ++#include ++#define HAS_STRINGPIECE 1 ++#endif ++ ++using Self = ipfs::SlashDelimited; ++ ++Self::SlashDelimited(std::string_view unowned) : remainder_{unowned} {} ++ ++Self::operator bool() const { ++ return remainder_.find_first_not_of("/") < remainder_.size(); ++} ++std::string_view Self::pop() { ++ if (remainder_.empty()) { ++ return remainder_; ++ } ++ auto slash = remainder_.find('/'); ++ if (slash == std::string_view::npos) { ++ return pop_all(); ++ } ++ auto result = remainder_.substr(0UL, slash); ++ remainder_.remove_prefix(slash + 1); ++ if (result.empty()) { ++ return pop(); ++ } else { ++ return result; ++ } ++} ++std::string_view Self::pop_all() { ++ auto result = remainder_; ++ remainder_ = ""; ++ return result; ++} ++std::string_view Self::pop_n(std::size_t n) { ++ std::size_t a = 0UL; ++ while (n) { ++ auto slash = remainder_.find('/', a); ++ auto non_slash = remainder_.find_first_not_of("/", a); ++ if (slash == std::string_view::npos) { ++ auto result = remainder_; ++ remainder_ = ""; ++ return result; ++ } ++ if (non_slash < slash) { ++ --n; ++ } ++ a = slash + 1UL; ++ } ++ auto result = remainder_.substr(0UL, a - 1); ++ remainder_.remove_prefix(a); ++ return result; ++} ++std::string_view Self::peek_back() const { ++ auto s = remainder_; ++ while (!s.empty() && s.back() == '/') { ++ s.remove_suffix(1); ++ } ++ if (s.empty()) { ++ return s; ++ } ++ auto last_slash = s.find_last_of('/'); ++ if (last_slash < remainder_.size()) { ++ return remainder_.substr(last_slash + 1); ++ } else { ++ return s; ++ } ++} ++std::string Self::pop_back() { ++ auto non_slash = remainder_.find_last_not_of('/'); ++ if (non_slash == std::string_view::npos) { ++ return ""; ++ } ++ auto slash = remainder_.find_last_of('/', non_slash); ++ std::string rv; ++ if (slash == std::string_view::npos) { ++ rv = remainder_; ++ remainder_ = ""; ++ } else { ++ rv = remainder_.substr(slash + 1, non_slash - slash); ++ remainder_ = remainder_.substr(0UL, slash); ++ } ++ return rv; ++} ++ ++std::ostream& operator<<(std::ostream& str, ipfs::SlashDelimited const& sd) { ++ return str << sd.to_view(); ++} ++ ++#if __has_include() ++#include ++ ++using namespace google::protobuf::internal; ++using namespace google::protobuf; ++ ++#if PROTOBUF_VERSION >= 3020000 ++#include ++LogMessage& operator<<(LogMessage& str, ipfs::SlashDelimited const& sd) { ++ return str << sd.to_view(); ++} ++#elif __has_include() ++#include ++LogMessage& operator<<(LogMessage& str, ipfs::SlashDelimited const& sd) { ++ return str << StringPiece{sd.to_view()}; ++} ++#else ++LogMessage& operator<<(LogMessage& str, ipfs::SlashDelimited const& sd) { ++ return str << std::string{sd.to_view()}; ++} ++#endif ++ ++#endif +diff --git a/third_party/ipfs_client/unix_fs.proto b/third_party/ipfs_client/unix_fs.proto +new file mode 100644 +index 0000000000000..9d117a4d66bdf +--- /dev/null ++++ b/third_party/ipfs_client/unix_fs.proto +@@ -0,0 +1,32 @@ ++syntax = "proto2"; ++option optimize_for = LITE_RUNTIME; ++package ipfs.unix_fs; ++ ++message Data { ++ enum DataType { ++ Raw = 0; ++ Directory = 1; ++ File = 2; ++ Metadata = 3; ++ Symlink = 4; ++ HAMTShard = 5; ++ } ++ ++ required DataType Type = 1; ++ optional bytes Data = 2; ++ optional uint64 filesize = 3; ++ repeated uint64 blocksizes = 4; ++ optional uint64 hashType = 5; ++ optional uint64 fanout = 6; ++ optional uint32 mode = 7; ++ optional UnixTime mtime = 8; ++} ++ ++message Metadata { ++ optional string MimeType = 1; ++} ++ ++message UnixTime { ++ required int64 Seconds = 1; ++ optional fixed32 FractionalNanoseconds = 2; ++} +diff --git a/url/BUILD.gn b/url/BUILD.gn +index c525c166979d6..ce2b1ae43c0a7 100644 +--- a/url/BUILD.gn ++++ b/url/BUILD.gn +@@ -5,6 +5,7 @@ + import("//build/buildflag_header.gni") + import("//testing/libfuzzer/fuzzer_test.gni") + import("//testing/test.gni") ++import("//third_party/ipfs_client/args.gni") + import("features.gni") + + import("//build/config/cronet/config.gni") +@@ -67,6 +68,7 @@ component("url") { + public_deps = [ + "//base", + "//build:robolectric_buildflags", ++ "//third_party/ipfs_client:ipfs_buildflags", + ] + + configs += [ "//build/config/compiler:wexit_time_destructors" ] +@@ -89,6 +91,11 @@ component("url") { + public_configs = [ "//third_party/jdk" ] + } + ++ if (enable_ipfs) { ++ sources += [ "url_canon_ipfs.cc" ] ++ deps += [ "//third_party/ipfs_client:ipfs_client" ] ++ } ++ + if (is_win) { + # Don't conflict with Windows' "url.dll". + output_name = "url_lib" +diff --git a/url/url_canon.h b/url/url_canon.h +index 913b3685c6fec..3c3c55e580564 100644 +--- a/url/url_canon.h ++++ b/url/url_canon.h +@@ -792,6 +792,23 @@ bool CanonicalizeMailtoURL(const char16_t* spec, + CanonOutput* output, + Parsed* new_parsed); + ++COMPONENT_EXPORT(URL) ++bool CanonicalizeIpfsURL(const char* spec, ++ int spec_len, ++ const Parsed& parsed, ++ SchemeType scheme_type, ++ CharsetConverter* query_converter, ++ CanonOutput* output, ++ Parsed* new_parsed); ++COMPONENT_EXPORT(URL) ++bool CanonicalizeIpfsURL(const char16_t* spec, ++ int spec_len, ++ const Parsed& parsed, ++ SchemeType scheme_type, ++ CharsetConverter* query_converter, ++ CanonOutput* output, ++ Parsed* new_parsed); ++ + // Part replacer -------------------------------------------------------------- + + // Internal structure used for storing separate strings for each component. +diff --git a/url/url_canon_ipfs.cc b/url/url_canon_ipfs.cc +new file mode 100644 +index 0000000000000..9511e3f5e6f5c +--- /dev/null ++++ b/url/url_canon_ipfs.cc +@@ -0,0 +1,55 @@ ++#include "url_canon_internal.h" ++ ++#include ++#include ++ ++#include ++ ++bool url::CanonicalizeIpfsURL(const char* spec, ++ int spec_len, ++ const Parsed& parsed, ++ SchemeType scheme_type, ++ CharsetConverter* charset_converter, ++ CanonOutput* output, ++ Parsed* output_parsed) { ++ if ( spec_len < 1 || !spec ) { ++ return false; ++ } ++ if ( parsed.host.len < 1 ) { ++ return false; ++ } ++ std::string_view cid_str{ spec + parsed.host.begin, static_cast(parsed.host.len) }; ++ auto cid = ipfs::Cid(cid_str); ++ if ( !cid.valid() ) { ++ cid = ipfs::id_cid::forText( std::string{cid_str} + " is not a valid CID." ); + } -+ std::string cid_str{ spec + parsed.host.begin, static_cast(parsed.host.len) }; -+ auto maybe_cid = CidCodec::fromString(cid_str); -+ if ( !maybe_cid.has_value() ) { -+ auto e = libp2p::multi::Stringify(maybe_cid.error()); -+ std::ostringstream err; -+ err << e << ' ' -+ << std::string_view{spec,static_cast(spec_len)}; -+ maybe_cid = ipfs::id_cid::forText( err.str() ); -+ } -+ auto cid = maybe_cid.value(); -+ if ( cid.version == Cid::Version::V0 ) { -+ //TODO dcheck content_type == DAG_PB && content_address.getType() == sha256 -+ cid = Cid{ -+ Cid::Version::V1, -+ cid.content_type, -+ cid.content_address -+ }; -+ } -+ auto as_str = CidCodec::toString(cid); -+ if ( !as_str.has_value() ) { ++ auto as_str = cid.to_string(); ++ if ( as_str.empty() ) { + return false; + } + std::string stdurl{ spec, static_cast(parsed.host.begin) }; -+ stdurl.append( as_str.value() ); ++ stdurl.append( as_str ); + stdurl.append( spec + parsed.host.end(), spec_len - parsed.host.end() ); + spec = stdurl.data(); + spec_len = static_cast(stdurl.size()); @@ -835,3 +11246,4 @@ index 9258cfcfada47..daf10e4c3b741 100644 } else if (DoIsStandard(spec, scheme, &scheme_type)) { // All "normal" URLs. ParseStandardURL(spec, spec_len, &parsed_input); + diff --git a/component/patches/120.0.6099.71.patch b/component/patches/122.0.6194.0.patch similarity index 87% rename from component/patches/120.0.6099.71.patch rename to component/patches/122.0.6194.0.patch index e337d213..2639e189 100644 --- a/component/patches/120.0.6099.71.patch +++ b/component/patches/122.0.6194.0.patch @@ -1,8 +1,8 @@ diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn -index 44d3b5e543101..ece46a3db0bea 100644 +index ea574b1863e18..7239008b4e215 100644 --- a/chrome/browser/BUILD.gn +++ b/chrome/browser/BUILD.gn -@@ -40,6 +40,7 @@ import("//rlz/buildflags/buildflags.gni") +@@ -39,6 +39,7 @@ import("//rlz/buildflags/buildflags.gni") import("//sandbox/features.gni") import("//testing/libfuzzer/fuzzer_test.gni") import("//third_party/blink/public/public_features.gni") @@ -10,11 +10,15 @@ index 44d3b5e543101..ece46a3db0bea 100644 import("//third_party/protobuf/proto_library.gni") import("//third_party/webrtc/webrtc.gni") import("//third_party/widevine/cdm/widevine.gni") -@@ -2660,6 +2661,10 @@ static_library("browser") { +@@ -2615,6 +2616,14 @@ static_library("browser") { ] } + if (enable_ipfs) { ++ sources += [ ++ "ipfs_extra_parts.cc", ++ "ipfs_extra_parts.h", ++ ] + deps += [ "//components/ipfs" ] + } + @@ -22,18 +26,18 @@ index 44d3b5e543101..ece46a3db0bea 100644 deps += [ "//chrome/browser/screen_ai:screen_ai_dlc_installer" ] } diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc -index 8736a91998f6b..da556109dcbb1 100644 +index b076446400486..2b2f2724d3b75 100644 --- a/chrome/browser/about_flags.cc +++ b/chrome/browser/about_flags.cc -@@ -213,6 +213,7 @@ +@@ -214,6 +214,7 @@ #include "third_party/blink/public/common/features_generated.h" #include "third_party/blink/public/common/forcedark/forcedark_switches.h" #include "third_party/blink/public/common/switches.h" +#include "third_party/ipfs_client/ipfs_buildflags.h" #include "ui/accessibility/accessibility_features.h" #include "ui/accessibility/accessibility_switches.h" - #include "ui/base/ui_base_features.h" -@@ -314,6 +315,10 @@ + #include "ui/base/ozone_buildflags.h" +@@ -310,6 +311,10 @@ #include "extensions/common/switches.h" #endif // BUILDFLAG(ENABLE_EXTENSIONS) @@ -44,7 +48,7 @@ index 8736a91998f6b..da556109dcbb1 100644 #if BUILDFLAG(ENABLE_PDF) #include "pdf/pdf_features.h" #endif -@@ -9922,6 +9927,14 @@ const FeatureEntry kFeatureEntries[] = { +@@ -9459,6 +9464,14 @@ const FeatureEntry kFeatureEntries[] = { flag_descriptions::kOmitCorsClientCertDescription, kOsAll, FEATURE_VALUE_TYPE(network::features::kOmitCorsClientCert)}, @@ -110,19 +114,10 @@ index 4c88614c68c25..f8bb12a3b0c2e 100644 // Also check for schemes registered via registerProtocolHandler(), which diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc -index d9c675c8aca73..cb360f4e7ca5b 100644 +index 667370e623970..c09550c753e8e 100644 --- a/chrome/browser/chrome_content_browser_client.cc +++ b/chrome/browser/chrome_content_browser_client.cc -@@ -228,6 +228,8 @@ - #include "components/error_page/common/localized_error.h" - #include "components/error_page/content/browser/net_error_auto_reloader.h" - #include "components/google/core/common/google_switches.h" -+#include "components/ipfs/interceptor.h" -+#include "components/ipfs/url_loader_factory.h" - #include "components/keep_alive_registry/keep_alive_types.h" - #include "components/keep_alive_registry/scoped_keep_alive.h" - #include "components/language/core/browser/pref_names.h" -@@ -364,6 +366,7 @@ +@@ -377,6 +377,7 @@ #include "third_party/blink/public/common/switches.h" #include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom.h" #include "third_party/blink/public/public_buildflags.h" @@ -130,11 +125,12 @@ index d9c675c8aca73..cb360f4e7ca5b 100644 #include "third_party/widevine/cdm/buildflags.h" #include "ui/base/clipboard/clipboard_format_type.h" #include "ui/base/l10n/l10n_util.h" -@@ -487,6 +490,12 @@ +@@ -499,6 +500,13 @@ #include "chrome/browser/fuchsia/chrome_browser_main_parts_fuchsia.h" #endif +#if BUILDFLAG(ENABLE_IPFS) ++#include "chrome/browser/ipfs_extra_parts.h" +#include "components/ipfs/interceptor.h" +#include "components/ipfs/ipfs_features.h" +#include "components/ipfs/url_loader_factory.h" @@ -142,8 +138,20 @@ index d9c675c8aca73..cb360f4e7ca5b 100644 + #if BUILDFLAG(IS_CHROMEOS) #include "base/debug/leak_annotations.h" - #include "chrome/browser/apps/intent_helper/chromeos_disabled_apps_throttle.h" -@@ -6180,12 +6189,23 @@ void ChromeContentBrowserClient:: + #include "chrome/browser/apps/app_service/app_install/app_install_navigation_throttle.h" +@@ -1711,6 +1719,11 @@ ChromeContentBrowserClient::CreateBrowserMainParts(bool is_integration_test) { + main_parts->AddParts( + std::make_unique()); + ++#if BUILDFLAG(ENABLE_IPFS) ++ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { ++ main_parts->AddParts(std::make_unique()); ++ } ++#endif + return main_parts; + } + +@@ -6075,12 +6088,25 @@ void ChromeContentBrowserClient:: const absl::optional& request_initiator_origin, NonNetworkURLLoaderFactoryMap* factories) { #if BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(ENABLE_EXTENSIONS) || \ @@ -153,23 +161,24 @@ index d9c675c8aca73..cb360f4e7ca5b 100644 RenderFrameHost::FromID(render_process_id, render_frame_id); WebContents* web_contents = WebContents::FromRenderFrameHost(frame_host); #endif // BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(ENABLE_EXTENSIONS) || \ -- // !BUILDFLAG(IS_ANDROID) -+ // !BUILDFLAG(IS_ANDROID) || BUILDFLAG(ENABLE_IPFS) + // !BUILDFLAG(IS_ANDROID) +#if BUILDFLAG(ENABLE_IPFS) + if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { + network::mojom::URLLoaderFactory* default_factory = g_browser_process->system_network_context_manager()->GetURLLoaderFactory(); ++ auto* context = web_contents->GetBrowserContext(); + ipfs::IpfsURLLoaderFactory::Create( + factories, -+ web_contents->GetBrowserContext(), ++ context, + default_factory, -+ GetSystemNetworkContext() ++ GetSystemNetworkContext(), ++ Profile::FromBrowserContext(context)->GetPrefs() + ); + } +#endif // BUILDFLAG(ENABLE_IPFS) #if BUILDFLAG(IS_CHROMEOS_ASH) if (web_contents) { -@@ -6327,6 +6347,11 @@ ChromeContentBrowserClient::WillCreateURLLoaderRequestInterceptors( +@@ -6222,6 +6248,11 @@ ChromeContentBrowserClient::WillCreateURLLoaderRequestInterceptors( scoped_refptr navigation_response_task_runner) { std::vector> interceptors; @@ -182,10 +191,10 @@ index d9c675c8aca73..cb360f4e7ca5b 100644 interceptors.push_back( std::make_unique( diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json -index 400505f9d64e6..320b730d894c6 100644 +index 7a0c7c06f4e8e..8af9a104e1fe8 100644 --- a/chrome/browser/flag-metadata.json +++ b/chrome/browser/flag-metadata.json -@@ -2868,6 +2868,11 @@ +@@ -2931,6 +2931,11 @@ "owners": [ "hanxi@chromium.org", "wychen@chromium.org" ], "expiry_milestone": 130 }, @@ -198,10 +207,10 @@ index 400505f9d64e6..320b730d894c6 100644 "name": "enable-isolated-sandboxed-iframes", "owners": [ "wjmaclean@chromium.org", "alexmos@chromium.org", "creis@chromium.org" ], diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc -index f92c5df0fc600..e131baa70cce2 100644 +index b4f75320e4669..fc2ef00f6eb5c 100644 --- a/chrome/browser/flag_descriptions.cc +++ b/chrome/browser/flag_descriptions.cc -@@ -248,6 +248,11 @@ const char kEnableBenchmarkingDescription[] = +@@ -289,6 +289,11 @@ const char kEnableBenchmarkingDescription[] = "after 3 restarts. On the third restart, the flag will appear to be off " "but the effect is still active."; @@ -214,10 +223,10 @@ index f92c5df0fc600..e131baa70cce2 100644 "Preloading Settings on Performance Page"; const char kPreloadingOnPerformancePageDescription[] = diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h -index 71eae84724eab..a8e4b29ee3cc5 100644 +index 65b8bcb40770c..d5ae8dca53910 100644 --- a/chrome/browser/flag_descriptions.h +++ b/chrome/browser/flag_descriptions.h -@@ -22,6 +22,7 @@ +@@ -23,6 +23,7 @@ #include "pdf/buildflags.h" #include "printing/buildflags/buildflags.h" #include "third_party/blink/public/common/buildflags.h" @@ -225,7 +234,7 @@ index 71eae84724eab..a8e4b29ee3cc5 100644 // This file declares strings used in chrome://flags. These messages are not // translated, because instead of end-users they target Chromium developers and -@@ -165,6 +166,11 @@ extern const char kDownloadWarningImprovementsDescription[]; +@@ -179,6 +180,11 @@ extern const char kDownloadWarningImprovementsDescription[]; extern const char kEnableBenchmarkingName[]; extern const char kEnableBenchmarkingDescription[]; @@ -237,6 +246,42 @@ index 71eae84724eab..a8e4b29ee3cc5 100644 #if BUILDFLAG(USE_FONTATIONS_BACKEND) extern const char kFontationsFontBackendName[]; extern const char kFontationsFontBackendDescription[]; +diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc +index 86a09cfe49376..d8e166f76179e 100644 +--- a/chrome/browser/prefs/browser_prefs.cc ++++ b/chrome/browser/prefs/browser_prefs.cc +@@ -189,6 +189,7 @@ + #include "printing/buildflags/buildflags.h" + #include "rlz/buildflags/buildflags.h" + #include "third_party/abseil-cpp/absl/types/optional.h" ++#include "third_party/ipfs_client/ipfs_buildflags.h" + + #if BUILDFLAG(ENABLE_BACKGROUND_MODE) + #include "chrome/browser/background/background_mode_manager.h" +@@ -233,6 +234,11 @@ + #include "chrome/browser/pdf/pdf_pref_names.h" + #endif // BUILDFLAG(ENABLE_PDF) + ++#if BUILDFLAG(ENABLE_IPFS) ++#include "components/ipfs/ipfs_features.h" ++#include "components/ipfs/preferences.h" ++#endif ++ + #if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE) + #include "chrome/browser/screen_ai/pref_names.h" + #endif +@@ -1678,6 +1684,11 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry, + IncognitoModePrefs::RegisterProfilePrefs(registry); + invalidation::PerUserTopicSubscriptionManager::RegisterProfilePrefs(registry); + invalidation::InvalidatorRegistrarWithMemory::RegisterProfilePrefs(registry); ++#if BUILDFLAG(ENABLE_IPFS) ++ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { ++ ipfs::RegisterPreferences(registry); ++ } ++#endif + language::LanguagePrefs::RegisterProfilePrefs(registry); + login_detection::prefs::RegisterProfilePrefs(registry); + lookalikes::RegisterProfilePrefs(registry); diff --git a/chrome/common/chrome_content_client.cc b/chrome/common/chrome_content_client.cc index 246ec9c5c911f..5d66d133a7907 100644 --- a/chrome/common/chrome_content_client.cc @@ -690,7 +735,7 @@ index 4eadf46ea0c24..d62fc7fb14e01 100644 for (auto& scheme : url::GetCorsEnabledSchemes()) cors_enabled_schemes.insert(scheme.c_str()); diff --git a/url/BUILD.gn b/url/BUILD.gn -index c525c166979d6..ce2b1ae43c0a7 100644 +index 0ac17c08dc5bc..2497d02658bb3 100644 --- a/url/BUILD.gn +++ b/url/BUILD.gn @@ -5,6 +5,7 @@ @@ -701,7 +746,7 @@ index c525c166979d6..ce2b1ae43c0a7 100644 import("features.gni") import("//build/config/cronet/config.gni") -@@ -67,6 +68,7 @@ component("url") { +@@ -68,6 +69,7 @@ component("url") { public_deps = [ "//base", "//build:robolectric_buildflags", @@ -709,7 +754,7 @@ index c525c166979d6..ce2b1ae43c0a7 100644 ] configs += [ "//build/config/compiler:wexit_time_destructors" ] -@@ -89,6 +91,11 @@ component("url") { +@@ -90,6 +92,11 @@ component("url") { public_configs = [ "//third_party/jdk" ] } @@ -722,10 +767,10 @@ index c525c166979d6..ce2b1ae43c0a7 100644 # Don't conflict with Windows' "url.dll". output_name = "url_lib" diff --git a/url/url_canon.h b/url/url_canon.h -index d3a7fabf09fa8..06db17242248f 100644 +index 8c48f9825d8cf..b9ad961e1b123 100644 --- a/url/url_canon.h +++ b/url/url_canon.h -@@ -697,6 +697,23 @@ bool CanonicalizeMailtoURL(const char16_t* spec, +@@ -804,6 +804,23 @@ bool CanonicalizeMailtoURL(const char16_t* spec, CanonOutput* output, Parsed* new_parsed); @@ -751,21 +796,17 @@ index d3a7fabf09fa8..06db17242248f 100644 // Internal structure used for storing separate strings for each component. diff --git a/url/url_canon_ipfs.cc b/url/url_canon_ipfs.cc new file mode 100644 -index 0000000000000..da3a5f032b5e8 +index 0000000000000..d7c9fdc78eb91 --- /dev/null +++ b/url/url_canon_ipfs.cc -@@ -0,0 +1,72 @@ +@@ -0,0 +1,55 @@ +#include "url_canon_internal.h" + -+#include ++#include +#include + +#include + -+namespace m = libp2p::multi; -+using Cid = m::ContentIdentifier; -+using CidCodec = m::ContentIdentifierCodec; -+ +bool url::CanonicalizeIpfsURL(const char* spec, + int spec_len, + const Parsed& parsed, @@ -779,37 +820,24 @@ index 0000000000000..da3a5f032b5e8 + if ( parsed.host.len < 1 ) { + return false; + } -+ std::string cid_str{ spec + parsed.host.begin, static_cast(parsed.host.len) }; -+ auto maybe_cid = CidCodec::fromString(cid_str); -+ if ( !maybe_cid.has_value() ) { -+ auto e = libp2p::multi::Stringify(maybe_cid.error()); -+ std::ostringstream err; -+ err << e << ' ' -+ << std::string_view{spec,static_cast(spec_len)}; -+ maybe_cid = ipfs::id_cid::forText( err.str() ); ++ std::string_view cid_str{ spec + parsed.host.begin, static_cast(parsed.host.len) }; ++ auto cid = ipfs::Cid(cid_str); ++ if ( !cid.valid() ) { ++ cid = ipfs::id_cid::forText( std::string{cid_str} + " is not a valid CID." ); + } -+ auto cid = maybe_cid.value(); -+ if ( cid.version == Cid::Version::V0 ) { -+ //TODO dcheck content_type == DAG_PB && content_address.getType() == sha256 -+ cid = Cid{ -+ Cid::Version::V1, -+ cid.content_type, -+ cid.content_address -+ }; -+ } -+ auto as_str = CidCodec::toString(cid); -+ if ( !as_str.has_value() ) { ++ auto as_str = cid.to_string(); ++ if ( as_str.empty() ) { + return false; + } + std::string stdurl{ spec, static_cast(parsed.host.begin) }; -+ stdurl.append( as_str.value() ); ++ stdurl.append( as_str ); + stdurl.append( spec + parsed.host.end(), spec_len - parsed.host.end() ); + spec = stdurl.data(); + spec_len = static_cast(stdurl.size()); + Parsed parsed_input; + ParseStandardURL(spec, spec_len, &parsed_input); + return CanonicalizeStandardURL( -+ spec, spec_len, ++ spec, + parsed_input, + scheme_type, + charset_converter, @@ -828,19 +856,25 @@ index 0000000000000..da3a5f032b5e8 + return CanonicalizeIpfsURL(as8.data(), as8.length(), parsed, scheme_type, query_converter, output, new_parsed); +} diff --git a/url/url_util.cc b/url/url_util.cc -index 9258cfcfada47..daf10e4c3b741 100644 +index 6f83f33c01c6b..a248e11c49445 100644 --- a/url/url_util.cc +++ b/url/url_util.cc -@@ -277,6 +277,12 @@ bool DoCanonicalize(const CHAR* spec, - charset_converter, output, - output_parsed); - +@@ -273,8 +273,15 @@ bool DoCanonicalize(const CHAR* spec, + } else if (DoCompareSchemeComponent(spec, scheme, url::kFileSystemScheme)) { + // Filesystem URLs are special. + ParseFileSystemURL(spec, spec_len, &parsed_input); +- success = CanonicalizeFileSystemURL(spec, parsed_input, charset_converter, +- output, output_parsed); ++ success = CanonicalizeFileSystemURL(spec, parsed_input, ++ charset_converter, output, ++ output_parsed); ++ + } else if (DoCompareSchemeComponent(spec, scheme, "ipfs")) { + // Switch multibase away from case-sensitive ones before continuing canonicalization. + ParseStandardURL(spec, spec_len, &parsed_input); + success = CanonicalizeIpfsURL(spec, spec_len, parsed_input, scheme_type, + charset_converter, output, output_parsed); -+ + } else if (DoIsStandard(spec, scheme, &scheme_type)) { // All "normal" URLs. - ParseStandardURL(spec, spec_len, &parsed_input); + diff --git a/component/patches/2f84c59146681c9207a4610d12669dd4a0603af2.patch b/component/patches/2f84c59146681c9207a4610d12669dd4a0603af2.patch new file mode 100644 index 00000000..68d44d66 --- /dev/null +++ b/component/patches/2f84c59146681c9207a4610d12669dd4a0603af2.patch @@ -0,0 +1,11532 @@ +diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn +index a188528a9e262..88df13b162858 100644 +--- a/chrome/browser/BUILD.gn ++++ b/chrome/browser/BUILD.gn +@@ -40,6 +40,7 @@ import("//rlz/buildflags/buildflags.gni") + import("//sandbox/features.gni") + import("//testing/libfuzzer/fuzzer_test.gni") + import("//third_party/blink/public/public_features.gni") ++import("//third_party/ipfs_client/args.gni") + import("//third_party/protobuf/proto_library.gni") + import("//third_party/webrtc/webrtc.gni") + import("//third_party/widevine/cdm/widevine.gni") +@@ -1912,7 +1913,6 @@ static_library("browser") { + "user_education/user_education_service_factory.h", + ] + } +- + configs += [ + "//build/config/compiler:wexit_time_destructors", + "//build/config:precompiled_headers", +@@ -2604,6 +2604,14 @@ static_library("browser") { + ] + } + ++ if (enable_ipfs) { ++ sources += [ ++ "ipfs_extra_parts.cc", ++ "ipfs_extra_parts.h", ++ ] ++ deps += [ "//components/ipfs" ] ++ } ++ + if (is_chromeos_ash) { + deps += [ "//chrome/browser/screen_ai:screen_ai_dlc_installer" ] + } +diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc +index a7907d8b188d8..68a96934ccf48 100644 +--- a/chrome/browser/about_flags.cc ++++ b/chrome/browser/about_flags.cc +@@ -213,6 +213,7 @@ + #include "third_party/blink/public/common/features_generated.h" + #include "third_party/blink/public/common/forcedark/forcedark_switches.h" + #include "third_party/blink/public/common/switches.h" ++#include "third_party/ipfs_client/ipfs_buildflags.h" + #include "ui/accessibility/accessibility_features.h" + #include "ui/accessibility/accessibility_switches.h" + #include "ui/base/ui_base_features.h" +@@ -308,6 +309,10 @@ + #include "extensions/common/switches.h" + #endif // BUILDFLAG(ENABLE_EXTENSIONS) + ++#if BUILDFLAG(ENABLE_IPFS) ++#include "components/ipfs/ipfs_features.h" ++#endif ++ + #if BUILDFLAG(ENABLE_PDF) + #include "pdf/pdf_features.h" + #endif +@@ -9731,6 +9736,14 @@ const FeatureEntry kFeatureEntries[] = { + flag_descriptions::kOmitCorsClientCertDescription, kOsAll, + FEATURE_VALUE_TYPE(network::features::kOmitCorsClientCert)}, + ++#if BUILDFLAG(ENABLE_IPFS) ++ {"enable-ipfs", ++ flag_descriptions::kEnableIpfsName, ++ flag_descriptions::kEnableIpfsDescription, ++ kOsMac | kOsWin | kOsLinux,//TODO: These are the only variants currently getting built, but that is not likely to remain the case ++ FEATURE_VALUE_TYPE(ipfs::kEnableIpfs)}, ++#endif ++ + {"use-idna2008-non-transitional", + flag_descriptions::kUseIDNA2008NonTransitionalName, + flag_descriptions::kUseIDNA2008NonTransitionalDescription, kOsAll, +diff --git a/chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.cc b/chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.cc +index 4c88614c68c25..f8bb12a3b0c2e 100644 +--- a/chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.cc ++++ b/chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.cc +@@ -10,6 +10,8 @@ + #include "chrome/browser/custom_handlers/protocol_handler_registry_factory.h" + #include "chrome/browser/external_protocol/external_protocol_handler.h" + #include "chrome/browser/profiles/profile.h" ++#include "third_party/ipfs_client/ipfs_buildflags.h" ++ + #if BUILDFLAG(IS_ANDROID) + #include "chrome/browser/profiles/profile_android.h" + #endif +@@ -18,6 +20,9 @@ + #include "chrome/browser/ui/android/omnibox/jni_headers/ChromeAutocompleteSchemeClassifier_jni.h" + #endif + #include "components/custom_handlers/protocol_handler_registry.h" ++#if BUILDFLAG(ENABLE_IPFS) ++#include "components/ipfs/ipfs_features.h" ++#endif + #include "content/public/common/url_constants.h" + #include "url/url_util.h" + +@@ -55,12 +60,20 @@ ChromeAutocompleteSchemeClassifier::GetInputTypeForScheme( + if (scheme.empty()) { + return metrics::OmniboxInputType::EMPTY; + } +- if (base::IsStringASCII(scheme) && +- (ProfileIOData::IsHandledProtocol(scheme) || +- base::EqualsCaseInsensitiveASCII(scheme, content::kViewSourceScheme) || +- base::EqualsCaseInsensitiveASCII(scheme, url::kJavaScriptScheme) || +- base::EqualsCaseInsensitiveASCII(scheme, url::kDataScheme))) { +- return metrics::OmniboxInputType::URL; ++ if (base::IsStringASCII(scheme)) { ++ if (ProfileIOData::IsHandledProtocol(scheme) || ++ base::EqualsCaseInsensitiveASCII(scheme, content::kViewSourceScheme) || ++ base::EqualsCaseInsensitiveASCII(scheme, url::kJavaScriptScheme) || ++ base::EqualsCaseInsensitiveASCII(scheme, url::kDataScheme)) { ++ return metrics::OmniboxInputType::URL; ++ } ++#if BUILDFLAG(ENABLE_IPFS) ++ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs) && ++ (base::EqualsCaseInsensitiveASCII(scheme, "ipfs") || base::EqualsCaseInsensitiveASCII(scheme, "ipns")) ++ ) { ++ return metrics::OmniboxInputType::URL; ++ } ++#endif + } + + // Also check for schemes registered via registerProtocolHandler(), which +diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc +index d3d67d83a514e..a5b1ef6339211 100644 +--- a/chrome/browser/chrome_content_browser_client.cc ++++ b/chrome/browser/chrome_content_browser_client.cc +@@ -378,6 +378,7 @@ + #include "third_party/blink/public/common/switches.h" + #include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom.h" + #include "third_party/blink/public/public_buildflags.h" ++#include "third_party/ipfs_client/ipfs_buildflags.h" + #include "third_party/widevine/cdm/buildflags.h" + #include "ui/base/clipboard/clipboard_format_type.h" + #include "ui/base/l10n/l10n_util.h" +@@ -500,6 +501,13 @@ + #include "chrome/browser/fuchsia/chrome_browser_main_parts_fuchsia.h" + #endif + ++#if BUILDFLAG(ENABLE_IPFS) ++#include "chrome/browser/ipfs_extra_parts.h" ++#include "components/ipfs/interceptor.h" ++#include "components/ipfs/ipfs_features.h" ++#include "components/ipfs/url_loader_factory.h" ++#endif ++ + #if BUILDFLAG(IS_CHROMEOS) + #include "base/debug/leak_annotations.h" + #include "chrome/browser/apps/app_service/app_install/app_install_navigation_throttle.h" +@@ -1712,6 +1720,11 @@ ChromeContentBrowserClient::CreateBrowserMainParts(bool is_integration_test) { + main_parts->AddParts( + std::make_unique()); + ++#if BUILDFLAG(ENABLE_IPFS) ++ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { ++ main_parts->AddParts(std::make_unique()); ++ } ++#endif + return main_parts; + } + +@@ -6084,12 +6097,25 @@ void ChromeContentBrowserClient:: + const absl::optional& request_initiator_origin, + NonNetworkURLLoaderFactoryMap* factories) { + #if BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(ENABLE_EXTENSIONS) || \ +- !BUILDFLAG(IS_ANDROID) ++ !BUILDFLAG(IS_ANDROID) || BUILDFLAG(ENABLE_IPFS) + content::RenderFrameHost* frame_host = + RenderFrameHost::FromID(render_process_id, render_frame_id); + WebContents* web_contents = WebContents::FromRenderFrameHost(frame_host); + #endif // BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(ENABLE_EXTENSIONS) || \ +- // !BUILDFLAG(IS_ANDROID) ++ // !BUILDFLAG(IS_ANDROID) || BUILDFLAG(ENABLE_IPFS) ++#if BUILDFLAG(ENABLE_IPFS) ++ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { ++ network::mojom::URLLoaderFactory* default_factory = g_browser_process->system_network_context_manager()->GetURLLoaderFactory(); ++ auto* context = web_contents->GetBrowserContext(); ++ ipfs::IpfsURLLoaderFactory::Create( ++ factories, ++ context, ++ default_factory, ++ GetSystemNetworkContext(), ++ Profile::FromBrowserContext(context)->GetPrefs() ++ ); ++ } ++#endif // BUILDFLAG(ENABLE_IPFS) + + #if BUILDFLAG(IS_CHROMEOS_ASH) + if (web_contents) { +@@ -6231,6 +6257,11 @@ ChromeContentBrowserClient::WillCreateURLLoaderRequestInterceptors( + scoped_refptr navigation_response_task_runner) { + std::vector> + interceptors; ++#if BUILDFLAG(ENABLE_IPFS) ++ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { ++ interceptors.push_back(std::make_unique(g_browser_process->system_network_context_manager()->GetURLLoaderFactory(), GetSystemNetworkContext())); ++ } ++#endif + #if BUILDFLAG(ENABLE_OFFLINE_PAGES) + interceptors.push_back( + std::make_unique( +diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json +index 0b51e78fcb8b9..9571b2c92c57f 100644 +--- a/chrome/browser/flag-metadata.json ++++ b/chrome/browser/flag-metadata.json +@@ -2948,6 +2948,11 @@ + "owners": [ "hanxi@chromium.org", "wychen@chromium.org" ], + "expiry_milestone": 130 + }, ++ { ++ "name": "enable-ipfs", ++ "owners": [ "//components/ipfs/OWNERS" ], ++ "expiry_milestone": 150 ++ }, + { + "name": "enable-isolated-sandboxed-iframes", + "owners": [ "wjmaclean@chromium.org", "alexmos@chromium.org", "creis@chromium.org" ], +diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc +index b2992e30f9811..f92d8a322b634 100644 +--- a/chrome/browser/flag_descriptions.cc ++++ b/chrome/browser/flag_descriptions.cc +@@ -288,6 +288,11 @@ const char kEnableBenchmarkingDescription[] = + "after 3 restarts. On the third restart, the flag will appear to be off " + "but the effect is still active."; + ++#if BUILDFLAG(ENABLE_IPFS) ++extern const char kEnableIpfsName[] = "Enable IPFS"; ++extern const char kEnableIpfsDescription[] = "Enable ipfs:// and ipns:// URLs"; ++#endif ++ + const char kPreloadingOnPerformancePageName[] = + "Preloading Settings on Performance Page"; + const char kPreloadingOnPerformancePageDescription[] = +diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h +index ad76d832395a1..438facecff519 100644 +--- a/chrome/browser/flag_descriptions.h ++++ b/chrome/browser/flag_descriptions.h +@@ -23,6 +23,7 @@ + #include "pdf/buildflags.h" + #include "printing/buildflags/buildflags.h" + #include "third_party/blink/public/common/buildflags.h" ++#include "third_party/ipfs_client/ipfs_buildflags.h" + + // This file declares strings used in chrome://flags. These messages are not + // translated, because instead of end-users they target Chromium developers and +@@ -179,6 +180,11 @@ extern const char kDownloadWarningImprovementsDescription[]; + extern const char kEnableBenchmarkingName[]; + extern const char kEnableBenchmarkingDescription[]; + ++#if BUILDFLAG(ENABLE_IPFS) ++extern const char kEnableIpfsName[]; ++extern const char kEnableIpfsDescription[]; ++#endif ++ + #if BUILDFLAG(USE_FONTATIONS_BACKEND) + extern const char kFontationsFontBackendName[]; + extern const char kFontationsFontBackendDescription[]; +diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc +index fc9fcf1ff478a..800961b3c8767 100644 +--- a/chrome/browser/prefs/browser_prefs.cc ++++ b/chrome/browser/prefs/browser_prefs.cc +@@ -190,6 +190,7 @@ + #include "printing/buildflags/buildflags.h" + #include "rlz/buildflags/buildflags.h" + #include "third_party/abseil-cpp/absl/types/optional.h" ++#include "third_party/ipfs_client/ipfs_buildflags.h" + + #if BUILDFLAG(ENABLE_BACKGROUND_MODE) + #include "chrome/browser/background/background_mode_manager.h" +@@ -241,6 +242,11 @@ + #include "chrome/browser/pdf/pdf_pref_names.h" + #endif // BUILDFLAG(ENABLE_PDF) + ++#if BUILDFLAG(ENABLE_IPFS) ++#include "components/ipfs/ipfs_features.h" ++#include "components/ipfs/preferences.h" ++#endif ++ + #if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE) + #include "chrome/browser/screen_ai/pref_names.h" + #endif +@@ -1658,6 +1664,11 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry, + IncognitoModePrefs::RegisterProfilePrefs(registry); + invalidation::PerUserTopicSubscriptionManager::RegisterProfilePrefs(registry); + invalidation::InvalidatorRegistrarWithMemory::RegisterProfilePrefs(registry); ++#if BUILDFLAG(ENABLE_IPFS) ++ if (base::FeatureList::IsEnabled(ipfs::kEnableIpfs)) { ++ ipfs::RegisterPreferences(registry); ++ } ++#endif + language::LanguagePrefs::RegisterProfilePrefs(registry); + login_detection::prefs::RegisterProfilePrefs(registry); + lookalikes::RegisterProfilePrefs(registry); +diff --git a/chrome/common/chrome_content_client.cc b/chrome/common/chrome_content_client.cc +index 246ec9c5c911f..5d66d133a7907 100644 +--- a/chrome/common/chrome_content_client.cc ++++ b/chrome/common/chrome_content_client.cc +@@ -296,6 +296,12 @@ void ChromeContentClient::AddAdditionalSchemes(Schemes* schemes) { + #if BUILDFLAG(IS_ANDROID) + schemes->local_schemes.push_back(url::kContentScheme); + #endif ++ for ( const char* ip_s : {"ipfs", "ipns"} ) { ++ schemes->standard_schemes.push_back(ip_s); ++ schemes->cors_enabled_schemes.push_back(ip_s); ++ schemes->secure_schemes.push_back(ip_s); ++ schemes->csp_bypassing_schemes.push_back(ip_s); ++ } + } + + std::u16string ChromeContentClient::GetLocalizedString(int message_id) { +diff --git a/components/cbor/reader.cc b/components/cbor/reader.cc +index 306ba52fa4944..6b13b3a679a65 100644 +--- a/components/cbor/reader.cc ++++ b/components/cbor/reader.cc +@@ -22,7 +22,7 @@ + namespace cbor { + + namespace constants { +-const char kUnsupportedMajorType[] = "Unsupported major type."; ++const char kUnsupportedMajorType[] = "Unsupported major type operation."; + } + + namespace { +@@ -156,7 +156,11 @@ absl::optional Reader::DecodeCompleteDataItem(const Config& config, + case Value::Type::FLOAT_VALUE: + // Floating point values also go here since they are also type 7. + return DecodeToSimpleValueOrFloat(*header, config); +- case Value::Type::TAG: // We explicitly don't support TAG. ++ case Value::Type::TAG: ++ if (config.parse_tags) { ++ return ReadTagContent(*header, config, max_nesting_level); ++ } ++ break; + case Value::Type::NONE: + case Value::Type::INVALID_UTF8: + break; +@@ -347,6 +351,17 @@ absl::optional Reader::ReadByteStringContent( + return Value(std::move(cbor_byte_string)); + } + ++absl::optional Reader::ReadTagContent( ++ const Reader::DataItemHeader& header, ++ const Config& config, ++ int max_nesting_level) { ++ auto tagged_content = DecodeCompleteDataItem(config, max_nesting_level); ++ if (tagged_content.has_value()) { ++ tagged_content.value().SetTag(header.value); ++ } ++ return tagged_content; ++} ++ + absl::optional Reader::ReadArrayContent( + const Reader::DataItemHeader& header, + const Config& config, +diff --git a/components/cbor/reader.h b/components/cbor/reader.h +index f0b43a5517528..a57e277a1bc66 100644 +--- a/components/cbor/reader.h ++++ b/components/cbor/reader.h +@@ -130,6 +130,11 @@ class CBOR_EXPORT Reader { + // during decoding will set raise the `UNSUPPORTED_FLOATING_POINT_VALUE` + // error. + bool allow_floating_point = false; ++ ++ // If the parser encounters a TAG element, should it be parsed out and ++ // the tag value saved (true), or should the entire node and its content ++ // be discarded (false) ++ bool parse_tags = false; + }; + + Reader(const Reader&) = delete; +@@ -204,6 +209,9 @@ class CBOR_EXPORT Reader { + absl::optional ReadMapContent(const DataItemHeader& header, + const Config& config, + int max_nesting_level); ++ absl::optional ReadTagContent(const DataItemHeader& header, ++ const Config& config, ++ int max_nesting_level); + absl::optional ReadByte(); + absl::optional> ReadBytes(uint64_t num_bytes); + bool IsKeyInOrder(const Value& new_key, +diff --git a/components/cbor/reader_unittest.cc b/components/cbor/reader_unittest.cc +index 83d44a48d6dfa..a6ec5299b3241 100644 +--- a/components/cbor/reader_unittest.cc ++++ b/components/cbor/reader_unittest.cc +@@ -1451,5 +1451,42 @@ TEST(CBORReaderTest, AllowInvalidUTF8) { + EXPECT_FALSE(cbor); + EXPECT_EQ(Reader::DecoderError::INVALID_UTF8, error); + } ++TEST(CBORReaderTest, RejectsTagUnderDefaultConfig) { ++ static const uint8_t kTaggedCbor[] = { ++ 0xd8, 0x2a, 0x58, 0x25, 0x00, 0x01, 0x71, 0x12, 0x20, 0x69, 0xea, 0x07, ++ 0x40, 0xf9, 0x80, 0x7a, 0x28, 0xf4, 0xd9, 0x32, 0xc6, 0x2e, 0x7c, 0x1c, ++ 0x83, 0xbe, 0x05, 0x5e, 0x55, 0x07, 0x2c, 0x90, 0x26, 0x6a, 0xb3, 0xe7, ++ 0x9d, 0xf6, 0x3a, 0x36, 0x5b ++ }; ++ Reader::Config config; ++ absl::optional cbor = Reader::Read(kTaggedCbor, config); ++ EXPECT_FALSE(cbor.has_value()); ++} ++TEST(CBORReaderTest, ReadsTagWhenConfiguredToDoSo) { ++ static const uint8_t kTaggedCbor[] = { ++ 0xd8, 0x2a, 0x58, 0x25, 0x00, 0x01, 0x71, 0x12, 0x20, 0x69, 0xea, 0x07, ++ 0x40, 0xf9, 0x80, 0x7a, 0x28, 0xf4, 0xd9, 0x32, 0xc6, 0x2e, 0x7c, 0x1c, ++ 0x83, 0xbe, 0x05, 0x5e, 0x55, 0x07, 0x2c, 0x90, 0x26, 0x6a, 0xb3, 0xe7, ++ 0x9d, 0xf6, 0x3a, 0x36, 0x5b ++ }; ++ Reader::Config config; ++ config.parse_tags = true; ++ absl::optional cbor = Reader::Read(kTaggedCbor, config); ++ EXPECT_TRUE(cbor.has_value()); ++ auto& v = cbor.value(); ++ EXPECT_TRUE(v.has_tag()); ++ EXPECT_EQ(v.GetTag(),42UL); ++ EXPECT_TRUE(v.is_bytestring()); ++ EXPECT_EQ(v.type(), Value::Type::BYTE_STRING); ++ auto& bytes = v.GetBytestring(); ++ EXPECT_EQ(bytes.size(), 37UL); ++ EXPECT_EQ(bytes.at(0), 0x00);//identity multibase (e.g. not base-encoded, bytes are themselves) ++ EXPECT_EQ(bytes.at(1), 0x01);//CID version 1 ++ EXPECT_EQ(bytes.at(2), 0x71);//codec = dag-cbor ++ EXPECT_EQ(bytes.at(3), 0x12);//multihash = 18 = sha2-256 ++ EXPECT_EQ(bytes.at(4), 0x20);//hash length = 32 bytes ++ EXPECT_EQ(bytes.at(5), 0x69);//first byte of hash digest ++ EXPECT_EQ(bytes.at(36),0x5b);//last byte of hash digest ++} + + } // namespace cbor +diff --git a/components/cbor/values.cc b/components/cbor/values.cc +index 02498209c820e..34055aef24cfe 100644 +--- a/components/cbor/values.cc ++++ b/components/cbor/values.cc +@@ -66,32 +66,34 @@ Value::Value(Type type) : type_(type) { + NOTREACHED(); + } + +-Value::Value(SimpleValue in_simple) +- : type_(Type::SIMPLE_VALUE), simple_value_(in_simple) { ++Value::Value(SimpleValue in_simple, uint64_t tag) ++ : type_(Type::SIMPLE_VALUE), simple_value_(in_simple), tag_(tag) { + CHECK(static_cast(in_simple) >= 20 && static_cast(in_simple) <= 23); + } + +-Value::Value(bool boolean_value) : type_(Type::SIMPLE_VALUE) { ++Value::Value(bool boolean_value, uint64_t tag) : type_(Type::SIMPLE_VALUE), tag_(tag) { + simple_value_ = boolean_value ? Value::SimpleValue::TRUE_VALUE + : Value::SimpleValue::FALSE_VALUE; + } + +-Value::Value(double float_value) +- : type_(Type::FLOAT_VALUE), float_value_(float_value) {} ++Value::Value(double float_value, uint64_t tag) ++ : type_(Type::FLOAT_VALUE), float_value_(float_value), tag_(tag) {} + +-Value::Value(int integer_value) +- : Value(base::checked_cast(integer_value)) {} ++Value::Value(int integer_value, uint64_t tag) ++ : Value(base::checked_cast(integer_value), tag) {} + +-Value::Value(int64_t integer_value) : integer_value_(integer_value) { ++Value::Value(int64_t integer_value, uint64_t tag) : integer_value_(integer_value), tag_(tag) { + type_ = integer_value >= 0 ? Type::UNSIGNED : Type::NEGATIVE; + } + +-Value::Value(base::span in_bytes) ++Value::Value(base::span in_bytes, uint64_t tag) + : type_(Type::BYTE_STRING), +- bytestring_value_(in_bytes.begin(), in_bytes.end()) {} ++ bytestring_value_(in_bytes.begin(), in_bytes.end()), ++ tag_(tag) ++ {} + +-Value::Value(base::span in_bytes, Type type) +- : type_(type), bytestring_value_(in_bytes.begin(), in_bytes.end()) { ++Value::Value(base::span in_bytes, Type type, uint64_t tag) ++ : type_(type), bytestring_value_(in_bytes.begin(), in_bytes.end()), tag_(tag) { + DCHECK(type_ == Type::BYTE_STRING || type_ == Type::INVALID_UTF8); + } + +@@ -117,7 +119,8 @@ Value::Value(std::string&& in_string, Type type) noexcept : type_(type) { + } + } + +-Value::Value(base::StringPiece in_string, Type type) : type_(type) { ++Value::Value(base::StringPiece in_string, Type type, uint64_t tag) ++: type_(type), tag_(tag) { + switch (type_) { + case Type::STRING: + new (&string_value_) std::string(); +@@ -133,16 +136,18 @@ Value::Value(base::StringPiece in_string, Type type) : type_(type) { + } + } + +-Value::Value(const ArrayValue& in_array) : type_(Type::ARRAY), array_value_() { ++Value::Value(const ArrayValue& in_array, uint64_t tag) ++: type_(Type::ARRAY), array_value_(), tag_(tag) { + array_value_.reserve(in_array.size()); + for (const auto& val : in_array) + array_value_.emplace_back(val.Clone()); + } + +-Value::Value(ArrayValue&& in_array) noexcept +- : type_(Type::ARRAY), array_value_(std::move(in_array)) {} ++Value::Value(ArrayValue&& in_array, uint64_t tag) noexcept ++ : type_(Type::ARRAY), array_value_(std::move(in_array)), tag_(tag) {} + +-Value::Value(const MapValue& in_map) : type_(Type::MAP), map_value_() { ++Value::Value(const MapValue& in_map, uint64_t tag) ++: type_(Type::MAP), map_value_(), tag_(tag) { + map_value_.reserve(in_map.size()); + for (const auto& it : in_map) + map_value_.emplace_hint(map_value_.end(), it.first.Clone(), +@@ -168,31 +173,36 @@ Value Value::Clone() const { + case Type::NONE: + return Value(); + case Type::INVALID_UTF8: +- return Value(bytestring_value_, Type::INVALID_UTF8); ++ return Value(bytestring_value_, Type::INVALID_UTF8, tag_); + case Type::UNSIGNED: + case Type::NEGATIVE: +- return Value(integer_value_); ++ return Value(integer_value_, tag_); + case Type::BYTE_STRING: +- return Value(bytestring_value_); ++ return Value(bytestring_value_, tag_); + case Type::STRING: +- return Value(string_value_); ++ return Value(string_value_, Type::STRING, tag_); + case Type::ARRAY: +- return Value(array_value_); ++ return Value(array_value_, tag_); + case Type::MAP: +- return Value(map_value_); ++ return Value(map_value_, tag_); + case Type::TAG: + NOTREACHED() << constants::kUnsupportedMajorType; + return Value(); + case Type::SIMPLE_VALUE: +- return Value(simple_value_); ++ return Value(simple_value_, tag_); + case Type::FLOAT_VALUE: +- return Value(float_value_); ++ return Value(float_value_, tag_); + } + + NOTREACHED(); + return Value(); + } + ++Value& Value::SetTag(uint64_t tag) noexcept { ++ tag_ = tag; ++ return *this; ++} ++ + Value::SimpleValue Value::GetSimpleValue() const { + CHECK(is_simple()); + return simple_value_; +@@ -258,9 +268,14 @@ const Value::BinaryValue& Value::GetInvalidUTF8() const { + return bytestring_value_; + } + ++uint64_t Value::GetTag() const { ++ CHECK(has_tag()); ++ return tag_; ++} ++ + void Value::InternalMoveConstructFrom(Value&& that) { + type_ = that.type_; +- ++ tag_ = that.tag_; + switch (type_) { + case Type::UNSIGNED: + case Type::NEGATIVE: +diff --git a/components/cbor/values.h b/components/cbor/values.h +index d81ef5607c55a..10216a8dcdc57 100644 +--- a/components/cbor/values.h ++++ b/components/cbor/values.h +@@ -127,28 +127,29 @@ class CBOR_EXPORT Value { + + explicit Value(Type type); + +- explicit Value(SimpleValue in_simple); +- explicit Value(bool boolean_value); +- explicit Value(double in_float); ++ explicit Value(SimpleValue in_simple, uint64_t tag = NO_TAG); ++ explicit Value(bool boolean_value, uint64_t tag = NO_TAG); ++ explicit Value(double in_float, uint64_t tag = NO_TAG); + +- explicit Value(int integer_value); +- explicit Value(int64_t integer_value); ++ explicit Value(int integer_value, uint64_t tag = NO_TAG); ++ explicit Value(int64_t integer_value, uint64_t tag = NO_TAG); + explicit Value(uint64_t integer_value) = delete; + +- explicit Value(base::span in_bytes); ++ explicit Value(base::span in_bytes, uint64_t tag = NO_TAG); + explicit Value(BinaryValue&& in_bytes) noexcept; + + explicit Value(const char* in_string, Type type = Type::STRING); + explicit Value(std::string&& in_string, Type type = Type::STRING) noexcept; +- explicit Value(base::StringPiece in_string, Type type = Type::STRING); ++ explicit Value(base::StringPiece in_string, Type type = Type::STRING, uint64_t tag = NO_TAG); + +- explicit Value(const ArrayValue& in_array); +- explicit Value(ArrayValue&& in_array) noexcept; ++ explicit Value(const ArrayValue& in_array, uint64_t tag = NO_TAG); ++ explicit Value(ArrayValue&& in_array, uint64_t tag = NO_TAG) noexcept; + +- explicit Value(const MapValue& in_map); ++ explicit Value(const MapValue& in_map, uint64_t tag = NO_TAG); + explicit Value(MapValue&& in_map) noexcept; + + Value& operator=(Value&& that) noexcept; ++ Value& SetTag(uint64_t) noexcept; + + Value(const Value&) = delete; + Value& operator=(const Value&) = delete; +@@ -179,6 +180,7 @@ class CBOR_EXPORT Value { + bool is_string() const { return type() == Type::STRING; } + bool is_array() const { return type() == Type::ARRAY; } + bool is_map() const { return type() == Type::MAP; } ++ bool has_tag() const { return tag_ != NO_TAG; } + + // These will all fatally assert if the type doesn't match. + SimpleValue GetSimpleValue() const; +@@ -194,12 +196,13 @@ class CBOR_EXPORT Value { + const ArrayValue& GetArray() const; + const MapValue& GetMap() const; + const BinaryValue& GetInvalidUTF8() const; ++ uint64_t GetTag() const; + + private: + friend class Reader; + // This constructor allows INVALID_UTF8 values to be created, which only + // |Reader| and InvalidUTF8StringValueForTesting() may do. +- Value(base::span in_bytes, Type type); ++ Value(base::span in_bytes, Type type, uint64_t tag = NO_TAG); + + Type type_; + +@@ -213,6 +216,11 @@ class CBOR_EXPORT Value { + MapValue map_value_; + }; + ++ //This value specified as Invalid, ++ // used here to represent absence of TAG ++ constexpr static uint64_t NO_TAG = 0xFFFF; ++ uint64_t tag_ = NO_TAG; ++ + void InternalMoveConstructFrom(Value&& that); + void InternalCleanup(); + }; +diff --git a/components/cbor/writer.cc b/components/cbor/writer.cc +index bb22754d36a07..aae4027836377 100644 +--- a/components/cbor/writer.cc ++++ b/components/cbor/writer.cc +@@ -47,6 +47,9 @@ bool Writer::EncodeCBOR(const Value& node, + if (max_nesting_level < 0) + return false; + ++ if (node.has_tag()) { ++ StartItem(Value::Type::TAG, node.GetTag()); ++ } + switch (node.type()) { + case Value::Type::NONE: { + StartItem(Value::Type::BYTE_STRING, 0); +diff --git a/components/cbor/writer_unittest.cc b/components/cbor/writer_unittest.cc +index e3bffe20734bc..0ed569ae164a0 100644 +--- a/components/cbor/writer_unittest.cc ++++ b/components/cbor/writer_unittest.cc +@@ -522,4 +522,31 @@ TEST(CBORWriterTest, OverlyNestedCBOR) { + EXPECT_FALSE(Writer::Write(Value(map), 4).has_value()); + } + ++TEST(CBORWriterTest, CanWriteTag) { ++ std::array content{ ++ 0x00, 0x01, 0x71, 0x12, 0x20, ++ 0x69, 0xea, 0x07, 0x40, 0xf9, ++ 0x80, 0x7a, 0x28, 0xf4, 0xd9, ++ 0x32, 0xc6, 0x2e, 0x7c, 0x1c, ++ 0x83, 0xbe, 0x05, 0x5e, 0x55, ++ 0x07, 0x2c, 0x90, 0x26, 0x6a, ++ 0xb3, 0xe7, 0x9d, 0xf6, 0x3a, ++ 0x36, 0x5b ++ }; ++ Value to_write(content); ++ to_write.SetTag(42); ++ auto result = Writer::Write(to_write); ++ EXPECT_TRUE(result.has_value()); ++ auto& bytes = result.value(); ++ EXPECT_EQ(bytes.size(), 41UL); ++ EXPECT_EQ(bytes.at(0), 0xd8); ++ EXPECT_EQ(bytes.at(1), 0x2a); ++ EXPECT_EQ(bytes.at(2), 0x58); ++ EXPECT_EQ(bytes.at(3), 0x25); ++ for (auto i = 0UL; i < content.size(); ++i) { ++ ASSERT_LT(i + 4UL, bytes.size()); ++ ASSERT_EQ(content.at(i), bytes.at(i+4UL)); ++ } ++} ++ + } // namespace cbor +diff --git a/components/ipfs/BUILD.gn b/components/ipfs/BUILD.gn +new file mode 100644 +index 0000000000000..572e93e493e7a +--- /dev/null ++++ b/components/ipfs/BUILD.gn +@@ -0,0 +1,62 @@ ++import("//testing/test.gni") ++import("//third_party/ipfs_client/args.gni") ++ ++if (enable_ipfs) { ++ ++ component("ipfs") { ++ sources = [ ++ "block_http_request.cc", ++ "block_http_request.h", ++ "cache_requestor.cc", ++ "cache_requestor.h", ++ "chromium_cbor_adapter.cc", ++ "chromium_cbor_adapter.h", ++ "chromium_ipfs_context.cc", ++ "chromium_ipfs_context.h", ++ "chromium_json_adapter.cc", ++ "chromium_json_adapter.h", ++ "crypto_api.cc", ++ "crypto_api.h", ++ "dns_txt_request.cc", ++ "dns_txt_request.h", ++ "export.h", ++ "inter_request_state.cc", ++ "inter_request_state.h", ++ "interceptor.cc", ++ "interceptor.h", ++ "ipfs_features.cc", ++ "ipfs_features.h", ++ "ipfs_url_loader.cc", ++ "ipfs_url_loader.h", ++ "preferences.cc", ++ "preferences.h", ++ "url_loader_factory.cc", ++ "url_loader_factory.h", ++ ] ++ defines = [ ] ++ include_dirs = [ ++ ".", ++ "ipfs_client", ++ "ipfs_client/unix_fs", ++ ] ++ deps = [ ++ "//content", ++ "//crypto", ++ "//base", ++ "//components/cbor", ++ "//components/prefs", ++ "//components/webcrypto:webcrypto", ++ "//mojo/public/cpp/bindings", ++ "//services/network:network_service", ++ "//services/network/public/cpp:cpp", ++ "//services/network/public/mojom:url_loader_base", ++ "//url", ++ "//third_party/blink/public:blink", ++ ] ++ public_deps = [ ++ "//third_party/ipfs_client", ++ ] ++ defines = [ "IS_IPFS_IMPL" ] ++ } ++ ++} +diff --git a/components/ipfs/README.md b/components/ipfs/README.md +new file mode 100644 +index 0000000000000..1333ed77b7e1e +--- /dev/null ++++ b/components/ipfs/README.md +@@ -0,0 +1 @@ ++TODO +diff --git a/components/ipfs/block_http_request.cc b/components/ipfs/block_http_request.cc +new file mode 100644 +index 0000000000000..c48ddd8f77c8d +--- /dev/null ++++ b/components/ipfs/block_http_request.cc +@@ -0,0 +1,102 @@ ++#include "block_http_request.h" ++ ++#include ++#include ++#include ++ ++using Self = ipfs::BlockHttpRequest; ++ ++namespace { ++constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation = ++ net::DefineNetworkTrafficAnnotation("ipfs_gateway_request", R"( ++ semantics { ++ sender: "IPFS component" ++ description: ++ "Sends a request to an IPFS gateway." ++ trigger: ++ "Processing of an ipfs:// or ipns:// URL." ++ data: "None" ++ destination: WEBSITE ++ } ++ policy { ++ cookies_allowed: NO ++ setting: "EnableIpfs" ++ } ++ )"); ++} ++ ++Self::BlockHttpRequest(ipfs::HttpRequestDescription req_inf, ++ HttpCompleteCallback cb) ++ : inf_{req_inf}, callback_{cb} {} ++Self::~BlockHttpRequest() noexcept {} ++ ++void Self::send(raw_ptr loader_factory) { ++ auto req = std::make_unique(); ++ req->url = GURL{inf_.url}; ++ req->priority = net::HIGHEST; // TODO ++ if (!inf_.accept.empty()) { ++ req->headers.SetHeader("Accept", inf_.accept); ++ } ++ using L = network::SimpleURLLoader; ++ loader_ = L::Create(std::move(req), kTrafficAnnotation, FROM_HERE); ++ loader_->SetTimeoutDuration(base::Seconds(inf_.timeout_seconds)); ++ loader_->SetAllowHttpErrorResults(true); ++ loader_->SetOnResponseStartedCallback( ++ base::BindOnce(&Self::OnResponseHead, base::Unretained(this))); ++ auto bound = base::BindOnce(&Self::OnResponse, base::Unretained(this), ++ shared_from_this()); ++ DCHECK(loader_factory); ++ if (auto sz = inf_.max_response_size) { ++ loader_->DownloadToString(loader_factory, std::move(bound), sz.value()); ++ } else { ++ loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(loader_factory, ++ std::move(bound)); ++ } ++} ++void Self::OnResponse(std::shared_ptr, ++ std::unique_ptr body) { ++ DCHECK(loader_); ++ int status; ++ switch (loader_->NetError()) { ++ case net::Error::OK: ++ status = 200; ++ break; ++ case net::Error::ERR_TIMED_OUT: ++ VLOG(2) << "HTTP request timed out: " << inf_.url << " after " ++ << inf_.timeout_seconds << "s."; ++ status = 408; ++ break; ++ default: ++ VLOG(2) << "NetErr " << loader_->NetError() << " for " << inf_.url; ++ status = 500; ++ } ++ // auto sz = body ? body->size() : 0UL; ++ auto const* head = loader_->ResponseInfo(); ++ if (head) { ++ OnResponseHead({}, *head); ++ } ++ auto sp = status_line_.find(' '); ++ if (sp < status_line_.size()) { ++ VLOG(2) << "HTTP response status='" << status_line_ << "'."; ++ status = std::atoi(status_line_.c_str() + sp + 1); ++ } ++ if (body) { ++ callback_(status, *body, header_accessor_); ++ } else { ++ callback_(status, "", header_accessor_); ++ } ++} ++void Self::OnResponseHead( ++ GURL const&, ++ network::mojom::URLResponseHead const& response_head) { ++ if (!response_head.headers) { ++ return; ++ } ++ auto head = response_head.headers; ++ status_line_ = head->GetStatusLine(); ++ header_accessor_ = [head](std::string_view k) { ++ std::string val; ++ head->EnumerateHeader(nullptr, k, &val); ++ return val; ++ }; ++} +diff --git a/components/ipfs/block_http_request.h b/components/ipfs/block_http_request.h +new file mode 100644 +index 0000000000000..a34d88b7a54cf +--- /dev/null ++++ b/components/ipfs/block_http_request.h +@@ -0,0 +1,46 @@ ++#ifndef IPFS_BLOCK_HTTP_REQUEST_H_ ++#define IPFS_BLOCK_HTTP_REQUEST_H_ ++ ++#include ++ ++#include ++#include ++ ++namespace network { ++struct ResourceRequest; ++class SimpleURLLoader; ++} // namespace network ++namespace network::mojom { ++class URLLoaderFactory; ++class URLResponseHead; ++} // namespace network::mojom ++class GURL; ++ ++namespace ipfs { ++class BlockHttpRequest : public std::enable_shared_from_this { ++ // TODO ween oneself off of SimpleURLLoader ++ // std::array buffer_; ++ std::unique_ptr loader_; ++ ++ public: ++ using HttpCompleteCallback = ipfs::ContextApi::HttpCompleteCallback; ++ BlockHttpRequest(ipfs::HttpRequestDescription, HttpCompleteCallback); ++ ~BlockHttpRequest() noexcept; ++ ++ void send(raw_ptr); ++ ++ private: ++ ipfs::HttpRequestDescription const inf_; ++ HttpCompleteCallback callback_; ++ std::string status_line_; ++ ContextApi::HeaderAccess header_accessor_ = [](auto) { ++ return std::string{}; ++ }; ++ ++ void OnResponseHead(GURL const&, network::mojom::URLResponseHead const&); ++ void OnResponse(std::shared_ptr, ++ std::unique_ptr body); ++}; ++} // namespace ipfs ++ ++#endif // IPFS_BLOCK_HTTP_REQUEST_H_ +diff --git a/components/ipfs/cache_requestor.cc b/components/ipfs/cache_requestor.cc +new file mode 100644 +index 0000000000000..ce446b608080a +--- /dev/null ++++ b/components/ipfs/cache_requestor.cc +@@ -0,0 +1,219 @@ ++#include "cache_requestor.h" ++ ++#include "chromium_ipfs_context.h" ++#include "inter_request_state.h" ++ ++#include ++#include ++ ++using Self = ipfs::CacheRequestor; ++namespace dc = disk_cache; ++ ++std::string_view Self::name() const { ++ return "Disk Cache"; ++} ++Self::CacheRequestor(InterRequestState& state, base::FilePath base) ++ : state_{state} { ++ if (!base.empty()) { ++ path_ = base.AppendASCII("IpfsBlockCache"); ++ } ++ Start(); ++} ++void Self::Start() { ++ if (pending_) { ++ return; ++ } ++ auto result = dc::CreateCacheBackend( ++ net::CacheType::DISK_CACHE, net::CACHE_BACKEND_DEFAULT, {}, path_, 0, ++ dc::ResetHandling::kNeverReset, ++ // dc::ResetHandling::kResetOnError, ++ nullptr, base::BindOnce(&Self::Assign, base::Unretained(this))); ++ LOG(INFO) << "Start(" << result.net_error << ')' << result.net_error; ++ pending_ = result.net_error == net::ERR_IO_PENDING; ++ if (!pending_) { ++ Assign(std::move(result)); ++ } ++} ++Self::~CacheRequestor() noexcept = default; ++ ++void Self::Assign(dc::BackendResult res) { ++ pending_ = false; ++ if (res.net_error == net::OK) { ++ LOG(INFO) << "Initialized disk cache"; ++ cache_.swap(res.backend); ++ } else { ++ LOG(ERROR) << "Trouble opening " << name() << ": " << res.net_error; ++ Start(); ++ } ++} ++auto Self::handle(RequestPtr req) -> HandleOutcome { ++ if (pending_) { ++ return HandleOutcome::NOT_HANDLED; ++ } ++ Task task; ++ task.key = req->main_param; ++ task.request = req; ++ StartFetch(task, net::MAXIMUM_PRIORITY); ++ return HandleOutcome::PENDING; ++} ++void Self::StartFetch(Task& task, net::RequestPriority priority) { ++ if (pending_) { ++ Start(); ++ Miss(task); ++ return; ++ } ++ auto bound = base::BindOnce(&Self::OnOpen, base::Unretained(this), task); ++ auto res = cache_->OpenEntry(task.key, priority, std::move(bound)); ++ if (res.net_error() != net::ERR_IO_PENDING) { ++ OnOpen(task, std::move(res)); ++ } ++} ++void Self::Miss(Task& task) { ++ if (task.request) { ++ VLOG(2) << "Cache miss on " << task.request->debug_string(); ++ auto req = task.request; ++ task.request->Hook([this, req](std::string_view bytes) { ++ Store(req->main_param, "TODO", std::string{bytes}); ++ }); ++ forward(req); ++ } ++} ++namespace { ++std::shared_ptr GetEntry(dc::EntryResult& result) { ++ auto* e = result.ReleaseEntry(); ++ auto deleter = [](auto e) { ++ if (e) { ++ e->Close(); ++ } ++ }; ++ return {e, deleter}; ++} ++} // namespace ++ ++void Self::OnOpen(Task task, dc::EntryResult res) { ++ VLOG(2) << "OnOpen(" << res.net_error() << ")"; ++ if (res.net_error() != net::OK) { ++ VLOG(2) << "Failed to find " << task.key << " in " << name(); ++ Miss(task); ++ return; ++ } ++ task.entry = GetEntry(res); ++ DCHECK(task.entry); ++ task.buf = base::MakeRefCounted(2 * 1024 * 1024); ++ DCHECK(task.buf); ++ auto bound = ++ base::BindOnce(&Self::OnHeaderRead, base::Unretained(this), task); ++ auto code = task.entry->ReadData(0, 0, task.buf.get(), task.buf->size(), ++ std::move(bound)); ++ if (code != net::ERR_IO_PENDING) { ++ OnHeaderRead(task, code); ++ } ++} ++void Self::OnHeaderRead(Task task, int code) { ++ if (code <= 0) { ++ LOG(ERROR) << "Failed to read headers for entry " << task.key << " in " ++ << name() << " " << code; ++ // Miss(task); ++ // return; ++ } ++ task.header.assign(task.buf->data(), static_cast(code)); ++ auto bound = base::BindOnce(&Self::OnBodyRead, base::Unretained(this), task); ++ code = task.entry->ReadData(1, 0, task.buf.get(), task.buf->size(), ++ std::move(bound)); ++ if (code != net::ERR_IO_PENDING) { ++ OnBodyRead(task, code); ++ } ++} ++void Self::OnBodyRead(Task task, int code) { ++ if (code <= 0) { ++ LOG(INFO) << "Failed to read body for entry " << task.key << " in " ++ << name(); ++ Miss(task); ++ return; ++ } ++ task.body.assign(task.buf->data(), static_cast(code)); ++ if (task.request) { ++ task.SetHeaders(name()); ++ if (task.request->RespondSuccessfully(task.body, api_)) { ++ VLOG(2) << "Cache hit on " << task.key << " for " ++ << task.request->debug_string(); ++ } else { ++ LOG(ERROR) << "Had a BAD cached response for " << task.key; ++ Expire(task.key); ++ Miss(task); ++ } ++ } ++} ++void Self::Store(std::string cid, std::string headers, std::string body) { ++ VLOG(1) << "Store(" << name() << ',' << cid << ',' << headers.size() << ',' ++ << body.size() << ')'; ++ auto bound = base::BindOnce(&Self::OnEntryCreated, base::Unretained(this), ++ cid, headers, body); ++ auto res = cache_->OpenOrCreateEntry(cid, net::LOW, std::move(bound)); ++ if (res.net_error() != net::ERR_IO_PENDING) { ++ OnEntryCreated(cid, headers, body, std::move(res)); ++ } ++} ++void Self::OnEntryCreated(std::string cid, ++ std::string headers, ++ std::string body, ++ disk_cache::EntryResult result) { ++ if (result.opened()) { ++ VLOG(1) << "No need to write an entry for " << cid << " in " << name() ++ << " as it is already there and immutable."; ++ } else if (result.net_error() == net::OK) { ++ auto entry = GetEntry(result); ++ auto buf = base::MakeRefCounted(headers); ++ DCHECK(buf); ++ auto bound = base::BindOnce(&Self::OnHeaderWritten, base::Unretained(this), ++ buf, body, entry); ++ auto code = ++ entry->WriteData(0, 0, buf.get(), buf->size(), std::move(bound), true); ++ if (code != net::ERR_IO_PENDING) { ++ OnHeaderWritten(buf, body, entry, code); ++ } ++ } else { ++ LOG(ERROR) << "Failed to create an entry for " << cid << " in " << name(); ++ } ++} ++void Self::OnHeaderWritten(scoped_refptr buf, ++ std::string body, ++ std::shared_ptr entry, ++ int code) { ++ if (code < 0) { ++ LOG(ERROR) << "Failed to write header info for " << entry->GetKey() ++ << " in " << name(); ++ return; ++ } ++ buf = base::MakeRefCounted(body); ++ DCHECK(buf); ++ auto f = [](scoped_refptr, int c) { ++ VLOG(1) << "body write " << c; ++ }; ++ auto bound = base::BindOnce(f, buf); ++ entry->WriteData(1, 0, buf.get(), buf->size(), std::move(bound), true); ++} ++ ++void Self::Task::SetHeaders(std::string_view source) { ++ auto heads = base::MakeRefCounted(header); ++ DCHECK(heads); ++ std::string value{"blockcache-"}; ++ value.append(key); ++ value.append(";desc=\"Load from local browser block cache\";dur="); ++ auto dur = base::TimeTicks::Now() - start; ++ value.append(std::to_string(dur.InMillisecondsRoundedUp())); ++ heads->SetHeader("Server-Timing", value); ++ VLOG(2) << "From cache: Server-Timing: " << value << "; Block-Cache-" << key ++ << ": " << source; ++ heads->SetHeader("Block-Cache-" + key, {source.data(), source.size()}); ++ header = heads->raw_headers(); ++} ++void Self::Expire(std::string const& key) { ++ if (cache_ && !pending_) { ++ cache_->DoomEntry(key, net::RequestPriority::LOWEST, base::DoNothing()); ++ } ++} ++ ++Self::Task::Task() = default; ++Self::Task::Task(Task const&) = default; ++Self::Task::~Task() noexcept = default; +diff --git a/components/ipfs/cache_requestor.h b/components/ipfs/cache_requestor.h +new file mode 100644 +index 0000000000000..b8c31d371ecb4 +--- /dev/null ++++ b/components/ipfs/cache_requestor.h +@@ -0,0 +1,71 @@ ++#ifndef CACHE_REQUESTOR_H_ ++#define CACHE_REQUESTOR_H_ ++ ++#include ++#include ++#include ++ ++#include ++#include ++#include ++ ++#include ++ ++#include ++ ++namespace ipfs { ++ ++class BlockStorage; ++class InterRequestState; ++ ++class CacheRequestor : public gw::Requestor { ++ public: ++ CacheRequestor(InterRequestState&, base::FilePath); ++ ~CacheRequestor() noexcept override; ++ void Store(std::string cid, std::string headers, std::string body); ++ void Expire(std::string const& key); ++ ++ std::string_view name() const override; ++ ++ private: ++ struct Task { ++ Task(); ++ Task(Task const&); ++ ~Task() noexcept; ++ std::string key; ++ base::TimeTicks start = base::TimeTicks::Now(); ++ std::string header; ++ std::string body; ++ scoped_refptr buf; ++ std::shared_ptr entry; ++ gw::RequestPtr request; ++ ++ void SetHeaders(std::string_view); ++ }; ++ raw_ref state_; ++ std::unique_ptr cache_; ++ bool pending_ = false; ++ base::FilePath path_; ++ ++ void Start(); ++ ++ void StartFetch(Task& t, net::RequestPriority priority); ++ void Assign(disk_cache::BackendResult); ++ void OnOpen(Task, disk_cache::EntryResult); ++ void OnHeaderRead(Task, int); ++ void OnBodyRead(Task, int); ++ ++ void OnEntryCreated(std::string c, ++ std::string h, ++ std::string b, ++ disk_cache::EntryResult); ++ void OnHeaderWritten(scoped_refptr buf, ++ std::string body, ++ std::shared_ptr entry, ++ int); ++ void Miss(Task&); ++ HandleOutcome handle(RequestPtr) override; ++}; ++} // namespace ipfs ++ ++#endif // CACHE_REQUESTOR_H_ +diff --git a/components/ipfs/chromium_cbor_adapter.cc b/components/ipfs/chromium_cbor_adapter.cc +new file mode 100644 +index 0000000000000..d7d43b81be96b +--- /dev/null ++++ b/components/ipfs/chromium_cbor_adapter.cc +@@ -0,0 +1,91 @@ ++#include "chromium_cbor_adapter.h" ++ ++#include ++ ++using Self = ipfs::ChromiumCborAdapter; ++ ++bool Self::is_map() const { ++ return cbor_.is_map(); ++} ++bool Self::is_array() const { ++ return cbor_.is_array(); ++} ++auto Self::at(std::string_view key) const -> std::unique_ptr { ++ if (is_map()) { ++ auto& m = cbor_.GetMap(); ++ auto it = m.find(cbor::Value{base::StringPiece{key}}); ++ if (m.end() != it) { ++ return std::make_unique(it->second.Clone()); ++ } ++ } ++ return {}; ++} ++std::optional Self::as_unsigned() const { ++ if (cbor_.is_unsigned()) { ++ return cbor_.GetUnsigned(); ++ } ++ return std::nullopt; ++} ++std::optional Self::as_signed() const { ++ if (cbor_.is_integer()) { ++ return cbor_.GetInteger(); ++ } ++ return {}; ++} ++std::optional Self::as_float() const { ++ return {}; ++} ++ ++std::optional Self::as_string() const { ++ if (cbor_.is_string()) { ++ return cbor_.GetString(); ++ } ++ return std::nullopt; ++} ++auto Self::as_bytes() const -> std::optional> { ++ if (cbor_.is_bytestring()) { ++ return cbor_.GetBytestring(); ++ } ++ return std::nullopt; ++} ++auto Self::as_link() const -> std::optional { ++ VLOG(1) << "Trying to do an as_link(" << static_cast(cbor_.type()) << ',' << std::boolalpha << cbor_.has_tag() << ")"; ++ if (!cbor_.has_tag() || cbor_.GetTag() != 42UL || !cbor_.is_bytestring()) { ++ VLOG(1) << "This is not a link."; ++ return std::nullopt; ++ } ++ auto& bytes = cbor_.GetBytestring(); ++ auto* byte_ptr = reinterpret_cast(bytes.data()) + 1; ++ auto result = Cid(ByteView{byte_ptr, bytes.size() - 1UL}); ++ if (result.valid()) { ++ return result; ++ } else { ++ LOG(ERROR) << "Unable to decode bytes from DAG-CBOR Link as CID."; ++ return std::nullopt; ++ } ++} ++std::optional Self::as_bool() const { ++ if (cbor_.is_bool()) { ++ return cbor_.GetBool(); ++ } ++ return std::nullopt; ++} ++void Self::iterate_map(MapElementCallback cb) const { ++ auto& m = cbor_.GetMap(); ++ for (auto& [k,v] : m) { ++ cb(k.GetString(), Self{v}); ++ } ++} ++void Self::iterate_array(ArrayElementCallback cb) const { ++ auto& a = cbor_.GetArray(); ++ for (auto& e : a) { ++ cb(Self{e}); ++ } ++} ++ ++Self::ChromiumCborAdapter(cbor::Value const& v) : cbor_{v.Clone()} {} ++Self::ChromiumCborAdapter(cbor::Value&& v) : cbor_{std::move(v)} {} ++Self::ChromiumCborAdapter(ChromiumCborAdapter const& rhs) ++ : cbor_{rhs.cbor_.Clone()} {} ++ ++Self::~ChromiumCborAdapter() noexcept {} +diff --git a/components/ipfs/chromium_cbor_adapter.h b/components/ipfs/chromium_cbor_adapter.h +new file mode 100644 +index 0000000000000..65c2d746e6630 +--- /dev/null ++++ b/components/ipfs/chromium_cbor_adapter.h +@@ -0,0 +1,33 @@ ++#ifndef IPFS_CHROMIUM_CBOR_ADAPTER_H_ ++#define IPFS_CHROMIUM_CBOR_ADAPTER_H_ ++ ++#include ++ ++#include ++ ++namespace ipfs { ++class ChromiumCborAdapter final : public DagCborValue { ++ cbor::Value cbor_; ++ ++ std::unique_ptr at(std::string_view) const override; ++ std::optional as_unsigned() const override; ++ std::optional as_signed() const override; ++ std::optional as_float() const override; ++ std::optional as_string() const override; ++ std::optional> as_bytes() const override; ++ std::optional as_link() const override; ++ std::optional as_bool() const override; ++ bool is_map() const override; ++ bool is_array() const override; ++ void iterate_map(MapElementCallback) const override; ++ void iterate_array(ArrayElementCallback) const override; ++ ++ public: ++ ChromiumCborAdapter(cbor::Value&&); ++ ChromiumCborAdapter(cbor::Value const&); ++ ChromiumCborAdapter(ChromiumCborAdapter const& rhs); ++ ~ChromiumCborAdapter() noexcept override; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_CHROMIUM_CBOR_ADAPTER_H_ +diff --git a/components/ipfs/chromium_ipfs_context.cc b/components/ipfs/chromium_ipfs_context.cc +new file mode 100644 +index 0000000000000..9346fc1ec3bf9 +--- /dev/null ++++ b/components/ipfs/chromium_ipfs_context.cc +@@ -0,0 +1,125 @@ ++#include "chromium_ipfs_context.h" ++ ++#include "block_http_request.h" ++#include "chromium_cbor_adapter.h" ++#include "chromium_json_adapter.h" ++#include "crypto_api.h" ++#include "inter_request_state.h" ++#include "preferences.h" ++ ++#include ++#include ++#include ++ ++#include ++#include ++ ++#include ++ ++#include ++#include ++ ++#include ++ ++#include ++#include ++ ++ ++using Self = ipfs::ChromiumIpfsContext; ++ ++void Self::SetLoaderFactory(network::mojom::URLLoaderFactory& lf) { ++ loader_factory_ = &lf; ++} ++ ++std::string Self::MimeType(std::string extension, ++ std::string_view content, ++ std::string const& url) const { ++ std::string result; ++ auto fp_ext = base::FilePath::FromUTF8Unsafe(extension).value(); ++ VLOG(2) << "extension=" << extension << "content.size()=" << content.size() ++ << "(as-if) url for mime type:" << url; ++ if (extension.empty()) { ++ result.clear(); ++ } else if (net::GetWellKnownMimeTypeFromExtension(fp_ext, &result)) { ++ VLOG(2) << "Got " << result << " from extension " << extension << " for " ++ << url; ++ } else { ++ result.clear(); ++ } ++ auto head_size = std::min(content.size(), 999'999UL); ++ if (net::SniffMimeType({content.data(), head_size}, GURL{url}, result, ++ net::ForceSniffFileUrlsForHtml::kDisabled, &result)) { ++ VLOG(2) << "Got " << result << " from content of " << url; ++ } ++ if (result.empty() || result == "application/octet-stream") { ++ net::SniffMimeTypeFromLocalData({content.data(), head_size}, &result); ++ VLOG(2) << "Falling all the way back to content type " << result; ++ } ++ return result; ++} ++std::string Self::UnescapeUrlComponent(std::string_view comp) const { ++ using Rule = base::UnescapeRule; ++ auto rules = Rule::PATH_SEPARATORS | ++ Rule::URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS | Rule::SPACES; ++ auto result = base::UnescapeURLComponent({comp.data(), comp.size()}, rules); ++ return result; ++} ++void Self::SendDnsTextRequest(std::string host, ++ DnsTextResultsCallback res, ++ DnsTextCompleteCallback don) { ++ if (dns_reqs_.find(host) != dns_reqs_.end()) { ++ LOG(ERROR) << "Requested resolution of DNSLink host " << host ++ << " multiple times."; ++ } ++ auto don_wrap = [don, this, host]() { ++ don(); ++ LOG(INFO) << "Finished resolving " << host << " via DNSLink"; ++ dns_reqs_.erase(host); ++ }; ++ auto* nc = state_->network_context(); ++ dns_reqs_[host] = std::make_unique(host, res, don_wrap, nc); ++} ++void Self::SendHttpRequest(HttpRequestDescription req_inf, ++ HttpCompleteCallback cb) const { ++ DCHECK(loader_factory_); ++ auto ptr = std::make_shared(req_inf, cb); ++ ptr->send(loader_factory_); ++} ++bool Self::VerifyKeySignature(SigningKeyType t, ++ ByteView signature, ++ ByteView data, ++ ByteView key_bytes) const { ++ return crypto_api::VerifySignature(static_cast(t), signature, ++ data, key_bytes); ++} ++auto Self::ParseCbor(ipfs::ContextApi::ByteView bytes) const ++ -> std::unique_ptr { ++ cbor::Reader::Config cfg; ++ cfg.parse_tags = true; ++ auto parsed = cbor::Reader::Read(as_octets(bytes), cfg); ++ if (parsed.has_value()) { ++ return std::make_unique(std::move(parsed.value())); ++ } ++ LOG(ERROR) << "Failed to parse CBOR."; ++ return {}; ++} ++auto Self::ParseJson(std::string_view j_str) const ++ -> std::unique_ptr { ++ auto d = base::JSONReader::Read(j_str, base::JSON_ALLOW_TRAILING_COMMAS); ++ if (d) { ++ return std::make_unique(std::move(d.value())); ++ } ++ return {}; ++} ++ ++Self::ChromiumIpfsContext(InterRequestState& state, PrefService* prefs) ++ : state_{state} { ++ auto l = GetGatewayList(prefs); ++ for (auto gs : l) { ++ LOG(INFO) << "From pref: " << gs.prefix << '=' << gs.strength; ++ } ++} ++Self::~ChromiumIpfsContext() noexcept { ++ LOG(WARNING) << "API dtor - are all URIs loaded?"; ++} ++ +diff --git a/components/ipfs/chromium_ipfs_context.h b/components/ipfs/chromium_ipfs_context.h +new file mode 100644 +index 0000000000000..80f914a5394d8 +--- /dev/null ++++ b/components/ipfs/chromium_ipfs_context.h +@@ -0,0 +1,60 @@ ++#ifndef IPFS_CHROMIUM_IPFS_CONTEXT_H_ ++#define IPFS_CHROMIUM_IPFS_CONTEXT_H_ ++ ++#include "dns_txt_request.h" ++ ++#include ++#include ++ ++#include ++#include ++ ++#include ++ ++#include ++ ++class PrefService; ++ ++namespace network { ++class SimpleURLLoader; ++namespace mojom { ++class URLLoaderFactory; ++} ++} // namespace network ++ ++namespace ipfs { ++class InterRequestState; ++class IpfsRequest; ++class NetworkRequestor; ++ ++class ChromiumIpfsContext final : public ContextApi { ++ raw_ptr loader_factory_ = nullptr; ++ raw_ref state_; ++ std::map> dns_reqs_; ++ ++ std::string MimeType(std::string extension, ++ std::string_view content, ++ std::string const& url) const override; ++ std::string UnescapeUrlComponent(std::string_view) const override; ++ void SendDnsTextRequest(std::string, ++ DnsTextResultsCallback, ++ DnsTextCompleteCallback) override; ++ void SendHttpRequest(HttpRequestDescription req_inf, ++ HttpCompleteCallback cb) const override; ++ bool VerifyKeySignature(SigningKeyType, ++ ByteView signature, ++ ByteView data, ++ ByteView key_bytes) const override; ++ ++ std::unique_ptr ParseCbor(ByteView) const override; ++ std::unique_ptr ParseJson(std::string_view) const override; ++ ++ public: ++ ChromiumIpfsContext(InterRequestState&, PrefService* prefs); ++ ~ChromiumIpfsContext() noexcept override; ++ void SetLoaderFactory(network::mojom::URLLoaderFactory&); ++}; ++ ++} // namespace ipfs ++ ++#endif // IPFS_CHROMIUM_IPFS_CONTEXT_H_ +diff --git a/components/ipfs/chromium_json_adapter.cc b/components/ipfs/chromium_json_adapter.cc +new file mode 100644 +index 0000000000000..92c1c19aa35ce +--- /dev/null ++++ b/components/ipfs/chromium_json_adapter.cc +@@ -0,0 +1,48 @@ ++#include "chromium_json_adapter.h" ++ ++using Self = ipfs::ChromiumJsonAdapter; ++ ++Self::ChromiumJsonAdapter(base::Value d) : data_(std::move(d)) {} ++Self::~ChromiumJsonAdapter() noexcept {} ++std::string Self::pretty_print() const { ++ return data_.DebugString(); ++} ++std::optional Self::get_if_string() const { ++ auto* s = data_.GetIfString(); ++ if (s) { ++ return *s; ++ } else { ++ return std::nullopt; ++ } ++} ++auto Self::operator[](std::string_view k) const ++ -> std::unique_ptr { ++ if (auto* m = data_.GetIfDict()) { ++ if (auto* v = m->Find(k)) { ++ return std::make_unique(v->Clone()); ++ } ++ } ++ return {}; ++} ++bool Self::iterate_list(std::function cb) const { ++ auto* l = data_.GetIfList(); ++ if (!l) { ++ return false; ++ } ++ for (auto& v : *l) { ++ Self wrap(v.Clone()); ++ cb(wrap); ++ } ++ return true; ++} ++std::optional> Self::object_keys() const { ++ auto* m = data_.GetIfDict(); ++ if (!m) { ++ return std::nullopt; ++ } ++ std::vector rv; ++ for (auto [k, v] : *m) { ++ rv.push_back(k); ++ } ++ return rv; ++} +\ No newline at end of file +diff --git a/components/ipfs/chromium_json_adapter.h b/components/ipfs/chromium_json_adapter.h +new file mode 100644 +index 0000000000000..8e5e26aa3150e +--- /dev/null ++++ b/components/ipfs/chromium_json_adapter.h +@@ -0,0 +1,22 @@ ++#ifndef IPFS_CHROMIUM_JSON_ADAPTER_H_ ++#define IPFS_CHROMIUM_JSON_ADAPTER_H_ ++ ++#include ++#include ++ ++namespace ipfs { ++class ChromiumJsonAdapter final : public ipfs::DagJsonValue { ++ base::Value data_; ++ std::string pretty_print() const override; ++ std::unique_ptr operator[](std::string_view) const override; ++ std::optional get_if_string() const override; ++ std::optional> object_keys() const override; ++ bool iterate_list(std::function) const override; ++ ++ public: ++ ChromiumJsonAdapter(base::Value); ++ ~ChromiumJsonAdapter() noexcept override; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_CHROMIUM_JSON_ADAPTER_H_ +diff --git a/components/ipfs/crypto_api.cc b/components/ipfs/crypto_api.cc +new file mode 100644 +index 0000000000000..d15a63f1f577c +--- /dev/null ++++ b/components/ipfs/crypto_api.cc +@@ -0,0 +1,62 @@ ++#include "crypto_api.h" ++ ++#include "base/logging.h" ++#include "components/webcrypto/algorithm_implementations.h" ++#include "components/webcrypto/status.h" ++#include "third_party/blink/public/platform/web_crypto_key.h" ++#include "third_party/boringssl/src/include/openssl/evp.h" ++ ++namespace { ++int ToEvpKeyType(ipfs::ipns::KeyType t) { ++ using ipfs::ipns::KeyType; ++ switch (t) { ++ case KeyType::ECDSA: ++ LOG(ERROR) << "TODO Check on ECDSA key type translation."; ++ return EVP_PKEY_EC; ++ case KeyType::Ed25519: ++ return EVP_PKEY_ED25519; ++ case KeyType::RSA: ++ return EVP_PKEY_RSA; ++ case KeyType::Secp256k1: ++ LOG(ERROR) << "TODO Check on Secp256k1 key type translation."; ++ return EVP_PKEY_DSA; ++ default: ++ LOG(ERROR) << "Invalid key type: " << static_cast(t); ++ return EVP_PKEY_NONE; ++ } ++} ++} // namespace ++ ++namespace cpto = ipfs::crypto_api; ++ ++bool cpto::VerifySignature(ipfs::ipns::KeyType key_type, ++ ipfs::ByteView signature, ++ ipfs::ByteView data, ++ ipfs::ByteView key_bytes) { ++ auto* key_p = reinterpret_cast(key_bytes.data()); ++ auto* data_p = reinterpret_cast(data.data()); ++ auto* sig_p = reinterpret_cast(signature.data()); ++ auto kt = ToEvpKeyType(key_type); ++ std::clog << "data:"; ++ for (auto b : data) { ++ std::clog << ' ' << std::hex << static_cast(b); ++ } ++ std::clog << ' ' << data.size() << " bytes.\n"; ++ bssl::UniquePtr pkey(EVP_PKEY_new_raw_public_key( ++ kt, /*engine*/ nullptr, key_p, key_bytes.size())); ++ bssl::ScopedEVP_MD_CTX ctx; ++ if (!EVP_DigestVerifyInit(ctx.get(), /*pctx=*/nullptr, /*type=*/nullptr, ++ /*e=*/nullptr, pkey.get())) { ++ LOG(ERROR) << "EVP_DigestVerifyInit failed"; ++ return false; ++ } ++ // auto* prefix = reinterpret_cast( ++ // "\x69\x70\x6e\x73\x2d\x73\x69\x67\x6e\x61\x74\x75\x72\x65\x3a"); ++ // std::basic_string to_verify = prefix; ++ // to_verify.append(data_p, data.size()); ++ auto result = ++ EVP_DigestVerify(ctx.get(), sig_p, signature.size(), data_p, data.size()); ++ // to_verify.data(), to_verify.size()); ++ LOG(INFO) << "EVP_DigestVerify returned " << result; ++ return result == 1; ++} +diff --git a/components/ipfs/crypto_api.h b/components/ipfs/crypto_api.h +new file mode 100644 +index 0000000000000..1363bb1fec6df +--- /dev/null ++++ b/components/ipfs/crypto_api.h +@@ -0,0 +1,22 @@ ++#ifndef IPFS_VALIDATE_SIGNATURE_H_ ++#define IPFS_VALIDATE_SIGNATURE_H_ ++ ++#include "components/webcrypto/algorithm_implementation.h" ++ ++#include "third_party/ipfs_client/keys.pb.h" ++ ++#include ++ ++namespace ipfs::crypto_api { ++/* ++using Algo = std::pair>; ++Algo GetAlgo(ipfs::ipns::KeyType); ++*/ ++bool VerifySignature(ipfs::ipns::KeyType, ++ ByteView signature, ++ ByteView data, ++ ByteView key); ++} // namespace ipfs::crypto_api ++ ++#endif // IPFS_VALIDATE_SIGNATURE_H_ +diff --git a/components/ipfs/dns_txt_request.cc b/components/ipfs/dns_txt_request.cc +new file mode 100644 +index 0000000000000..c7e8e667d5f05 +--- /dev/null ++++ b/components/ipfs/dns_txt_request.cc +@@ -0,0 +1,39 @@ ++#include "dns_txt_request.h" ++ ++#include ++#include ++ ++namespace moj = network::mojom; ++using Self = ipfs::DnsTxtRequest; ++ ++Self::DnsTxtRequest(std::string host, ++ ipfs::ContextApi::DnsTextResultsCallback res, ++ ipfs::ContextApi::DnsTextCompleteCallback don, ++ moj::NetworkContext* network_context) ++ : results_callback_{res}, completion_callback_{don} { ++ auto params = moj::ResolveHostParameters::New(); ++ params->dns_query_type = net::DnsQueryType::TXT; ++ params->initial_priority = net::RequestPriority::HIGHEST; ++ params->source = net::HostResolverSource::ANY; ++ params->cache_usage = moj::ResolveHostParameters_CacheUsage::STALE_ALLOWED; ++ params->secure_dns_policy = moj::SecureDnsPolicy::ALLOW; ++ params->purpose = moj::ResolveHostParameters::Purpose::kUnspecified; ++ LOG(INFO) << "Querying DNS for TXT records on " << host; ++ auto hrh = moj::HostResolverHost::NewHostPortPair({host, 0}); ++ auto nak = net::NetworkAnonymizationKey::CreateTransient(); ++ network_context->ResolveHost(std::move(hrh), nak, std::move(params), ++ recv_.BindNewPipeAndPassRemote()); ++} ++Self::~DnsTxtRequest() {} ++ ++void Self::OnTextResults(std::vector const& results) { ++ LOG(INFO) << "Hit " << results.size() << " DNS TXT results."; ++ results_callback_(results); ++} ++void Self::OnComplete(int32_t result, ++ const ::net::ResolveErrorInfo&, ++ const absl::optional<::net::AddressList>&, ++ const absl::optional&) { ++ LOG(INFO) << "DNS Results done with code: " << result; ++ completion_callback_(); ++} +diff --git a/components/ipfs/dns_txt_request.h b/components/ipfs/dns_txt_request.h +new file mode 100644 +index 0000000000000..a6c72e467d11f +--- /dev/null ++++ b/components/ipfs/dns_txt_request.h +@@ -0,0 +1,36 @@ ++#ifndef IPFS_DNS_TXT_REQUEST_H_ ++#define IPFS_DNS_TXT_REQUEST_H_ ++ ++#include ++ ++#include ++#include ++ ++namespace network::mojom { ++class NetworkContext; ++} ++ ++namespace ipfs { ++class DnsTxtRequest final : public network::ResolveHostClientBase { ++ ipfs::ContextApi::DnsTextResultsCallback results_callback_; ++ ipfs::ContextApi::DnsTextCompleteCallback completion_callback_; ++ mojo::Receiver recv_{this}; ++ ++ using Endpoints = std::vector<::net::HostResolverEndpointResult>; ++ void OnTextResults(std::vector const&) override; ++ void OnComplete(int32_t result, ++ ::net::ResolveErrorInfo const&, ++ absl::optional<::net::AddressList> const&, ++ absl::optional const&) override; ++ ++ public: ++ DnsTxtRequest(std::string, ++ ipfs::ContextApi::DnsTextResultsCallback, ++ ipfs::ContextApi::DnsTextCompleteCallback, ++ network::mojom::NetworkContext*); ++ DnsTxtRequest(DnsTxtRequest&&) = delete; ++ ~DnsTxtRequest() noexcept override; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_DNS_TXT_REQUEST_H_ +diff --git a/components/ipfs/export.h b/components/ipfs/export.h +new file mode 100644 +index 0000000000000..8161da33aca16 +--- /dev/null ++++ b/components/ipfs/export.h +@@ -0,0 +1,16 @@ ++#ifndef IPFS_EXPORT_H_ ++#define IPFS_EXPORT_H_ ++ ++#if __has_include() ++#include ++#else ++ ++#ifndef IS_IPFS_IMPL ++#if !defined(COMPONENT_EXPORT) ++#define COMPONENT_EXPORT(IPFS) ++#endif ++#endif ++ ++#endif ++ ++#endif // IPFS_EXPORT_H_ +diff --git a/components/ipfs/inter_request_state.cc b/components/ipfs/inter_request_state.cc +new file mode 100644 +index 0000000000000..b20a013a9d646 +--- /dev/null ++++ b/components/ipfs/inter_request_state.cc +@@ -0,0 +1,72 @@ ++#include "inter_request_state.h" ++ ++#include "chromium_ipfs_context.h" ++#include "preferences.h" ++ ++#include ++#include "content/public/browser/browser_context.h" ++ ++#include ++#include ++#include ++#include ++ ++using Self = ipfs::InterRequestState; ++ ++namespace { ++constexpr char user_data_key[] = "ipfs_request_userdata"; ++} ++ ++void Self::CreateForBrowserContext(content::BrowserContext* c, PrefService* p) { ++ DCHECK(c); ++ DCHECK(p); ++ LOG(INFO) << "Creating new IPFS state for this browser context."; ++ auto owned = std::make_unique(c->GetPath(), p); ++ c->SetUserData(user_data_key, std::move(owned)); ++} ++auto Self::FromBrowserContext(content::BrowserContext* context) ++ -> InterRequestState& { ++ static ipfs::InterRequestState static_state({}, {}); ++ if (!context) { ++ LOG(WARNING) << "No browser context! Using a default IPFS state."; ++ return static_state; ++ } ++ base::SupportsUserData::Data* existing = context->GetUserData(user_data_key); ++ if (existing) { ++ VLOG(2) << "Re-using existing IPFS state."; ++ return *static_cast(existing); ++ } else { ++ LOG(ERROR) << "Browser context has no IPFS state! It must be set earlier!"; ++ return static_state; ++ } ++} ++std::shared_ptr Self::api() { ++ return api_; ++} ++auto Self::cache() -> std::shared_ptr& { ++ if (!cache_) { ++ cache_ = std::make_shared(*this, disk_path_); ++ } ++ return cache_; ++} ++auto Self::orchestrator() -> Orchestrator& { ++ if (!orc_) { ++ auto rtor = ++ gw::default_requestor(Gateways::DefaultGateways(), cache(), api()); ++ orc_ = std::make_shared(rtor, api()); ++ } ++ return *orc_; ++} ++void Self::network_context(network::mojom::NetworkContext* val) { ++ network_context_ = val; ++} ++network::mojom::NetworkContext* Self::network_context() const { ++ return network_context_; ++} ++Self::InterRequestState(base::FilePath p, PrefService* prefs) ++ : api_{std::make_shared(*this, prefs)}, ++ disk_path_{p} {} ++Self::~InterRequestState() noexcept { ++ network_context_ = nullptr; ++ cache_.reset(); ++} +diff --git a/components/ipfs/inter_request_state.h b/components/ipfs/inter_request_state.h +new file mode 100644 +index 0000000000000..3471cfe5d15c9 +--- /dev/null ++++ b/components/ipfs/inter_request_state.h +@@ -0,0 +1,51 @@ ++#ifndef IPFS_INTER_REQUEST_STATE_H_ ++#define IPFS_INTER_REQUEST_STATE_H_ ++ ++#include "cache_requestor.h" ++ ++#include "ipfs_client/gateways.h" ++#include "ipfs_client/ipns_names.h" ++#include "ipfs_client/orchestrator.h" ++ ++#include "base/supports_user_data.h" ++#include "services/network/network_context.h" ++ ++class PrefService; ++ ++namespace content { ++class BrowserContext; ++} ++ ++namespace ipfs { ++class Scheduler; ++class ChromiumIpfsContext; ++class COMPONENT_EXPORT(IPFS) InterRequestState ++ : public base::SupportsUserData::Data { ++ IpnsNames names_; ++ std::shared_ptr api_; ++ std::time_t last_discovery_ = 0; ++ std::shared_ptr cache_; ++ base::FilePath const disk_path_; ++ std::shared_ptr orc_; // TODO - map of origin to Orchestrator ++ raw_ptr network_context_; ++ ++ std::shared_ptr& cache(); ++ ++ public: ++ InterRequestState(base::FilePath, PrefService*); ++ ~InterRequestState() noexcept override; ++ ++ IpnsNames& names() { return names_; } ++ Scheduler& scheduler(); ++ std::shared_ptr api(); ++ std::array,2> serialized_caches(); ++ Orchestrator& orchestrator(); ++ void network_context(network::mojom::NetworkContext*); ++ network::mojom::NetworkContext* network_context() const; ++ ++ static void CreateForBrowserContext(content::BrowserContext*, PrefService*); ++ static InterRequestState& FromBrowserContext(content::BrowserContext*); ++}; ++} // namespace ipfs ++ ++#endif // IPFS_INTER_REQUEST_STATE_H_ +diff --git a/components/ipfs/interceptor.cc b/components/ipfs/interceptor.cc +new file mode 100644 +index 0000000000000..39b5de32b87ef +--- /dev/null ++++ b/components/ipfs/interceptor.cc +@@ -0,0 +1,36 @@ ++#include "interceptor.h" ++ ++#include "inter_request_state.h" ++#include "ipfs_url_loader.h" ++ ++#include "base/logging.h" ++#include "services/network/public/cpp/resource_request.h" ++#include "services/network/public/mojom/url_response_head.mojom.h" ++#include "services/network/url_loader_factory.h" ++#include "url/url_util.h" ++ ++using Interceptor = ipfs::Interceptor; ++ ++Interceptor::Interceptor(network::mojom::URLLoaderFactory* handles_http, ++ network::mojom::NetworkContext* network_context) ++ : loader_factory_{handles_http}, network_context_{network_context} {} ++ ++void Interceptor::MaybeCreateLoader(network::ResourceRequest const& req, ++ content::BrowserContext* context, ++ LoaderCallback loader_callback) { ++ auto& state = InterRequestState::FromBrowserContext(context); ++ state.network_context(network_context_); ++ if (req.url.SchemeIs("ipfs") || req.url.SchemeIs("ipns")) { ++ auto hdr_str = req.headers.ToString(); ++ std::replace(hdr_str.begin(), hdr_str.end(), '\r', ' '); ++ VLOG(1) << req.url.spec() << " getting intercepted! Headers: \n" << hdr_str; ++ DCHECK(context); ++ auto loader = ++ std::make_shared(*loader_factory_, state); ++ std::move(loader_callback) ++ .Run(base::BindOnce(&ipfs::IpfsUrlLoader::StartRequest, loader)); ++ ++ } else { ++ std::move(loader_callback).Run({}); // SEP ++ } ++} +diff --git a/components/ipfs/interceptor.h b/components/ipfs/interceptor.h +new file mode 100644 +index 0000000000000..0321ea5481864 +--- /dev/null ++++ b/components/ipfs/interceptor.h +@@ -0,0 +1,30 @@ ++#ifndef IPFS_INTERCEPTOR_H_ ++#define IPFS_INTERCEPTOR_H_ ++ ++#include "content/public/browser/url_loader_request_interceptor.h" ++ ++class PrefService; ++namespace network::mojom { ++class URLLoaderFactory; ++class NetworkContext; ++} // namespace network::mojom ++ ++namespace ipfs { ++ ++class COMPONENT_EXPORT(IPFS) Interceptor final ++ : public content::URLLoaderRequestInterceptor { ++ raw_ptr loader_factory_; ++ raw_ptr network_context_; ++ raw_ptr pref_svc_; ++ ++ void MaybeCreateLoader(network::ResourceRequest const&, ++ content::BrowserContext*, ++ LoaderCallback) override; ++ ++ public: ++ Interceptor(network::mojom::URLLoaderFactory* handles_http, ++ network::mojom::NetworkContext*); ++}; ++} // namespace ipfs ++ ++#endif // IPFS_INTERCEPTOR_H_ +diff --git a/components/ipfs/ipfs_features.cc b/components/ipfs/ipfs_features.cc +new file mode 100644 +index 0000000000000..a0a729d5aa8e6 +--- /dev/null ++++ b/components/ipfs/ipfs_features.cc +@@ -0,0 +1,7 @@ ++#include "ipfs_features.h" ++ ++namespace ipfs { ++ ++BASE_FEATURE(kEnableIpfs, "EnableIpfs", base::FEATURE_DISABLED_BY_DEFAULT); ++ ++} +diff --git a/components/ipfs/ipfs_features.h b/components/ipfs/ipfs_features.h +new file mode 100644 +index 0000000000000..2e54462b135a9 +--- /dev/null ++++ b/components/ipfs/ipfs_features.h +@@ -0,0 +1,13 @@ ++#ifndef IPFS_IPFS_FEATURES_H_ ++#define IPFS_IPFS_FEATURES_H_ ++ ++#include "base/component_export.h" ++#include "base/feature_list.h" ++ ++namespace ipfs { ++ ++COMPONENT_EXPORT(IPFS) BASE_DECLARE_FEATURE(kEnableIpfs); ++ ++} // namespace ipfs ++ ++#endif // IPFS_IPFS_FEATURES_H_ +diff --git a/components/ipfs/ipfs_url_loader.cc b/components/ipfs/ipfs_url_loader.cc +new file mode 100644 +index 0000000000000..afc97dc425b4f +--- /dev/null ++++ b/components/ipfs/ipfs_url_loader.cc +@@ -0,0 +1,194 @@ ++#include "ipfs_url_loader.h" ++ ++#include "chromium_ipfs_context.h" ++#include "inter_request_state.h" ++ ++#include "ipfs_client/gateways.h" ++#include "ipfs_client/ipfs_request.h" ++ ++#include "base/debug/stack_trace.h" ++#include "base/notreached.h" ++#include "base/strings/stringprintf.h" ++#include "base/threading/platform_thread.h" ++#include "net/http/http_status_code.h" ++#include "services/network/public/cpp/parsed_headers.h" ++#include "services/network/public/cpp/simple_url_loader.h" ++#include "services/network/public/mojom/url_loader_factory.mojom.h" ++#include "services/network/public/mojom/url_response_head.mojom.h" ++#include "services/network/url_loader_factory.h" ++ ++#include ++ ++ipfs::IpfsUrlLoader::IpfsUrlLoader( ++ network::mojom::URLLoaderFactory& handles_http, ++ InterRequestState& state) ++ : state_{state}, lower_loader_factory_{handles_http}, api_{state_->api()} {} ++ipfs::IpfsUrlLoader::~IpfsUrlLoader() noexcept { ++ if (!complete_) { ++ LOG(ERROR) << "Premature IPFS URLLoader dtor, uri was '" << original_url_ ++ << "' " << base::debug::StackTrace(); ++ } ++} ++ ++void ipfs::IpfsUrlLoader::FollowRedirect( ++ std::vector const& // removed_headers ++ , ++ net::HttpRequestHeaders const& // modified_headers ++ , ++ net::HttpRequestHeaders const& // modified_cors_exempt_headers ++ , ++ absl::optional<::GURL> const& // new_url ++) { ++ NOTIMPLEMENTED(); ++} ++ ++void ipfs::IpfsUrlLoader::SetPriority(net::RequestPriority priority, ++ int32_t intra_prio_val) { ++ VLOG(1) << "TODO SetPriority(" << priority << ',' << intra_prio_val << ')'; ++} ++ ++void ipfs::IpfsUrlLoader::PauseReadingBodyFromNet() { ++ NOTIMPLEMENTED(); ++} ++ ++void ipfs::IpfsUrlLoader::ResumeReadingBodyFromNet() { ++ NOTIMPLEMENTED(); ++} ++ ++void ipfs::IpfsUrlLoader::StartRequest( ++ std::shared_ptr me, ++ network::ResourceRequest const& resource_request, ++ mojo::PendingReceiver receiver, ++ mojo::PendingRemote client) { ++ DCHECK(!me->receiver_.is_bound()); ++ DCHECK(!me->client_.is_bound()); ++ me->receiver_.Bind(std::move(receiver)); ++ me->client_.Bind(std::move(client)); ++ if (me->original_url_.empty()) { ++ me->original_url_ = resource_request.url.spec(); ++ } ++ if (resource_request.url.SchemeIs("ipfs") || ++ resource_request.url.SchemeIs("ipns")) { ++ auto ns = resource_request.url.scheme(); ++ auto cid_str = resource_request.url.host(); ++ auto path = resource_request.url.path(); ++ auto abs_path = "/" + ns + "/" + cid_str + path; ++ VLOG(1) << resource_request.url.spec() << " -> " << abs_path; ++ me->root_ = cid_str; ++ me->api_->SetLoaderFactory(*(me->lower_loader_factory_)); ++ auto whendone = [me](IpfsRequest const& req, ipfs::Response const& res) { ++ VLOG(1) << "whendone(" << req.path().to_string() << ',' << res.status_ ++ << ',' << res.body_.size() << "B mime=" << res.mime_ << ')'; ++ if (!res.body_.empty()) { ++ me->ReceiveBlockBytes(res.body_); ++ } ++ me->status_ = res.status_; ++ me->resp_loc_ = res.location_; ++ if (res.status_ == Response::IMMUTABLY_GONE.status_) { ++ auto p = req.path(); ++ p.pop(); ++ std::string cid{p.pop()}; ++ me->DoesNotExist(cid, p.to_string()); ++ } else { ++ me->BlocksComplete(res.mime_); ++ } ++ DCHECK(me->complete_); ++ }; ++ auto req = std::make_shared(abs_path, whendone); ++ me->state_->orchestrator().build_response(req); ++ } else { ++ LOG(ERROR) << "Wrong scheme: " << resource_request.url.scheme(); ++ } ++} ++ ++void ipfs::IpfsUrlLoader::OverrideUrl(GURL u) { ++ original_url_ = u.spec(); ++} ++void ipfs::IpfsUrlLoader::AddHeader(std::string_view a, std::string_view b) { ++ VLOG(1) << "AddHeader(" << a << ',' << b << ')'; ++ additional_outgoing_headers_.emplace_back(a, b); ++} ++ ++void ipfs::IpfsUrlLoader::BlocksComplete(std::string mime_type) { ++ VLOG(1) << "Resolved from unix-fs dag a file of type: " << mime_type ++ << " will report it as " << original_url_; ++ if (complete_) { ++ return; ++ } ++ auto result = ++ mojo::CreateDataPipe(partial_block_.size(), pipe_prod_, pipe_cons_); ++ if (result) { ++ LOG(ERROR) << " ERROR: TaskFailed to create data pipe: " << result; ++ return; ++ } ++ complete_ = true; ++ auto head = network::mojom::URLResponseHead::New(); ++ if (mime_type.size()) { ++ head->mime_type = mime_type; ++ } ++ std::uint32_t byte_count = partial_block_.size(); ++ VLOG(1) << "Calling WriteData(" << byte_count << ")"; ++ pipe_prod_->WriteData(partial_block_.data(), &byte_count, ++ MOJO_BEGIN_WRITE_DATA_FLAG_ALL_OR_NONE); ++ VLOG(1) << "Called WriteData(" << byte_count << ")"; ++ head->content_length = byte_count; ++ head->headers = ++ net::HttpResponseHeaders::TryToCreate("access-control-allow-origin: *"); ++ if (resp_loc_.size()) { ++ head->headers->AddHeader("Location", resp_loc_); ++ } ++ if (!head->headers) { ++ LOG(ERROR) << "\n\tFailed to create headers!\n"; ++ return; ++ } ++ auto* reason = ++ net::GetHttpReasonPhrase(static_cast(status_)); ++ auto status_line = base::StringPrintf("HTTP/1.1 %d %s", status_, reason); ++ VLOG(1) << "Returning with status line '" << status_line << "'.\n"; ++ head->headers->ReplaceStatusLine(status_line); ++ if (mime_type.size()) { ++ head->headers->SetHeader("Content-Type", mime_type); ++ } ++ head->headers->SetHeader("Access-Control-Allow-Origin", "*"); ++ head->was_fetched_via_spdy = false; ++ for (auto& [n, v] : additional_outgoing_headers_) { ++ VLOG(1) << "Appending 'additional' header:" << n << '=' << v << '.'; ++ head->headers->AddHeader(n, v); ++ } ++ VLOG(1) << "Calling PopulateParsedHeaders"; ++ head->parsed_headers = ++ network::PopulateParsedHeaders(head->headers.get(), GURL{original_url_}); ++ VLOG(1) << "Sending response for " << original_url_ << " with mime type " ++ << head->mime_type << " and status line " << status_line; ++ if (status_ / 100 == 3 && resp_loc_.size()) { ++ auto ri = net::RedirectInfo::ComputeRedirectInfo( ++ "GET", GURL{original_url_}, net::SiteForCookies{}, ++ net::RedirectInfo::FirstPartyURLPolicy::UPDATE_URL_ON_REDIRECT, ++ net::ReferrerPolicy::NO_REFERRER, "", status_, GURL{resp_loc_}, ++ std::nullopt, false); ++ client_->OnReceiveRedirect(ri, std::move(head)); ++ } else { ++ client_->OnReceiveResponse(std::move(head), std::move(pipe_cons_), ++ absl::nullopt); ++ } ++ client_->OnComplete(network::URLLoaderCompletionStatus{}); ++ stepper_.reset(); ++} ++ ++void ipfs::IpfsUrlLoader::DoesNotExist(std::string_view cid, ++ std::string_view path) { ++ LOG(ERROR) << "Immutable data 404 for " << cid << '/' << path; ++ complete_ = true; ++ client_->OnComplete( ++ network::URLLoaderCompletionStatus{net::ERR_FILE_NOT_FOUND}); ++ stepper_.reset(); ++} ++void ipfs::IpfsUrlLoader::NotHere(std::string_view cid, std::string_view path) { ++ LOG(INFO) << "TODO " << __func__ << '(' << cid << ',' << path << ')'; ++} ++ ++void ipfs::IpfsUrlLoader::ReceiveBlockBytes(std::string_view content) { ++ partial_block_.append(content); ++ VLOG(2) << "Recived a block of size " << content.size() << " now have " ++ << partial_block_.size() << " bytes."; ++} +diff --git a/components/ipfs/ipfs_url_loader.h b/components/ipfs/ipfs_url_loader.h +new file mode 100644 +index 0000000000000..dc324f7b11f2d +--- /dev/null ++++ b/components/ipfs/ipfs_url_loader.h +@@ -0,0 +1,96 @@ ++#ifndef COMPONENTS_IPFS_URL_LOADER_H_ ++#define COMPONENTS_IPFS_URL_LOADER_H_ 1 ++ ++#include "base/debug/debugging_buildflags.h" ++#include "base/timer/timer.h" ++#include "mojo/public/cpp/bindings/receiver_set.h" ++#include "mojo/public/cpp/system/data_pipe.h" ++#include "net/http/http_request_headers.h" ++#include "services/network/public/cpp/resolve_host_client_base.h" ++#include "services/network/public/cpp/resource_request.h" ++#include "services/network/public/mojom/url_loader.mojom.h" ++ ++#include ++ ++namespace ipfs { ++class ChromiumIpfsContext; ++} // namespace ipfs ++ ++namespace network::mojom { ++class URLLoaderFactory; ++class HostResolver; ++class NetworkContext; ++} // namespace network::mojom ++namespace network { ++class SimpleURLLoader; ++} ++ ++namespace ipfs { ++class InterRequestState; ++ ++class IpfsUrlLoader final : public network::mojom::URLLoader { ++ void FollowRedirect( ++ std::vector const& removed_headers, ++ net::HttpRequestHeaders const& modified_headers, ++ net::HttpRequestHeaders const& modified_cors_exempt_headers, ++ absl::optional<::GURL> const& new_url) override; ++ void SetPriority(net::RequestPriority priority, ++ int32_t intra_priority_value) override; ++ void PauseReadingBodyFromNet() override; ++ void ResumeReadingBodyFromNet() override; ++ ++ public: ++ explicit IpfsUrlLoader(network::mojom::URLLoaderFactory& handles_http, ++ InterRequestState& state); ++ ~IpfsUrlLoader() noexcept override; ++ ++ using ptr = std::shared_ptr; ++ ++ // Passed as the RequestHandler for ++ // Interceptor::MaybeCreateLoader. ++ static void StartRequest( ++ ptr, ++ network::ResourceRequest const& resource_request, ++ mojo::PendingReceiver receiver, ++ mojo::PendingRemote client); ++ ++ void OverrideUrl(GURL); ++ void AddHeader(std::string_view,std::string_view); ++ void extra(std::shared_ptr xtra) { extra_ = xtra; } ++ ++ private: ++ using RequestHandle = std::unique_ptr; ++ ++ raw_ref state_; ++ mojo::Receiver receiver_{this}; ++ mojo::Remote client_; ++ raw_ref lower_loader_factory_; ++ mojo::ScopedDataPipeProducerHandle pipe_prod_ = {}; ++ mojo::ScopedDataPipeConsumerHandle pipe_cons_ = {}; ++ bool complete_ = false; ++ std::shared_ptr api_; ++ std::string original_url_; ++ std::string partial_block_; ++ std::vector> additional_outgoing_headers_; ++ std::shared_ptr extra_; ++ std::unique_ptr stepper_; ++ std::string root_; ++ int status_ = 200; ++ std::string resp_loc_; ++ ++ void CreateBlockRequest(std::string cid); ++ ++ void ReceiveBlockBytes(std::string_view); ++ void BlocksComplete(std::string mime_type); ++ void DoesNotExist(std::string_view cid, std::string_view path); ++ void NotHere(std::string_view cid, std::string_view path); ++ ++ void StartUnixFsProc(ptr, std::string_view); ++ void AppendGatewayHeaders(std::vector const& cids, net::HttpResponseHeaders&); ++ void AppendGatewayInfoHeader(std::string const&, net::HttpResponseHeaders&); ++ void TakeStep(); ++}; ++ ++} // namespace ipfs ++ ++#endif +diff --git a/components/ipfs/url_loader_factory.cc b/components/ipfs/url_loader_factory.cc +new file mode 100644 +index 0000000000000..9a80284098748 +--- /dev/null ++++ b/components/ipfs/url_loader_factory.cc +@@ -0,0 +1,56 @@ ++#include "url_loader_factory.h" ++ ++#include "inter_request_state.h" ++#include "ipfs_url_loader.h" ++ ++void ipfs::IpfsURLLoaderFactory::Create( ++ NonNetworkURLLoaderFactoryMap* in_out, ++ content::BrowserContext* context, ++ URLLoaderFactory* default_factory, ++ network::mojom::NetworkContext* net_ctxt, ++ PrefService* pref_svc) { ++ for (char const* scheme : {"ipfs", "ipns"}) { ++ mojo::PendingRemote pending; ++ new IpfsURLLoaderFactory(scheme, pending.InitWithNewPipeAndPassReceiver(), ++ context, default_factory, net_ctxt, pref_svc); ++ in_out->emplace(scheme, std::move(pending)); ++ } ++} ++ ++ipfs::IpfsURLLoaderFactory::IpfsURLLoaderFactory( ++ std::string scheme, ++ mojo::PendingReceiver factory_receiver, ++ content::BrowserContext* context, ++ URLLoaderFactory* default_factory, ++ network::mojom::NetworkContext* net_ctxt, ++ PrefService* pref_svc) ++ : network::SelfDeletingURLLoaderFactory(std::move(factory_receiver)), ++ scheme_{scheme}, ++ context_{context}, ++ default_factory_{default_factory}, ++ network_context_{net_ctxt}, ++ pref_svc_{pref_svc} {} ++ ++ipfs::IpfsURLLoaderFactory::~IpfsURLLoaderFactory() noexcept { ++ context_ = nullptr; ++ default_factory_ = nullptr; ++ network_context_ = nullptr; ++} ++ ++void ipfs::IpfsURLLoaderFactory::CreateLoaderAndStart( ++ mojo::PendingReceiver loader, ++ int32_t /*request_id*/, ++ uint32_t /*options*/, ++ network::ResourceRequest const& request, ++ mojo::PendingRemote client, ++ net::MutableNetworkTrafficAnnotationTag const& // traffic_annotation ++) { ++ VLOG(2) << "IPFS subresource: case=" << scheme_ ++ << " url=" << request.url.spec(); ++ DCHECK(default_factory_); ++ if (scheme_ == "ipfs" || scheme_ == "ipns") { ++ auto ptr = std::make_shared( ++ *default_factory_, InterRequestState::FromBrowserContext(context_)); ++ ptr->StartRequest(ptr, request, std::move(loader), std::move(client)); ++ } ++} +diff --git a/components/ipfs/url_loader_factory.h b/components/ipfs/url_loader_factory.h +new file mode 100644 +index 0000000000000..01cd66ea6ed8f +--- /dev/null ++++ b/components/ipfs/url_loader_factory.h +@@ -0,0 +1,58 @@ ++#ifndef IPFS_URL_LOADER_FACTORY_H_ ++#define IPFS_URL_LOADER_FACTORY_H_ ++ ++#include "services/network/public/cpp/self_deleting_url_loader_factory.h" ++#include "services/network/public/mojom/url_loader_factory.mojom.h" ++ ++#include ++ ++class PrefService; ++namespace content { ++class BrowserContext; ++} ++namespace network { ++namespace mojom { ++class NetworkContext; ++} ++} // namespace network ++ ++namespace ipfs { ++using NonNetworkURLLoaderFactoryMap = ++ std::map>; ++ ++class COMPONENT_EXPORT(IPFS) IpfsURLLoaderFactory ++ : public network::SelfDeletingURLLoaderFactory { ++ public: ++ static void Create(NonNetworkURLLoaderFactoryMap* in_out, ++ content::BrowserContext*, ++ URLLoaderFactory*, ++ network::mojom::NetworkContext*, ++ PrefService*); ++ ++ private: ++ IpfsURLLoaderFactory(std::string, ++ mojo::PendingReceiver, ++ content::BrowserContext*, ++ network::mojom::URLLoaderFactory*, ++ network::mojom::NetworkContext*, ++ PrefService*); ++ ~IpfsURLLoaderFactory() noexcept override; ++ void CreateLoaderAndStart( ++ mojo::PendingReceiver loader, ++ int32_t request_id, ++ uint32_t options, ++ network::ResourceRequest const& request, ++ mojo::PendingRemote client, ++ net::MutableNetworkTrafficAnnotationTag const& traffic_annotation) ++ override; ++ ++ std::string scheme_; ++ raw_ptr context_; ++ raw_ptr default_factory_; ++ raw_ptr network_context_; ++ raw_ptr pref_svc_; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_URL_LOADER_FACTORY_H_ +diff --git a/components/open_from_clipboard/clipboard_recent_content_generic.cc b/components/open_from_clipboard/clipboard_recent_content_generic.cc +index 4dcafecbc66c6..d205209c08162 100644 +--- a/components/open_from_clipboard/clipboard_recent_content_generic.cc ++++ b/components/open_from_clipboard/clipboard_recent_content_generic.cc +@@ -20,7 +20,7 @@ + namespace { + // Schemes appropriate for suggestion by ClipboardRecentContent. + const char* kAuthorizedSchemes[] = { +- url::kAboutScheme, url::kDataScheme, url::kHttpScheme, url::kHttpsScheme, ++ url::kAboutScheme, url::kDataScheme, url::kHttpScheme, url::kHttpsScheme, "ipfs", "ipns" + // TODO(mpearson): add support for chrome:// URLs. Right now the scheme + // for that lives in content and is accessible via + // GetEmbedderRepresentationOfAboutScheme() or content::kChromeUIScheme +diff --git a/net/dns/dns_config_service_linux.cc b/net/dns/dns_config_service_linux.cc +index 5273da5190277..12b28b86a4c00 100644 +--- a/net/dns/dns_config_service_linux.cc ++++ b/net/dns/dns_config_service_linux.cc +@@ -272,11 +272,11 @@ bool IsNsswitchConfigCompatible( + // Ignore any entries after `kDns` because Chrome will fallback to the + // system resolver if a result was not found in DNS. + return true; +- ++ case NsswitchReader::Service::kResolve: ++ break; + case NsswitchReader::Service::kMdns: + case NsswitchReader::Service::kMdns4: + case NsswitchReader::Service::kMdns6: +- case NsswitchReader::Service::kResolve: + case NsswitchReader::Service::kNis: + RecordIncompatibleNsswitchReason( + IncompatibleNsswitchReason::kIncompatibleService, +diff --git a/third_party/blink/renderer/platform/weborigin/scheme_registry.cc b/third_party/blink/renderer/platform/weborigin/scheme_registry.cc +index 4eadf46ea0c24..d62fc7fb14e01 100644 +--- a/third_party/blink/renderer/platform/weborigin/scheme_registry.cc ++++ b/third_party/blink/renderer/platform/weborigin/scheme_registry.cc +@@ -67,7 +67,7 @@ class URLSchemesRegistry final { + // is considered secure. Additional checks are performed to ensure that + // other http pages are filtered out. + service_worker_schemes({"http", "https"}), +- fetch_api_schemes({"http", "https"}), ++ fetch_api_schemes({"http", "https", "ipfs", "ipns"}), + allowed_in_referrer_schemes({"http", "https"}) { + for (auto& scheme : url::GetCorsEnabledSchemes()) + cors_enabled_schemes.insert(scheme.c_str()); +diff --git a/third_party/ipfs_client/BUILD.gn b/third_party/ipfs_client/BUILD.gn +new file mode 100644 +index 0000000000000..9eb6d56505851 +--- /dev/null ++++ b/third_party/ipfs_client/BUILD.gn +@@ -0,0 +1,200 @@ ++import("args.gni") ++import("//build/buildflag_header.gni") ++ ++buildflag_header("ipfs_buildflags") { ++ header = "ipfs_buildflags.h" ++ flags = [ "ENABLE_IPFS=$enable_ipfs" ] ++} ++ ++config("external_config") { ++ include_dirs = [ ++ "include", ++ ] ++} ++ ++if (enable_ipfs) { ++ cxx_sources = [ ++ "include/ipfs_client/block_requestor.h", ++ "include/ipfs_client/block_storage.h", ++ "include/ipfs_client/cid.h", ++ "include/ipfs_client/context_api.h", ++ "include/ipfs_client/crypto/hasher.h", ++ "include/ipfs_client/dag_cbor_value.h", ++ "include/ipfs_client/dag_json_value.h", ++ "include/ipfs_client/gateways.h", ++ "include/ipfs_client/gw/block_request_splitter.h", ++ "include/ipfs_client/gw/default_requestor.h", ++ "include/ipfs_client/gw/dnslink_requestor.h", ++ "include/ipfs_client/gw/gateway_request.h", ++ "include/ipfs_client/gw/inline_request_handler.h", ++ "include/ipfs_client/gw/requestor.h", ++ "include/ipfs_client/gw/terminating_requestor.h", ++ "include/ipfs_client/http_request_description.h", ++ "include/ipfs_client/identity_cid.h", ++ "include/ipfs_client/ipfs_request.h", ++ "include/ipfs_client/ipld/dag_node.h", ++ "include/ipfs_client/ipld/link.h", ++ "include/ipfs_client/ipld/resolution_state.h", ++ "include/ipfs_client/ipns_cbor_entry.h", ++ "include/ipfs_client/ipns_names.h", ++ "include/ipfs_client/ipns_record.h", ++ "include/ipfs_client/json_cbor_adapter.h", ++ "include/ipfs_client/logger.h", ++ "include/ipfs_client/multi_base.h", ++ "include/ipfs_client/multi_hash.h", ++ "include/ipfs_client/multicodec.h", ++ "include/ipfs_client/orchestrator.h", ++ "include/ipfs_client/pb_dag.h", ++ "include/ipfs_client/response.h", ++ "include/ipfs_client/signing_key_type.h", ++ "include/ipfs_client/url_spec.h", ++ "include/libp2p/common/types.hpp", ++ "include/libp2p/crypto/key.h", ++ "include/libp2p/crypto/protobuf/protobuf_key.hpp", ++ "include/libp2p/multi/multibase_codec.hpp", ++ "include/libp2p/multi/multibase_codec/codecs/base16.h", ++ "include/libp2p/multi/multibase_codec/codecs/base32.hpp", ++ "include/libp2p/multi/multibase_codec/codecs/base36.hpp", ++ "include/libp2p/multi/multibase_codec/codecs/base_error.hpp", ++ "include/libp2p/multi/multicodec_type.hpp", ++ "include/libp2p/multi/uvarint.hpp", ++ "include/multibase/algorithm.h", ++ "include/multibase/basic_algorithm.h", ++ "include/multibase/encoding.h", ++ "include/smhasher/MurmurHash3.h", ++ "include/vocab/byte.h", ++ "include/vocab/byte_view.h", ++ "include/vocab/endian.h", ++ "include/vocab/expected.h", ++ "include/vocab/flat_mapset.h", ++ "include/vocab/html_escape.h", ++ "include/vocab/i128.h", ++ "include/vocab/raw_ptr.h", ++ "include/vocab/slash_delimited.h", ++ "include/vocab/span.h", ++ "include/vocab/stringify.h", ++ "src/ipfs_client/bases/b16_upper.h", ++ "src/ipfs_client/bases/b32.h", ++ "src/ipfs_client/block_requestor.cc", ++ "src/ipfs_client/car.cc", ++ "src/ipfs_client/car.h", ++ "src/ipfs_client/cid.cc", ++ "src/ipfs_client/context_api.cc", ++ "src/ipfs_client/crypto/openssl_sha2_256.cc", ++ "src/ipfs_client/crypto/openssl_sha2_256.h", ++ "src/ipfs_client/dag_cbor_value.cc", ++ "src/ipfs_client/dag_json_value.cc", ++ "src/ipfs_client/gateways.cc", ++ "src/ipfs_client/generated_directory_listing.cc", ++ "src/ipfs_client/generated_directory_listing.h", ++ "src/ipfs_client/gw/block_request_splitter.cc", ++ "src/ipfs_client/gw/default_requestor.cc", ++ "src/ipfs_client/gw/dnslink_requestor.cc", ++ "src/ipfs_client/gw/gateway_http_requestor.cc", ++ "src/ipfs_client/gw/gateway_http_requestor.h", ++ "src/ipfs_client/gw/gateway_request.cc", ++ "src/ipfs_client/gw/inline_request_handler.cc", ++ "src/ipfs_client/gw/requestor.cc", ++ "src/ipfs_client/gw/requestor_pool.cc", ++ "src/ipfs_client/gw/requestor_pool.h", ++ "src/ipfs_client/gw/terminating_requestor.cc", ++ "src/ipfs_client/http_request_description.cc", ++ "src/ipfs_client/identity_cid.cc", ++ "src/ipfs_client/ipfs_request.cc", ++ "src/ipfs_client/ipld/chunk.cc", ++ "src/ipfs_client/ipld/chunk.h", ++ "src/ipfs_client/ipld/dag_cbor_node.cc", ++ "src/ipfs_client/ipld/dag_cbor_node.h", ++ "src/ipfs_client/ipld/dag_json_node.cc", ++ "src/ipfs_client/ipld/dag_json_node.h", ++ "src/ipfs_client/ipld/dag_node.cc", ++ "src/ipfs_client/ipld/directory_shard.cc", ++ "src/ipfs_client/ipld/directory_shard.h", ++ "src/ipfs_client/ipld/ipns_name.cc", ++ "src/ipfs_client/ipld/ipns_name.h", ++ "src/ipfs_client/ipld/link.cc", ++ "src/ipfs_client/ipld/resolution_state.cc", ++ "src/ipfs_client/ipld/root.cc", ++ "src/ipfs_client/ipld/root.h", ++ "src/ipfs_client/ipld/small_directory.cc", ++ "src/ipfs_client/ipld/small_directory.h", ++ "src/ipfs_client/ipld/symlink.cc", ++ "src/ipfs_client/ipld/symlink.h", ++ "src/ipfs_client/ipld/unixfs_file.cc", ++ "src/ipfs_client/ipld/unixfs_file.h", ++ "src/ipfs_client/ipns_names.cc", ++ "src/ipfs_client/ipns_record.cc", ++ "src/ipfs_client/logger.cc", ++ "src/ipfs_client/multi_base.cc", ++ "src/ipfs_client/multi_hash.cc", ++ "src/ipfs_client/multicodec.cc", ++ "src/ipfs_client/orchestrator.cc", ++ "src/ipfs_client/path2url.cc", ++ "src/ipfs_client/path2url.h", ++ "src/ipfs_client/pb_dag.cc", ++ "src/ipfs_client/redirects.cc", ++ "src/ipfs_client/redirects.h", ++ "src/ipfs_client/response.cc", ++ "src/ipfs_client/signing_key_type.cc", ++ "src/libp2p/crypto/protobuf_key.hpp", ++ "src/libp2p/multi/multibase_codec/codecs/base16.cc", ++ "src/libp2p/multi/multibase_codec/codecs/base32.cc", ++ "src/libp2p/multi/multibase_codec/codecs/base36.cc", ++ "src/libp2p/multi/uvarint.cc", ++ "src/log_macros.h", ++ "src/smhasher/MurmurHash3.cc", ++ "src/vocab/byte_view.cc", ++ "src/vocab/slash_delimited.cc", ++ ] ++ static_library("ipfs_client") { ++ if (is_nacl) { ++ sources = cxx_sources - [ ++ "src/ipfs_client/dag_block.cc", ++ "src/ipfs_client/gw/gateway_request.cc", ++ "src/ipfs_client/gw/gateway_http_requestor.cc", ++ "src/ipfs_client/gw/requestor.cc", ++ "src/ipfs_client/ipld/dag_node.cc", ++ "src/ipfs_client/ipns_names.cc", ++ "src/ipfs_client/ipns_record.cc", ++ "src/ipfs_client/logger.cc", ++ "src/ipfs_client/signing_key_type.cc", ++ ] ++ } else { ++ sources = cxx_sources ++ } ++ include_dirs = [ ++ "include", ++ "src", ++ "..", ++ "../boringssl/src/include" ++ ] ++ public_configs = [ ++ ":external_config" ++ ] ++ public_deps = [ ++ "//third_party/abseil-cpp:absl", ++ "//base", ++ ] ++ deps = [ ++ "//third_party/abseil-cpp:absl", ++ "//base", ++ ] ++ if (!is_nacl) { ++ public_deps += [ ++ ":protos", ++ "//third_party/protobuf:protobuf_lite", ++ ] ++ } ++ } ++} ++ ++import("//third_party/protobuf/proto_library.gni") ++ ++proto_library("protos") { ++ sources = [ ++ "ipns_record.proto", ++ "keys.proto", ++ "pb_dag.proto", ++ "unix_fs.proto", ++ ] ++} +diff --git a/third_party/ipfs_client/README.chromium b/third_party/ipfs_client/README.chromium +new file mode 100644 +index 0000000000000..e69de29bb2d1d +diff --git a/third_party/ipfs_client/README.md b/third_party/ipfs_client/README.md +new file mode 100644 +index 0000000000000..0e6ffadd2ebbc +--- /dev/null ++++ b/third_party/ipfs_client/README.md +@@ -0,0 +1,6 @@ ++# ipfs-client ++ ++## TODO ++ ++Need to fill out this README to explain how to use ipfs-client in other contexts. ++ +diff --git a/third_party/ipfs_client/args.gni b/third_party/ipfs_client/args.gni +new file mode 100644 +index 0000000000000..bb13519b23e89 +--- /dev/null ++++ b/third_party/ipfs_client/args.gni +@@ -0,0 +1,3 @@ ++declare_args() { ++ enable_ipfs = false ++} +diff --git a/third_party/ipfs_client/conanfile.py b/third_party/ipfs_client/conanfile.py +new file mode 100644 +index 0000000000000..289e3b48f8ad1 +--- /dev/null ++++ b/third_party/ipfs_client/conanfile.py +@@ -0,0 +1,79 @@ ++from conan import ConanFile ++from conan.tools.cmake import CMake, CMakeToolchain, CMakeDeps, cmake_layout ++from shutil import copyfile, which ++import sys ++from os.path import dirname, isfile, join, realpath ++ ++here = realpath(dirname(__file__)) ++sys.path.append(realpath(join(here, '..', 'cmake'))) ++sys.path.append(here) ++ ++try: ++ import version ++ VERSION = version.deduce() ++except ImportError: ++ VERSION = open(join(here,'version.txt'), 'r').read().strip() ++ ++ ++class IpfsChromium(ConanFile): ++ name = "ipfs_client" ++ version = VERSION ++ settings = "os", "compiler", "build_type", "arch" ++ # generators = "CMakeDeps", 'CMakeToolchain' ++ _PB = 'protobuf/3.20.0' ++ require_transitively = [ ++ 'abseil/20230125.3', ++ 'boost/1.81.0', ++ 'bzip2/1.0.8', ++ 'c-ares/1.22.1', ++ 'nlohmann_json/3.11.2', ++ 'openssl/1.1.1t', ++ _PB, ++ ] ++ # default_options = {"boost/*:header_only": True} ++ default_options = { ++ "boost/*:bzip2": True, ++ "boost/*:with_stacktrace_backtrace": True ++ } ++ tool_requires = [ ++ 'cmake/3.22.6', ++ 'ninja/1.11.1', ++ _PB, ++ ] ++ extensions = ['h', 'cc', 'hpp', 'proto'] ++ exports_sources = [ '*.txt' ] + [f'**/*.{e}' for e in extensions] ++ exports = 'version.txt' ++ package_type = 'static-library' ++ ++ ++ def generate(self): ++ tc = CMakeToolchain(self, 'Ninja') ++ tc.generate() ++ d = CMakeDeps(self) ++ d.generate() ++ ++ def build(self): ++ cmake = CMake(self) ++ cmake.configure(variables={ ++ "CXX_VERSION": 20, ++ "INSIDE_CONAN": True ++ }) ++ cmake.build(build_tool_args=['--verbose']) ++ ++ def package(self): ++ cmake = CMake(self) ++ cmake.install() ++ print(self.cpp_info.objects) ++ ++ def package_info(self): ++ self.cpp_info.libs = ["ipfs_client"] ++ ++ def build_requirements(self): ++ if not which("doxygen"): ++ self.tool_requires("doxygen/1.9.4") ++ def layout(self): ++ cmake_layout(self) ++ ++ def requirements(self): ++ for l in self.require_transitively: ++ self.requires(l, transitive_headers=True) +diff --git a/third_party/ipfs_client/include/ipfs_client/block_requestor.h b/third_party/ipfs_client/include/ipfs_client/block_requestor.h +new file mode 100644 +index 0000000000000..42ae26e519760 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/block_requestor.h +@@ -0,0 +1,48 @@ ++#ifndef BLOCK_REQUESTOR_H_ ++#define BLOCK_REQUESTOR_H_ ++ ++#include ++ ++#include ++#include ++#include ++ ++namespace ipfs { ++ ++/*! ++ * \brief The urgency of a gateway request ++ * \details Determines how many gateways should be involved, and how burdened a ++ * gateway should be before not also taking this one on concurrently. Zero is ++ * a special value that indicates the block isn't actually required now, but ++ * rather might be required soonish (prefetch). There are some cases of ++ * special handling for that. ++ */ ++using Priority = std::uint_least16_t; ++ ++class DagListener; ++ ++/*! ++ * \brief Interface for classes that can asynchronously fetch a block for a CID ++ * \details This is one of the interfaces using code is meant to implement. ++ * Common usages: ++ * * A class that requests blocks from gateways ++ * * A cache that must act asynchronously (perhaps on-disk) ++ * * ChainedRequestors : a chain-of-responsibility combining multiple ++ */ ++class BlockRequestor { ++ public: ++ /** ++ * \brief Request a single block from gateway(s). ++ * \param cid - MB-MH string representation of the Content IDentifier ++ * \param dl - Someone who may be interested ++ * \param priority - Urgency of the request ++ * \note The DagListener is mostly about lifetime extension, since it's ++ * waiting on something which is waiting on this ++ */ ++ virtual void RequestByCid(std::string cid, ++ std::shared_ptr dl, ++ Priority priority) = 0; ++}; ++} // namespace ipfs ++ ++#endif // BLOCK_REQUESTOR_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/block_storage.h b/third_party/ipfs_client/include/ipfs_client/block_storage.h +new file mode 100644 +index 0000000000000..525bae463f50d +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/block_storage.h +@@ -0,0 +1,144 @@ ++#ifndef IPFS_BLOCKS_H_ ++#define IPFS_BLOCKS_H_ ++ ++#include "pb_dag.h" ++#include "vocab/flat_mapset.h" ++ ++#include ++#include ++#include ++ ++namespace libp2p::multi { ++struct ContentIdentifier; ++} ++ ++namespace ipfs { ++class DagListener; ++class ContextApi; ++ ++class UnixFsPathResolver; ++ ++/*! ++ * \brief Immediate access to recently-accessed blocks ++ * \details Blocks are held in-memory, using pretty standard containers, as ++ * already-parsed ipfs::Block objects. ++ */ ++class BlockStorage { ++ public: ++ BlockStorage(); ++ ++ BlockStorage(BlockStorage const&) = delete; ++ ++ ~BlockStorage() noexcept; ++ ++ /*! ++ * \brief Store a Block for later access. ++ * \param cid_str - The string representation of cid ++ * \param cid - The Content IDentifier ++ * \param headers - Associated HTTP headers ++ * \param body - The raw bytes of the block ++ * \param block - The block being stored ++ * \return Whether this block is now stored in *this ++ */ ++ bool Store(std::string cid_str, ++ Cid const& cid, ++ std::string headers, ++ std::string const& body, ++ PbDag&& block); ++ ++ /*! ++ * \name Store (Convenience) ++ * Convenience functions for ++ * ipfs::BlockStorage::Store(std::string,Cid const&,std::string,std::string ++ * const&,Block&&) ++ */ ++ ///@{ ++ bool Store(std::string headers, std::string const& body, PbDag&& block); ++ bool Store(std::string const& cid, std::string headers, std::string body); ++ bool Store(std::string cid_str, ++ Cid const& cid, ++ std::string headers, ++ std::string body); ++ bool Store(Cid const& cid, ++ std::string headers, ++ std::string const& body, ++ PbDag&&); ++ ///@} ++ ++ /*! ++ * \brief Get a block! ++ * \details cid must match string-wise exactly: same multibase & all. ++ * For identity codecs, returns the data even if not stored. ++ * \param cid - String representation of the CID for the block. ++ * \return Non-owning pointer if found, nullptr ++ * otherwise ++ */ ++ PbDag const* Get(std::string const& cid); ++ ++ /*! ++ * \brief Get HTTP headers associated with the block ++ * \param cid - String representation of the CID for the block. ++ * \return nullptr iff ! Get(cid) ; ++ * Empty string if the headers have never been set ; ++ * Otherwise, application-specific std::string (as-stored) ++ */ ++ std::string const* GetHeaders(std::string const& cid); ++ ++ /*! ++ * \brief Indicate that a particular path resolver is waiting on a CID to ++ * become available ++ */ ++ void AddListening(UnixFsPathResolver*); ++ ++ /*! ++ * \brief Indicate that a particular path resolver is no longer waiting ++ */ ++ void StopListening(UnixFsPathResolver*); ++ ++ /*! ++ * \brief Normally called internally ++ * \details Checks to see if any listening path resolver appears to be waiting ++ * on a CID which is now available. ++ */ ++ void CheckListening(); ++ ++ /*! ++ * \brief Type for callbacks about new blocks ++ * \details The parameters to the hook are ++ * * CID string ++ * * HTTP headers ++ * * raw bytes of the block ++ */ ++ using SerializedStorageHook = ++ std::function; ++ ++ /*! ++ * \brief Register a callback that will be called when any new block goes into ++ * storage ++ */ ++ void AddStorageHook(SerializedStorageHook); ++ ++ private: ++ struct Record { ++ Record(); ++ ~Record() noexcept; ++ std::time_t last_access = 0L; ++ std::string cid_str = {}; ++ PbDag block = {}; ++ std::string headers = {}; ++ }; ++ std::list records_ = std::list(0xFFUL); ++ using Iter = decltype(records_)::iterator; ++ flat_map cid2record_; ++ flat_set listening_; ++ bool checking_ = false; ++ std::vector hooks_; ++ ++ Record const* GetInternal(std::string const&); ++ Record* FindFree(std::time_t); ++ Record* Allocate(); ++ Record* StoreIdentity(std::string const&, Cid const&); ++}; ++} // namespace ipfs ++ ++#endif // IPFS_BLOCKS_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/cid.h b/third_party/ipfs_client/include/ipfs_client/cid.h +new file mode 100644 +index 0000000000000..d957d23e5e7e4 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/cid.h +@@ -0,0 +1,38 @@ ++#ifndef IPFS_CID_H_ ++#define IPFS_CID_H_ ++ ++#include "multi_hash.h" ++#include "multicodec.h" ++ ++#include ++ ++#include ++#include ++ ++namespace ipfs { ++class Cid { ++ MultiCodec codec_ = MultiCodec::INVALID; ++ MultiHash hash_; ++ ++ public: ++ Cid() = default; ++ Cid(MultiCodec, MultiHash); ++ explicit Cid(std::string_view); ++ explicit Cid(ByteView); ++ bool ReadStart(ByteView&); ++ ++ bool valid() const; ++ MultiCodec codec() const { return codec_; } ++ MultiHash const& multi_hash() const { return hash_; } ++ ByteView hash() const; ++ HashType hash_type() const; ++ ++ std::string to_string() const; ++ ++ constexpr static std::size_t MinSerializedLength = ++ 1 /*cid version*/ + 1 /*codec*/ + 1 /*hash type*/ + ++ 1 /*hash len, could be zero*/; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_CID_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/context_api.h b/third_party/ipfs_client/include/ipfs_client/context_api.h +new file mode 100644 +index 0000000000000..da524bb9a86a5 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/context_api.h +@@ -0,0 +1,86 @@ ++#ifndef IPFS_CONTEXT_API_H_ ++#define IPFS_CONTEXT_API_H_ ++ ++#include "crypto/hasher.h" ++#include "dag_cbor_value.h" ++#include "http_request_description.h" ++#include "ipns_cbor_entry.h" ++#include "multi_hash.h" ++#include "signing_key_type.h" ++ ++#include ++ ++#include ++#include ++#include ++#include ++#include ++ ++namespace ipfs { ++class IpfsRequest; ++class DagJsonValue; ++ ++/** ++ * \brief Interface that provides functionality from whatever ++ * environment you're using this library in. ++ * \note A user of this library must implement this, but will probably do so ++ * only once. ++ */ ++class ContextApi : public std::enable_shared_from_this { ++ public: ++ ContextApi(); ++ virtual ~ContextApi() noexcept {} ++ ++ using HttpRequestDescription = ::ipfs::HttpRequestDescription; ++ using HeaderAccess = std::function; ++ using HttpCompleteCallback = ++ std::function; ++ virtual void SendHttpRequest(HttpRequestDescription, ++ HttpCompleteCallback cb) const = 0; ++ ++ using DnsTextResultsCallback = ++ std::function const&)>; ++ using DnsTextCompleteCallback = std::function; ++ virtual void SendDnsTextRequest(std::string hostname, ++ DnsTextResultsCallback, ++ DnsTextCompleteCallback) = 0; ++ ++ /*! ++ * \brief Determine a mime type for a given file. ++ * \param extension - "File extension" not including ., e.g. "html" ++ * \param content - The content of the resource or a large prefix thereof ++ * \param url - A URL it was fetched from (of any sort, ipfs:// is fine) ++ */ ++ virtual std::string MimeType(std::string extension, ++ std::string_view content, ++ std::string const& url) const = 0; ++ ++ /*! ++ * \brief Remove URL escaping, e.g. %20 ++ * \param url_comp - a single component of the URL, e.g. a element of the path ++ * not including / ++ * \return The unescaped string ++ */ ++ virtual std::string UnescapeUrlComponent(std::string_view url_comp) const = 0; ++ ++ virtual std::unique_ptr ParseCbor(ByteView) const = 0; ++ virtual std::unique_ptr ParseJson(std::string_view) const = 0; ++ ++ using IpnsCborEntry = ::ipfs::IpnsCborEntry; ++ ++ using SigningKeyType = ::ipfs::SigningKeyType; ++ using ByteView = ::ipfs::ByteView; ++ virtual bool VerifyKeySignature(SigningKeyType, ++ ByteView signature, ++ ByteView data, ++ ByteView key_bytes) const = 0; ++ ++ std::optional> Hash(HashType, ByteView data); ++ ++ protected: ++ std::unordered_map> hashers_; ++}; ++ ++} // namespace ipfs ++ ++#endif +diff --git a/third_party/ipfs_client/include/ipfs_client/crypto/hasher.h b/third_party/ipfs_client/include/ipfs_client/crypto/hasher.h +new file mode 100644 +index 0000000000000..5222d622ce998 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/crypto/hasher.h +@@ -0,0 +1,18 @@ ++#ifndef IPFS_HASHER_H_ ++#define IPFS_HASHER_H_ ++ ++#include ++ ++#include ++#include ++ ++namespace ipfs::crypto { ++class Hasher { ++ public: ++ virtual ~Hasher() noexcept {} ++ ++ virtual std::optional> hash(ByteView) = 0; ++}; ++} // namespace ipfs::crypto ++ ++#endif // IPFS_HASHER_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/dag_cbor_value.h b/third_party/ipfs_client/include/ipfs_client/dag_cbor_value.h +new file mode 100644 +index 0000000000000..71cb538776361 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/dag_cbor_value.h +@@ -0,0 +1,35 @@ ++#ifndef IPFS_DAG_CBOR_VALUE_H_ ++#define IPFS_DAG_CBOR_VALUE_H_ ++ ++#include ++ ++#include ++#include ++#include ++#include ++#include ++ ++namespace ipfs { ++class DagCborValue { ++ public: ++ virtual std::unique_ptr at(std::string_view) const = 0; ++ virtual std::optional as_unsigned() const = 0; ++ virtual std::optional as_signed() const = 0; ++ virtual std::optional as_float() const = 0; ++ virtual std::optional as_string() const = 0; ++ virtual std::optional> as_bytes() const = 0; ++ virtual std::optional as_bool() const = 0; ++ virtual std::optional as_link() const = 0; ++ virtual bool is_map() const = 0; ++ virtual bool is_array() const = 0; ++ using MapElementCallback = std::function; ++ using ArrayElementCallback = std::function; ++ virtual void iterate_map(MapElementCallback) const = 0; ++ virtual void iterate_array(ArrayElementCallback) const = 0; ++ std::string html() const; ++ void html(std::ostream&) const; ++ virtual ~DagCborValue() noexcept {} ++}; ++} ++ ++#endif // IPFS_DAG_CBOR_VALUE_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/dag_json_value.h b/third_party/ipfs_client/include/ipfs_client/dag_json_value.h +new file mode 100644 +index 0000000000000..32e170c439438 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/dag_json_value.h +@@ -0,0 +1,26 @@ ++#ifndef IPFS_DAG_JSON_VALUE_H_ ++#define IPFS_DAG_JSON_VALUE_H_ ++ ++#include ++ ++#include ++#include ++#include ++#include ++#include ++ ++namespace ipfs { ++class DagJsonValue { ++ public: ++ virtual std::string pretty_print() const = 0; ++ virtual std::unique_ptr operator[](std::string_view) const = 0; ++ virtual std::optional get_if_string() const = 0; ++ virtual std::optional> object_keys() const = 0; ++ virtual bool iterate_list(std::function) const = 0; ++ virtual ~DagJsonValue() noexcept; ++ ++ std::optional get_if_link() const; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_DAG_JSON_VALUE_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/gateways.h b/third_party/ipfs_client/include/ipfs_client/gateways.h +new file mode 100644 +index 0000000000000..0063b52525df6 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/gateways.h +@@ -0,0 +1,66 @@ ++#ifndef CHROMIUM_IPFS_GATEWAYS_H_ ++#define CHROMIUM_IPFS_GATEWAYS_H_ ++ ++#include "vocab/flat_mapset.h" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace ipfs { ++struct GatewaySpec { ++ std::string prefix; ++ unsigned strength; ++ bool operator<(GatewaySpec const& r) const { ++ if (strength == r.strength) { ++ return prefix < r.prefix; ++ } ++ return strength > r.strength; ++ } ++}; ++using GatewayList = std::vector; ++class ContextApi; ++ ++/*! ++ * \brief All known IPFS gateways ++ */ ++class Gateways { ++ flat_map known_gateways_; ++ std::default_random_engine random_engine_; ++ std::geometric_distribution dist_; ++ int up_log_ = 1; ++ ++ public: ++ /*! ++ * \brief The hard-coded list of gateways at startup ++ */ ++ static GatewayList DefaultGateways(); ++ ++ Gateways(); ++ ~Gateways(); ++ GatewayList GenerateList(); ///< Get a sorted list of gateways for requesting ++ ++ /*! ++ * \brief Good gateway, handle more! ++ * \param prefix - identify the gateway by its URL prefix ++ */ ++ void promote(std::string const& prefix); ++ ++ /*! ++ * \brief Bad gateway, move toward the back of the line. ++ * \param prefix - identify the gateway by its URL prefix ++ */ ++ void demote(std::string const& prefix); ++ ++ /*! ++ * \brief Bulk load a bunch of new gateways ++ * \param prefices - list of URL gateways by prefix ++ */ ++ void AddGateways(std::vector prefices); ++}; ++} // namespace ipfs ++ ++#endif // CHROMIUM_IPFS_GATEWAYS_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/gw/block_request_splitter.h b/third_party/ipfs_client/include/ipfs_client/gw/block_request_splitter.h +new file mode 100644 +index 0000000000000..0f308a996d360 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/gw/block_request_splitter.h +@@ -0,0 +1,17 @@ ++#ifndef IPFS_BLOCK_REQUEST_SPLITTER_H_ ++#define IPFS_BLOCK_REQUEST_SPLITTER_H_ ++ ++#include "requestor.h" ++ ++namespace ipfs { ++class ContextApi; ++} ++ ++namespace ipfs::gw { ++class BlockRequestSplitter final : public Requestor { ++ HandleOutcome handle(RequestPtr) override; ++ std::string_view name() const override; ++}; ++} // namespace ipfs::gw ++ ++#endif // IPFS_BLOCK_REQUEST_SPLITTER_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/gw/default_requestor.h b/third_party/ipfs_client/include/ipfs_client/gw/default_requestor.h +new file mode 100644 +index 0000000000000..06b5970e1d103 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/gw/default_requestor.h +@@ -0,0 +1,14 @@ ++#ifndef IPFS_DEFAULT_REQUESTOR_LIST_H_ ++#define IPFS_DEFAULT_REQUESTOR_LIST_H_ ++ ++#include "requestor.h" ++ ++#include ++ ++namespace ipfs::gw { ++std::shared_ptr default_requestor(GatewayList, ++ std::shared_ptr early, ++ std::shared_ptr); ++} ++ ++#endif // IPFS_DEFAULT_REQUESTOR_LIST_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/gw/dnslink_requestor.h b/third_party/ipfs_client/include/ipfs_client/gw/dnslink_requestor.h +new file mode 100644 +index 0000000000000..4910fe61976c8 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/gw/dnslink_requestor.h +@@ -0,0 +1,20 @@ ++#ifndef IPFS_DNSLINK_REQUESTOR_H_ ++#define IPFS_DNSLINK_REQUESTOR_H_ ++ ++#include "requestor.h" ++ ++namespace ipfs { ++class ContextApi; ++} ++ ++namespace ipfs::gw { ++class DnsLinkRequestor final : public Requestor { ++ public: ++ explicit DnsLinkRequestor(std::shared_ptr); ++ ++ HandleOutcome handle(RequestPtr) override; ++ std::string_view name() const override; ++}; ++} // namespace ipfs::gw ++ ++#endif // IPFS_DNSLINK_REQUESTOR_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/gw/gateway_request.h b/third_party/ipfs_client/include/ipfs_client/gw/gateway_request.h +new file mode 100644 +index 0000000000000..efded265680b8 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/gw/gateway_request.h +@@ -0,0 +1,77 @@ ++#ifndef IPFS_TRUSTLESS_REQUEST_H_ ++#define IPFS_TRUSTLESS_REQUEST_H_ ++ ++#include ++#include ++ ++#include ++ ++#include ++#include ++#include ++#include ++ ++namespace ipfs { ++class IpfsRequest; ++class Orchestrator; ++namespace ipld { ++class DagNode; ++} ++} // namespace ipfs ++ ++namespace ipfs::gw { ++class Requestor; ++ ++enum class Type : char { ++ Block, ++ Car, ++ Ipns, ++ DnsLink, ++ Providers, ++ Identity, ++ Zombie ++}; ++std::string_view name(Type); ++ ++constexpr std::size_t BLOCK_RESPONSE_BUFFER_SIZE = 2 * 1024 * 1024; ++ ++class GatewayRequest { ++ std::shared_ptr orchestrator_; ++ std::vector> bytes_received_hooks; ++ ++ void ParseNodes(std::string_view, ContextApi* api); ++ ++ public: ++ Type type; ++ std::string main_param; ///< CID, IPNS name, hostname ++ std::string path; ///< For CAR requests ++ std::shared_ptr dependent; ++ std::optional cid; ++ short parallel = 0; ++ std::string affinity; ++ ++ std::string url_suffix() const; ++ std::string_view accept() const; ++ std::string_view identity_data() const; ++ short timeout_seconds() const; ++ bool is_http() const; ++ std::optional max_response_size() const; ++ std::optional describe_http() const; ++ std::string debug_string() const; ++ void orchestrator(std::shared_ptr const&); ++ ++ bool RespondSuccessfully(std::string_view, ++ std::shared_ptr const& api); ++ void Hook(std::function); ++ bool PartiallyRedundant() const; ++ ++ static std::shared_ptr fromIpfsPath(SlashDelimited); ++}; ++ ++} // namespace ipfs::gw ++ ++inline std::ostream& operator<<(std::ostream& s, ipfs::gw::Type t) { ++ return s << name(t); ++} ++ ++#endif // IPFS_TRUSTLESS_REQUEST_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/gw/inline_request_handler.h b/third_party/ipfs_client/include/ipfs_client/gw/inline_request_handler.h +new file mode 100644 +index 0000000000000..0301c561c5735 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/gw/inline_request_handler.h +@@ -0,0 +1,14 @@ ++#ifndef IPFS_INLINE_REQUEST_HANDLER_H_ ++#define IPFS_INLINE_REQUEST_HANDLER_H_ ++ ++#include "requestor.h" ++ ++namespace ipfs::gw { ++class InlineRequestHandler final : public Requestor { ++ public: ++ HandleOutcome handle(RequestPtr) override; ++ std::string_view name() const override; ++}; ++} // namespace ipfs::gw ++ ++#endif // IPFS_INLINE_REQUEST_HANDLER_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/gw/requestor.h b/third_party/ipfs_client/include/ipfs_client/gw/requestor.h +new file mode 100644 +index 0000000000000..634c36730b1ea +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/gw/requestor.h +@@ -0,0 +1,55 @@ ++#ifndef IPFS_REQUESTOR_H_ ++#define IPFS_REQUESTOR_H_ ++ ++#include ++#include ++#include ++ ++namespace ipfs::ipld { ++class DagNode; ++} ++namespace ipfs { ++class ContextApi; ++struct Response; ++} // namespace ipfs ++ ++namespace ipfs::gw { ++class GatewayRequest; ++using RequestPtr = std::shared_ptr; ++ ++class Requestor : public std::enable_shared_from_this { ++ protected: ++ Requestor() {} ++ ++ friend class RequestorPool; ++ enum class HandleOutcome : char { ++ NOT_HANDLED = 'N', ++ PENDING = 'P', ++ DONE = 'D', ++ PARALLEL = 'L', ++ MAYBE_LATER = 'M' ++ }; ++ virtual HandleOutcome handle(RequestPtr) = 0; ++ ++ void definitive_failure(RequestPtr) const; ++ void forward(RequestPtr) const; ++ ++ std::shared_ptr api_; ++ ++ public: ++ using RequestPtr = ::ipfs::gw::RequestPtr; ++ virtual std::string_view name() const = 0; ++ ++ virtual ~Requestor() noexcept {} ++ void request(std::shared_ptr); ++ Requestor& or_else(std::shared_ptr p); ++ void api(std::shared_ptr); ++ ++ void TestAccess(void*); ++ ++ private: ++ std::shared_ptr next_; ++}; ++} // namespace ipfs::gw ++ ++#endif // IPFS_REQUESTOR_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/gw/terminating_requestor.h b/third_party/ipfs_client/include/ipfs_client/gw/terminating_requestor.h +new file mode 100644 +index 0000000000000..3fe7a01e752f5 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/gw/terminating_requestor.h +@@ -0,0 +1,15 @@ ++#ifndef IPFS_TERMINATING_REQUESTOR_H_ ++#define IPFS_TERMINATING_REQUESTOR_H_ ++ ++#include "requestor.h" ++ ++namespace ipfs::gw { ++class TerminatingRequestor : public Requestor { ++ public: ++ using HandleOutcome = Requestor::HandleOutcome; ++ std::string_view name() const override; ++ HandleOutcome handle(RequestPtr) override; ++}; ++} // namespace ipfs::gw ++ ++#endif // IPFS_TERMINATING_REQUESTOR_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/http_request_description.h b/third_party/ipfs_client/include/ipfs_client/http_request_description.h +new file mode 100644 +index 0000000000000..f3f07d58ea199 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/http_request_description.h +@@ -0,0 +1,20 @@ ++#ifndef IPFS_HTTP_REQUEST_DESCRIPTION_H_ ++#define IPFS_HTTP_REQUEST_DESCRIPTION_H_ ++ ++#include ++#include ++ ++#include ++ ++namespace ipfs { ++struct HttpRequestDescription { ++ std::string url; ++ int timeout_seconds; ++ std::string accept; ++ std::optional max_response_size; ++ bool operator==(HttpRequestDescription const&) const; ++ bool operator<(HttpRequestDescription const&) const; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_HTTP_REQUEST_DESCRIPTION_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/identity_cid.h b/third_party/ipfs_client/include/ipfs_client/identity_cid.h +new file mode 100644 +index 0000000000000..29efd30d1c6b2 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/identity_cid.h +@@ -0,0 +1,14 @@ ++#ifndef IPFS_IDENTITY_CID_H_ ++#define IPFS_IDENTITY_CID_H_ 1 ++ ++#include ++ ++#include ++ ++namespace ipfs { ++namespace id_cid { ++ipfs::Cid forText(std::string_view); ++} // namespace id_cid ++} // namespace ipfs ++ ++#endif +diff --git a/third_party/ipfs_client/include/ipfs_client/ipfs_request.h b/third_party/ipfs_client/include/ipfs_client/ipfs_request.h +new file mode 100644 +index 0000000000000..eda8bdfa7010b +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/ipfs_request.h +@@ -0,0 +1,33 @@ ++#ifndef IPFS_IPFS_REQUEST_H_ ++#define IPFS_IPFS_REQUEST_H_ ++ ++#include ++ ++#include ++#include ++#include ++ ++namespace ipfs { ++struct Response; ++class IpfsRequest { ++ public: ++ using Finisher = std::function; ++ ++ private: ++ std::string path_; ++ Finisher callback_; ++ std::size_t waiting_ = 0UL; ++ ++ public: ++ IpfsRequest(std::string path, Finisher); ++ SlashDelimited path() const { return SlashDelimited{path_}; } ++ void finish(Response& r); ++ void till_next(std::size_t); ++ bool ready_after(); ++ void new_path(std::string_view); ++ ++ static std::shared_ptr fromUrl(std::string url, Finisher); ++}; ++} // namespace ipfs ++ ++#endif // IPFS_IPFS_REQUEST_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/ipld/dag_node.h b/third_party/ipfs_client/include/ipfs_client/ipld/dag_node.h +new file mode 100644 +index 0000000000000..1c66f4fd1c755 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/ipld/dag_node.h +@@ -0,0 +1,102 @@ ++#ifndef IPFS_DAG_NODE_H_ ++#define IPFS_DAG_NODE_H_ ++ ++#include "link.h" ++#include "resolution_state.h" ++ ++#include ++#include ++#include ++#include ++ ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace ipfs { ++class PbDag; ++class ContextApi; ++struct ValidatedIpns; ++} // namespace ipfs ++namespace libp2p::multi { ++struct ContentIdentifier; ++} ++namespace ipfs::ipld { ++ ++using NodePtr = std::shared_ptr; ++class DirShard; ++ ++struct MoreDataNeeded { ++ MoreDataNeeded(std::string one) : ipfs_abs_paths_{{one}} {} ++ template ++ MoreDataNeeded(Range const& many) ++ : ipfs_abs_paths_(many.begin(), many.end()) {} ++ std::vector ipfs_abs_paths_; ++ bool insist_on_car = false; ++}; ++enum class ProvenAbsent {}; ++struct PathChange { ++ std::string new_path; ++}; ++ ++using ResolveResult = ++ std::variant; ++/** ++ * @brief A block, an IPNS record, etc. ++ */ ++class DagNode : public std::enable_shared_from_this { ++ Link* FindChild(std::string_view); ++ static void Descend(ResolutionState&); ++ ++ protected: ++ std::vector> links_; ++ std::shared_ptr api_; ++ ++ ///< When the next path element is what's needed, and it should already be a ++ ///< link known about... ++ ResolveResult CallChild(ResolutionState&); ++ ++ ///< As before, but it might be possible to create on the fly if not known ++ ResolveResult CallChild(ResolutionState&, ++ std::function gen_child); ++ ++ ///< When the child's name is not the next element in the path, but it must be ++ ///< known about. e.g. index.html for a path ending in a directory ++ ResolveResult CallChild(ResolutionState&, std::string_view link_key); ++ ++ ///< Add the link if not present, then CallChild(ResolutionState) ++ ResolveResult CallChild(ResolutionState&, ++ std::string_view link_key, ++ std::string_view block_key); ++ ++ public: ++ virtual ResolveResult resolve(ResolutionState& params) = 0; ++ ResolveResult resolve(SlashDelimited initial_path, BlockLookup); ++ ++ static NodePtr fromBytes(std::shared_ptr const& api, ++ Cid const&, ++ ByteView bytes); ++ static NodePtr fromBytes(std::shared_ptr const& api, ++ Cid const&, ++ std::string_view bytes); ++ static NodePtr fromBlock(PbDag const&); ++ static NodePtr fromIpnsRecord(ValidatedIpns const&); ++ ++ virtual ~DagNode() noexcept {} ++ ++ virtual NodePtr rooted(); ++ virtual NodePtr deroot(); ++ virtual DirShard* as_hamt(); // Wish I had access to dynamic_cast ++ ++ void set_api(std::shared_ptr); ++}; ++} // namespace ipfs::ipld ++ ++std::ostream& operator<<(std::ostream&, ipfs::ipld::PathChange const&); ++ ++#endif // IPFS_DAG_NODE_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/ipld/link.h b/third_party/ipfs_client/include/ipfs_client/ipld/link.h +new file mode 100644 +index 0000000000000..a0d290b25dd3d +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/ipld/link.h +@@ -0,0 +1,22 @@ ++#ifndef IPFS_LINK_H_ ++#define IPFS_LINK_H_ ++ ++#include ++#include ++ ++namespace ipfs::ipld { ++ ++class DagNode; ++using Ptr = std::shared_ptr; ++ ++class Link { ++ public: ++ std::string cid; ++ Ptr node; ++ ++ Link(std::string); ++ explicit Link(std::string, std::shared_ptr); ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_LINK_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/ipld/resolution_state.h b/third_party/ipfs_client/include/ipfs_client/ipld/resolution_state.h +new file mode 100644 +index 0000000000000..82e330cea4355 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/ipld/resolution_state.h +@@ -0,0 +1,36 @@ ++#ifndef IPFS_RESOLUTION_STATE_H_ ++#define IPFS_RESOLUTION_STATE_H_ ++ ++#include ++ ++#include ++#include ++ ++namespace ipfs { ++class ContextApi; ++} ++ ++namespace ipfs::ipld { ++class DagNode; ++using NodePtr = std::shared_ptr; ++using BlockLookup = std::function; ++ ++class ResolutionState { ++ friend class DagNode; ++ std::string resolved_path_components; ++ SlashDelimited unresolved_path; ++ BlockLookup get_available_block; ++ ++ public: ++ SlashDelimited MyPath() const; ++ SlashDelimited PathToResolve() const; ++ bool IsFinalComponent() const; ++ std::string NextComponent(ContextApi const*) const; ++ NodePtr GetBlock(std::string const& block_key) const; ++ ++ ResolutionState WithPath(std::string_view) const; ++ ResolutionState RestartResolvedPath() const; ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_RESOLUTION_STATE_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/ipns_cbor_entry.h b/third_party/ipfs_client/include/ipfs_client/ipns_cbor_entry.h +new file mode 100644 +index 0000000000000..230339793543c +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/ipns_cbor_entry.h +@@ -0,0 +1,21 @@ ++#ifndef IPFS_IPNS_CBOR_ENTRY_H_ ++#define IPFS_IPNS_CBOR_ENTRY_H_ ++ ++#include ++#include ++ ++namespace ipfs { ++/*! ++ * \brief Parsed out data contained in the CBOR data of an IPNS record. ++ */ ++struct IpnsCborEntry { ++ std::string value; ///< The "value" (target) the name points at ++ std::string validity; ///< Value to compare for validity (i.e. expiration) ++ std::uint64_t validityType; ///< Way to deterimine current validity ++ std::uint64_t sequence; ///< Distinguish other IPNS records for the same name ++ std::uint64_t ttl; ///< Recommended caching time ++}; ++ ++} // namespace ipfs ++ ++#endif // IPFS_IPNS_CBOR_ENTRY_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/ipns_names.h b/third_party/ipfs_client/include/ipfs_client/ipns_names.h +new file mode 100644 +index 0000000000000..b611365b87874 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/ipns_names.h +@@ -0,0 +1,69 @@ ++#ifndef IPNS_NAME_RESOLVER_H_ ++#define IPNS_NAME_RESOLVER_H_ ++ ++#include ++#include ++ ++#include ++ ++namespace ipfs { ++ ++/*! ++ * \brief Fast synchronous access to IPNS & DNSLink name resolution ++ */ ++class IpnsNames { ++ flat_map names_; ++ ++ public: ++ IpnsNames(); ++ ~IpnsNames(); ++ ++ /*! ++ * \brief Get the already-known "value"/target of a given name ++ * \param name - either a mb-mf IPNS (key) name, or a host with DNSLink ++ * \return ++ * * if resolution is incomplete: "" ++ * * if it is known not to resolve: kNoSuchName ++ * * otherwise an IPFS path witout leading /, e.g.: ++ * - ipfs/bafybeicfqz46dj67nkhxaylqd5sknnidsr4oaw4hhsjrgdmcwt73sow2d4/ ++ * - ipns/k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8 ++ */ ++ std::string_view NameResolvedTo(std::string_view name) const; ++ ++ /*! ++ * \brief Store an IPNS record that already validated for this name ++ * \param name - The name that resolves with this ++ * \param rec - The record modulo validation bits ++ */ ++ void AssignName(std::string const& name, ValidatedIpns rec); ++ ++ /*! ++ * \brief Assign a target path to a DNSLink host ++ * \param host - The original host NOT including a "_dnslink." prefix ++ * \param target - an IPFS path witout leading / ++ */ ++ void AssignDnsLink(std::string const& host, std::string_view target); ++ ++ /*! ++ * \brief Store the definitive absence of a resolution ++ * \details This is useful because code will check resolution here before ++ * trying to resolve it fresh again, and you can stop that if you know ++ * it will never work. ++ */ ++ void NoSuchName(std::string const& name); ++ ++ /*! ++ * \brief Fetch the all the stored IPNS record data ++ * \param name - the IPNS name it was stored with ++ * \return nullptr if missing, otherwise non-owning pointer to record ++ */ ++ ValidatedIpns const* Entry(std::string const& name); ++ ++ /*! ++ * \brief A special value constant ++ */ ++ static constexpr std::string_view kNoSuchName{"NO_SUCH_NAME"}; ++}; ++} // namespace ipfs ++ ++#endif // IPNS_NAME_RESOLVER_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/ipns_record.h b/third_party/ipfs_client/include/ipfs_client/ipns_record.h +new file mode 100644 +index 0000000000000..a6bd168a4af60 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/ipns_record.h +@@ -0,0 +1,75 @@ ++#ifndef IPFS_IPNS_RECORD_H_ ++#define IPFS_IPNS_RECORD_H_ ++ ++#include ++ ++#include ++ ++#if __has_include() ++#include ++#else ++#include "ipfs_client/keys.pb.h" ++#endif ++ ++#include ++#include ++ ++namespace libp2p::peer { ++class PeerId; ++} ++namespace libp2p::multi { ++struct ContentIdentifier; ++} ++ ++namespace ipfs { ++ ++class Cid; ++class ContextApi; ++ ++constexpr static std::size_t MAX_IPNS_PB_SERIALIZED_SIZE = 10 * 1024; ++ ++std::optional ValidateIpnsRecord(ByteView top_level_bytes, ++ Cid const& name, ++ ContextApi&); ++ ++/*! ++ * \brief Data from IPNS record modulo the verification parts ++ */ ++struct ValidatedIpns { ++ std::string value; ///< The path the record claims the IPNS name points to ++ std::time_t use_until; ///< An expiration timestamp ++ std::time_t cache_until; ///< Inspired by TTL ++ ++ /*! ++ * \brief The version of the record ++ * \details Higher sequence numbers obsolete lower ones ++ */ ++ std::uint64_t sequence; ++ std::int64_t resolution_ms; ///< How long it took to fetch the record ++ ++ /*! ++ * \brief When the record was fetched ++ */ ++ std::time_t fetch_time = std::time(nullptr); ++ std::string gateway_source; ///< Who gave us this record? ++ ++ ValidatedIpns(); ///< Create an invalid default object ++ ValidatedIpns(IpnsCborEntry const&); ++ ValidatedIpns(ValidatedIpns&&); ++ ValidatedIpns(ValidatedIpns const&); ++ ValidatedIpns& operator=(ValidatedIpns const&); ++ ++ std::string Serialize() const; ///< Turn into a well-defined list of bytes ++ ++ /*! ++ * \brief Create a ValidatedIpns from untyped bytes ++ * \param bytes - Output from a former call to Serialize() ++ * \note Is used by disk cache ++ * \return Recreation of the old object ++ */ ++ static ValidatedIpns Deserialize(std::string bytes); ++}; ++ ++} // namespace ipfs ++ ++#endif // IPFS_IPNS_RECORD_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/json_cbor_adapter.h b/third_party/ipfs_client/include/ipfs_client/json_cbor_adapter.h +new file mode 100644 +index 0000000000000..5ed52ad465b0c +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/json_cbor_adapter.h +@@ -0,0 +1,155 @@ ++#ifndef IPFS_JSON_CBOR_ADAPTER_H_ ++#define IPFS_JSON_CBOR_ADAPTER_H_ ++ ++#include ++#include ++ ++#include ++#include ++ ++#if __has_include() ++ ++#include ++#define HAS_JSON_CBOR_ADAPTER 1 ++ ++namespace ipfs { ++// LCOV_EXCL_START ++class JsonCborAdapter final : public DagCborValue, public DagJsonValue { ++ nlohmann::json data_; ++ ++ public: ++ using Cid = ipfs::Cid; ++ JsonCborAdapter(nlohmann::json data) : data_{data} { ++ if (data_.is_array() && data_.size() == 1UL) { ++ data_ = data_[0]; ++ } ++ } ++ std::unique_ptr at(std::string_view k) const override { ++ if (data_.is_object() && data_.contains(k)) { ++ return std::make_unique(data_.at(k)); ++ } ++ return {}; ++ } ++ std::unique_ptr operator[](std::string_view k) const override { ++ if (data_.is_object() && data_.contains(k)) { ++ return std::make_unique(data_[k]); ++ } ++ return {}; ++ } ++ std::optional as_unsigned() const override { ++ if (data_.is_number_unsigned()) { ++ return data_.get(); ++ } ++ return std::nullopt; ++ } ++ std::optional as_signed() const { ++ if (data_.is_number_integer()) { ++ return data_.get(); ++ } else if (auto ui = as_unsigned()) { ++ if (*ui <= std::numeric_limits::max()) { ++ return static_cast(*ui); ++ } ++ } ++ return std::nullopt; ++ } ++ std::optional as_float() const override { ++ if (data_.is_number_float()) { ++ return data_.get(); ++ } ++ return std::nullopt; ++ } ++ std::optional as_string() const override { ++ if (data_.is_string()) { ++ return data_.get(); ++ } ++ return std::nullopt; ++ } ++ std::optional get_if_string() const override { ++ return as_string(); ++ } ++ std::optional as_bool() const override { ++ if (data_.is_boolean()) { ++ return data_.get(); ++ } ++ return std::nullopt; ++ } ++ std::optional> as_bytes() const override { ++ if (data_.is_binary()) { ++ return data_.get_binary(); ++ } ++ return std::nullopt; ++ } ++ std::optional as_link() const override { ++ if (!data_.is_binary()) { ++ return std::nullopt; ++ } ++ auto& bin = data_.get_binary(); ++ if (!bin.has_subtype() || bin.subtype() != 42) { ++ return std::nullopt; ++ } ++ if (bin.size() < 6) { ++ return std::nullopt; ++ } ++ if (bin[0]) { ++ return std::nullopt; ++ } ++ auto p = reinterpret_cast(bin.data()) + 1UL; ++ Cid from_binary(ByteView{p, bin.size() - 1UL}); ++ if (from_binary.valid()) { ++ return from_binary; ++ } else { ++ return std::nullopt; ++ } ++ } ++ bool is_map() const override {return data_.is_object();} ++ bool is_array() const override {return data_.is_array();} ++ void iterate_map(MapElementCallback cb) const override { ++ if (!is_map()) { ++ return; ++ } ++ for (auto& [k,v] : data_.items()) { ++ JsonCborAdapter el(v); ++ cb(k, el); ++ } ++ } ++ void iterate_array(ArrayElementCallback cb) const override { ++ if (!is_array()) { ++ return; ++ } ++ for (auto& v : data_) { ++ JsonCborAdapter el(v); ++ cb(el); ++ } ++ } ++ std::string pretty_print() const override { ++ std::ostringstream result; ++ result << std::setw(2) << data_; ++ return result.str(); ++ } ++ std::optional> object_keys() const override { ++ if (!data_.is_object()) { ++ return std::nullopt; ++ } ++ std::vector rv; ++ for (auto& [k, v] : data_.items()) { ++ rv.push_back(k); ++ } ++ return rv; ++ } ++ bool iterate_list( ++ std::function cb) const override { ++ if (!data_.is_array()) { ++ return false; ++ } ++ for (auto& v : data_) { ++ JsonCborAdapter wrap(v); ++ cb(wrap); ++ } ++ return true; ++ } ++}; ++} // namespace ipfs ++ ++#endif ++ ++#endif // IPFS_JSON_CBOR_ADAPTER_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/logger.h b/third_party/ipfs_client/include/ipfs_client/logger.h +new file mode 100644 +index 0000000000000..35191ac5f832c +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/logger.h +@@ -0,0 +1,34 @@ ++#ifndef IPFS_LOGGER_H_ ++#define IPFS_LOGGER_H_ ++ ++#include ++ ++namespace ipfs::log { ++ ++enum class Level { ++ TRACE = -2, ++ DEBUG = -1, ++ INFO = 0, ++ WARN = 1, ++ ERROR = 2, ++ FATAL = 3, ++ OFF ++}; ++ ++void SetLevel(Level); ++ ++using Handler = void (*)(std::string const&, char const*, int, Level); ++void SetHandler(Handler); ++ ++void DefaultHandler(std::string const& message, ++ char const* source_file, ++ int source_line, ++ Level for_prefix); ++ ++std::string_view LevelDescriptor(Level); ++ ++bool IsInitialized(); ++ ++} // namespace ipfs::log ++ ++#endif // LOGGER_H +diff --git a/third_party/ipfs_client/include/ipfs_client/multi_base.h b/third_party/ipfs_client/include/ipfs_client/multi_base.h +new file mode 100644 +index 0000000000000..8c09b97345635 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/multi_base.h +@@ -0,0 +1,42 @@ ++#ifndef IPFS_MB_PREFIXES_H_ ++#define IPFS_MB_PREFIXES_H_ ++ ++#include ++ ++#include ++#include ++#include ++#include ++ ++namespace ipfs::mb { ++ ++// https://github.com/multiformats/multibase/blob/master/multibase.csv ++enum class Code : char { ++ IDENTITY = '\0', ++ UNSUPPORTED = '1', ++ BASE16_LOWER = 'f', ++ BASE16_UPPER = 'F', ++ BASE32_LOWER = 'b', ++ BASE32_UPPER = 'B', ++ BASE36_LOWER = 'k', ++ BASE36_UPPER = 'K', ++ BASE58_BTC = 'z', ++ BASE64 = 'm' ++}; ++Code CodeFromPrefix(char c); ++std::string_view GetName(Code); ++ ++using Decoder = std::vector (*)(std::string_view); ++using Encoder = std::string (*)(ByteView); ++struct Codec { ++ Decoder const decode; ++ Encoder const encode; ++ std::string_view const name; ++ static Codec const* Get(Code); ++}; ++ ++std::string encode(Code, ByteView); ++std::optional> decode(std::string_view mb_str); ++} // namespace ipfs::mb ++ ++#endif // IPFS_MB_PREFIXES_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/multi_hash.h b/third_party/ipfs_client/include/ipfs_client/multi_hash.h +new file mode 100644 +index 0000000000000..6ed78f5e674dc +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/multi_hash.h +@@ -0,0 +1,32 @@ ++#ifndef IPFS_MULTI_HASH_H_ ++#define IPFS_MULTI_HASH_H_ ++ ++#include ++ ++#include ++ ++namespace ipfs { ++enum class HashType { INVALID = -1, IDENTITY = 0, SHA2_256 = 0X12 }; ++constexpr std::uint16_t MaximumHashLength = 127; ++ ++HashType Validate(HashType); ++std::string_view GetName(HashType); ++class MultiHash { ++ public: ++ MultiHash() = default; ++ explicit MultiHash(ByteView); ++ explicit MultiHash(HashType, ByteView digest); ++ ++ bool ReadPrefix(ByteView&); ++ ++ bool valid() const; ++ HashType type() const { return type_; } ++ ByteView digest() const { return hash_; } ++ ++ private: ++ HashType type_ = HashType::INVALID; ++ std::vector hash_; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_MULTI_HASH_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/multicodec.h b/third_party/ipfs_client/include/ipfs_client/multicodec.h +new file mode 100644 +index 0000000000000..bf8d89b6c27e2 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/multicodec.h +@@ -0,0 +1,23 @@ ++#ifndef IPFS_MUTLICODEC_H_ ++#define IPFS_MUTLICODEC_H_ ++ ++#include ++#include ++ ++#include ++ ++namespace ipfs { ++enum class MultiCodec : std::uint32_t { ++ INVALID = std::numeric_limits::max(), ++ IDENTITY = 0x00, ++ RAW = 0x55, ++ DAG_PB = 0x70, ++ DAG_CBOR = 0x71, ++ LIBP2P_KEY = 0x72, ++ DAG_JSON = 0x0129, ++}; ++MultiCodec Validate(MultiCodec); ++std::string_view GetName(MultiCodec); ++} // namespace ipfs ++ ++#endif // IPFS_MUTLICODEC_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/orchestrator.h b/third_party/ipfs_client/include/ipfs_client/orchestrator.h +new file mode 100644 +index 0000000000000..f204dde799b3e +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/orchestrator.h +@@ -0,0 +1,45 @@ ++#ifndef IPFS_ORCHESTRATOR_H_ ++#define IPFS_ORCHESTRATOR_H_ ++ ++#include "ipfs_client/ipld/dag_node.h" ++ ++#include ++#include ++ ++#include ++#include ++ ++namespace ipfs { ++ ++class ContextApi; ++ ++class Orchestrator : public std::enable_shared_from_this { ++ public: ++ using GatewayAccess = ++ std::function)>; ++ using MimeDetection = std::function< ++ std::string(std::string, std::string_view, std::string const&)>; ++ explicit Orchestrator(std::shared_ptr requestor, ++ std::shared_ptr = {}); ++ void build_response(std::shared_ptr); ++ bool add_node(std::string key, ipld::NodePtr); ++ bool has_key(std::string const& k) const; ++ ++ private: ++ flat_map dags_; ++ // GatewayAccess gw_requestor_; ++ std::shared_ptr api_; ++ std::shared_ptr requestor_; ++ ++ void from_tree(std::shared_ptr, ++ ipld::NodePtr&, ++ SlashDelimited, ++ std::string const&); ++ bool gw_request(std::shared_ptr, ++ SlashDelimited path, ++ std::string const& aff); ++ std::string sniff(SlashDelimited, std::string const&) const; ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_ORCHESTRATOR_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/response.h b/third_party/ipfs_client/include/ipfs_client/response.h +new file mode 100644 +index 0000000000000..3c277994d8b9c +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/response.h +@@ -0,0 +1,27 @@ ++#ifndef IPFS_RESPONSE_H_ ++#define IPFS_RESPONSE_H_ ++ ++#include ++ ++#include ++#include ++#include ++ ++namespace ipfs { ++ ++struct Response { ++ std::string mime_; ++ std::uint16_t status_; ++ std::string body_; ++ std::string location_; ++ ++ static Response PLAIN_NOT_FOUND; ++ static Response IMMUTABLY_GONE; ++ static Response HOST_NOT_FOUND; ++ ++ constexpr static std::uint16_t HOST_NOT_FOUND_STATUS = 503; ++}; ++ ++} // namespace ipfs ++ ++#endif // IPFS_RESPONSE_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/signing_key_type.h b/third_party/ipfs_client/include/ipfs_client/signing_key_type.h +new file mode 100644 +index 0000000000000..4a74ad0f6967b +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/signing_key_type.h +@@ -0,0 +1,14 @@ ++#ifndef IPFS_SIGNING_KEY_TYPE_H_ ++#define IPFS_SIGNING_KEY_TYPE_H_ ++ ++namespace ipfs { ++enum class SigningKeyType : int { ++ RSA, ++ Ed25519, ++ Secp256k1, ++ ECDSA, ++ KeyTypeCount ++}; ++} ++ ++#endif // IPFS_SIGNING_KEY_TYPE_H_ +diff --git a/third_party/ipfs_client/include/ipfs_client/url_spec.h b/third_party/ipfs_client/include/ipfs_client/url_spec.h +new file mode 100644 +index 0000000000000..a61aec25d5968 +--- /dev/null ++++ b/third_party/ipfs_client/include/ipfs_client/url_spec.h +@@ -0,0 +1,24 @@ ++#ifndef IPFS_URL_SPEC_H_ ++#define IPFS_URL_SPEC_H_ ++ ++// TODO - Give more thought to how this interplays with gw::Request ++ ++#include ++#include ++ ++namespace ipfs { ++struct UrlSpec { ++ std::string suffix; ++ std::string_view accept; ++ ++ bool operator<(UrlSpec const& rhs) const { ++ if (suffix != rhs.suffix) { ++ return suffix < rhs.suffix; ++ } ++ return accept < rhs.accept; ++ } ++ bool none() const { return suffix.empty(); } ++}; ++} // namespace ipfs ++ ++#endif // IPFS_URL_SPEC_H_ +diff --git a/third_party/ipfs_client/include/libp2p/common/types.hpp b/third_party/ipfs_client/include/libp2p/common/types.hpp +new file mode 100644 +index 0000000000000..a112d1bf5d3db +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/common/types.hpp +@@ -0,0 +1,39 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef LIBP2P_P2P_COMMON_TYPES_HPP ++#define LIBP2P_P2P_COMMON_TYPES_HPP ++ ++#include "vocab/byte_view.h" ++ ++#include ++#include ++#include ++#include ++ ++namespace libp2p::common { ++/** ++ * Sequence of bytes ++ */ ++using ByteArray = std::vector; ++// using ByteArray = std::string; ++ ++template ++void append(Collection& c, Item&& g) { ++ c.insert(c.end(), g.begin(), g.end()); ++} ++ ++template ++void append(Collection& c, char g) { ++ c.push_back(g); ++} ++ ++/// Hash256 as a sequence of 32 bytes ++using Hash256 = std::array; ++/// Hash512 as a sequence of 64 bytes ++using Hash512 = std::array; ++} // namespace libp2p::common ++ ++#endif // LIBP2P_P2P_COMMON_TYPES_HPP +diff --git a/third_party/ipfs_client/include/libp2p/crypto/key.h b/third_party/ipfs_client/include/libp2p/crypto/key.h +new file mode 100644 +index 0000000000000..8198e41122fdd +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/crypto/key.h +@@ -0,0 +1,100 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef LIBP2P_LIBP2P_CRYPTO_KEY_HPP ++#define LIBP2P_LIBP2P_CRYPTO_KEY_HPP ++ ++#include ++ ++#include "libp2p/common/types.hpp" ++ ++namespace libp2p::crypto { ++ ++using Buffer = libp2p::common::ByteArray; ++ ++struct Key { ++ /** ++ * Supported types of all keys ++ */ ++ enum class Type { ++ UNSPECIFIED = 100, ++ RSA = 0, ++ Ed25519 = 1, ++ Secp256k1 = 2, ++ ECDSA = 3 ++ }; ++ ++ Key(Type, std::vector); ++ ~Key() noexcept; ++ Type type = Type::UNSPECIFIED; ///< key type ++ std::vector data{}; ///< key content ++}; ++ ++inline bool operator==(const Key& lhs, const Key& rhs) { ++ return lhs.type == rhs.type && lhs.data == rhs.data; ++} ++ ++inline bool operator!=(const Key& lhs, const Key& rhs) { ++ return !(lhs == rhs); ++} ++ ++struct PublicKey : public Key {}; ++ ++struct PrivateKey : public Key {}; ++ ++struct KeyPair { ++ PublicKey publicKey; ++ PrivateKey privateKey; ++}; ++ ++using Signature = std::vector; ++ ++inline bool operator==(const KeyPair& a, const KeyPair& b) { ++ return a.publicKey == b.publicKey && a.privateKey == b.privateKey; ++} ++ ++/** ++ * Result of ephemeral key generation ++ * ++struct EphemeralKeyPair { ++ Buffer ephemeral_public_key; ++ std::function(Buffer)> shared_secret_generator; ++}; ++*/ ++ ++/** ++ * Type of the stretched key ++ * ++struct StretchedKey { ++ Buffer iv; ++ Buffer cipher_key; ++ Buffer mac_key; ++}; ++*/ ++} // namespace libp2p::crypto ++ ++namespace std { ++template <> ++struct hash { ++ size_t operator()(const libp2p::crypto::Key& x) const; ++}; ++ ++template <> ++struct hash { ++ size_t operator()(const libp2p::crypto::PrivateKey& x) const; ++}; ++ ++template <> ++struct hash { ++ size_t operator()(const libp2p::crypto::PublicKey& x) const; ++}; ++ ++template <> ++struct hash { ++ size_t operator()(const libp2p::crypto::KeyPair& x) const; ++}; ++} // namespace std ++ ++#endif // LIBP2P_LIBP2P_CRYPTO_KEY_HPP +diff --git a/third_party/ipfs_client/include/libp2p/crypto/protobuf/protobuf_key.hpp b/third_party/ipfs_client/include/libp2p/crypto/protobuf/protobuf_key.hpp +new file mode 100644 +index 0000000000000..1a0d7ae7a2d4e +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/crypto/protobuf/protobuf_key.hpp +@@ -0,0 +1,29 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef KAGOME_PROTOBUF_KEY_HPP ++#define KAGOME_PROTOBUF_KEY_HPP ++ ++// #include ++ ++#include ++ ++#include ++ ++namespace libp2p::crypto { ++/** ++ * Strict type for key, which is encoded into Protobuf format ++ */ ++struct ProtobufKey { //: public boost::equality_comparable { ++ explicit ProtobufKey(std::vector key); ++ ~ProtobufKey() noexcept; ++ ++ std::vector key; ++ ++ bool operator==(const ProtobufKey& other) const { return key == other.key; } ++}; ++} // namespace libp2p::crypto ++ ++#endif // KAGOME_PROTOBUF_KEY_HPP +diff --git a/third_party/ipfs_client/include/libp2p/multi/multibase_codec.hpp b/third_party/ipfs_client/include/libp2p/multi/multibase_codec.hpp +new file mode 100644 +index 0000000000000..c7b9cbd1f7d40 +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/multi/multibase_codec.hpp +@@ -0,0 +1,65 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef LIBP2P_MULTIBASE_HPP ++#define LIBP2P_MULTIBASE_HPP ++ ++#include "vocab/expected.h" ++ ++#include ++#include ++#include ++ ++#include ++ ++namespace libp2p::multi { ++/** ++ * Allows to distinguish between different base-encoded binaries ++ * See more: https://github.com/multiformats/multibase ++ */ ++class MultibaseCodec { ++ public: ++ enum class Error { UNSUPPORTED_BASE = 1, INPUT_TOO_SHORT, BASE_CODEC_ERROR }; ++ ++ using ByteBuffer = common::ByteArray; ++ using FactoryResult = ipfs::expected; ++ ++ virtual ~MultibaseCodec() = default; ++ /** ++ * Encodings, supported by this Multibase ++ * @sa https://github.com/multiformats/multibase#multibase-table ++ */ ++ enum class Encoding : char { ++ BASE16_LOWER = 'f', ++ BASE16_UPPER = 'F', ++ BASE32_LOWER = 'b', ++ BASE32_UPPER = 'B', ++ BASE36 = 'k', ++ BASE58 = 'z', ++ BASE64 = 'm' ++ }; ++ ++ /** ++ * Encode the incoming bytes ++ * @param bytes to be encoded ++ * @param encoding - base of the desired encoding ++ * @return encoded string WITH an encoding prefix ++ */ ++ virtual std::string encode(const ByteBuffer& bytes, ++ Encoding encoding) const = 0; ++ ++ /** ++ * Decode the incoming string ++ * @param string to be decoded ++ * @return bytes, if decoding was successful, error otherwise ++ */ ++ virtual FactoryResult decode(std::string_view string) const = 0; ++}; ++ ++bool case_critical(MultibaseCodec::Encoding); ++ ++} // namespace libp2p::multi ++ ++#endif // LIBP2P_MULTIBASE_HPP +diff --git a/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base16.h b/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base16.h +new file mode 100644 +index 0000000000000..72a74237eb2ee +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base16.h +@@ -0,0 +1,24 @@ ++#ifndef IPFS_BASE32_H_ ++#define IPFS_BASE32_H_ ++ ++#include "base_error.hpp" ++ ++#include ++#include ++ ++#include ++ ++#include ++ ++namespace ipfs::base16 { ++std::string encodeLower(ByteView bytes); ++std::string encodeUpper(ByteView bytes); ++ ++using libp2p::common::ByteArray; ++using libp2p::multi::detail::BaseError; ++using Decoded = ipfs::expected; ++Decoded decode(std::string_view string); ++ ++} // namespace ipfs::base16 ++ ++#endif // IPFS_BASE32_H_ +diff --git a/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base32.hpp b/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base32.hpp +new file mode 100644 +index 0000000000000..c24dc59d54121 +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base32.hpp +@@ -0,0 +1,52 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef LIBP2P_BASE32_HPP ++#define LIBP2P_BASE32_HPP ++ ++#include "base_error.hpp" ++ ++#include ++#include ++ ++/** ++ * Encode/decode to/from base32 format ++ * Implementation is taken from ++ * https://github.com/mjg59/tpmtotp/blob/master/base32.c ++ */ ++namespace libp2p::multi::detail { ++ ++/** ++ * Encode bytes to base32 uppercase string ++ * @param bytes to be encoded ++ * @return encoded string ++ */ ++std::string encodeBase32Upper(ipfs::ByteView bytes); ++/** ++ * Encode bytes to base32 lowercase string ++ * @param bytes to be encoded ++ * @return encoded string ++ */ ++std::string encodeBase32Lower(ipfs::ByteView bytes); ++ ++/** ++ * Decode base32 uppercase to bytes ++ * @param string to be decoded ++ * @return decoded bytes in case of success ++ */ ++ipfs::expected decodeBase32Upper( ++ std::string_view string); ++ ++/** ++ * Decode base32 lowercase string to bytes ++ * @param string to be decoded ++ * @return decoded bytes in case of success ++ */ ++ipfs::expected decodeBase32Lower( ++ std::string_view string); ++ ++} // namespace libp2p::multi::detail ++ ++#endif // LIBP2P_BASE32_HPP +diff --git a/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base36.hpp b/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base36.hpp +new file mode 100644 +index 0000000000000..20006df216d51 +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base36.hpp +@@ -0,0 +1,42 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef LIBP2P_BASE36_HPP ++#define LIBP2P_BASE36_HPP ++ ++#include "base_error.hpp" ++ ++#include ++#include ++ ++/** ++ * Encode/decode to/from base36 format ++ */ ++namespace libp2p::multi::detail { ++ ++/** ++ * Encode bytes to base36 uppercase string ++ * @param bytes to be encoded ++ * @return encoded string ++ */ ++std::string encodeBase36Upper(ipfs::ByteView bytes); ++/** ++ * Encode bytes to base36 lowercase string ++ * @param bytes to be encoded ++ * @return encoded string ++ */ ++std::string encodeBase36Lower(ipfs::ByteView bytes); ++ ++/** ++ * Decode base36 (case-insensitively) to bytes ++ * @param string to be decoded ++ * @return decoded bytes in case of success ++ */ ++ipfs::expected decodeBase36( ++ std::string_view string); ++ ++} // namespace libp2p::multi::detail ++ ++#endif // LIBP2P_BASE36_HPP +diff --git a/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base_error.hpp b/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base_error.hpp +new file mode 100644 +index 0000000000000..a0ab1b6c54be5 +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/multi/multibase_codec/codecs/base_error.hpp +@@ -0,0 +1,24 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef LIBP2P_BASE_ERROR_HPP ++#define LIBP2P_BASE_ERROR_HPP ++ ++namespace libp2p::multi::detail { ++ ++enum class BaseError { ++ INVALID_BASE58_INPUT = 1, ++ INVALID_BASE64_INPUT, ++ INVALID_BASE32_INPUT, ++ INVALID_BASE36_INPUT, ++ NON_UPPERCASE_INPUT, ++ NON_LOWERCASE_INPUT, ++ UNIMPLEMENTED_MULTIBASE, ++ INVALID_BASE16_INPUT ++}; ++ ++} ++ ++#endif // LIBP2P_BASE_ERROR_HPP +diff --git a/third_party/ipfs_client/include/libp2p/multi/multicodec_type.hpp b/third_party/ipfs_client/include/libp2p/multi/multicodec_type.hpp +new file mode 100644 +index 0000000000000..bda027bb29567 +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/multi/multicodec_type.hpp +@@ -0,0 +1,78 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef LIBP2P_MULTICODECTYPE_HPP ++#define LIBP2P_MULTICODECTYPE_HPP ++ ++#include ++ ++namespace libp2p::multi { ++ ++/** ++ * LibP2P uses "protocol tables" to agree upon the mapping from one multicodec ++ * code. These tables can be application specific, though, like with other ++ * multiformats, there is a globally agreed upon table with common protocols ++ * and formats. ++ */ ++class MulticodecType { ++ public: ++ enum class Code { ++ IDENTITY = 0x00, ++ SHA1 = 0x11, ++ SHA2_256 = 0x12, ++ SHA2_512 = 0x13, ++ SHA3_512 = 0x14, ++ SHA3_384 = 0x15, ++ SHA3_256 = 0x16, ++ SHA3_224 = 0x17, ++ RAW = 0x55, ++ DAG_PB = 0x70, ++ DAG_CBOR = 0x71, ++ LIBP2P_KEY = 0x72, ++ DAG_JSON = 0x0129, ++ FILECOIN_COMMITMENT_UNSEALED = 0xf101, ++ FILECOIN_COMMITMENT_SEALED = 0xf102, ++ }; ++ ++ constexpr static std::string_view getName(Code code) { ++ switch (code) { ++ case Code::IDENTITY: ++ return "identity"; ++ case Code::SHA1: ++ return "sha1"; ++ case Code::SHA2_256: ++ return "sha2-256"; ++ case Code::SHA2_512: ++ return "sha2-512"; ++ case Code::SHA3_224: ++ return "sha3-224"; ++ case Code::SHA3_256: ++ return "sha3-256"; ++ case Code::SHA3_384: ++ return "sha3-384"; ++ case Code::SHA3_512: ++ return "sha3-512"; ++ case Code::RAW: ++ return "raw"; ++ case Code::DAG_PB: ++ return "dag-pb"; ++ case Code::DAG_CBOR: ++ return "dag-cbor"; ++ case Code::DAG_JSON: ++ return "dag-json"; ++ case Code::LIBP2P_KEY: ++ return "libp2p-key"; ++ case Code::FILECOIN_COMMITMENT_UNSEALED: ++ return "fil-commitment-unsealed"; ++ case Code::FILECOIN_COMMITMENT_SEALED: ++ return "fil-commitment-sealed"; ++ } ++ return "unknown"; ++ } ++}; ++ ++} // namespace libp2p::multi ++ ++#endif // LIBP2P_MULTICODECTYPE_HPP +diff --git a/third_party/ipfs_client/include/libp2p/multi/uvarint.hpp b/third_party/ipfs_client/include/libp2p/multi/uvarint.hpp +new file mode 100644 +index 0000000000000..4dd452abffba4 +--- /dev/null ++++ b/third_party/ipfs_client/include/libp2p/multi/uvarint.hpp +@@ -0,0 +1,98 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef LIBP2P_VARINT_HPP ++#define LIBP2P_VARINT_HPP ++ ++#include "vocab/byte_view.h" ++ ++#include ++ ++#include ++#include ++#include ++ ++namespace libp2p::multi { ++ ++/** ++ * @class Encodes and decodes unsigned integers into and from ++ * variable-length byte arrays using LEB128 algorithm. ++ */ ++class UVarint { ++ public: ++ /** ++ * Constructs a varint from an unsigned integer 'number' ++ * @param number ++ */ ++ explicit UVarint(uint64_t number); ++ ++ /** ++ * Constructs a varint from an array of raw bytes, which are ++ * meant to be an already encoded unsigned varint ++ * @param varint_bytes an array of bytes representing an unsigned varint ++ */ ++ explicit UVarint(ipfs::ByteView varint_bytes); ++ ++ /** ++ * Constructs a varint from an array of raw bytes, which beginning may or ++ * may not be an encoded varint ++ * @param varint_bytes an array of bytes, possibly representing an unsigned ++ * varint ++ */ ++ static std::optional create(ipfs::ByteView varint_bytes); ++ ++ /** ++ * Converts a varint back to a usual unsigned integer. ++ * @return an integer previously encoded to the varint ++ */ ++ uint64_t toUInt64() const; ++ ++ /** ++ * @return an array view to raw bytes of the stored varint ++ */ ++ ipfs::ByteView toBytes() const; ++ ++ std::vector const& toVector() const; ++ ++ std::string toHex() const; ++ ++ /** ++ * Assigns the varint to an unsigned integer, encoding the latter ++ * @param n the integer to encode and store ++ * @return this varint ++ */ ++ UVarint& operator=(uint64_t n); ++ ++ bool operator==(const UVarint& r) const; ++ bool operator!=(const UVarint& r) const; ++ bool operator<(const UVarint& r) const; ++ ++ /** ++ * @return the number of bytes currently stored in a varint ++ */ ++ size_t size() const; ++ ++ /** ++ * @param varint_bytes an array with a raw byte representation of a varint ++ * @return the size of the varint stored in the array, if its content is a ++ * valid varint. Otherwise, the result is undefined ++ */ ++ static size_t calculateSize(ipfs::ByteView varint_bytes); ++ ++ UVarint() = delete; ++ UVarint(UVarint const&); ++ UVarint& operator=(UVarint const&); ++ ~UVarint() noexcept; ++ ++ private: ++ /// private ctor for unsafe creation ++ UVarint(ipfs::ByteView varint_bytes, size_t varint_size); ++ ++ std::vector bytes_{}; ++}; ++ ++} // namespace libp2p::multi ++ ++#endif // LIBP2P_VARINT_HPP +diff --git a/third_party/ipfs_client/include/multibase/algorithm.h b/third_party/ipfs_client/include/multibase/algorithm.h +new file mode 100644 +index 0000000000000..2cea1cabd296e +--- /dev/null ++++ b/third_party/ipfs_client/include/multibase/algorithm.h +@@ -0,0 +1,27 @@ ++#pragma once ++ ++#include ++ ++namespace multibase { ++ ++class algorithm { ++ public: ++ /** Tag identifying algorithms which operate on blocks */ ++ class block_tag {}; ++ ++ /** Tag identifying algorithms which operate on continuous data */ ++ class stream_tag {}; ++ ++ virtual ~algorithm() = default; ++ ++ /** Returns the input size required to decode a single block */ ++ virtual std::size_t block_size() { return 0; } ++ ++ /** Returns the size of a processed block */ ++ virtual std::size_t output_size() { return 0; } ++ ++ /** Processes an input block returning any intermediate result */ ++ virtual std::string process(std::string_view input) = 0; ++}; ++ ++} // namespace multibase +diff --git a/third_party/ipfs_client/include/multibase/basic_algorithm.h b/third_party/ipfs_client/include/multibase/basic_algorithm.h +new file mode 100644 +index 0000000000000..5da225c885fd4 +--- /dev/null ++++ b/third_party/ipfs_client/include/multibase/basic_algorithm.h +@@ -0,0 +1,322 @@ ++/* From: https://github.com/lockblox/multibase ++ * Copyright (c) 2018 markovchainy ++ * MIT License ++ */ ++#pragma once ++ ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++ ++namespace multibase { ++ ++template ++struct traits { ++ static const std::array charset; ++ static const char name[]; ++ static const char padding = 0; ++ using execution_style = algorithm::block_tag; ++}; ++ ++/** Template implementation of base encoding which computes a lookup table at ++ * compile time and avoids the virtual algorithm lookup penalty */ ++template > ++class basic_algorithm { ++ public: ++ class encoder : public algorithm { ++ public: ++ size_t output_size() override; ++ size_t block_size() override; ++ std::string process(std::string_view input) override; ++ ++ private: ++ constexpr size_t input_size() { return ratio.den; } ++ }; ++ ++ class decoder : public algorithm { ++ public: ++ size_t output_size() override; ++ size_t block_size() override; ++ std::string process(std::string_view input) override; ++ ++ private: ++ constexpr size_t input_size() { return ratio.num; } ++ }; ++ ++ private: ++ constexpr static auto first = Traits::charset.cbegin(); ++ constexpr static auto last = Traits::charset.cend(); ++ using CharsetT = decltype(Traits::charset); ++ using value_type = typename CharsetT::value_type; ++ using iterator = typename CharsetT::const_iterator; ++ ++ /** Find a value at compile time */ ++ constexpr static iterator find(iterator b, iterator e, ++ value_type const& v) noexcept { ++ return (b != e && *b != v) ? find(++b, e, v) : b; ++ } ++ ++ /** Determine the character encoding for a given value ++ @return character encoding, or xFF if none such encoding exists */ ++ constexpr static unsigned char getval(unsigned char p) noexcept { ++ return find(first, last, p) == last ++ ? static_cast(255) ++ : static_cast( ++ std::distance(first, find(first, last, p))); ++ } ++ ++ /** Compute base-2 logarithm */ ++ constexpr static std::intmax_t log2(std::intmax_t n) noexcept { ++ return (n == 1) ? 0 : ((n < 2) ? 1 : 1 + log2(n / 2)); ++ } ++ ++ /** encoding as determined by size of character set */ ++ constexpr static auto radix = sizeof(Traits::charset) / sizeof(value_type); ++ /** Ratio of encoded characters per byte */ ++ constexpr static auto ratio = std::ratio{}; ++ /** Map from value to corresponding character in base encoding */ ++ static const std::array valset; ++ ++ constexpr static auto base = T; ++}; ++ ++template ++const std::array basic_algorithm::valset = { ++ getval(0), getval(1), getval(2), getval(3), getval(4), ++ getval(5), getval(6), getval(7), getval(8), getval(9), ++ getval(10), getval(11), getval(12), getval(13), getval(14), ++ getval(15), getval(16), getval(17), getval(18), getval(19), ++ getval(20), getval(21), getval(22), getval(23), getval(24), ++ getval(25), getval(26), getval(27), getval(28), getval(29), ++ getval(30), getval(31), getval(32), getval(33), getval(34), ++ getval(35), getval(36), getval(37), getval(38), getval(39), ++ getval(40), getval(41), getval(42), getval(43), getval(44), ++ getval(45), getval(46), getval(47), getval(48), getval(49), ++ getval(50), getval(51), getval(52), getval(53), getval(54), ++ getval(55), getval(56), getval(57), getval(58), getval(59), ++ getval(60), getval(61), getval(62), getval(63), getval(64), ++ getval(65), getval(66), getval(67), getval(68), getval(69), ++ getval(70), getval(71), getval(72), getval(73), getval(74), ++ getval(75), getval(76), getval(77), getval(78), getval(79), ++ getval(80), getval(81), getval(82), getval(83), getval(84), ++ getval(85), getval(86), getval(87), getval(88), getval(89), ++ getval(90), getval(91), getval(92), getval(93), getval(94), ++ getval(95), getval(96), getval(97), getval(98), getval(99), ++ getval(100), getval(101), getval(102), getval(103), getval(104), ++ getval(105), getval(106), getval(107), getval(108), getval(109), ++ getval(110), getval(111), getval(112), getval(113), getval(114), ++ getval(115), getval(116), getval(117), getval(118), getval(119), ++ getval(120), getval(121), getval(122), getval(123), getval(124), ++ getval(125), getval(126), getval(127), getval(128), getval(129), ++ getval(130), getval(131), getval(132), getval(133), getval(134), ++ getval(135), getval(136), getval(137), getval(138), getval(139), ++ getval(140), getval(141), getval(142), getval(143), getval(144), ++ getval(145), getval(146), getval(147), getval(148), getval(149), ++ getval(150), getval(151), getval(152), getval(153), getval(154), ++ getval(155), getval(156), getval(157), getval(158), getval(159), ++ getval(160), getval(161), getval(162), getval(163), getval(164), ++ getval(165), getval(166), getval(167), getval(168), getval(169), ++ getval(170), getval(171), getval(172), getval(173), getval(174), ++ getval(175), getval(176), getval(177), getval(178), getval(179), ++ getval(180), getval(181), getval(182), getval(183), getval(184), ++ getval(185), getval(186), getval(187), getval(188), getval(189), ++ getval(190), getval(191), getval(192), getval(193), getval(194), ++ getval(195), getval(196), getval(197), getval(198), getval(199), ++ getval(200), getval(201), getval(202), getval(203), getval(204), ++ getval(205), getval(206), getval(207), getval(208), getval(209), ++ getval(210), getval(211), getval(212), getval(213), getval(214), ++ getval(215), getval(216), getval(217), getval(218), getval(219), ++ getval(220), getval(221), getval(222), getval(223), getval(224), ++ getval(225), getval(226), getval(227), getval(228), getval(229), ++ getval(230), getval(231), getval(232), getval(233), getval(234), ++ getval(235), getval(236), getval(237), getval(238), getval(239), ++ getval(240), getval(241), getval(242), getval(243), getval(244), ++ getval(245), getval(246), getval(247), getval(248), getval(249), ++ getval(250), getval(251), getval(252), getval(253), getval(254), ++ getval(255)}; ++ ++template ++std::string basic_algorithm::encoder::process( ++ std::string_view input) { ++ std::string output; ++ std::size_t isize = input.size(); ++ auto partial_blocks = static_cast(input.size()) / input_size(); ++ auto num_blocks = static_cast(partial_blocks); ++ auto osize = static_cast(std::ceil(partial_blocks * output_size())); ++ if constexpr (std::is_same_v) { ++ num_blocks = static_cast(std::ceil(partial_blocks)); ++ isize = input_size() * num_blocks; ++ } ++ output.resize(std::max(osize, (output_size() * num_blocks))); ++ auto input_it = std::begin(input); ++ int length = 0; ++ for (std::size_t i = 0; i < isize; ++i, ++input_it) { ++ int carry = i >= input.size() ? 0 : static_cast(*input_it); ++ int j = 0; ++ for (auto oi = output.rbegin(); ++ (oi != output.rend()) && (carry != 0 || j < length); ++oi, ++j) { ++ carry += 256 * (*oi); ++ auto byte = (unsigned char*)(&(*oi)); ++ *byte = carry % radix; ++ carry /= radix; ++ } ++ length = j; ++ } ++ std::transform(output.rbegin(), output.rend(), output.rbegin(), ++ [](auto c) { return Traits::charset[c]; }); ++ if constexpr (Traits::padding == 0) { ++ output.resize(osize); ++ } else { ++ auto pad_size = output.size() - osize; ++ output.replace(osize, pad_size, pad_size, Traits::padding); ++ } ++ if constexpr (std::is_same_v) { ++ output.erase(0, output.size() % output_size() ? output.size() - length : 0); ++ } ++ return output; ++} ++ ++template ++std::size_t basic_algorithm::encoder::block_size() { ++ return std::is_same_v ++ ? input_size() ++ : 0; ++} ++ ++template ++std::size_t basic_algorithm::encoder::output_size() { ++ return ratio.num; ++} ++ ++template ++std::size_t basic_algorithm::decoder::block_size() { ++ return std::is_same_v ++ ? input_size() ++ : 0; ++} ++ ++template ++std::size_t basic_algorithm::decoder::output_size() { ++ return ratio.den; ++} ++ ++template ++std::string basic_algorithm::decoder::process( ++ std::string_view input) { ++ std::string output; ++ auto end = std::find(input.begin(), input.end(), Traits::padding); ++ size_t input_size = std::distance(input.begin(), end); ++ auto partial_blocks = static_cast(input_size) / this->input_size(); ++ auto output_size = static_cast(this->output_size() * partial_blocks); ++ if constexpr (std::is_same_v) { ++ std::size_t num_blocks = 0; ++ auto input_size_float = static_cast(input.size()); ++ num_blocks = ++ static_cast(std::ceil(input_size_float / this->input_size())); ++ output.resize(this->output_size() * num_blocks); ++ input_size = this->input_size() * num_blocks; ++ } else { ++ output.resize(output_size); ++ } ++ auto input_it = input.begin(); ++ for (size_t i = 0; i < input_size; ++i, ++input_it) { ++ int carry = i > input.size() || *input_it == Traits::padding ++ ? 0 ++ : valset[(unsigned char)(*input_it)]; ++ if (carry == 255) { ++ // throw std::invalid_argument(std::string{"Invalid input character ++ // "} + *input_it); ++ return {}; ++ } ++ auto j = output.size(); ++ while (carry != 0 || j > 0) { ++ auto index = j - 1; ++ carry += radix * static_cast(output[index]); ++ output[index] = static_cast(carry % 256); ++ carry /= 256; ++ if (carry > 0 && index == 0) { ++ output.insert(0, 1, 0); ++ } else { ++ j = index; ++ } ++ } ++ } ++ if constexpr (std::is_same_v) { ++ output.erase(output_size, output.size()); ++ } ++ return output; ++} ++ ++template <> ++struct traits { ++ constexpr static const std::array charset = { ++ '0', '1', '2', '3', '4', '5', '6', '7', ++ '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; ++ constexpr static const char name[] = "base_16"; ++ using execution_style = algorithm::block_tag; ++ constexpr static const char padding = 0; ++}; ++using base_16 = basic_algorithm; ++ ++template <> ++struct traits { ++ constexpr static const std::array charset = { ++ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ++ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', ++ 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', ++ 'u', 'v', 'w', 'x', 'y', 'z'}; ++ constexpr static const char name[] = "base_36"; ++ using execution_style = algorithm::stream_tag; ++ constexpr static const char padding = 0; ++}; ++using base_36_btc = basic_algorithm; ++ ++ ++template <> ++struct traits { ++ constexpr static const std::array charset = { ++ '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', ++ 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', ++ 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', ++ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; ++ constexpr static const char name[] = "base_58_btc"; ++ using execution_style = algorithm::stream_tag; ++ constexpr static const char padding = 0; ++}; ++using base_58_btc = basic_algorithm; ++ ++template <> ++struct traits { ++ constexpr static const std::array charset = { ++ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', ++ 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ++ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', ++ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ++ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}; ++ constexpr static const char name[] = "base_64_pad"; ++ using execution_style = algorithm::block_tag; ++ constexpr static const char padding = '='; ++}; ++using base_64_pad = basic_algorithm; ++ ++template <> ++struct traits { ++ constexpr static const std::array charset = { ++ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', ++ 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ++ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', ++ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ++ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}; ++ constexpr static const char name[] = "base_64"; ++ using base_64 = basic_algorithm; ++ using execution_style = algorithm::block_tag; ++ constexpr static const char padding = 0; ++}; ++using base_64 = basic_algorithm; ++ ++} // namespace multibase +diff --git a/third_party/ipfs_client/include/multibase/encoding.h b/third_party/ipfs_client/include/multibase/encoding.h +new file mode 100644 +index 0000000000000..7675ca6e8445a +--- /dev/null ++++ b/third_party/ipfs_client/include/multibase/encoding.h +@@ -0,0 +1,21 @@ ++#pragma once ++#include ++#include ++ ++namespace multibase { ++ ++enum class encoding : unsigned char { ++ base_unknown = '?', ++ base_256 = 0, ++ base_16 = 'f', ++ base_16_upper = 'F', ++ base_32 = 'b', ++ base_32_upper = 'B', ++ base_36 = 'k', ++ base_58_btc = 'Z', ++ base_64 = 'm', ++ base_64_pad = 'M' ++ ++}; ++ ++} // namespace multibase +diff --git a/third_party/ipfs_client/include/smhasher/MurmurHash3.h b/third_party/ipfs_client/include/smhasher/MurmurHash3.h +new file mode 100644 +index 0000000000000..e1c6d34976c6a +--- /dev/null ++++ b/third_party/ipfs_client/include/smhasher/MurmurHash3.h +@@ -0,0 +1,37 @@ ++//----------------------------------------------------------------------------- ++// MurmurHash3 was written by Austin Appleby, and is placed in the public ++// domain. The author hereby disclaims copyright to this source code. ++ ++#ifndef _MURMURHASH3_H_ ++#define _MURMURHASH3_H_ ++ ++//----------------------------------------------------------------------------- ++// Platform-specific functions and macros ++ ++// Microsoft Visual Studio ++ ++#if defined(_MSC_VER) && (_MSC_VER < 1600) ++ ++typedef unsigned char uint8_t; ++typedef unsigned int uint32_t; ++typedef unsigned __int64 uint64_t; ++ ++// Other compilers ++ ++#else // defined(_MSC_VER) ++ ++#include ++ ++#endif // !defined(_MSC_VER) ++ ++//----------------------------------------------------------------------------- ++ ++void MurmurHash3_x86_32 ( const void * key, int len, uint32_t seed, void * out ); ++ ++void MurmurHash3_x86_128 ( const void * key, int len, uint32_t seed, void * out ); ++ ++void MurmurHash3_x64_128 ( const void * key, int len, uint32_t seed, void * out ); ++ ++//----------------------------------------------------------------------------- ++ ++#endif // _MURMURHASH3_H_ +diff --git a/third_party/ipfs_client/include/vocab/byte.h b/third_party/ipfs_client/include/vocab/byte.h +new file mode 100644 +index 0000000000000..17477c11c2a6f +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/byte.h +@@ -0,0 +1,43 @@ ++#ifndef IPFS_BYTE_H_ ++#define IPFS_BYTE_H_ ++ ++#include ++#include ++ ++#include ++#include ++#include ++ ++#ifdef __cpp_lib_byte ++ ++namespace ipfs { ++using Byte = std::byte; ++} // namespace ipfs ++ ++#else ++namespace ipfs { ++enum class Byte : std::uint_least8_t {}; ++} // namespace ipfs ++#endif ++ ++namespace { ++[[maybe_unused]] std::ostream& operator<<(std::ostream& str, ipfs::Byte b) { ++ return str << std::hex << std::setw(2) << std::setfill('0') ++ << static_cast(b); ++} ++} // namespace ++ ++namespace { ++// libc++ provides this, but for some reason libstdc++ does not ++[[maybe_unused]] std::uint8_t to_integer(ipfs::Byte b) { ++ return static_cast(b); ++} ++} // namespace ++ ++namespace ipfs { ++inline bool operator==(Byte a, Byte b) { ++ return to_integer(a) == to_integer(b); ++} ++} // namespace ipfs ++ ++#endif // IPFS_BYTE_H_ +diff --git a/third_party/ipfs_client/include/vocab/byte_view.h b/third_party/ipfs_client/include/vocab/byte_view.h +new file mode 100644 +index 0000000000000..69858d1972a30 +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/byte_view.h +@@ -0,0 +1,24 @@ ++#ifndef CHROMIUM_IPFS_BYTE_VIEW_H ++#define CHROMIUM_IPFS_BYTE_VIEW_H ++ ++#include "byte.h" ++#include "span.h" ++ ++#include ++ ++namespace ipfs { ++using ByteView = span; ++ ++// ByteView is a view over arbitrary opaque byte ++// Cast it to a view over 8-bit unsigned integers for inspection ++inline span as_octets(ByteView bytes) { ++ return {reinterpret_cast(bytes.data()), bytes.size()}; ++} ++template ++inline ByteView as_bytes(ContiguousBytes const& b) { ++ auto p = reinterpret_cast(b.data()); ++ return ByteView{p, b.size()}; ++} ++} // namespace ipfs ++ ++#endif // CHROMIUM_IPFS_BYTE_VIEW_H +diff --git a/third_party/ipfs_client/include/vocab/endian.h b/third_party/ipfs_client/include/vocab/endian.h +new file mode 100644 +index 0000000000000..2423006c7c02b +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/endian.h +@@ -0,0 +1,21 @@ ++#ifndef IPFS_ENDIAN_H_ ++#define IPFS_ENDIAN_H_ ++ ++#if __has_include() ++#include ++#endif ++#if __has_include() ++#include ++#endif ++ ++#ifdef htobe64 ++// Good ++#elif __has_include() ++#include ++#define htobe64 absl::ghtonll ++#elif __has_include() ++#include ++#define htobe64 native_to_big ++#endif ++ ++#endif // IPFS_ENDIAN_H_ +diff --git a/third_party/ipfs_client/include/vocab/expected.h b/third_party/ipfs_client/include/vocab/expected.h +new file mode 100644 +index 0000000000000..2006f2bf01397 +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/expected.h +@@ -0,0 +1,44 @@ ++#ifndef IPFS_EXPECTED_H_ ++#define IPFS_EXPECTED_H_ ++ ++// std::expected isn't available until C++23 and we need to support C++17 ++// boost::outcome isn't available inside the Chromium tree ++// absl::StatusOr doesn't allow templating or extending the error type, and ++// translating the specific error codes into generic ones isn't great. ++ ++#if __has_include("base/types/expected.h") ++#include "base/types/expected.h" ++namespace ipfs { ++template ++using expected = base::expected; ++template ++using unexpected = base::unexpected; ++} // namespace ipfs ++#elif __has_cpp_attribute(__cpp_lib_expected) ++ ++#include ++namespace ipfs { ++template ++using expected = std::expected; ++template ++using unexpected = std::unexpected; ++} // namespace ipfs ++ ++#elif __has_include() ++ ++// If the API differences between std::expected and boost::outcome::checked ++// become a problem, consider wrapping as proposed in the FAQ: ++// https://www.boost.org/doc/libs/master/libs/outcome/doc/html/faq.html#how-far-away-from-the-proposed-std-expected-t-e-is-outcome-s-checked-t-e ++#include ++namespace ipfs { ++template ++using expected = boost::outcome_v2::checked; ++template ++using unexpected = Error; ++} // namespace ipfs ++ ++#else ++#error Get an expected implementation ++#endif ++ ++#endif // IPFS_EXPECTED_H_ +diff --git a/third_party/ipfs_client/include/vocab/flat_mapset.h b/third_party/ipfs_client/include/vocab/flat_mapset.h +new file mode 100644 +index 0000000000000..1630e3f9ca358 +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/flat_mapset.h +@@ -0,0 +1,39 @@ ++#ifndef CHROMIUM_IPFS_VOCAB_MAP_SET_H_ ++#define CHROMIUM_IPFS_VOCAB_MAP_SET_H_ ++ ++#if __has_include("base/containers/flat_map.h") // Chromium ++ ++#include "base/containers/flat_map.h" ++#include "base/containers/flat_set.h" ++#include "base/debug/debugging_buildflags.h" ++namespace ipfs { ++using base::flat_map; ++using base::flat_set; ++} // namespace ipfs ++ ++#elif __has_cpp_attribute(__cpp_lib_flat_map) && \ ++ __has_cpp_attribute(__cpp_lib_flat_set) ++ ++#include ++#include ++namespace ipfs { ++using std::flat_map; ++using std::flat_set; ++} // namespace ipfs ++ ++#elif __has_include() //Boost ++#include ++#include ++namespace ipfs { ++using boost::container::flat_map; ++using boost::container::flat_set; ++} // namespace ipfs ++ ++#else ++ ++#error \ ++ "Provide an implementation for flat_map and flat_set, or install boost or have a Chromium tree or use a newer C++ version." ++ ++#endif ++ ++#endif // CHROMIUM_IPFS_VOCAB_MAP_SET_H_ +diff --git a/third_party/ipfs_client/include/vocab/html_escape.h b/third_party/ipfs_client/include/vocab/html_escape.h +new file mode 100644 +index 0000000000000..60339ad7d45bd +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/html_escape.h +@@ -0,0 +1,23 @@ ++#ifndef IPFS_HTML_ESCAPE_H_ ++#define IPFS_HTML_ESCAPE_H_ ++ ++#include ++ ++constexpr inline std::string_view html_escape(char& c) { ++ switch (c) { ++ case '"': ++ return """; ++ case '\'': ++ return "'"; ++ case '<': ++ return "<"; ++ case '>': ++ return ">"; ++ case '&': ++ return "&"; ++ default: ++ return {&c, 1UL}; ++ } ++} ++ ++#endif // IPFS_HTML_ESCAPE_H_ +diff --git a/third_party/ipfs_client/include/vocab/i128.h b/third_party/ipfs_client/include/vocab/i128.h +new file mode 100644 +index 0000000000000..4aa36cc09877f +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/i128.h +@@ -0,0 +1,16 @@ ++#ifndef IPFS_I128_H_ ++#define IPFS_I128_H_ ++ ++#if __has_include() ++#include ++namespace ipfs { ++using Int_128 = absl::int128; ++} ++#else ++namespace ipfs { ++// TODO Check if available, if not use boost multiprecision ++using Int_128 = __int128; ++} // namespace ipfs ++#endif ++ ++#endif // IPFS_I128_H_ +diff --git a/third_party/ipfs_client/include/vocab/raw_ptr.h b/third_party/ipfs_client/include/vocab/raw_ptr.h +new file mode 100644 +index 0000000000000..25405d3ea30ba +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/raw_ptr.h +@@ -0,0 +1,64 @@ ++#ifndef IPFS_OBSERVER_PTR_H_ ++#define IPFS_OBSERVER_PTR_H_ ++ ++#if __has_include("base/memory/raw_ptr.h") ++#include "base/memory/raw_ptr.h" ++ ++namespace ipfs { ++template ++using raw_ptr = base::raw_ptr; ++} ++ ++#elif defined(__has_cpp_attribute) && \ ++ __has_cpp_attribute(__cpp_lib_experimental_observer_ptr) ++#include ++ ++namespace ipfs { ++template ++using raw_ptr = std::experimental::observer_ptr; ++} ++ ++#else ++ ++#include ++ ++namespace ipfs { ++ ++/*! ++ * \brief Just an observing (non-owning) pointer. ++ */ ++template ++class raw_ptr { ++ T* ptr_; ++ ++ public: ++ // Chromium's raw_ptr has a default ctor whose semantics depend on build ++ // config. For components/ipfs purposes, there is no reason to ever default ++ // construct. Set it to nullptr. We have time needed to read_start a word. ++ raw_ptr() = delete; ++ ++ raw_ptr(T* p) : ptr_{p} {} ++ raw_ptr(raw_ptr&&) = default; ++ raw_ptr(raw_ptr const&) = default; ++ ++ raw_ptr& operator=(raw_ptr const&) = default; ++ ++ T* get() { return ptr_; } ++ T const* get() const { return ptr_; } ++ explicit operator bool() const { return !!ptr_; } ++ T* operator->() { return ptr_; } ++ T const* operator->() const { return ptr_; } ++ raw_ptr& operator=(T* p) { ++ ptr_ = p; ++ return *this; ++ } ++ T& operator*() { ++ assert(ptr_); ++ return *ptr_; ++ } ++}; ++} // namespace ipfs ++ ++#endif ++ ++#endif // IPFS_OBSERVER_PTR_H_ +diff --git a/third_party/ipfs_client/include/vocab/slash_delimited.h b/third_party/ipfs_client/include/vocab/slash_delimited.h +new file mode 100644 +index 0000000000000..53fd142465028 +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/slash_delimited.h +@@ -0,0 +1,35 @@ ++#ifndef IPFS_SLASH_DELIMITED_H_ ++#define IPFS_SLASH_DELIMITED_H_ ++ ++#include ++#include ++#include ++ ++namespace google::protobuf::internal { ++class LogMessage; ++} ++ ++namespace ipfs { ++struct SlashDelimited { ++ std::string_view remainder_; ++ ++ public: ++ SlashDelimited() : remainder_{""} {} ++ explicit SlashDelimited(std::string_view unowned); ++ explicit operator bool() const; ++ std::string_view pop(); ++ std::string_view pop_all(); ++ std::string_view pop_n(std::size_t); ++ std::string_view peek_back() const; ++ std::string pop_back(); ++ std::string to_string() const { return std::string{remainder_}; } ++ std::string_view to_view() const { return remainder_; } ++}; ++} // namespace ipfs ++ ++std::ostream& operator<<(std::ostream&, ipfs::SlashDelimited const&); ++google::protobuf::internal::LogMessage& operator<<( ++ google::protobuf::internal::LogMessage&, ++ ipfs::SlashDelimited const&); ++ ++#endif // IPFS_SLASH_DELIMITED_H_ +diff --git a/third_party/ipfs_client/include/vocab/span.h b/third_party/ipfs_client/include/vocab/span.h +new file mode 100644 +index 0000000000000..f9c05d2a7dd61 +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/span.h +@@ -0,0 +1,65 @@ ++#ifndef IPFS_SPAN_H_ ++#define IPFS_SPAN_H_ ++ ++#if __cpp_lib_span ++#include ++ ++namespace ipfs { ++template ++using span = std::span; ++} // namespace ipfs ++ ++#elif __has_include("base/containers/span.h") ++ ++#include "base/containers/span.h" ++namespace ipfs { ++template ++using span = base::span; ++} // namespace ipfs ++ ++#elif __has_include() ++ ++#include ++namespace ipfs { ++template ++using span = absl::Span; ++} // namespace ipfs ++ ++#elif __has_include() ++ ++#include ++namespace ipfs { ++template ++using span = boost::span; ++} // namespace ipfs ++ ++#elif __has_include() ++ ++// Prior to Boost 1.78, span did not exist in core yet ++#include ++#include ++namespace ipfs { ++template ++class span : public boost::beast::span { ++ public: ++ span(Value* d, std::size_t n) : boost::beast::span{d, n} {} ++ ++ template ++ span(std::vector const& v) ++ : boost::beast::span{v.data(), v.size()} {} ++ ++ span subspan(std::size_t off) const { ++ return span{this->data() + off, this->size() - off}; ++ } ++ Value& operator[](std::size_t i) { return this->data()[i]; } ++}; ++} // namespace ipfs ++ ++#else ++ ++#error \ ++ "No good implementation of span available. Implement one, move to a newer C++, or provide Boost or Abseil." ++ ++#endif ++ ++#endif // IPFS_SPAN_H_ +diff --git a/third_party/ipfs_client/include/vocab/stringify.h b/third_party/ipfs_client/include/vocab/stringify.h +new file mode 100644 +index 0000000000000..8572ebfef7165 +--- /dev/null ++++ b/third_party/ipfs_client/include/vocab/stringify.h +@@ -0,0 +1,17 @@ ++#ifndef IPFS_STRINGIFY_H_ ++#define IPFS_STRINGIFY_H_ ++ ++#include ++ ++namespace ipfs { ++namespace { ++template ++std::string Stringify(T const& t) { ++ std::ostringstream oss; ++ oss << t; ++ return oss.str(); ++} ++} // namespace ++} // namespace ipfs ++ ++#endif // IPFS_STRINGIFY_H_ +diff --git a/third_party/ipfs_client/ipns_record.proto b/third_party/ipfs_client/ipns_record.proto +new file mode 100644 +index 0000000000000..6018931b7466f +--- /dev/null ++++ b/third_party/ipfs_client/ipns_record.proto +@@ -0,0 +1,38 @@ ++syntax = "proto2"; ++option optimize_for = LITE_RUNTIME; ++package ipfs.ipns; ++ ++message IpnsEntry { ++ enum ValidityType { ++ // setting an EOL says "this record is valid until..." ++ EOL = 0; ++ } ++ ++ // deserialized copy of data[value] ++ optional bytes value = 1; ++ ++ // legacy field, verify 'signatureV2' instead ++ optional bytes signatureV1 = 2; ++ ++ // deserialized copies of data[validityType] and data[validity] ++ optional ValidityType validityType = 3; ++ optional bytes validity = 4; ++ ++ // deserialized copy of data[sequence] ++ optional uint64 sequence = 5; ++ ++ // record TTL in nanoseconds, a deserialized copy of data[ttl] ++ optional uint64 ttl = 6; ++ ++ // in order for nodes to properly validate a record upon receipt, they need the public ++ // key associated with it. For old RSA keys, its easiest if we just send this as part of ++ // the record itself. For newer Ed25519 keys, the public key can be embedded in the ++ // IPNS Name itself, making this field unnecessary. ++ optional bytes pubKey = 7; ++ ++ // the signature of the IPNS record ++ optional bytes signatureV2 = 8; ++ ++ // extensible record data in DAG-CBOR format ++ optional bytes data = 9; ++} +diff --git a/third_party/ipfs_client/keys.proto b/third_party/ipfs_client/keys.proto +new file mode 100644 +index 0000000000000..a6f4f75ddba93 +--- /dev/null ++++ b/third_party/ipfs_client/keys.proto +@@ -0,0 +1,22 @@ ++syntax = "proto2"; ++option optimize_for = LITE_RUNTIME; ++package ipfs.ipns; ++ ++enum KeyType { ++ RSA = 0; ++ Ed25519 = 1; ++ Secp256k1 = 2; ++ ECDSA = 3; ++} ++ ++// PublicKey ++message PublicKey { ++ required KeyType Type = 1; ++ required bytes Data = 2; ++} ++ ++// PrivateKey ++message PrivateKey { ++ required KeyType Type = 1; ++ required bytes Data = 2; ++} +diff --git a/third_party/ipfs_client/pb_dag.proto b/third_party/ipfs_client/pb_dag.proto +new file mode 100644 +index 0000000000000..5cd027631c6de +--- /dev/null ++++ b/third_party/ipfs_client/pb_dag.proto +@@ -0,0 +1,23 @@ ++syntax = "proto2"; ++option optimize_for = LITE_RUNTIME; ++package ipfs.pb_dag; ++ ++message PBLink { ++ // binary CID (with no multibase prefix) of the target object ++ optional bytes Hash = 1; ++ ++ // UTF-8 string name ++ optional string Name = 2; ++ ++ // cumulative size of target object ++ optional uint64 Tsize = 3; ++} ++ ++message PBNode { ++ // refs to other objects ++ repeated PBLink Links = 2; ++ ++ // opaque user data ++ optional bytes Data = 1; ++} ++ +diff --git a/third_party/ipfs_client/src/ipfs_client/bases/b16_upper.h b/third_party/ipfs_client/src/ipfs_client/bases/b16_upper.h +new file mode 100644 +index 0000000000000..9d0056ecb98b9 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/bases/b16_upper.h +@@ -0,0 +1,23 @@ ++#ifndef IPFS_B16_UPPER_H_ ++#define IPFS_B16_UPPER_H_ ++ ++#include ++ ++namespace multibase { ++template <> ++struct traits<::multibase::encoding::base_16_upper> { ++ constexpr static const std::array charset = { ++ '0', '1', '2', '3', '4', '5', '6', '7', ++ '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; ++ constexpr static const char name[] = "BASE_16"; ++ using execution_style = multibase::algorithm::block_tag; ++ constexpr static const char padding = 0; ++}; ++} // namespace multibase ++ ++namespace ipfs::mb { ++using base_16_upper = ++ multibase::basic_algorithm; ++} // namespace ipfs::mb ++ ++#endif // IPFS_B16_UPPER_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/bases/b32.h b/third_party/ipfs_client/src/ipfs_client/bases/b32.h +new file mode 100644 +index 0000000000000..9dac14db53ac3 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/bases/b32.h +@@ -0,0 +1,35 @@ ++#ifndef IPFS_B32_UPPER_H_ ++#define IPFS_B32_UPPER_H_ ++ ++#include ++ ++namespace multibase { ++template <> ++struct traits<::multibase::encoding::base_32> { ++ constexpr static const std::array charset = { ++ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', ++ 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', ++ 'w', 'x', 'y', 'z', '2', '3', '4', '5', '6', '7'}; ++ constexpr static const char name[] = "base_32"; ++ using execution_style = multibase::algorithm::block_tag; ++ constexpr static const char padding = 0; ++}; ++template <> ++struct traits<::multibase::encoding::base_32_upper> { ++ constexpr static const std::array charset = { ++ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', ++ 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', ++ 'W', 'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7'}; ++ constexpr static const char name[] = "base_32_upper"; ++ using execution_style = multibase::algorithm::block_tag; ++ constexpr static const char padding = 0; ++}; ++} // namespace multibase ++ ++namespace ipfs::mb { ++using base_32 = multibase::basic_algorithm; ++using base_32_upper = ++ multibase::basic_algorithm; ++} // namespace ipfs::mb ++ ++#endif // IPFS_B32_UPPER_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/block_requestor.cc b/third_party/ipfs_client/src/ipfs_client/block_requestor.cc +new file mode 100644 +index 0000000000000..8a63e6f7ae0cc +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/block_requestor.cc +@@ -0,0 +1 @@ ++#include +diff --git a/third_party/ipfs_client/src/ipfs_client/car.cc b/third_party/ipfs_client/src/ipfs_client/car.cc +new file mode 100644 +index 0000000000000..e36442347415f +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/car.cc +@@ -0,0 +1,132 @@ ++#include "car.h" ++ ++#include ++ ++#include ++ ++#include "log_macros.h" ++ ++#include ++ ++using Self = ipfs::Car; ++using Byte = ipfs::Byte; ++using ByteView = ipfs::ByteView; ++using VarInt = libp2p::multi::UVarint; ++ ++namespace { ++short ReadHeader(ByteView&, ipfs::ContextApi const&); ++std::pair GetV1PayloadPos(ByteView); ++} // namespace ++ ++Self::Car(ByteView bytes, ContextApi const& api) { ++ auto after_header = bytes; ++ auto version = ReadHeader(after_header, api); ++ switch (version) { ++ case 0: ++ LOG(ERROR) << "Problem parsing CAR header."; ++ break; ++ case 1: ++ LOG(INFO) << "Reading CARv1"; ++ data_ = after_header; ++ break; ++ case 2: { ++ auto [off, siz] = GetV1PayloadPos(after_header); ++ LOG(INFO) << "CARv2 carries a payload of " << siz << "B @ " << off; ++ // TODO validate off and siz are sane, e.g. not pointing back into pragma ++ // or whatever ++ data_ = bytes.subspan(off, siz); ++ ReadHeader(data_, api); ++ break; ++ } ++ default: ++ LOG(ERROR) << "Unsupported CAR format version " << version; ++ } ++} ++auto Self::NextBlock() -> std::optional { ++ auto len = VarInt::create(data_); ++ if (!len) { ++ return std::nullopt; ++ } ++ data_ = data_.subspan(len->size()); ++ if (len->toUInt64() > data_.size()) { ++ LOG(ERROR) << "Length prefix claims cid+block is " << len->toUInt64() ++ << " bytes, but I only have " << data_.size() ++ << " bytes left in the CAR payload."; ++ data_ = {}; ++ return std::nullopt; ++ } ++ Block rv; ++ rv.bytes = data_.subspan(0U, len->toUInt64()); ++ data_ = data_.subspan(len->toUInt64()); ++ if (rv.cid.ReadStart(rv.bytes)) { ++ // TODO : check hash ++ return rv; ++ } ++ return std::nullopt; ++} ++ ++namespace { ++// https://ipld.io/specs/transport/car/carv2/ ++short ReadHeader(ByteView& bytes, ipfs::ContextApi const& api) { ++ auto header_len = VarInt::create(bytes); ++ if (!header_len || ++ header_len->toUInt64() + header_len->size() > bytes.size()) { ++ return 0; ++ } ++ bytes = bytes.subspan(header_len->size()); ++ auto header_bytes = bytes.subspan(0UL, header_len->toUInt64()); ++ auto header = api.ParseCbor(header_bytes); ++ if (!header) { ++ return 0; ++ } ++ auto version_node = header->at("version"); ++ if (!version_node) { ++ return 0; ++ } ++ auto version = version_node->as_unsigned(); ++ if (version) { ++ bytes = bytes.subspan(header_len->toUInt64()); ++ return version.value(); ++ } ++ return 0; ++} ++std::uint64_t read_le_u64(ByteView bytes, unsigned& off) { ++ auto b = bytes.subspan(off, off + 8); ++ off += 8U; ++ auto shift_in = [](std::uint64_t i, Byte y) { ++ return (i << 8) | static_cast(y); ++ }; ++ return std::accumulate(b.rbegin(), b.rend(), 0UL, shift_in); ++} ++std::pair GetV1PayloadPos(ByteView bytes) { ++ // Following the 11 byte pragma, the CARv2 [header] is a fixed-length sequence ++ // of 40 bytes, broken into the following sections: ++ if (bytes.size() < 40) { ++ return {}; ++ } ++ ++ // Characteristics: A 128-bit (16-byte) bitfield used to describe certain ++ // features of the enclosed data. ++ auto reading_off = 16U; ++ ++ // Data offset: A 64-bit (8-byte) unsigned ++ // little-endian integer indicating the byte-offset from the beginning of the ++ // CARv2 [pragma] to the first byte of the CARv1 data payload. ++ auto data_offset = read_le_u64(bytes, reading_off); ++ ++ // Data size: A 64-bit ++ // (8-byte) unsigned little-endian integer indicating the byte-length of the ++ // CARv1 data payload. ++ auto data_size = read_le_u64(bytes, reading_off); ++ ++ // Index offset: A 64-bit (8-byte) unsigned little-endian ++ // integer indicating the byte-offset from the beginning of the CARv2 to the ++ // first byte of the index payload. This value may be 0 to indicate the ++ // absence of index data. ++ reading_off += 8; // Ignoring index and therefore index offset ++ ++ assert(reading_off == 40UL); ++ ++ return {data_offset, data_size}; ++} ++} // namespace +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/car.h b/third_party/ipfs_client/src/ipfs_client/car.h +new file mode 100644 +index 0000000000000..619ac48ed8cd3 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/car.h +@@ -0,0 +1,26 @@ ++#ifndef IPFS_CAR_H_ ++#define IPFS_CAR_H_ ++ ++#include ++#include ++ ++#include ++#include ++ ++namespace ipfs { ++class ContextApi; ++class Car { ++ public: ++ Car(ByteView, ContextApi const&); ++ struct Block { ++ Cid cid; ++ ByteView bytes; ++ }; ++ std::optional NextBlock(); ++ ++ private: ++ ByteView data_; ++}; ++} // namespace ipfs ++ ++#endif // IPFS_CAR_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/cid.cc b/third_party/ipfs_client/src/ipfs_client/cid.cc +new file mode 100644 +index 0000000000000..b20686086bca6 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/cid.cc +@@ -0,0 +1,86 @@ ++#include ++ ++#include ++#include ++ ++#include "log_macros.h" ++ ++using Self = ipfs::Cid; ++using VarInt = libp2p::multi::UVarint; ++ ++Self::Cid(ipfs::MultiCodec cdc, ipfs::MultiHash hsh) ++ : codec_{cdc}, hash_{hsh} {} ++ ++Self::Cid(ipfs::ByteView bytes) { ++ ReadStart(bytes); ++} ++ ++Self::Cid(std::string_view s) { ++ if (s.size() == 46 && s[0] == 'Q' && s[1] == 'm') { ++ auto bytes = mb::Codec::Get(mb::Code::BASE58_BTC)->decode(s); ++ auto view = ByteView{bytes}; ++ ReadStart(view); ++ } else if (auto bytes = mb::decode(s)) { ++ if (bytes->size() > 4) { ++ auto view = ByteView{bytes.value()}; ++ ReadStart(view); ++ } ++ } else { ++ LOG(WARNING) << "Failed to decode the multibase for a CID: " << s; ++ } ++} ++ ++bool Self::ReadStart(ByteView& bytes) { ++ if (bytes.size() >= 34 && bytes[0] == ipfs::Byte{0x12} && ++ bytes[1] == ipfs::Byte{0x20}) { ++ hash_ = MultiHash{bytes}; ++ codec_ = hash_.valid() ? MultiCodec::DAG_PB : MultiCodec::INVALID; ++ bytes = bytes.subspan(34); ++ return true; ++ } ++ auto version = VarInt::create(bytes); ++ if (!version) { ++ return false; ++ } ++ if (version->toUInt64() != 1U) { ++ LOG(ERROR) << "CID version " << version->toUInt64() << " not supported."; ++ return false; ++ } ++ bytes = bytes.subspan(version->size()); ++ auto codec = VarInt::create(bytes); ++ if (!codec) { ++ return false; ++ } ++ auto cdc = static_cast(codec->toUInt64()); ++ codec_ = Validate(cdc); ++ bytes = bytes.subspan(codec->size()); ++ return hash_.ReadPrefix(bytes); ++} ++ ++bool Self::valid() const { ++ return codec_ != MultiCodec::INVALID && hash_.valid(); ++} ++ ++auto Self::hash() const -> ByteView { ++ return hash_.digest(); ++} ++auto Self::hash_type() const -> HashType { ++ return multi_hash().type(); ++} ++ ++std::string Self::to_string() const { ++ std::vector binary; ++ auto append_varint = [&binary](auto x) { ++ auto i = static_cast(x); ++ VarInt v{i}; ++ auto b = v.toBytes(); ++ binary.insert(binary.end(), b.begin(), b.end()); ++ }; ++ append_varint(1); // CID version 1 ++ append_varint(codec()); ++ append_varint(hash_type()); ++ append_varint(hash().size()); ++ auto h = hash(); ++ binary.insert(binary.end(), h.begin(), h.end()); ++ return mb::encode(mb::Code::BASE32_LOWER, binary); ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/context_api.cc b/third_party/ipfs_client/src/ipfs_client/context_api.cc +new file mode 100644 +index 0000000000000..972e3df2fa320 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/context_api.cc +@@ -0,0 +1,21 @@ ++#include ++ ++#include "crypto/openssl_sha2_256.h" ++ ++using Self = ipfs::ContextApi; ++ ++Self::ContextApi() { ++#if HAS_OPENSSL_SHA ++ hashers_.emplace(HashType::SHA2_256, ++ std::make_unique()); ++#endif ++} ++ ++auto Self::Hash(HashType ht, ByteView data) ++ -> std::optional> { ++ auto it = hashers_.find(ht); ++ if (hashers_.end() == it || !(it->second)) { ++ return std::nullopt; ++ } ++ return it->second->hash(data); ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/crypto/openssl_sha2_256.cc b/third_party/ipfs_client/src/ipfs_client/crypto/openssl_sha2_256.cc +new file mode 100644 +index 0000000000000..ff5c7a24d23bb +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/crypto/openssl_sha2_256.cc +@@ -0,0 +1,32 @@ ++#include "openssl_sha2_256.h" ++ ++using Self = ipfs::crypto::OpensslSha2_256; ++ ++#include "log_macros.h" ++ ++#if HAS_OPENSSL_SHA ++ ++#include ++ ++Self::~OpensslSha2_256() {} ++auto Self::hash(ipfs::ByteView data) -> std::optional> { ++ SHA256_CTX ctx; ++ if (1 != SHA256_Init(&ctx)) { ++ LOG(ERROR) << "Failed to initialize SHA256"; ++ return std::nullopt; ++ } ++ if (1 != SHA256_Update(&ctx, data.data(), data.size())) { ++ LOG(ERROR) << "Failure injesting data into SHA256."; ++ return {}; ++ } ++ std::vector rv(SHA256_DIGEST_LENGTH, Byte{}); ++ auto p = reinterpret_cast(rv.data()); ++ if (1 == SHA256_Final(p, &ctx)) { ++ return rv; ++ } else { ++ LOG(ERROR) << "Error calculating sha2-256 hash."; ++ return std::nullopt; ++ } ++} ++ ++#endif +diff --git a/third_party/ipfs_client/src/ipfs_client/crypto/openssl_sha2_256.h b/third_party/ipfs_client/src/ipfs_client/crypto/openssl_sha2_256.h +new file mode 100644 +index 0000000000000..c4e7bb975d366 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/crypto/openssl_sha2_256.h +@@ -0,0 +1,18 @@ ++#ifndef IPFS_OPENSSL_SHA2_256_H_ ++#define IPFS_OPENSSL_SHA2_256_H_ ++ ++#if __has_include() ++#define HAS_OPENSSL_SHA 1 ++#endif ++ ++#include ++ ++namespace ipfs::crypto { ++class OpensslSha2_256 final : public Hasher { ++ public: ++ ~OpensslSha2_256() noexcept override; ++ std::optional> hash(ByteView) override; ++}; ++} // namespace ipfs::crypto ++ ++#endif // IPFS_OPENSSL_SHA2_256_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/dag_cbor_value.cc b/third_party/ipfs_client/src/ipfs_client/dag_cbor_value.cc +new file mode 100644 +index 0000000000000..20a6fc713ad4e +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/dag_cbor_value.cc +@@ -0,0 +1,69 @@ ++#include ++ ++#include ++ ++#include "log_macros.h" ++ ++#include ++ ++using Self = ipfs::DagCborValue; ++ ++void Self::html(std::ostream& str) const { ++ if (auto u = as_unsigned()) { ++ str << "" << *u << "\n"; ++ } else if (auto si = as_signed()) { ++ str << "" << *si << "\n"; ++ } else if (auto fl = as_float()) { ++ str << "" << *si << "\n"; ++ } else if (auto s = as_string()) { ++ str << "

""; ++ for (auto c : *s) { ++ str << html_escape(c); ++ } ++ str << ""

\n"; ++ } else if (auto cid = as_link()) { ++ auto cs = cid.value().to_string(); ++ if (cs.size()) { ++ str << "
" << cs ++ << "\n"; ++ } else { ++ str << "\n"; ++ } ++ } else if (auto bin = as_bytes()) { ++ str << "

0x"; ++ for (auto b : *bin) { ++ str << ' ' << std::hex << std::setw(2) << std::setfill('0') ++ << static_cast(b); ++ } ++ str << "

\n"; ++ } else if (is_array()) { ++ str << "
    \n"; ++ iterate_array([&str](auto& v) { ++ str << "
  1. \n"; ++ v.html(str); ++ str << "
  2. \n"; ++ }); ++ str << "
\n"; ++ } else if (is_map()) { ++ str << "\n"; ++ iterate_map([&str](auto k, auto& v) { ++ str << " \n"; ++ }); ++ str << "
" << k << "\n"; ++ v.html(str); ++ str << "
\n"; ++ } else if (auto bul = as_bool()) { ++ auto val = (bul.value() ? "True" : "False"); ++ str << " " << val << "\n"; ++ } else { ++ str << "\n"; ++ } ++} ++ ++std::string Self::html() const { ++ std::ostringstream oss; ++ oss << "DAG-CBOR Preview\n"; ++ html(oss); ++ oss << ""; ++ return oss.str(); ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/dag_json_value.cc b/third_party/ipfs_client/src/ipfs_client/dag_json_value.cc +new file mode 100644 +index 0000000000000..12a493cbd92cb +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/dag_json_value.cc +@@ -0,0 +1,22 @@ ++#include ++ ++#include ++ ++using Self = ipfs::DagJsonValue; ++ ++Self::~DagJsonValue() noexcept {} ++auto Self::get_if_link() const -> std::optional { ++ auto slash = (*this)["/"]; ++ if (!slash) { ++ return std::nullopt; ++ } ++ auto str = slash->get_if_string(); ++ if (!str) { ++ return std::nullopt; ++ } ++ auto cid = Cid(*str); ++ if (cid.valid()) { ++ return cid; ++ } ++ return std::nullopt; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/gateways.cc b/third_party/ipfs_client/src/ipfs_client/gateways.cc +new file mode 100644 +index 0000000000000..fe49949b10537 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gateways.cc +@@ -0,0 +1,121 @@ ++#include ++ ++#include ++ ++#include "log_macros.h" ++ ++#include ++#include ++#include ++ ++using namespace std::string_literals; ++ ++ipfs::Gateways::Gateways() ++ : random_engine_{std::random_device{}()}, dist_{0.01} { ++ auto gws = DefaultGateways(); ++ for (auto [k, v] : gws) { ++ known_gateways_[k] = v; ++ } ++} ++ipfs::Gateways::~Gateways() {} ++ ++auto ipfs::Gateways::GenerateList() -> GatewayList { ++ GatewayList result; ++ for (auto [k, v] : known_gateways_) { ++ result.push_back({k, v + dist_(random_engine_)}); ++ } ++ std::sort(result.begin(), result.end()); ++ return result; ++} ++ ++void ipfs::Gateways::promote(std::string const& key) { ++ auto it = known_gateways_.find(key); ++ if (known_gateways_.end() == it) { ++ LOG(ERROR) << "Can't promote (" << key ++ << ") because I don't know that one."; ++ } else { ++ auto l = known_gateways_.at(key)++; ++ if (l % (++up_log_ / 2) <= 9) { ++ LOG(INFO) << "Promote(" << key << ")"; ++ } ++ } ++} ++void ipfs::Gateways::demote(std::string const& key) { ++ auto it = known_gateways_.find(key); ++ if (known_gateways_.end() == it) { ++ VLOG(2) << "Can't demote " << key << " as I don't have that gateway."; ++ } else if (it->second) { ++ if (it->second-- % 3 == 0) { ++ LOG(INFO) << "Demote(" << key << ") to " << it->second; ++ } ++ } else { ++ LOG(INFO) << "Demoted(" << key << ") for the last time - dropping."; ++ known_gateways_.erase(it); ++ } ++} ++ ++void ipfs::Gateways::AddGateways(std::vector v) { ++ LOG(INFO) << "AddGateways(" << v.size() << ')'; ++ for (auto& ip : v) { ++ if (ip.empty()) { ++ LOG(ERROR) << "ERROR: Attempted to add empty string as gateway!"; ++ continue; ++ } ++ std::string prefix; ++ if (ip.find("://") == std::string::npos) { ++ prefix = "http://"; ++ prefix.append(ip); ++ } else { ++ prefix = ip; ++ } ++ if (prefix.back() != '/') { ++ prefix.push_back('/'); ++ } ++ if (known_gateways_.insert({prefix, 99}).second) { ++ VLOG(1) << "Adding discovered gateway " << prefix; ++ } ++ } ++} ++ ++auto ipfs::Gateways::DefaultGateways() -> GatewayList { ++ auto* ovr = std::getenv("IPFS_GATEWAY"); ++ if (ovr && *ovr) { ++ std::istringstream user_override{ovr}; ++ GatewayList result; ++ std::string gw; ++ while (user_override >> gw) { ++ if ( gw.empty() ) { ++ continue; ++ } ++ if ( gw.back() != '/' ) { ++ gw.push_back('/'); ++ } ++ result.push_back( {gw, 0} ); ++ } ++ auto N = static_cast(result.size()); ++ for (auto i = 0; i < N; ++i) { ++ auto& r = result[i]; ++ r.strength = N - i; ++ LOG(INFO) << "User-specified gateway: " << r.prefix << '=' << r.strength; ++ } ++ return result; ++ } ++ return {{"http://localhost:8080/"s, 929}, ++ {"https://jcsl.hopto.org/"s, 863}, ++ {"https://human.mypinata.cloud/"s, 798}, ++ {"https://ipfs.io/"s, 753}, ++ {"https://gateway.ipfs.io/"s, 678}, ++ {"https://dweb.link/"s, 598}, ++ {"https://gateway.pinata.cloud/"s, 519}, ++ {"https://ipfs.joaoleitao.org/"s, 434}, ++ {"https://ipfs.runfission.com/"s, 371}, ++ {"https://nftstorage.link/"s, 307}, ++ {"https://w3s.link/"s, 243}, ++ {"https://ipfs.fleek.co/"s, 203}, ++ {"https://ipfs.jpu.jp/"s, 162}, ++ {"https://permaweb.eu.org/"s, 121}, ++ {"https://jorropo.net/"s, 76}, ++ {"https://hardbin.com/"s, 39}, ++ {"https://ipfs.soul-network.com/"s, 1}, ++ {"https://storry.tv/"s, 0}}; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/generated_directory_listing.cc b/third_party/ipfs_client/src/ipfs_client/generated_directory_listing.cc +new file mode 100644 +index 0000000000000..5a7c4fce733d6 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/generated_directory_listing.cc +@@ -0,0 +1,45 @@ ++#include "generated_directory_listing.h" ++ ++#include "log_macros.h" ++ ++ipfs::GeneratedDirectoryListing::GeneratedDirectoryListing( ++ std::string_view base_path) ++ : html_("\n "), base_path_(base_path) { ++ if (base_path.empty() || base_path[0] != '/') { ++ base_path_.insert(0UL, 1UL, '/'); ++ } ++ if (base_path_.back() != '/') { ++ base_path_.push_back('/'); ++ } ++ html_.append(base_path_) ++ .append(" (directory listing)\n") ++ .append(" \n") ++ .append("
    \n"); ++ if (base_path.find_first_not_of("/") < base_path.size()) { ++ std::string_view dotdotpath{base_path_}; ++ dotdotpath.remove_suffix(1); // Remove that trailing / ++ auto last_slash = dotdotpath.find_last_of("/"); ++ dotdotpath = dotdotpath.substr(0, last_slash + 1UL); ++ AddLink("..", dotdotpath); ++ } ++} ++ ++void ipfs::GeneratedDirectoryListing::AddEntry(std::string_view name) { ++ auto path = base_path_; ++ path.append(name); ++ AddLink(name, path); ++} ++void ipfs::GeneratedDirectoryListing::AddLink(std::string_view name, ++ std::string_view path) { ++ html_.append("
  • \n") ++ .append(" ") ++ .append(name) ++ .append("\n") ++ .append("
  • \n"); ++} ++ ++std::string const& ipfs::GeneratedDirectoryListing::Finish() { ++ return html_.append("
\n").append(" \n").append("\n"); ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/generated_directory_listing.h b/third_party/ipfs_client/src/ipfs_client/generated_directory_listing.h +new file mode 100644 +index 0000000000000..8daa0ec01cb9e +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/generated_directory_listing.h +@@ -0,0 +1,41 @@ ++#ifndef IPFS_GENERATED_DIRECTORY_LISTING_H_ ++#define IPFS_GENERATED_DIRECTORY_LISTING_H_ ++ ++#include ++#include ++ ++namespace ipfs { ++ ++/*! ++ * \brief An index.html listing out a directory node's content ++ */ ++class GeneratedDirectoryListing { ++ public: ++ ++ /*! ++ * \brief Get the HTML preamble going ++ * \param base_path - The path _to_ this directory ++ */ ++ GeneratedDirectoryListing(std::string_view base_path); ++ ++ /*! ++ * \brief Add an entry to the list ++ * \param name - The directory's way of referring to that CID ++ */ ++ void AddEntry(std::string_view name); ++ ++ /*! ++ * \brief Finish up all the HTML stuff at the end. ++ * \return The generated HTML ++ */ ++ std::string const& Finish(); ++ ++ private: ++ std::string html_; ++ std::string base_path_; ++ ++ void AddLink(std::string_view name, std::string_view path); ++}; ++} // namespace ipfs ++ ++#endif // IPFS_GENERATED_DIRECTORY_LISTING_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/block_request_splitter.cc b/third_party/ipfs_client/src/ipfs_client/gw/block_request_splitter.cc +new file mode 100644 +index 0000000000000..3ded788f6bdf1 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/block_request_splitter.cc +@@ -0,0 +1,30 @@ ++#include ++ ++#include ++ ++using Self = ipfs::gw::BlockRequestSplitter; ++ ++std::string_view Self::name() const { ++ return "BlockRequestSplitter"; ++} ++auto Self::handle(ipfs::gw::RequestPtr r) -> HandleOutcome { ++ if (r->type != Type::Car) { ++ return HandleOutcome::NOT_HANDLED; ++ } ++ { ++ auto br = std::make_shared(*r); ++ br->type = Type::Block; ++ br->path.clear(); ++ forward(br); ++ } ++ /* ++ { ++ auto pr = std::make_shared(*r); ++ pr->type = Type::Providers; ++ pr->path.clear(); ++ pr->affinity.clear(); ++ forward(pr); ++ } ++ */ ++ return HandleOutcome::NOT_HANDLED; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/default_requestor.cc b/third_party/ipfs_client/src/ipfs_client/gw/default_requestor.cc +new file mode 100644 +index 0000000000000..46dbf88033b7c +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/default_requestor.cc +@@ -0,0 +1,30 @@ ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++auto ipfs::gw::default_requestor(ipfs::GatewayList gws, ++ std::shared_ptr early, ++ std::shared_ptr api) ++ -> std::shared_ptr { ++ auto result = std::make_shared(); ++ result->or_else(std::make_shared()); ++ if (early) { ++ result->or_else(early); ++ early->api(api); ++ } ++ auto pool = std::make_shared(); ++ result->or_else(std::make_shared(api)) ++ .or_else(pool) ++ .or_else(std::make_shared()); ++ for (auto& gw : gws) { ++ auto gwr = ++ std::make_shared(gw.prefix, gw.strength, api); ++ pool->add(gwr); ++ } ++ return result; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/dnslink_requestor.cc b/third_party/ipfs_client/src/ipfs_client/gw/dnslink_requestor.cc +new file mode 100644 +index 0000000000000..731b750ffd43a +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/dnslink_requestor.cc +@@ -0,0 +1,69 @@ ++#include ++ ++#include "ipfs_client/ipld/ipns_name.h" ++ ++#include ++#include ++ ++#include ++#include ++#include ++ ++#include "log_macros.h" ++ ++#include ++ ++using Self = ipfs::gw::DnsLinkRequestor; ++using namespace std::literals; ++ ++Self::DnsLinkRequestor(std::shared_ptr api) { ++ api_ = api; ++} ++std::string_view Self::name() const { ++ return "DNSLink requestor"; ++} ++namespace { ++bool parse_results(ipfs::gw::RequestPtr req, ++ std::vector const& results, ++ std::shared_ptr const&); ++} ++auto Self::handle(ipfs::gw::RequestPtr req) -> HandleOutcome { ++ if (req->type != Type::DnsLink) { ++ return HandleOutcome::NOT_HANDLED; ++ } ++ // std::function requires target be copy-constructible ++ auto success = std::make_shared(); ++ *success = false; ++ auto a = api_; ++ auto res = [req, success, a](std::vector const& results) { ++ *success = *success || parse_results(req, results, a); ++ }; ++ auto don = [success, req]() { ++ LOG(INFO) << "DNSLink request completed for " << req->main_param ++ << " success=" << *success; ++ if (!*success) { ++ req->dependent->finish(ipfs::Response::HOST_NOT_FOUND); ++ } ++ }; ++ api_->SendDnsTextRequest("_dnslink." + req->main_param, res, std::move(don)); ++ return HandleOutcome::PENDING; ++} ++namespace { ++bool parse_results(ipfs::gw::RequestPtr req, ++ std::vector const& results, ++ std::shared_ptr const& api) { ++ constexpr auto prefix = "dnslink="sv; ++ LOG(INFO) << "Scanning " << results.size() << " DNS TXT records for " ++ << req->main_param << " looking for dnslink..."; ++ for (auto& result : results) { ++ if (starts_with(result, prefix)) { ++ LOG(INFO) << "DNSLink result=" << result; ++ req->RespondSuccessfully(result.substr(prefix.size()), api); ++ return true; ++ } else { ++ LOG(INFO) << "Irrelevant TXT result, ignored: " << result; ++ } ++ } ++ return false; ++} ++} // namespace +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/gateway_http_requestor.cc b/third_party/ipfs_client/src/ipfs_client/gw/gateway_http_requestor.cc +new file mode 100644 +index 0000000000000..94ab27fc14228 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/gateway_http_requestor.cc +@@ -0,0 +1,141 @@ ++#include "gateway_http_requestor.h" ++ ++#include ++#include ++ ++#include ++#include ++#include ++ ++#include "log_macros.h" ++ ++using Self = ipfs::gw::GatewayHttpRequestor; ++using ReqTyp = ipfs::gw::Type; ++ ++std::string_view Self::name() const { ++ return "simplistic HTTP requestor"; ++} ++auto Self::handle(ipfs::gw::RequestPtr r) -> HandleOutcome { ++ DCHECK(r); ++ DCHECK(r->dependent); ++ DCHECK_GT(prefix_.size(), 0UL); ++ if (!r->is_http()) { ++ LOG(ERROR) << name() << " only handles HTTP requests"; ++ return HandleOutcome::NOT_HANDLED; ++ } ++ auto req_key = r->url_suffix().append(r->accept()); ++ if (seen_[req_key] > 0xFD) { ++ return HandleOutcome::NOT_HANDLED; ++ } ++ if (target(*r) <= r->parallel + pending_ + seen_[req_key]) { ++ return HandleOutcome::MAYBE_LATER; ++ } ++ auto desc = r->describe_http(); ++ if (!desc.has_value() || desc.value().url.empty()) { ++ LOG(ERROR) ++ << r->debug_string() ++ << " is HTTP but can't describe the HTTP request that would happen?"; ++ return HandleOutcome::NOT_HANDLED; ++ } ++ if (prefix_.back() == '/' && desc.value().url[0] == '/') { ++ desc.value().url.insert(0, prefix_, 0UL, prefix_.size() - 1UL); ++ } else { ++ desc.value().url.insert(0, prefix_); ++ } ++ desc.value().timeout_seconds += extra_seconds_; ++ auto cb = [this, r, desc, req_key](std::int16_t status, std::string_view body, ++ ContextApi::HeaderAccess ha) { ++ if (r->parallel) { ++ r->parallel--; ++ } ++ if (pending_) { ++ pending_--; ++ } ++ if (r->type == Type::Zombie) { ++ return; ++ } else if (status == 408 || status == 504) { ++ // Timeouts ++ extra_seconds_++; ++ forward(r); ++ return; ++ } else if (status / 100 == 2) { ++ auto ct = ha("content-type"); ++ std::transform(ct.begin(), ct.end(), ct.begin(), ::tolower); ++ if (ct.empty()) { ++ LOG(ERROR) << "No content-type header?"; ++ } ++ if (ct.size() && desc->accept.size() && ++ ct.find(desc->accept) == std::string::npos) { ++ LOG(WARNING) << "Requested with Accept: " << desc->accept ++ << " but received response with content-type: " << ct; ++ LOG(INFO) << "Demote(" << prefix_ << ')'; ++ } else if (!r->RespondSuccessfully(body, api_)) { ++ LOG(ERROR) << "Got an unuseful response from " << prefix_ ++ << " forwarding request " << r->debug_string() ++ << " to next requestor."; ++ } else { ++ // Good cases ++ if (typ_good_.insert(r->type).second) { ++ VLOG(1) << prefix_ << " OK with requests of type " ++ << static_cast(r->type); ++ } else if (typ_bad_.erase(r->type)) { ++ VLOG(1) << prefix_ << " truly OK with requests of type " ++ << static_cast(r->type); ++ } ++ if (aff_good_.insert(r->affinity).second) { ++ VLOG(1) << prefix_ << " likes requests in the neighborhood of " ++ << r->affinity; ++ } else if (aff_bad_.erase(r->affinity)) { ++ VLOG(1) << prefix_ << " truly OK with affinity " << r->affinity; ++ } ++ VLOG(2) << prefix_ << " had a success on " << r->debug_string(); ++ LOG(INFO) << "Promote(" << prefix_ << ')'; ++ ++strength_; ++ return; ++ } ++ } else if (status / 100 == 4) { ++ seen_[req_key] += 9; ++ } ++ seen_[req_key] += 9; ++ LOG(INFO) << "Demote(" << prefix_ << ')'; ++ if (strength_ > 0) { ++ --strength_; ++ } ++ aff_bad_.insert(r->affinity); ++ typ_bad_.insert(r->type); ++ forward(r); ++ }; ++ DCHECK(api_); ++ api_->SendHttpRequest(desc.value(), cb); ++ seen_[req_key]++; ++ pending_++; ++ return HandleOutcome::PENDING; ++} ++ ++Self::GatewayHttpRequestor(std::string gateway_prefix, ++ int strength, ++ std::shared_ptr api) ++ : prefix_{gateway_prefix}, strength_{strength} { ++ api_ = api; ++} ++Self::~GatewayHttpRequestor() {} ++ ++int Self::target(GatewayRequest const& r) const { ++ int result = (strength_ - pending_) / 2; ++ if (!pending_) { ++ ++result; ++ } ++ if (typ_good_.count(r.type)) { ++ result += 3; ++ } ++ if (!typ_bad_.count(r.type)) { ++ result += 2; ++ } ++ if (aff_good_.count(r.affinity)) { ++ result += 5; ++ } ++ if (aff_bad_.count(r.affinity) == 0UL) { ++ result += 4; ++ } ++ return result; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/gateway_http_requestor.h b/third_party/ipfs_client/src/ipfs_client/gw/gateway_http_requestor.h +new file mode 100644 +index 0000000000000..8c61bef879db8 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/gateway_http_requestor.h +@@ -0,0 +1,34 @@ ++#ifndef IPFS_GATEWAY_HTTP_REQUESTOR_H_ ++#define IPFS_GATEWAY_HTTP_REQUESTOR_H_ ++ ++#include ++#include ++#include ++ ++#include ++#include ++#include ++ ++namespace ipfs::gw { ++class GatewayHttpRequestor final : public Requestor { ++ std::string prefix_; ++ int strength_; ++ std::unordered_map seen_; ++ std::set aff_good_, aff_bad_; ++ std::set typ_good_, typ_bad_; ++ int pending_ = 0; ++ int extra_seconds_ = 0; ++ ++ HandleOutcome handle(RequestPtr) override; ++ std::string_view name() const override; ++ int target(GatewayRequest const&) const; ++ ++ public: ++ GatewayHttpRequestor(std::string gateway_prefix, ++ int strength, ++ std::shared_ptr); ++ ~GatewayHttpRequestor() noexcept override; ++}; ++} // namespace ipfs::gw ++ ++#endif // IPFS_GATEWAY_HTTP_REQUESTOR_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/gateway_request.cc b/third_party/ipfs_client/src/ipfs_client/gw/gateway_request.cc +new file mode 100644 +index 0000000000000..1b251b1fc57a8 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/gateway_request.cc +@@ -0,0 +1,305 @@ ++#include ++ ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "log_macros.h" ++ ++#include ++#include ++ ++using namespace std::literals; ++ ++using Self = ipfs::gw::GatewayRequest; ++ ++std::shared_ptr Self::fromIpfsPath(ipfs::SlashDelimited p) { ++ auto name_space = p.pop(); ++ auto r = std::make_shared(); ++ r->main_param = p.pop(); ++ Cid cid(r->main_param); ++ if (cid.valid()) { ++ r->cid = std::move(cid); ++ } else { ++ r->cid = std::nullopt; ++ } ++ if (name_space == "ipfs") { ++ if (!r->cid.has_value()) { ++ LOG(ERROR) << "IPFS request with invalid/unsupported CID " ++ << r->main_param; ++ return {}; ++ } ++ if (r->cid.value().hash_type() == HashType::IDENTITY) { ++ r->type = Type::Identity; ++ } else { ++ r->path = p.pop_all(); ++ r->type = r->path.empty() ? Type::Block : Type::Car; ++ } ++ } else if (name_space == "ipns") { ++ r->path = p.pop_all(); ++ if (Cid(r->main_param).valid()) { ++ r->type = Type::Ipns; ++ } else { ++ r->type = Type::DnsLink; ++ } ++ } else { ++ LOG(FATAL) << "Unsupported namespace in ipfs path: /" << name_space << '/' ++ << p.pop_all(); ++ } ++ return r; ++} ++ ++std::string Self::url_suffix() const { ++ switch (type) { ++ case Type::Block: ++ return "/ipfs/" + main_param; ++ case Type::Car: ++ return "/ipfs/" + main_param + "/" + path + "?dag-scope=entity"; ++ case Type::Ipns: ++ return "/ipns/" + main_param; ++ case Type::Providers: ++ return "/routing/v1/providers/" + main_param; ++ case Type::DnsLink: ++ LOG(FATAL) << "Don't try to use HTTP(s) for DNS TXT records."; ++ return {}; ++ case Type::Identity: ++ case Type::Zombie: ++ return {}; ++ } ++ LOG(FATAL) << "Unhandled gateway request type: " << static_cast(type); ++ return {}; ++} ++std::string_view Self::accept() const { ++ switch (type) { ++ case Type::Block: ++ return "application/vnd.ipld.raw"sv; ++ case Type::Ipns: ++ return "application/vnd.ipfs.ipns-record"sv; ++ case Type::Car: ++ return "application/vnd.ipld.car"sv; ++ case Type::Providers: ++ return "application/json"sv; ++ case Type::DnsLink: ++ // TODO : not sure this advice is 100% good, actually. ++ // If the user's system setup allows for text records to actually work, ++ // it would be good to respect their autonomy and try to follow the ++ // system's DNS setup. However, it's extremely easy to get yourself in a ++ // situation where Chromium _cannot_ access text records. If you're in ++ // that scenario, it might be better to try to use an IPFS gateway with ++ // DNSLink capability. ++ LOG(FATAL) << "Don't try to use HTTP(s) for DNS TXT records."; ++ return {}; ++ case Type::Identity: ++ case Type::Zombie: ++ return {}; ++ } ++ LOG(FATAL) << "Invalid gateway request type: " << static_cast(type); ++ return {}; ++} ++short Self::timeout_seconds() const { ++ switch (type) { ++ case Type::DnsLink: ++ return 16; ++ case Type::Block: ++ return 39; ++ case Type::Providers: ++ return 64; ++ case Type::Car: ++ return 128; ++ case Type::Ipns: ++ return 256; ++ case Type::Identity: ++ case Type::Zombie: ++ return 0; ++ } ++ LOG(FATAL) << "timeout_seconds() called for unsupported gateway request type " ++ << static_cast(type); ++ return 0; ++} ++ ++auto Self::identity_data() const -> std::string_view { ++ if (type != Type::Identity) { ++ return ""; ++ } ++ auto hash = cid.value().hash(); ++ auto d = reinterpret_cast(hash.data()); ++ return std::string_view{d, hash.size()}; ++} ++ ++bool Self::is_http() const { ++ return type != Type::DnsLink && type != Type::Identity; ++} ++auto Self::describe_http() const -> std::optional { ++ if (!is_http()) { ++ return {}; ++ } ++ return HttpRequestDescription{url_suffix(), timeout_seconds(), ++ std::string{accept()}, max_response_size()}; ++} ++std::optional Self::max_response_size() const { ++ switch (type) { ++ case Type::Identity: ++ return 0; ++ case Type::DnsLink: ++ return std::nullopt; ++ case Type::Ipns: ++ return MAX_IPNS_PB_SERIALIZED_SIZE; ++ case Type::Block: ++ return BLOCK_RESPONSE_BUFFER_SIZE; ++ case Type::Car: { ++ // There could be an unlimited number of blocks in the CAR ++ // The _floor_ is the number of path components. ++ // But one path component could be a HAMT sharded directory that we may ++ // need to pass through several layers on. ++ // And the final path component could be a UnixFS file with an unlimited ++ // number of blocks in it. ++ return std::nullopt; ++ } ++ case Type::Zombie: ++ return 0; ++ case Type::Providers: ++ // This one's tricky. ++ // One could easily guess a pracitical limit to the size of a Peer, ++ // and the spec says it SHOULD be limited to 100 peers. ++ // But there's no guaranteed limits. A peer could have an unlimited ++ // number of multiaddrs. And they're allowed to throw in arbitrary ++ // fields I'm supposed to ignore. So in theory it could be infinitely ++ // large. ++ return std::nullopt; ++ } ++ LOG(ERROR) << "Invalid gateway request type " << static_cast(type); ++ return std::nullopt; ++} ++std::string_view ipfs::gw::name(ipfs::gw::Type t) { ++ using ipfs::gw::Type; ++ switch (t) { ++ case Type::Block: ++ return "Block"; ++ case Type::Car: ++ return "Car"; ++ case Type::Ipns: ++ return "Ipns"; ++ case Type::DnsLink: ++ return "DnsLink"; ++ case Type::Providers: ++ return "Providers"; ++ case Type::Identity: ++ return "Identity"; ++ case Type::Zombie: ++ return "CompletedRequest"; ++ } ++ static std::array buf; ++ std::sprintf(buf.data(), "InvalidType %d", static_cast(t)); ++ return buf.data(); ++} ++std::string Self::debug_string() const { ++ std::ostringstream oss; ++ oss << "Request{Type=" << type << ' ' << main_param; ++ if (!path.empty()) { ++ oss << ' ' << path; ++ } ++ if (dependent) { ++ oss << " for=" << dependent->path().to_string(); ++ } ++ oss << " plel=" << parallel << '}'; ++ return oss.str(); ++} ++bool Self::RespondSuccessfully(std::string_view bytes, ++ std::shared_ptr const& api) { ++ using namespace ipfs::ipld; ++ bool success = false; ++ switch (type) { ++ case Type::Block: { ++ DCHECK(cid.has_value()); ++ if (!cid.has_value()) { ++ LOG(ERROR) << "Your CID doesn't even have a value!"; ++ return false; ++ } ++ DCHECK(api); ++ auto node = DagNode::fromBytes(api, cid.value(), bytes); ++ success = orchestrator_->add_node(main_param, node); ++ } break; ++ case Type::Identity: ++ success = orchestrator_->add_node( ++ main_param, std::make_shared(std::string{bytes})); ++ break; ++ case Type::Ipns: ++ if (cid.has_value()) { ++ DCHECK(api); ++ auto byte_ptr = reinterpret_cast(bytes.data()); ++ auto rec = ipfs::ValidateIpnsRecord({byte_ptr, bytes.size()}, ++ cid.value(), *api); ++ if (rec.has_value()) { ++ auto node = DagNode::fromIpnsRecord(rec.value()); ++ success = orchestrator_->add_node(main_param, node); ++ } else { ++ LOG(ERROR) << "IPNS record failed to validate!"; ++ return false; ++ } ++ } ++ break; ++ case Type::DnsLink: ++ LOG(INFO) << "Resolved " << debug_string() << " to " << bytes; ++ if (orchestrator_) { ++ success = orchestrator_->add_node( ++ main_param, std::make_shared(bytes)); ++ } else { ++ LOG(FATAL) << "I have no orchestrator!!"; ++ } ++ break; ++ case Type::Car: { ++ DCHECK(api); ++ Car car(as_bytes(bytes), *api); ++ auto added = 0; ++ while (auto block = car.NextBlock()) { ++ auto cid_s = block->cid.to_string(); ++ auto n = DagNode::fromBytes(api, block->cid, block->bytes); ++ if (!n) { ++ LOG(ERROR) << "Unable to handle block from CAR: " << cid_s; ++ } else if (orchestrator_->add_node(cid_s, n)) { ++ ++added; ++ } else { ++ LOG(INFO) << "Did not add node from CAR: " << cid_s; ++ } ++ } ++ LOG(INFO) << "Added " << added << " nodes from a CAR."; ++ success = added > 0; ++ break; ++ } ++ case Type::Providers: ++ LOG(WARNING) << "TODO - handle responses to providers requests."; ++ break; ++ case Type::Zombie: ++ LOG(WARNING) << "Responding to a zombie is ill-advised."; ++ break; ++ default: ++ LOG(ERROR) << "TODO " << static_cast(type); ++ } ++ if (success) { ++ for (auto& hook : bytes_received_hooks) { ++ hook(bytes); ++ } ++ bytes_received_hooks.clear(); ++ orchestrator_->build_response(dependent); ++ } ++ return success; ++} ++void Self::Hook(std::function f) { ++ bytes_received_hooks.push_back(f); ++} ++void Self::orchestrator(std::shared_ptr const& orc) { ++ orchestrator_ = orc; ++} ++bool Self::PartiallyRedundant() const { ++ if (!orchestrator_) { ++ return false; ++ } ++ return orchestrator_->has_key(main_param); ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/inline_request_handler.cc b/third_party/ipfs_client/src/ipfs_client/gw/inline_request_handler.cc +new file mode 100644 +index 0000000000000..435142a1a74ee +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/inline_request_handler.cc +@@ -0,0 +1,23 @@ ++#include ++ ++#include ++#include ++#include ++ ++#include "log_macros.h" ++ ++using Self = ipfs::gw::InlineRequestHandler; ++ ++std::string_view Self::name() const { ++ return "InlineRequestHandler"; ++} ++auto Self::handle(ipfs::gw::RequestPtr req) -> HandleOutcome { ++ if (req->type != gw::Type::Identity) { ++ VLOG(2) << ipfs::gw::name(req->type); ++ return HandleOutcome::NOT_HANDLED; ++ } ++ std::string data{req->identity_data()}; ++ LOG(INFO) << "Responding to inline CID without using network."; ++ req->RespondSuccessfully(data, api_); ++ return HandleOutcome::DONE; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/requestor.cc b/third_party/ipfs_client/src/ipfs_client/gw/requestor.cc +new file mode 100644 +index 0000000000000..72e6746c7a807 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/requestor.cc +@@ -0,0 +1,70 @@ ++#include ++ ++#include ++#include ++ ++#include ++#include ++#include ++#include ++ ++#include "log_macros.h" ++ ++using Self = ipfs::gw::Requestor; ++using ReqPtr = std::shared_ptr; ++ ++Self& Self::or_else(std::shared_ptr p) { ++ if (next_) { ++ next_->or_else(p); ++ } else { ++ VLOG(2) << name() << " is followed by " << p->name(); ++ next_ = p; ++ } ++ if (api_ && !p->api_) { ++ VLOG(1) << name() << " granting context to " << p->name(); ++ p->api_ = api_; ++ } ++ return *this; ++} ++ ++void Self::request(ReqPtr req) { ++ if (!req || req->type == Type::Zombie) { ++ return; ++ } ++ switch (handle(req)) { ++ case HandleOutcome::MAYBE_LATER: ++ // TODO ++ forward(req); ++ break; ++ case HandleOutcome::PARALLEL: ++ case HandleOutcome::NOT_HANDLED: ++ if (next_) { ++ next_->request(req); ++ } else { ++ LOG(ERROR) << "Ran out of Requestors in the chain while looking for " ++ "one that can handle " ++ << req->debug_string(); ++ definitive_failure(req); ++ } ++ break; ++ case HandleOutcome::PENDING: ++ break; ++ case HandleOutcome::DONE: ++ VLOG(2) << req->debug_string() << " finished synchronously: " << name(); ++ break; ++ } ++} ++void Self::definitive_failure(ipfs::gw::RequestPtr r) const { ++ DCHECK(r); ++ DCHECK(r->dependent); ++ r->dependent->finish(Response::PLAIN_NOT_FOUND); ++} ++ ++void Self::forward(ipfs::gw::RequestPtr req) const { ++ if (next_) { ++ next_->request(req); ++ } ++} ++void Self::api(std::shared_ptr a) { ++ api_ = a; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/requestor_pool.cc b/third_party/ipfs_client/src/ipfs_client/gw/requestor_pool.cc +new file mode 100644 +index 0000000000000..b337dda1b1529 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/requestor_pool.cc +@@ -0,0 +1,73 @@ ++#include "requestor_pool.h" ++ ++#include ++ ++#include "log_macros.h" ++ ++using Self = ipfs::gw::RequestorPool; ++ ++std::string_view Self::name() const { ++ return "requestor pool"; ++} ++Self& Self::add(std::shared_ptr r) { ++ if (api_ && !(r->api_)) { ++ r->api_ = api_; ++ } ++ pool_.push_back(r); ++ r->or_else(shared_from_this()); ++ return *this; ++} ++auto Self::handle(ipfs::gw::RequestPtr req) -> HandleOutcome { ++ auto now = std::time(nullptr); ++ for (auto i = 0UL; i * 2 < waiting_.size(); ++i) { ++ auto& t = waiting_.front().when; ++ if (t != now) { ++ auto to_pop = waiting_.front(); ++ waiting_.pop(); ++ check(to_pop); ++ } ++ } ++ return check({req, 0UL, 0L}); ++} ++auto Self::check(Waiting w) -> HandleOutcome { ++ using O = HandleOutcome; ++ auto next_retry = pool_.size(); ++ auto req = w.req; ++ if (req->PartiallyRedundant()) { ++ return O::DONE; ++ } ++ for (auto i = w.at_idx; i < pool_.size(); ++i) { ++ if (req->type == Type::Zombie) { ++ return O::DONE; ++ } ++ auto& tor = pool_[i]; ++ switch (tor->handle(req)) { ++ case O::DONE: ++ LOG(INFO) << "RequestorPool::handle returning DONE because a member of " ++ "the pool's handle returned DONE."; ++ return O::DONE; ++ case O::PENDING: ++ case O::PARALLEL: ++ req->parallel++; ++ break; ++ case O::MAYBE_LATER: ++ if (next_retry == pool_.size()) { ++ next_retry = i; ++ } ++ break; ++ case O::NOT_HANDLED: ++ break; ++ } ++ } ++ if (req->parallel > 0) { ++ return O::PENDING; ++ } ++ if (next_retry < pool_.size()) { ++ w.when = std::time(nullptr); ++ waiting_.emplace(w); ++ return O::PENDING; ++ } ++ VLOG(1) << "Have exhausted all requestors in pool looking for " ++ << req->debug_string(); ++ return O::NOT_HANDLED; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/requestor_pool.h b/third_party/ipfs_client/src/ipfs_client/gw/requestor_pool.h +new file mode 100644 +index 0000000000000..86104319a32eb +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/requestor_pool.h +@@ -0,0 +1,32 @@ ++#ifndef IPFS_REQUESTOR_POOL_H_ ++#define IPFS_REQUESTOR_POOL_H_ ++ ++#include ++ ++#include ++ ++#include ++#include ++#include ++ ++namespace ipfs::gw { ++class RequestorPool : public Requestor { ++ std::string_view name() const override; ++ HandleOutcome handle(RequestPtr) override; ++ ++ std::vector> pool_; ++ struct Waiting { ++ RequestPtr req; ++ std::size_t at_idx; ++ std::time_t when; ++ }; ++ std::queue waiting_; ++ ++ HandleOutcome check(Waiting); ++ ++ public: ++ RequestorPool& add(std::shared_ptr); ++}; ++} // namespace ipfs::gw ++ ++#endif // IPFS_REQUESTOR_POOL_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/gw/terminating_requestor.cc b/third_party/ipfs_client/src/ipfs_client/gw/terminating_requestor.cc +new file mode 100644 +index 0000000000000..791ffd849ddd4 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/gw/terminating_requestor.cc +@@ -0,0 +1,23 @@ ++#include "ipfs_client/gw/terminating_requestor.h" ++ ++#include ++ ++#include "log_macros.h" ++ ++using Self = ipfs::gw::TerminatingRequestor; ++ ++std::string_view Self::name() const { ++ return "Terminating requestor"; ++} ++auto Self::handle(ipfs::gw::RequestPtr r) -> HandleOutcome { ++ if (r->type == Type::Zombie) { ++ return HandleOutcome::DONE; ++ } else if (r->parallel) { ++ return HandleOutcome::PENDING; ++ } else { ++ VLOG(2) << "Out of options, giving up on gateway request " ++ << r->debug_string(); ++ definitive_failure(r); ++ return HandleOutcome::DONE; ++ } ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/http_request_description.cc b/third_party/ipfs_client/src/ipfs_client/http_request_description.cc +new file mode 100644 +index 0000000000000..19b29d0ccde51 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/http_request_description.cc +@@ -0,0 +1,12 @@ ++#include ++ ++using Self = ipfs::HttpRequestDescription; ++ ++bool Self::operator==(HttpRequestDescription const& r) const { ++ // The concept of identity does NOT involve feedback-looping timeout fudge ++ // Nor is the acceptable size of a response necessary to distinguish. ++ return url == r.url && accept == r.accept; ++} ++bool Self::operator<(HttpRequestDescription const& r) const { ++ return url == r.url ? accept < r.accept : url < r.url; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/identity_cid.cc b/third_party/ipfs_client/src/ipfs_client/identity_cid.cc +new file mode 100644 +index 0000000000000..9ea2421d5cdf3 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/identity_cid.cc +@@ -0,0 +1,19 @@ ++#include ++ ++#include ++ ++namespace Self = ipfs::id_cid; ++ ++auto Self::forText(std::string_view txt) -> Cid { ++ txt = txt.substr(0UL, MaximumHashLength); ++ auto p = reinterpret_cast(txt.data()); ++ auto b = ByteView{p, txt.size()}; ++ MultiHash mh(HashType::IDENTITY, b); ++ if (mh.valid()) { ++ return Cid{MultiCodec::RAW, mh}; ++ } else { ++ LOG(FATAL) ++ << "We really shouldn't be able to fail to 'hash' using identity."; ++ return forText("Unreachable"); ++ } ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/ipfs_request.cc b/third_party/ipfs_client/src/ipfs_client/ipfs_request.cc +new file mode 100644 +index 0000000000000..e4f3e1e4b47e3 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipfs_request.cc +@@ -0,0 +1,43 @@ ++#include ++ ++#include ++ ++#include "log_macros.h" ++ ++#include ++ ++using Self = ipfs::IpfsRequest; ++ ++// Self::IpfsRequest(std::string path_p) ++// : path_{path_p}, callback_([](auto&, auto&) {}) {} ++Self::IpfsRequest(std::string path_p, Finisher f) ++ : path_{path_p}, callback_{f} {} ++ ++std::shared_ptr Self::fromUrl(std::string url, ipfs::IpfsRequest::Finisher f) { ++ url.erase(4UL, 2UL ); ++ url.insert(0UL, 1UL, '/'); ++ return std::make_shared(std::move(url), std::move(f)); ++} ++ ++void Self::till_next(std::size_t w) { ++ waiting_ = w; ++} ++void Self::finish(ipfs::Response& r) { ++ VLOG(2) << "IpfsRequest::finish(" << waiting_ << ',' << r.status_ << ");"; ++ if (waiting_) { ++ if (--waiting_) { ++ return; ++ } ++ } ++ callback_(*this, r); ++ // TODO - cancel other gw req pointing into this ++ callback_ = [](auto& q, auto&) { ++ VLOG(2) << "IPFS request " << q.path().pop_all() << " satisfied multiply"; ++ }; ++} ++bool Self::ready_after() { ++ return waiting_ == 0 || 0 == --waiting_; ++} ++void Self::new_path(std::string_view sv) { ++ path_.assign(sv); ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/chunk.cc b/third_party/ipfs_client/src/ipfs_client/ipld/chunk.cc +new file mode 100644 +index 0000000000000..e5540b080e4b3 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/chunk.cc +@@ -0,0 +1,19 @@ ++#include "chunk.h" ++ ++#include "log_macros.h" ++ ++using Chunk = ipfs::ipld::Chunk; ++ ++Chunk::Chunk(std::string data) : data_{data} {} ++Chunk::~Chunk() {} ++ ++auto Chunk::resolve(ResolutionState& params) -> ResolveResult { ++ if (params.IsFinalComponent()) { ++ return Response{"", 200, data_, params.MyPath().to_string()}; ++ } else { ++ LOG(ERROR) << "Can't resolve a path (" << params.MyPath() ++ << ") inside of a file chunk!"; ++ return ProvenAbsent{}; ++ } ++} ++ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/chunk.h b/third_party/ipfs_client/src/ipfs_client/ipld/chunk.h +new file mode 100644 +index 0000000000000..b846cc5379171 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/chunk.h +@@ -0,0 +1,18 @@ ++#ifndef IPFS_CHUNK_H_ ++#define IPFS_CHUNK_H_ ++ ++#include ++ ++namespace ipfs::ipld { ++class Chunk : public DagNode { ++ std::string const data_; ++ ++ ResolveResult resolve(ResolutionState&) override; ++ ++ public: ++ explicit Chunk(std::string); ++ virtual ~Chunk() noexcept; ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_CHUNK_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/dag_cbor_node.cc b/third_party/ipfs_client/src/ipfs_client/ipld/dag_cbor_node.cc +new file mode 100644 +index 0000000000000..064a89373f743 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/dag_cbor_node.cc +@@ -0,0 +1,25 @@ ++#include "dag_cbor_node.h" ++ ++#include "log_macros.h" ++ ++using Self = ipfs::ipld::DagCborNode; ++ ++auto Self::resolve(ResolutionState& params) -> ResolveResult { ++ if (auto cid = doc_->as_link()) { ++ auto cid_str = cid.value().to_string(); ++ return CallChild(params, "", cid_str); ++ } ++ if (params.IsFinalComponent()) { ++ return Response{"text/html", 200, doc_->html(), ++ params.PathToResolve().to_string()}; ++ } ++ return CallChild(params, [this](std::string_view element_name) -> NodePtr { ++ if (auto child = doc_->at(element_name)) { ++ return std::make_shared(std::move(child)); ++ } ++ return {}; ++ }); ++} ++ ++Self::DagCborNode(std::unique_ptr p) : doc_{std::move(p)} {} ++Self::~DagCborNode() {} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/dag_cbor_node.h b/third_party/ipfs_client/src/ipfs_client/ipld/dag_cbor_node.h +new file mode 100644 +index 0000000000000..c9ba53331674a +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/dag_cbor_node.h +@@ -0,0 +1,22 @@ ++#ifndef IPFS_DAG_CBOR_NODE_H_ ++#define IPFS_DAG_CBOR_NODE_H_ ++ ++#include ++ ++#include ++ ++namespace ipfs::ipld { ++class DagCborNode final : public DagNode { ++ ResolveResult resolve(ResolutionState&) override; ++ ++ public: ++ using Data = DagCborValue; ++ explicit DagCborNode(std::unique_ptr); ++ ~DagCborNode() noexcept override; ++ ++ private: ++ std::unique_ptr doc_; ++}; ++} ++ ++#endif // IPFS_DAG_CBOR_NODE_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/dag_json_node.cc b/third_party/ipfs_client/src/ipfs_client/ipld/dag_json_node.cc +new file mode 100644 +index 0000000000000..dfb3f38b0a0e1 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/dag_json_node.cc +@@ -0,0 +1,95 @@ ++#include "dag_json_node.h" ++ ++#include ++ ++#include ++ ++using Self = ipfs::ipld::DagJsonNode; ++ ++Self::DagJsonNode(std::unique_ptr j) : data_(std::move(j)) { ++ auto cid = data_->get_if_link(); ++ if (!cid) { ++ return; ++ } ++ auto cid_str = cid->to_string(); ++ if (cid_str.size()) { ++ links_.emplace_back("", Link(cid_str)); ++ } ++} ++Self::~DagJsonNode() noexcept {} ++ ++auto Self::resolve(ResolutionState& params) -> ResolveResult { ++ auto respond_as_link = CallChild(params, ""); ++ if (!std::get_if(&respond_as_link)) { ++ return respond_as_link; ++ } ++ if (params.IsFinalComponent()) { ++ return Response{"text/html", 200, html(), params.MyPath().to_string()}; ++ } ++ return CallChild(params, [this](std::string_view name) -> NodePtr { ++ auto child_data = (*data_)[name]; ++ if (child_data) { ++ return std::make_shared(std::move(child_data)); ++ } ++ return {}; ++ }); ++} ++ ++auto Self::is_link() -> Link* { ++ if (links_.size() == 1UL && links_.front().first.empty()) { ++ return &links_.front().second; ++ } else { ++ return nullptr; ++ } ++} ++namespace { ++void write_body(std::ostream& str, ipfs::DagJsonValue const& val) { ++ if (auto link = val.get_if_link()) { ++ auto cid_str = link.value().to_string(); ++ str << "" << cid_str << "\n"; ++ } else if (auto keys = val.object_keys()) { ++ str << "{\n"; ++ for (auto& key : keys.value()) { ++ str << " \n \n" ++ << " \n \n \n"; ++ } ++ str << "
  ""; ++ for (auto c : key) { ++ str << html_escape(c); ++ } ++ str << "":\n"; ++ auto child = val[key]; ++ write_body(str, *child); ++ str << ",
}\n"; ++ } else if (val.iterate_list([](auto&) {})) { ++ str << "[\n"; ++ val.iterate_list([&str](auto& child) { ++ str << " \n \n \n \n"; ++ }); ++ str << "
  \n"; ++ write_body(str, child); ++ str << "
]\n"; ++ } else { ++ auto plain = val.pretty_print(); ++ // str << "

"; ++ for (auto c : plain) { ++ if (c == '\n') { ++ str << "
\n"; ++ } else { ++ str << html_escape(c); ++ } ++ } ++ // str << "

\n"; ++ } ++} ++} // namespace ++std::string const& Self::html() { ++ if (html_.empty()) { ++ std::ostringstream html; ++ html << "Preview of DAG-JSON\n"; ++ write_body(html, *data_); ++ html << "\n"; ++ html_ = html.str(); ++ } ++ return html_; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/dag_json_node.h b/third_party/ipfs_client/src/ipfs_client/ipld/dag_json_node.h +new file mode 100644 +index 0000000000000..1d8012a007487 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/dag_json_node.h +@@ -0,0 +1,22 @@ ++#ifndef IPFS_DAG_JSON_NODE_H_ ++#define IPFS_DAG_JSON_NODE_H_ ++ ++#include ++#include ++ ++namespace ipfs::ipld { ++class DagJsonNode final : public DagNode { ++ std::unique_ptr data_; ++ std::string html_; ++ ResolveResult resolve(ResolutionState& params) override; ++ Link* is_link(); ++ std::string const& html(); ++ ++ public: ++ DagJsonNode(std::unique_ptr); ++ ~DagJsonNode() noexcept override; ++}; ++ ++} // namespace ipfs::ipld ++ ++#endif // IPFS_DAG_JSON_NODE_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/dag_node.cc b/third_party/ipfs_client/src/ipfs_client/ipld/dag_node.cc +new file mode 100644 +index 0000000000000..4d11f6f23bc78 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/dag_node.cc +@@ -0,0 +1,248 @@ ++#include ++ ++#include "chunk.h" ++#include "dag_cbor_node.h" ++#include "dag_json_node.h" ++#include "directory_shard.h" ++#include "ipns_name.h" ++#include "root.h" ++#include "small_directory.h" ++#include "symlink.h" ++#include "unixfs_file.h" ++ ++#include ++#include ++#include ++ ++#include "log_macros.h" ++ ++#include ++#include ++ ++using Node = ipfs::ipld::DagNode; ++ ++std::shared_ptr Node::fromBytes(std::shared_ptr const& api, ++ Cid const& cid, ++ std::string_view bytes) { ++ return fromBytes(api, cid, as_bytes(bytes)); ++} ++auto Node::fromBytes(std::shared_ptr const& api, ++ ipfs::Cid const& cid, ++ ipfs::ByteView bytes) -> NodePtr { ++ std::shared_ptr result = nullptr; ++ auto hash = api->Hash(cid.hash_type(), bytes); ++ if (!hash.has_value()) { ++ LOG(ERROR) << "Could not hash response for " << cid.to_string(); ++ return {}; ++ } ++ if (hash.value().size() != cid.hash().size()) { ++ return {}; ++ } ++ for (auto i = 0U; i < hash.value().size(); ++i) { ++ auto e = cid.hash()[i]; ++ auto a = hash.value().at(i); ++ if (e != a) { ++ return {}; ++ } ++ } ++ auto required = cid.hash(); ++ auto calculated = hash.value(); ++ if (!std::equal(required.begin(), required.end(), calculated.begin(), ++ calculated.end())) { ++ LOG(ERROR) << "Hash of response did not match the one in the CID " ++ << cid.to_string(); ++ return {}; ++ } ++ switch (cid.codec()) { ++ case MultiCodec::DAG_CBOR: { ++ auto p = reinterpret_cast(bytes.data()); ++ auto cbor = api->ParseCbor({p, bytes.size()}); ++ if (cbor) { ++ result = std::make_shared(std::move(cbor)); ++ } else { ++ LOG(ERROR) << "CBOR node " << cid.to_string() ++ << " does not parse as CBOR."; ++ } ++ } break; ++ case MultiCodec::DAG_JSON: { ++ auto p = reinterpret_cast(bytes.data()); ++ auto json = api->ParseJson({p, bytes.size()}); ++ if (json) { ++ result = std::make_shared(std::move(json)); ++ } else { ++ LOG(ERROR) << "JSON node " << cid.to_string() ++ << " does not parse as JSON."; ++ } ++ } break; ++ case MultiCodec::RAW: ++ case MultiCodec::DAG_PB: { ++ ipfs::PbDag b{cid, bytes}; ++ if (b.valid()) { ++ result = fromBlock(b); ++ } else { ++ std::ostringstream hex; ++ for (auto byt : bytes) { ++ hex << ' ' << std::hex ++ << static_cast(static_cast(byt)); ++ } ++ LOG(ERROR) ++ << "Have a response that did not parse as a valid block, cid: " ++ << cid.to_string() << " contents: " << bytes.size() ++ << " bytes = " << hex.str(); ++ } ++ } break; ++ case MultiCodec::INVALID: ++ case MultiCodec::IDENTITY: ++ case MultiCodec::LIBP2P_KEY: ++ default: ++ LOG(ERROR) << "Response for unhandled CID Codec: " ++ << GetName(cid.codec()); ++ } ++ if (result) { ++ result->set_api(api); ++ } ++ return result; ++} ++std::shared_ptr Node::fromBlock(ipfs::PbDag const& block) { ++ std::shared_ptr result; ++ switch (block.type()) { ++ case PbDag::Type::FileChunk: ++ return std::make_shared(block.chunk_data()); ++ case PbDag::Type::NonFs: ++ return std::make_shared(block.unparsed()); ++ case PbDag::Type::Symlink: ++ return std::make_shared(block.chunk_data()); ++ case PbDag::Type::Directory: ++ result = std::make_shared(); ++ break; ++ case PbDag::Type::File: ++ case PbDag::Type::Raw: ++ result = std::make_shared(); ++ break; ++ case PbDag::Type::HAMTShard: ++ if (block.fsdata().has_fanout()) { ++ result = std::make_shared(block.fsdata().fanout()); ++ } else { ++ result = std::make_shared(); ++ } ++ break; ++ case PbDag::Type::Metadata: ++ LOG(ERROR) << "Metadata blocks unhandled."; ++ return result; ++ case PbDag::Type::Invalid: ++ LOG(ERROR) << "Invalid block."; ++ return result; ++ default: ++ LOG(FATAL) << "TODO " << static_cast(block.type()); ++ } ++ auto add_link = [&result](auto& n, auto c) { ++ result->links_.emplace_back(n, c); ++ return true; ++ }; ++ block.List(add_link); ++ return result; ++} ++ ++auto Node::fromIpnsRecord(ipfs::ValidatedIpns const& v) -> NodePtr { ++ return std::make_shared(v.value); ++} ++ ++std::shared_ptr Node::deroot() { ++ return shared_from_this(); ++} ++std::shared_ptr Node::rooted() { ++ return std::make_shared(shared_from_this()); ++} ++auto Node::as_hamt() -> DirShard* { ++ return nullptr; ++} ++void Node::set_api(std::shared_ptr api) { ++ api_ = api; ++} ++auto Node::resolve(SlashDelimited initial_path, BlockLookup blu) ++ -> ResolveResult { ++ ResolutionState state; ++ state.resolved_path_components = ""; ++ state.unresolved_path = initial_path; ++ state.get_available_block = blu; ++ return resolve(state); ++} ++auto Node::CallChild(ipfs::ipld::ResolutionState& state) -> ResolveResult { ++ return CallChild(state, state.NextComponent(api_.get())); ++} ++auto Node::CallChild(ipfs::ipld::ResolutionState& state, ++ std::string_view link_key, ++ std::string_view block_key) -> ResolveResult { ++ auto child = FindChild(link_key); ++ if (!child) { ++ links_.emplace_back(link_key, Link{std::string{block_key}, {}}); ++ } ++ return CallChild(state, link_key); ++} ++auto Node::CallChild(ResolutionState& state, std::string_view link_key) ++ -> ResolveResult { ++ auto* child = FindChild(link_key); ++ if (!child) { ++ return ProvenAbsent{}; ++ } ++ auto& node = child->node; ++ if (!node) { ++ node = state.GetBlock(child->cid); ++ } ++ if (node) { ++ Descend(state); ++ return node->resolve(state); ++ } else { ++ std::string needed{"/ipfs/"}; ++ needed.append(child->cid); ++ auto more = state.unresolved_path.to_view(); ++ if (more.size()) { ++ if (more.front() != '/') { ++ needed.push_back('/'); ++ } ++ needed.append(more); ++ } ++ return MoreDataNeeded{needed}; ++ } ++} ++auto Node::CallChild(ResolutionState& state, ++ std::function gen_child) ++ -> ResolveResult { ++ auto link_key = state.NextComponent(api_.get()); ++ auto child = FindChild(link_key); ++ if (!child) { ++ links_.emplace_back(link_key, Link{{}, {}}); ++ child = &links_.back().second; ++ } ++ auto& node = child->node; ++ if (!node) { ++ node = gen_child(link_key); ++ if (!node) { ++ return ProvenAbsent{}; ++ } ++ } ++ Descend(state); ++ return node->resolve(state); ++} ++auto Node::FindChild(std::string_view link_key) -> Link* { ++ for (auto& [name, link] : links_) { ++ if (name == link_key) { ++ return &link; ++ } ++ } ++ return nullptr; ++} ++void Node::Descend(ResolutionState& state) { ++ auto next = state.unresolved_path.pop(); ++ if (next.empty()) { ++ return; ++ } ++ if (!state.resolved_path_components.ends_with('/')) { ++ state.resolved_path_components.push_back('/'); ++ } ++ state.resolved_path_components.append(next); ++} ++ ++std::ostream& operator<<(std::ostream& s, ipfs::ipld::PathChange const& c) { ++ return s << "PathChange{" << c.new_path << '}'; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/directory_shard.cc b/third_party/ipfs_client/src/ipfs_client/ipld/directory_shard.cc +new file mode 100644 +index 0000000000000..7839e62b66247 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/directory_shard.cc +@@ -0,0 +1,101 @@ ++#include "directory_shard.h" ++ ++#include "log_macros.h" ++ ++#include ++#include ++ ++#include ++ ++#include ++#include ++#include ++ ++using namespace std::literals; ++ ++using Self = ipfs::ipld::DirShard; ++ ++auto Self::resolve(ResolutionState& parms) -> ResolveResult { ++ if (parms.IsFinalComponent()) { ++ auto index_parm = parms.WithPath("index.html"sv); ++ auto result = resolve(index_parm); ++ // TODO generate index.html if not present ++ auto resp = std::get_if(&result); ++ if (resp) { ++ resp->mime_ = "text/html"; ++ } ++ return result; ++ } ++ std::string name{parms.NextComponent(api_.get())}; ++ auto hash = hexhash(name); ++ return resolve_internal(hash.begin(), hash.end(), name, parms); ++} ++auto Self::resolve_internal(ipfs::ipld::DirShard::HashIter hash_b, ++ ipfs::ipld::DirShard::HashIter hash_e, ++ std::string_view human_name, ++ ResolutionState& parms) -> ResolveResult { ++ auto hash_chunk = hash_b == hash_e ? std::string{} : *hash_b; ++ for (auto& [name, link] : links_) { ++ if (!starts_with(name, hash_chunk)) { ++ continue; ++ } ++ if (ends_with(name, human_name)) { ++ VLOG(2) << "Found " << human_name << ", leaving HAMT sharded directory " ++ << name << "->" << link.cid; ++ return CallChild(parms, name); ++ } ++ auto node = parms.GetBlock(link.cid); ++ if (!node) { ++ // Unfortunately we can't really append more path and do a full Car ++ // request ++ // The gateway would hash whatever we gave it and compare it to a ++ // partially-consumed hash ++ return MoreDataNeeded{{"/ipfs/" + link.cid}}; ++ } ++ auto downcast = node->as_hamt(); ++ if (downcast) { ++ if (hash_b == hash_e) { ++ LOG(ERROR) << "Ran out of hash bits."; ++ return ProvenAbsent{}; ++ } ++ VLOG(2) << "Found hash chunk, continuing to next level of HAMT sharded " ++ "directory " ++ << name << "->" << link.cid; ++ return downcast->resolve_internal(std::next(hash_b), hash_e, human_name, ++ parms); ++ } else { ++ return ProvenAbsent{}; ++ } ++ } ++ return ProvenAbsent{}; ++} ++std::vector Self::hexhash(std::string_view path_element) const { ++ auto hex_width = 0U; ++ for (auto x = fanout_; (x >>= 4); ++hex_width) ++ ; ++ std::array digest = {0U, 0U}; ++ MurmurHash3_x64_128(path_element.data(), path_element.size(), 0, ++ digest.data()); ++ std::vector result; ++ for (auto d : digest) { ++ auto hash_bits = htobe64(d); ++ while (hash_bits) { ++ // 2. Pop the log2(fanout_) lowest bits from the path component hash ++ // digest,... ++ auto popped = hash_bits % fanout_; ++ hash_bits /= fanout_; ++ std::ostringstream oss; ++ // ... then hex encode (using 0-F) using little endian those bits ... ++ oss << std::setfill('0') << std::setw(hex_width) << std::uppercase ++ << std::hex << popped; ++ result.push_back(oss.str()); ++ } ++ } ++ return result; ++} ++ ++Self::DirShard(std::uint64_t fanout) : fanout_{fanout} {} ++Self::~DirShard() {} ++Self* Self::as_hamt() { ++ return this; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/directory_shard.h b/third_party/ipfs_client/src/ipfs_client/ipld/directory_shard.h +new file mode 100644 +index 0000000000000..c1cb811355922 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/directory_shard.h +@@ -0,0 +1,26 @@ ++#ifndef IPFS_DIRECTORY_SHARD_H_ ++#define IPFS_DIRECTORY_SHARD_H_ 1 ++ ++#include ++ ++namespace ipfs::ipld { ++class DirShard : public DagNode { ++ std::uint64_t const fanout_; ++ ++ ResolveResult resolve(ResolutionState&) override; ++ DirShard* as_hamt() override; ++ ++ std::vector hexhash(std::string_view path_element) const; ++ using HashIter = std::vector::const_iterator; ++ ResolveResult resolve_internal(HashIter, ++ HashIter, ++ std::string_view, ++ ResolutionState&); ++ ++ public: ++ explicit DirShard(std::uint64_t fanout = 256UL); ++ virtual ~DirShard() noexcept; ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_DIRECTORY_SHARD_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/ipns_name.cc b/third_party/ipfs_client/src/ipfs_client/ipld/ipns_name.cc +new file mode 100644 +index 0000000000000..f38200923f49f +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/ipns_name.cc +@@ -0,0 +1,25 @@ ++#include "ipns_name.h" ++ ++#include "log_macros.h" ++ ++using Self = ipfs::ipld::IpnsName; ++ ++Self::IpnsName(std::string_view target_abs_path) ++ : target_path_{target_abs_path} {} ++ ++auto Self::resolve(ResolutionState& params) -> ResolveResult { ++ // Can't use PathChange, as the target is truly absolute (rootless) ++ SlashDelimited t{target_path_}; ++ t.pop(); // Discard namespace, though realistically it's going to be ipfs ++ // basically all the time ++ auto name = t.pop(); ++ if (t) { ++ LOG(WARNING) << "Odd case: name points at /ns/root/MORE/PATH (" ++ << target_path_ << "): " << params.MyPath(); ++ auto path = t.to_string() + "/" + params.PathToResolve().to_string(); ++ auto altered = params.WithPath(path); ++ return CallChild(altered, "", name); ++ } else { ++ return CallChild(params, "", name); ++ } ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/ipns_name.h b/third_party/ipfs_client/src/ipfs_client/ipld/ipns_name.h +new file mode 100644 +index 0000000000000..8b50d6e86e397 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/ipns_name.h +@@ -0,0 +1,18 @@ ++#ifndef IPFS_IPLD_IPNS_NAME_H_ ++#define IPFS_IPLD_IPNS_NAME_H_ ++ ++#include "ipfs_client/ipld/dag_node.h" ++ ++namespace ipfs::ipld { ++class IpnsName : public DagNode { ++ std::string const target_path_; ++ ++ ResolveResult resolve(ResolutionState& params) override; ++ ++ public: ++ IpnsName(std::string_view target_abs_path); ++ virtual ~IpnsName() noexcept {} ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_IPLD_IPNS_NAME_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/link.cc b/third_party/ipfs_client/src/ipfs_client/ipld/link.cc +new file mode 100644 +index 0000000000000..f9dad98e58840 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/link.cc +@@ -0,0 +1,6 @@ ++#include "ipfs_client/ipld/link.h" ++ ++using Self = ipfs::ipld::Link; ++ ++Self::Link(std::string cid_s) : cid{cid_s} {} ++Self::Link(std::string s, std::shared_ptr n) : cid{s}, node{n} {} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/resolution_state.cc b/third_party/ipfs_client/src/ipfs_client/ipld/resolution_state.cc +new file mode 100644 +index 0000000000000..7b51513d83d6f +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/resolution_state.cc +@@ -0,0 +1,36 @@ ++#include ++ ++#include ++ ++using Self = ipfs::ipld::ResolutionState; ++ ++bool Self::IsFinalComponent() const { ++ return !unresolved_path; ++} ++auto Self::PathToResolve() const -> SlashDelimited { ++ return unresolved_path; ++} ++auto Self::MyPath() const -> SlashDelimited { ++ return SlashDelimited{resolved_path_components}; ++} ++std::string Self::NextComponent(ContextApi const* api) const { ++ auto copy = unresolved_path; ++ if (api) { ++ return api->UnescapeUrlComponent(copy.pop()); ++ } else { ++ return std::string{copy.pop()}; ++ } ++} ++auto Self::GetBlock(std::string const& block_key) const -> NodePtr { ++ return get_available_block(block_key); ++} ++Self Self::WithPath(std::string_view p) const { ++ auto rv = *this; ++ rv.unresolved_path = SlashDelimited{p}; ++ return rv; ++} ++auto Self::RestartResolvedPath() const -> ResolutionState { ++ auto rv = *this; ++ rv.resolved_path_components.clear(); ++ return rv; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/root.cc b/third_party/ipfs_client/src/ipfs_client/ipld/root.cc +new file mode 100644 +index 0000000000000..fd5af1b2891b2 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/root.cc +@@ -0,0 +1,90 @@ ++#include "root.h" ++ ++#include "log_macros.h" ++ ++using namespace std::literals; ++ ++using Self = ipfs::ipld::Root; ++using Ptr = std::shared_ptr; ++ ++Self::Root(std::shared_ptr under) { ++ links_.push_back({{}, Link{{}, under}}); ++} ++Self::~Root() {} ++ ++Ptr Self::deroot() { ++ return links_.at(0).second.node; ++} ++Ptr Self::rooted() { ++ return shared_from_this(); ++} ++ ++auto Self::resolve(ResolutionState& params) -> ResolveResult { ++ auto location = params.PathToResolve().to_string(); ++ auto result = deroot()->resolve(params); ++ if (auto pc = std::get_if(&result)) { ++ auto lower = params.WithPath(pc->new_path); ++ result = resolve(lower); ++ location.assign(lower.MyPath().to_view()); ++ } else if (std::get_if(&result)) { ++ if (params.NextComponent(api_.get()) == "_redirects") { ++ return result; ++ } ++ if (!redirects_.has_value()) { ++ auto redirects_path = params.WithPath("_redirects"); ++ result = resolve(redirects_path); ++ auto redirect_resp = std::get_if(&result); ++ if (redirect_resp && redirect_resp->status_ == 200) { ++ redirects_ = redirects::File(redirect_resp->body_); ++ } else { ++ // Either this is ProvenAbsent, in which case this will be interpreted ++ // as the original ProvenAbsent Or it's MoreDataNeeded but for ++ // _redirects, which is what we need now ++ return result; ++ } ++ } ++ if (redirects_.has_value() && redirects_.value().valid()) { ++ Response* resp = nullptr; ++ auto status = redirects_.value().rewrite(location); ++ if (location.find("://") < location.size()) { ++ LOG(INFO) << "_redirects file sent us to a whole URL, scheme-and-all: " ++ << location << " status=" << status; ++ return Response{"", status, "", location}; ++ } ++ auto lower_parm = params.WithPath(location).RestartResolvedPath(); ++ switch (status / 100) { ++ case 0: // no rewrites available ++ break; ++ case 2: ++ result = deroot()->resolve(lower_parm); ++ location.assign(lower_parm.MyPath().to_view()); ++ break; ++ case 3: ++ // Let the redirect happen ++ return Response{"", status, "", location}; ++ case 4: { ++ result = deroot()->resolve(lower_parm); ++ location.assign(lower_parm.MyPath().to_view()); ++ if (std::get_if(&result)) { ++ return Response{"", 500, "", location}; ++ } ++ resp = std::get_if(&result); ++ if (resp) { ++ resp->status_ = status; ++ return *resp; ++ } ++ break; // MoreDataNeeded to fetch e.g. custom 404 page ++ } ++ default: ++ LOG(ERROR) << "Unsupported status came back from _redirects file: " ++ << status; ++ return ProvenAbsent{}; ++ } ++ } ++ } ++ auto resp = std::get_if(&result); ++ if (resp && resp->location_.empty()) { ++ resp->location_ = location; ++ } ++ return result; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/root.h b/third_party/ipfs_client/src/ipfs_client/ipld/root.h +new file mode 100644 +index 0000000000000..b57951b42f7f1 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/root.h +@@ -0,0 +1,23 @@ ++#ifndef IPFS_ROOT_H_ ++#define IPFS_ROOT_H_ ++ ++#include ++#include ++ ++#include ++ ++namespace ipfs::ipld { ++class Root : public DagNode { ++ std::optional redirects_; ++ ++ ResolveResult resolve(ResolutionState& params) override; ++ std::shared_ptr rooted() override; ++ std::shared_ptr deroot() override; ++ ++ public: ++ Root(std::shared_ptr); ++ virtual ~Root() noexcept; ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_ROOT_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/small_directory.cc b/third_party/ipfs_client/src/ipfs_client/ipld/small_directory.cc +new file mode 100644 +index 0000000000000..b8613663932ca +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/small_directory.cc +@@ -0,0 +1,35 @@ ++#include "small_directory.h" ++ ++#include ++#include "ipfs_client/generated_directory_listing.h" ++#include "ipfs_client/path2url.h" ++ ++#include "log_macros.h" ++ ++#include ++ ++using namespace std::literals; ++ ++using Self = ipfs::ipld::SmallDirectory; ++ ++auto Self::resolve(ResolutionState& params) -> ResolveResult { ++ if (params.IsFinalComponent()) { ++ LOG(INFO) << "Directory listing requested for " << params.MyPath(); ++ auto result = CallChild(params, "index.html"); ++ if (auto resp = std::get_if(&result)) { ++ resp->mime_ = "text/html"; ++ } ++ if (!std::get_if(&result)) { ++ return result; ++ } ++ auto dir_path = params.MyPath().to_view(); ++ GeneratedDirectoryListing index_html{dir_path}; ++ for (auto& [name, link] : links_) { ++ index_html.AddEntry(name); ++ } ++ return Response{"text/html", 200, index_html.Finish(), ""}; ++ } ++ return CallChild(params); ++} ++ ++Self::~SmallDirectory() {} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/small_directory.h b/third_party/ipfs_client/src/ipfs_client/ipld/small_directory.h +new file mode 100644 +index 0000000000000..a076122c5041f +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/small_directory.h +@@ -0,0 +1,17 @@ ++#ifndef IPFS_UNIXFS_DIRECTORY_H_ ++#define IPFS_UNIXFS_DIRECTORY_H_ ++ ++#include "ipfs_client/ipld/link.h" ++ ++#include ++ ++namespace ipfs::ipld { ++class SmallDirectory : public DagNode { ++ ResolveResult resolve(ResolutionState&) override; ++ ++ public: ++ virtual ~SmallDirectory() noexcept; ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_UNIXFS_DIRECTORY_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/symlink.cc b/third_party/ipfs_client/src/ipfs_client/ipld/symlink.cc +new file mode 100644 +index 0000000000000..b35725ad7f703 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/symlink.cc +@@ -0,0 +1,37 @@ ++#include "symlink.h" ++ ++#include "log_macros.h" ++ ++using Self = ipfs::ipld::Symlink; ++ ++Self::Symlink(std::string target) : target_{target} {} ++ ++Self::~Symlink() {} ++ ++auto Self::resolve(ResolutionState& params) -> ResolveResult { ++ std::string result; ++ if (!is_absolute()) { ++ auto left_path = params.MyPath(); ++ left_path.pop_n(2); // Returning a path relative to content root. ++ left_path.pop_back(); // Because the final component refers to this ++ // symlink, which is getting replaced with target ++ result.assign(left_path.to_view()); ++ } ++ result.append("/").append(target_); ++ if (!params.IsFinalComponent()) { ++ result.append("/").append(params.PathToResolve().to_string()); ++ } ++ std::size_t i; ++ while ((i = result.find("//")) != std::string::npos) { ++ result.erase(i, 1); ++ } ++ if (result.ends_with('/')) { ++ result.resize(result.size() - 1); ++ } ++ LOG(INFO) << "symlink: '" << params.MyPath() << "' -> '" << result << "'."; ++ return PathChange{result}; ++} ++ ++bool Self::is_absolute() const { ++ return target_.at(0) == '/'; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/symlink.h b/third_party/ipfs_client/src/ipfs_client/ipld/symlink.h +new file mode 100644 +index 0000000000000..937f0d248c25d +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/symlink.h +@@ -0,0 +1,20 @@ ++#ifndef IPFS_SYMLINK_H_ ++#define IPFS_SYMLINK_H_ ++ ++#include ++ ++namespace ipfs::ipld { ++class Symlink : public DagNode { ++ std::string const target_; ++ ++ ResolveResult resolve(ResolutionState& params) override; ++ ++ bool is_absolute() const; ++ ++ public: ++ Symlink(std::string target); ++ ~Symlink() noexcept override; ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_SYMLINK_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/unixfs_file.cc b/third_party/ipfs_client/src/ipfs_client/ipld/unixfs_file.cc +new file mode 100644 +index 0000000000000..784a1df367152 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/unixfs_file.cc +@@ -0,0 +1,50 @@ ++#include "unixfs_file.h" ++ ++#include "log_macros.h" ++ ++using namespace std::literals; ++ ++using Self = ipfs::ipld::UnixfsFile; ++ ++auto Self::resolve(ResolutionState& params) -> ResolveResult { ++ if (!params.IsFinalComponent()) { ++ LOG(ERROR) << "Can't path through a file, (at " << params.MyPath() ++ << ") but given the path " << params.PathToResolve(); ++ return ProvenAbsent{}; ++ } ++ std::vector missing; ++ std::string body; ++ for (auto& child : links_) { ++ auto& link = child.second; ++ if (!link.node) { ++ link.node = params.GetBlock(link.cid); ++ } ++ if (link.node) { ++ auto recurse = link.node->resolve(params); ++ auto mdn = std::get_if(&recurse); ++ if (mdn) { ++ missing.insert(missing.end(), mdn->ipfs_abs_paths_.begin(), ++ mdn->ipfs_abs_paths_.end()); ++ continue; ++ } ++ if (missing.empty()) { ++ body.append(std::get(recurse).body_); ++ } ++ } else { ++ missing.push_back("/ipfs/" + link.cid); ++ } ++ } ++ if (missing.empty()) { ++ return Response{ ++ "", ++ 200, ++ body, ++ params.MyPath().to_string(), ++ }; ++ } ++ auto result = MoreDataNeeded{missing}; ++ result.insist_on_car = true; ++ return result; ++} ++ ++Self::~UnixfsFile() {} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipld/unixfs_file.h b/third_party/ipfs_client/src/ipfs_client/ipld/unixfs_file.h +new file mode 100644 +index 0000000000000..3447e949d330e +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipld/unixfs_file.h +@@ -0,0 +1,15 @@ ++#ifndef IPFS_UNIXFS_FILE_H_ ++#define IPFS_UNIXFS_FILE_H_ ++ ++#include ++ ++namespace ipfs::ipld { ++class UnixfsFile : public DagNode { ++ ResolveResult resolve(ResolutionState&) override; ++ ++ public: ++ virtual ~UnixfsFile() noexcept; ++}; ++} // namespace ipfs::ipld ++ ++#endif // IPFS_UNIXFS_FILE_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/ipns_names.cc b/third_party/ipfs_client/src/ipfs_client/ipns_names.cc +new file mode 100644 +index 0000000000000..6eccfaa7dc51b +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipns_names.cc +@@ -0,0 +1,101 @@ ++#include ++ ++#include ++ ++#include "log_macros.h" ++ ++using Self = ipfs::IpnsNames; ++ ++void Self::NoSuchName(std::string const& name) { ++ names_[name]; // If it already exists, leave it. ++} ++void Self::AssignName(std::string const& name, ValidatedIpns entry) { ++ auto& res = entry.value; ++ if (res.size() && res.front() == '/') { ++ res.erase(0, 1); ++ } ++ auto endofcid = res.find_first_of("/?#", 6); ++ using namespace libp2p::multi; ++ auto cid_str = res.substr(5, endofcid); ++ LOG(INFO) << "IPNS points to CID " << cid_str; ++ auto cid = Cid(cid_str); ++ if (cid.valid()) { ++ auto desensitized = res.substr(0, 5); ++ desensitized.append(cid_str); ++ if (endofcid < res.size()) { ++ auto extra = res.substr(endofcid); ++ LOG(INFO) << name << " resolution contains oddity '" << extra; ++ desensitized.append(extra); ++ } ++ LOG(INFO) << name << " now resolves to (desensitized)" << desensitized; ++ entry.value = desensitized; ++ } else { ++ LOG(INFO) << name << " now resolves to (extra level)" << res; ++ } ++ auto it = names_.find(name); ++ if (it == names_.end()) { ++ names_.emplace(name, std::move(entry)); ++ } else if (it->second.sequence < entry.sequence) { ++ LOG(INFO) << "Updating IPNS record for " << name << " from sequence " ++ << it->second.sequence << " where it pointed to " ++ << it->second.value << " to sequence " << entry.sequence ++ << " where it points to " << entry.value; ++ it->second = std::move(entry); ++ } else { ++ LOG(INFO) << "Discarding redundant IPNS record for " << name; ++ } ++} ++void Self::AssignDnsLink(std::string const& name, std::string_view target) { ++ ValidatedIpns v; ++ v.value.assign(target); ++ auto t = std::time(nullptr); ++ v.use_until = v.cache_until = t + 300; ++ AssignName(name, std::move(v)); ++} ++ ++std::string_view Self::NameResolvedTo(std::string_view original_name) const { ++ std::string name{original_name}; ++ std::string_view prev = ""; ++ auto trailer = names_.end(); ++ auto trail_step = false; ++ auto now = std::time(nullptr); ++ while (true) { ++ auto it = names_.find(name); ++ if (names_.end() == it) { ++ LOG(INFO) << "Host not in immediate access map: " << name << " (" ++ << std::string{original_name} << ')'; ++ return prev; ++ } else if (it == trailer) { ++ LOG(ERROR) << "Host cycle found in IPNS: " << std::string{original_name} ++ << ' ' << name; ++ return ""; ++ } ++ auto& target = it->second.value; ++ if (target.empty()) { ++ return kNoSuchName; ++ } ++ if (target.at(2) == 'f') { ++ return target; ++ } ++ if (it->second.use_until < now) { ++ return prev; ++ } ++ if (trail_step) { ++ if (trailer == names_.end()) { ++ trailer = names_.find(name); ++ } else { ++ trailer = names_.find(trailer->second.value.substr(5)); ++ } ++ } ++ trail_step = !trail_step; ++ prev = it->second.value; ++ name.assign(prev, 5); ++ } ++} ++auto Self::Entry(std::string const& name) -> ValidatedIpns const* { ++ auto it = names_.find(name); ++ return it == names_.end() ? nullptr : &(it->second); ++} ++ ++Self::IpnsNames() {} ++Self::~IpnsNames() {} +diff --git a/third_party/ipfs_client/src/ipfs_client/ipns_record.cc b/third_party/ipfs_client/src/ipfs_client/ipns_record.cc +new file mode 100644 +index 0000000000000..86be780f9066f +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/ipns_record.cc +@@ -0,0 +1,244 @@ ++#include ++ ++#include ++#include ++#include ++ ++#include "log_macros.h" ++ ++#include ++#include ++ ++#if __has_include() ++#include ++#else ++#include "ipfs_client/ipns_record.pb.h" ++#endif ++ ++namespace { ++bool matches(ipfs::MultiHash const& hash, ++ ipfs::ByteView pubkey_bytes, ++ ipfs::ContextApi& api) { ++ auto result = api.Hash(hash.type(), pubkey_bytes); ++ if (!result.has_value()) { ++ return false; ++ } ++ return std::equal(result->begin(), result->end(), hash.digest().begin(), ++ hash.digest().end()); ++} ++} // namespace ++ ++namespace { ++void assign(std::string& out, ++ ipfs::DagCborValue& top, ++ std::string_view key) { ++ auto p = top.at(key); ++ if (!p) { ++ out.assign("Key '").append(key).append("' not present in IPNS CBOR!"); ++ } else { ++ // YEP! as_bytes() . There are only 2 string values here, they are logically ++ // text, but they are defined in the spec to be bytes. ++ auto o = p->as_bytes(); ++ if (o.has_value()) { ++ auto chars = reinterpret_cast(o.value().data()); ++ out.assign(chars, o.value().size()); ++ } else { ++ out.assign("Key '").append(key).append( ++ "' was not a string in IPNS CBOR!"); ++ } ++ } ++} ++void assign(std::uint64_t& out, ++ ipfs::DagCborValue& top, ++ std::string_view key) { ++ auto p = top.at(key); ++ if (!p) { ++ LOG(ERROR) << "Key '" << key << "' is not present in IPNS CBOR!"; ++ out = std::numeric_limits::max(); ++ } else { ++ auto o = p->as_unsigned(); ++ if (o.has_value()) { ++ out = o.value(); ++ } else { ++ LOG(ERROR) << "Key '" << key ++ << "' is not an unsigned integer in IPNS CBOR!"; ++ out = std::numeric_limits::max(); ++ } ++ } ++} ++} // namespace ++ ++auto ipfs::ValidateIpnsRecord(ipfs::ByteView top_level_bytes, ++ Cid const& name, ++ ContextApi& api) -> std::optional { ++ DCHECK_EQ(name.codec(), MultiCodec::LIBP2P_KEY); ++ if (name.codec() != MultiCodec::LIBP2P_KEY) { ++ return {}; ++ } ++ // https://github.com/ipfs/specs/blob/main/ipns/IPNS.md#record-verification ++ ++ // Before parsing the protobuf, confirm that the serialized IpnsEntry bytes ++ // sum to less than or equal to the size limit. ++ if (top_level_bytes.size() > MAX_IPNS_PB_SERIALIZED_SIZE) { ++ LOG(ERROR) << "IPNS record too large: " << top_level_bytes.size(); ++ return {}; ++ } ++ ++ ipfs::ipns::IpnsEntry entry; ++ if (!entry.ParseFromArray(top_level_bytes.data(), top_level_bytes.size())) { ++ LOG(ERROR) << "Failed to parse top-level bytes as a protobuf"; ++ return {}; ++ } ++ ++ // Confirm IpnsEntry.signatureV2 and IpnsEntry.data are present and are not ++ // empty ++ if (!entry.has_signaturev2()) { ++ LOG(ERROR) << "IPNS record contains no .signatureV2!"; ++ return {}; ++ } ++ if (!entry.has_data() || entry.data().empty()) { ++ LOG(ERROR) << "IPNS record has no .data"; ++ return {}; ++ } ++ ++ // The only supported value is 0, which indicates the validity field contains ++ // the expiration date after which the IPNS record becomes invalid. ++ DCHECK_EQ(entry.validitytype(), 0); ++ ++ auto parsed = ++ api.ParseCbor({reinterpret_cast(entry.data().data()), ++ entry.data().size()}); ++ if (!parsed) { ++ LOG(ERROR) << "CBOR parsing failed."; ++ return {}; ++ } ++ IpnsCborEntry result; ++ assign(result.value, *parsed, "Value"); ++ if (entry.has_value() && result.value != entry.value()) { ++ LOG(ERROR) << "Mismatch on Value field in IPNS record... CBOR(v2): '" ++ << result.value << "' but PB(v1): '" << entry.value() ++ << "' : " << parsed->html(); ++ return {}; ++ } ++ ipfs::ByteView public_key; ++ if (entry.has_pubkey()) { ++ public_key = ipfs::ByteView{ ++ reinterpret_cast(entry.pubkey().data()), ++ entry.pubkey().size()}; ++ if (!matches(name.multi_hash(), public_key, api)) { ++ LOG(ERROR) << "Given IPNS record contains a pubkey that does not match " ++ "the hash from the IPNS name that fetched it!"; ++ return {}; ++ } ++ } else if (name.hash_type() == HashType::IDENTITY) { ++ public_key = name.hash(); ++ } else { ++ LOG(ERROR) << "IPNS record contains no public key, and the IPNS name " ++ << name.to_string() ++ << " is a true hash, not identity. Validation impossible."; ++ return {}; ++ } ++ ipfs::ipns::PublicKey pk; ++ auto* pkbp = reinterpret_cast(public_key.data()); ++ if (!pk.ParseFromArray(pkbp, public_key.size())) { ++ LOG(ERROR) << "Failed to parse public key bytes"; ++ return {}; ++ } ++ LOG(INFO) << "Record contains a public key of type " << pk.type() ++ << " and points to " << entry.value(); ++ auto& signature_str = entry.signaturev2(); ++ ByteView signature{reinterpret_cast(signature_str.data()), ++ signature_str.size()}; ++ // https://specs.ipfs.tech/ipns/ipns-record/#record-verification ++ // Create bytes for signature verification by concatenating ++ // ipto_hex(ns-signature:// prefix (bytes in hex: ++ // 69706e732d7369676e61747572653a) with raw CBOR bytes from IpnsEntry.data ++ auto bytes_str = entry.data(); ++ bytes_str.insert( ++ 0, "\x69\x70\x6e\x73\x2d\x73\x69\x67\x6e\x61\x74\x75\x72\x65\x3a"); ++ ByteView bytes{reinterpret_cast(bytes_str.data()), ++ bytes_str.size()}; ++ ByteView key_bytes{reinterpret_cast(pk.data().data()), ++ pk.data().size()}; ++ if (!api.VerifyKeySignature(static_cast(pk.type()), signature, ++ bytes, key_bytes)) { ++ LOG(ERROR) << "Verification failed!!"; ++ return {}; ++ } ++ // TODO check expiration date ++ if (entry.has_value() && entry.value() != result.value) { ++ LOG(ERROR) << "IPNS " << name.to_string() << " has different values for V1(" ++ << entry.value() << ") and V2(" << result.value << ')'; ++ return {}; ++ } ++ assign(result.validity, *parsed, "Validity"); ++ if (entry.has_validity() && entry.validity() != result.validity) { ++ LOG(ERROR) << "IPNS " << name.to_string() ++ << " has different validity for V1(" << entry.validity() ++ << ") and V2(" << result.validity << ')'; ++ return {}; ++ } ++ assign(result.validityType, *parsed, "ValidityType"); ++ if (entry.has_validitytype() && ++ entry.validitytype() != static_cast(result.validityType)) { ++ LOG(ERROR) << "IPNS " << name.to_string() ++ << " has different validity types for V1(" ++ << entry.validitytype() << ") and V2(" << result.validityType ++ << ')'; ++ return {}; ++ } ++ assign(result.sequence, *parsed, "Sequence"); ++ if (entry.has_sequence() && entry.sequence() != result.sequence) { ++ LOG(ERROR) << "IPNS " << name.to_string() ++ << " has different validity types for V1(" << entry.sequence() ++ << ") and V2(" << result.sequence << ')'; ++ return {}; ++ } ++ assign(result.ttl, *parsed, "TTL"); ++ if (entry.has_ttl() && entry.ttl() != result.ttl) { ++ LOG(ERROR) << "IPNS " << name.to_string() ++ << " has different validity types for V1(" << entry.ttl() ++ << ") and V2(" << result.ttl << ')'; ++ return {}; ++ } ++ LOG(INFO) << "IPNS record verification passes for " << name.to_string() ++ << " sequence: " << result.sequence << " points at " ++ << result.value; ++ return result; ++} ++ ++ipfs::ValidatedIpns::ValidatedIpns() = default; ++ipfs::ValidatedIpns::ValidatedIpns(ValidatedIpns&&) = default; ++ipfs::ValidatedIpns::ValidatedIpns(ValidatedIpns const&) = default; ++auto ipfs::ValidatedIpns::operator=(ValidatedIpns const&) ++ -> ValidatedIpns& = default; ++ipfs::ValidatedIpns::ValidatedIpns(IpnsCborEntry const& e) ++ : value{e.value}, sequence{e.sequence} { ++ std::istringstream ss{e.validity}; ++ std::tm t = {}; ++ ss >> std::get_time(&t, "%Y-%m-%dT%H:%M:%S"); ++ long ttl = (e.ttl / 1'000'000'000UL) + 1; ++#ifdef _MSC_VER ++ use_until = _mkgmtime(&t); ++#else ++ use_until = timegm(&t); ++#endif ++ cache_until = std::time(nullptr) + ttl; ++} ++ ++std::string ipfs::ValidatedIpns::Serialize() const { ++ DCHECK_EQ(value.find(' '), std::string::npos); ++ DCHECK_EQ(gateway_source.find(' '), std::string::npos); ++ std::ostringstream ss; ++ ss << std::hex << sequence << ' ' << use_until << ' ' << cache_until << ' ' ++ << fetch_time << ' ' << resolution_ms << ' ' << value << ' ' ++ << gateway_source; ++ return ss.str(); ++} ++auto ipfs::ValidatedIpns::Deserialize(std::string s) -> ValidatedIpns { ++ std::istringstream ss(s); ++ ValidatedIpns e; ++ ss >> std::hex >> e.sequence >> e.use_until >> e.cache_until >> ++ e.fetch_time >> e.resolution_ms >> e.value >> e.gateway_source; ++ return e; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/logger.cc b/third_party/ipfs_client/src/ipfs_client/logger.cc +new file mode 100644 +index 0000000000000..8c093bfca89aa +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/logger.cc +@@ -0,0 +1,76 @@ ++#include ++ ++#include ++ ++#include ++ ++namespace lg = ipfs::log; ++ ++namespace { ++lg::Level current_level = lg::Level::WARN; ++lg::Handler current_handler = nullptr; ++ ++void CheckLevel(google::protobuf::LogLevel lv, ++ char const* f, ++ int l, ++ std::string const& m) { ++ auto lev = static_cast(lv); ++ if (lev < static_cast(current_level)) { ++ return; ++ } ++ if (!current_handler) { ++ return; ++ } ++ current_handler(m, f, l, static_cast(lev)); ++} ++} // namespace ++ ++void lg::SetLevel(Level lev) { ++ IsInitialized(); ++ current_level = lev; ++} ++ ++void lg::SetHandler(Handler h) { ++ current_handler = h; ++ google::protobuf::SetLogHandler(&CheckLevel); ++} ++ ++void lg::DefaultHandler(std::string const& message, ++ char const* source_file, ++ int source_line, ++ Level lev) { ++ std::clog << source_file << ':' << source_line << ": " << LevelDescriptor(lev) ++ << ": " << message << '\n'; ++ if (lev == Level::FATAL) { ++ std::abort(); ++ } ++} ++ ++std::string_view lg::LevelDescriptor(Level l) { ++ switch (l) { ++ case Level::TRACE: ++ return "trace"; ++ case Level::DEBUG: ++ return "debug"; ++ case Level::INFO: ++ return "note"; // The next 3 are gcc- & clang-inspired ++ case Level::WARN: ++ return "warning"; ++ case Level::ERROR: ++ return "error"; ++ case Level::FATAL: ++ return " ### FATAL ERROR ### "; ++ case Level::OFF: ++ return "off"; ++ default: ++ return "Unknown log level used: possible corruption?"; ++ } ++} ++ ++bool lg::IsInitialized() { ++ if (current_handler) { ++ return true; ++ } ++ SetHandler(&DefaultHandler); ++ return false; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/multi_base.cc b/third_party/ipfs_client/src/ipfs_client/multi_base.cc +new file mode 100644 +index 0000000000000..58c9a0f18d100 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/multi_base.cc +@@ -0,0 +1,133 @@ ++#include "ipfs_client/multi_base.h" ++ ++#include "bases/b16_upper.h" ++#include "bases/b32.h" ++ ++#include ++ ++#include "log_macros.h" ++ ++using namespace std::literals; ++ ++namespace imb = ipfs::mb; ++namespace { ++constexpr std::string_view UnsupportedMultibase = "unsupported-multibase"; ++ ++template ++std::string encode_adapt(ipfs::ByteView bytes) { ++ auto p = reinterpret_cast(bytes.data()); ++ typename Target::encoder target; ++ return target.process({p, bytes.size()}); ++} ++enum class EncodedCase { lower, UPPER, Sensitive }; ++template ++std::vector decode_adapt(std::string_view encoded_sv) { ++ typename Target::decoder target; ++ std::string encoded_s{encoded_sv}; ++ switch (ec) { ++ case EncodedCase::lower: ++ for (auto& c : encoded_s) { ++ if (c >= 'A' && c <= 'Z') { ++ c = c - 'A' + 'a'; ++ } ++ } ++ break; ++ case EncodedCase::UPPER: ++ for (auto& c : encoded_s) { ++ if (c >= 'a' && c <= 'z') { ++ c = c - 'a' + 'A'; ++ } ++ } ++ break; ++ case EncodedCase::Sensitive: ++ break; ++ } ++ auto s = target.process(encoded_s); ++ auto b = reinterpret_cast(s.data()); ++ auto e = b + s.size(); ++ return std::vector(b, e); ++} ++template ++constexpr imb::Codec adapt(std::string_view name) { ++ return imb::Codec{&decode_adapt, ++ &encode_adapt, name}; ++} ++} // namespace ++ ++auto imb::Codec::Get(Code c) -> Codec const* { ++ switch (c) { ++ case Code::IDENTITY: ++ return nullptr; ++ case Code::UNSUPPORTED: ++ return nullptr; ++ case Code::BASE16_LOWER: { ++ static auto b16 = ++ adapt("base16"sv); ++ return &b16; ++ } ++ case Code::BASE16_UPPER: { ++ static auto b16u = ++ adapt("base16upper"sv); ++ return &b16u; ++ } ++ case Code::BASE32_LOWER: { ++ static auto b32 = adapt("base32"sv); ++ return &b32; ++ } ++ case Code::BASE32_UPPER: { ++ static auto b32u = ++ adapt("base32upper"sv); ++ return &b32u; ++ } ++ case Code::BASE36_LOWER: { ++ static auto b36 = ++ adapt("base36"sv); ++ return &b36; ++ } ++ case Code::BASE36_UPPER: ++ return nullptr; ++ case Code::BASE58_BTC: { ++ static auto b58 = ++ adapt("base58btc"sv); ++ return &b58; ++ } ++ case Code::BASE64: ++ return nullptr; ++ } ++ return nullptr; ++} ++std::string_view imb::GetName(Code c) { ++ if (auto codec = Codec::Get(c)) { ++ return codec->name; ++ } ++ return UnsupportedMultibase; ++} ++auto imb::CodeFromPrefix(char ch) -> Code { ++ auto c = static_cast(ch); ++ return Codec::Get(c) ? Code::UNSUPPORTED : c; ++} ++auto imb::decode(std::string_view mb_str) -> std::optional> { ++ if (mb_str.empty()) { ++ return std::nullopt; ++ } ++ if (auto* codec = Codec::Get(static_cast(mb_str[0]))) { ++ return codec->decode(mb_str.substr(1)); ++ } else { ++ return std::nullopt; ++ } ++} ++std::string imb::encode(Code c, ByteView bs) { ++ if (auto codec = Codec::Get(c)) { ++ auto rv = codec->encode(bs); ++ if (rv.size() >= bs.size()) { ++ rv.insert(0UL, 1UL, static_cast(c)); ++ return rv; ++ } else { ++ LOG(ERROR) << "Error encoding into base " << codec->name; ++ } ++ } else { ++ LOG(ERROR) << "Can't encode to multibase " << static_cast(c) ++ << " because I can't find a codec??"; ++ } ++ return {}; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/multi_hash.cc b/third_party/ipfs_client/src/ipfs_client/multi_hash.cc +new file mode 100644 +index 0000000000000..20cf5b19a16c8 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/multi_hash.cc +@@ -0,0 +1,56 @@ ++#include ++ ++#include ++ ++using Self = ipfs::MultiHash; ++using VarInt = libp2p::multi::UVarint; ++ ++Self::MultiHash(ipfs::HashType t, ipfs::ByteView digest) ++ : type_{t}, hash_(digest.begin(), digest.end()) {} ++ ++Self::MultiHash(ipfs::ByteView bytes) { ++ ReadPrefix(bytes); ++} ++bool Self::ReadPrefix(ipfs::ByteView& bytes) { ++ auto i = VarInt::create(bytes); ++ if (!i) { ++ return false; ++ } ++ bytes = bytes.subspan(i->size()); ++ auto type = Validate(static_cast(i->toUInt64())); ++ i = VarInt::create(bytes); ++ if (!i) { ++ return false; ++ } ++ auto length = i->toUInt64(); ++ if (length > bytes.size()) { ++ return false; ++ } ++ bytes = bytes.subspan(i->size()); ++ hash_.assign(bytes.begin(), std::next(bytes.begin(), length)); ++ bytes = bytes.subspan(length); ++ type_ = type; ++ return true; ++} ++bool Self::valid() const { ++ return type_ != HashType::INVALID && hash_.size() > 0UL; ++} ++namespace { ++constexpr std::string_view InvalidHashTypeName; ++} ++std::string_view ipfs::GetName(HashType t) { ++ switch (t) { ++ case HashType::INVALID: ++ return InvalidHashTypeName; ++ case HashType::IDENTITY: ++ return "identity"; ++ case HashType::SHA2_256: ++ return "sha2-256"; ++ } ++ // Don't use default: -> let it fall through. We want compiler warnings about ++ // unhandled cases. ++ return InvalidHashTypeName; ++} ++auto ipfs::Validate(HashType t) -> HashType { ++ return GetName(t) == InvalidHashTypeName ? HashType::INVALID : t; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/multicodec.cc b/third_party/ipfs_client/src/ipfs_client/multicodec.cc +new file mode 100644 +index 0000000000000..68cad03ea5862 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/multicodec.cc +@@ -0,0 +1,33 @@ ++#include ++ ++using Cdc = ipfs::MultiCodec; ++ ++namespace { ++constexpr std::string_view InvalidMulticodecLabel{"invalid-multicodec"}; ++} ++ ++std::string_view ipfs::GetName(Cdc c) { ++ switch (c) { ++ case Cdc::INVALID: ++ return InvalidMulticodecLabel; ++ case Cdc::IDENTITY: ++ return "identity"; ++ case Cdc::RAW: ++ return "raw"; ++ case Cdc::DAG_PB: ++ return "dag-pb"; ++ case Cdc::DAG_CBOR: ++ return "dag-cbor"; ++ case Cdc::LIBP2P_KEY: ++ return "libp2p-key"; ++ case Cdc::DAG_JSON: ++ return "dag-json"; ++ } ++ return InvalidMulticodecLabel; ++} ++Cdc ipfs::Validate(Cdc c) { ++ if (GetName(c) == InvalidMulticodecLabel) { ++ return Cdc::INVALID; ++ } ++ return c; ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/orchestrator.cc b/third_party/ipfs_client/src/ipfs_client/orchestrator.cc +new file mode 100644 +index 0000000000000..3392ae1126e39 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/orchestrator.cc +@@ -0,0 +1,146 @@ ++#include "ipfs_client/orchestrator.h" ++ ++#include ++#include ++#include ++ ++#include "log_macros.h" ++#include "path2url.h" ++ ++using namespace std::literals; ++ ++using Self = ipfs::Orchestrator; ++ ++Self::Orchestrator(std::shared_ptr requestor, ++ std::shared_ptr api) ++ // : gw_requestor_{ga}, api_{api}, requestor_{requestor} { ++ : api_{api}, requestor_{requestor} { ++ DCHECK(requestor); ++} ++ ++void Self::build_response(std::shared_ptr req) { ++ if (!req || !req->ready_after()) { ++ return; ++ } ++ auto req_path = req->path(); ++ VLOG(2) << "build_response(" << req_path.to_string() << ')'; ++ req_path.pop(); // namespace ++ std::string affinity{req_path.pop()}; ++ auto it = dags_.find(affinity); ++ if (dags_.end() == it) { ++ if (gw_request(req, req->path(), affinity)) { ++ build_response(req); ++ } ++ } else { ++ VLOG(2) << "Requesting root " << affinity << " resolve path " ++ << req_path.to_string(); ++ auto root = it->second->rooted(); ++ if (root != it->second) { ++ it->second = root; ++ } ++ from_tree(req, root, req_path, affinity); ++ } ++} ++void Self::from_tree(std::shared_ptr req, ++ ipfs::ipld::NodePtr& node, ++ SlashDelimited relative_path, ++ std::string const& affinity) { ++ auto root = node->rooted(); ++ auto block_look_up = [this](auto& k) { ++ auto i = dags_.find(k); ++ return i == dags_.end() ? ipld::NodePtr{} : i->second; ++ }; ++ auto start = std::string{req->path().pop_n(2)}; ++ auto result = root->resolve(relative_path, block_look_up); ++ auto response = std::get_if(&result); ++ if (response) { ++ VLOG(2) << "Tree gave us a response: status=" << response->status_ ++ << " mime=" << response->mime_ ++ << " location=" << response->location_ << " body is " ++ << response->body_.size() << " bytes."; ++ if (response->mime_.empty() && !response->body_.empty()) { ++ if (response->location_.empty()) { ++ LOG(INFO) << "Request for " << req->path() ++ << " returned no location, so sniffing from request path and " ++ "body of " ++ << response->body_.size() << "B."; ++ response->mime_ = sniff(req->path(), response->body_); ++ } else { ++ std::string hit_path{req->path().pop_n(2)}; ++ if (!hit_path.ends_with('/') && ++ !(response->location_.starts_with('/'))) { ++ hit_path.push_back('/'); ++ } ++ hit_path.append(response->location_); ++ LOG(INFO) << "Request for " << req->path() << " returned a location of " ++ << response->location_ << " and a body of " ++ << response->body_.size() << " bytes, sniffing mime from " ++ << hit_path; ++ response->mime_ = sniff(SlashDelimited{hit_path}, response->body_); ++ } ++ } ++ req->finish(*response); ++ } else if (std::holds_alternative(result)) { ++ auto& np = std::get(result); ++ LOG(INFO) << "Symlink converts request to " << req->path().to_string() ++ << " into " << np.new_path ++ << ". TODO - check for infinite loops."; ++ req->new_path(np.new_path); ++ build_response(req); ++ } else if (std::get_if(&result)) { ++ req->finish(Response::IMMUTABLY_GONE); ++ } else { ++ auto& mps = std::get(result).ipfs_abs_paths_; ++ req->till_next(mps.size()); ++ if (std::any_of(mps.begin(), mps.end(), [this, &req, &affinity](auto& p) { ++ return gw_request(req, SlashDelimited{p}, affinity); ++ })) { ++ from_tree(req, node, relative_path, affinity); ++ } ++ } ++} ++bool Self::gw_request(std::shared_ptr ir, ++ ipfs::SlashDelimited path, ++ std::string const& aff) { ++ VLOG(1) << "Seeking " << path.to_string(); ++ auto req = gw::GatewayRequest::fromIpfsPath(path); ++ if (req) { ++ req->dependent = ir; ++ req->orchestrator(shared_from_this()); ++ req->affinity = aff; ++ requestor_->request(req); ++ } else { ++ LOG(ERROR) << "Failed to create a request for " << path.to_string(); ++ } ++ return false; ++} ++ ++bool Self::add_node(std::string key, ipfs::ipld::NodePtr p) { ++ if (p) { ++ if (dags_.insert({key, p}).second) { ++ p->set_api(api_); ++ } ++ return true; ++ } else { ++ LOG(INFO) << "NULL block attempted to be added for " << key; ++ } ++ return false; ++} ++ ++std::string Self::sniff(ipfs::SlashDelimited p, std::string const& body) const { ++ auto fake_url = path2url(p.to_string()); ++ auto file_name = p.peek_back(); ++ auto dot = file_name.find_last_of('.'); ++ std::string ext = ""; ++ if (dot < file_name.size()) { ++ ext.assign(file_name, dot + 1); ++ } ++ auto result = api_->MimeType(ext, body, fake_url); ++ LOG(INFO) << "Deduced mime from (ext=" << ext << " body of " << body.size() ++ << " bytes, 'url'=" << fake_url << ")=" << result; ++ return result; ++} ++ ++bool Self::has_key(std::string const& k) const { ++ return dags_.count(k); ++} +\ No newline at end of file +diff --git a/third_party/ipfs_client/src/ipfs_client/path2url.cc b/third_party/ipfs_client/src/ipfs_client/path2url.cc +new file mode 100644 +index 0000000000000..0d7cf305a47b4 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/path2url.cc +@@ -0,0 +1,16 @@ ++#include "path2url.h" ++ ++#include "log_macros.h" ++ ++std::string ipfs::path2url(std::string p) { ++ while (!p.empty() && p[0] == '/') { ++ p.erase(0UL, 1UL); ++ } ++ DCHECK_EQ(p.at(0), 'i'); ++ DCHECK_EQ(p.at(1), 'p'); ++ DCHECK(p.at(2) == 'f' || p.at(2) == 'n'); ++ DCHECK_EQ(p.at(3), 's'); ++ DCHECK_EQ(p.at(4), '/'); ++ p.insert(4, ":/"); ++ return p; ++} +diff --git a/third_party/ipfs_client/src/ipfs_client/path2url.h b/third_party/ipfs_client/src/ipfs_client/path2url.h +new file mode 100644 +index 0000000000000..683e92d759b4e +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/path2url.h +@@ -0,0 +1,10 @@ ++#ifndef IPFS_PATH2URL_H_ ++#define IPFS_PATH2URL_H_ ++ ++#include ++ ++namespace ipfs { ++std::string path2url(std::string path_as_string); ++} ++ ++#endif // IPFS_PATH2URL_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/redirects.cc b/third_party/ipfs_client/src/ipfs_client/redirects.cc +new file mode 100644 +index 0000000000000..b2395dcae75c0 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/redirects.cc +@@ -0,0 +1,259 @@ ++#include "redirects.h" ++ ++#include "log_macros.h" ++ ++#include ++ ++#include ++#include ++ ++namespace r = ipfs::redirects; ++using namespace std::literals; ++ ++namespace { ++// 2.4.4 Max File Size ++// The file size must not exceed 64 KiB. ++constexpr std::size_t MAX_SIZE = 64UL * 1024UL * 1024UL; ++ ++// Not including \n which terminates lines ++constexpr std::string_view WHITESPACE = " \t\f\r\v\n"; ++ ++// https://specs.ipfs.tech/http-gateways/web-redirects-file/#status ++constexpr int DEFAULT_STATUS = 301; ++// https://specs.ipfs.tech/http-gateways/web-redirects-file/#error-handling ++constexpr int PARSE_ERROR_STATUS = 500; ++} // namespace ++ ++r::Directive::Directive(std::string_view from, std::string_view to, int status) ++ : to_{to}, status_{status} { ++ SlashDelimited comp_str_s{from}; ++ std::unordered_set placeholders; ++ while (comp_str_s) { ++ auto comp_str = comp_str_s.pop(); ++ if (comp_str.empty()) { ++ LOG(ERROR) << "Got empty slash-delimited component. Should not have."; ++ return; ++ } else if (comp_str == "*") { ++ components_.emplace_back(ComponentType::SPLAT, comp_str); ++ } else if (comp_str[0] == ':') { ++ if (placeholders.insert(comp_str).second) { ++ components_.emplace_back(ComponentType::PLACEHOLDER, comp_str); ++ } else { ++ to_.assign("ERROR: Duplicate placeholder ").append(comp_str); ++ return; ++ } ++ } else { ++ components_.emplace_back(ComponentType::LITERAL, comp_str); ++ } ++ } ++} ++std::uint16_t r::Directive::rewrite(std::string& path) const { ++ auto input = SlashDelimited{path}; ++ auto result = to_; ++ auto replace = [&result](std::string_view ph, std::string_view val) { ++ std::size_t pos; ++ while ((pos = result.find(ph)) < result.size()) { ++ result.replace(pos, ph.size(), val); ++ } ++ }; ++ for (auto [type, comp_str] : components_) { ++ if (!input) { ++ VLOG(2) << "Ran out of input in [" << path ++ << "] before running out of pattern components to match against " ++ "(was looking for [" ++ << comp_str << "]. Not a match."; ++ return 0; ++ } ++ if (type == ComponentType::LITERAL) { ++ if (comp_str != input.pop()) { ++ return 0; ++ } ++ } else if (type == ComponentType::PLACEHOLDER) { ++ replace(comp_str, input.pop()); ++ } else { ++ replace(":splat"sv, input.pop_all()); ++ } ++ } ++ if (input) { ++ return 0; ++ } else { ++ path = result; ++ return status_; ++ } ++} ++std::string r::Directive::error() const { ++ if (starts_with(to_, "ERROR: ")) { ++ return to_; ++ } ++ if (status_ < 200 || status_ > 451) { ++ return "UNSUPPORTED STATUS " + std::to_string(status_); ++ } ++ if (components_.empty()) { ++ return "Empty directive pattern"; ++ } ++ if (to_.empty()) { ++ return "Empty redirect target location"; ++ } ++ if (to_.at(0) != '/' && to_.find("://") == std::string::npos) { ++ return "Location must begin with / or be a URL"; ++ } ++ return {}; ++} ++ ++std::uint16_t r::File::rewrite(std::string& missing_path) const { ++ for (auto& directive : directives_) { ++ auto status = directive.rewrite(missing_path); ++ if (status) { ++ return status; ++ } ++ } ++ return 0; ++} ++r::File::File(std::string_view to_parse) { ++ if (to_parse.size() > MAX_SIZE) { ++ error_ = "INPUT FILE TOO LARGE " + std::to_string(to_parse.size()); ++ return; ++ } ++ for (auto line_number = 1; valid() && to_parse.size(); ++line_number) { ++ auto line_end = to_parse.find('\n'); ++ auto line = to_parse.substr(0UL, line_end); ++ if (!parse_line(line, line_number)) { ++ LOG(INFO) << "Line #" << line_number << " ignored: [" << line << ']'; ++ } else if (directives_.empty()) { ++ LOG(ERROR) << "Expected to have a directive after parsing line #" ++ << line_number << ": " << line; ++ } else if (directives_.back().valid()) { ++ VLOG(1) << "Line #" << line_number << " parsed. " << line; ++ } else { ++ error_ = "FAILURE PARSING LINE # " + std::to_string(line_number); ++ error_.append(": ") ++ .append(directives_.back().error()) ++ .append(" [") ++ .append(line) ++ .push_back(']'); ++ LOG(ERROR) << error_; ++ return; ++ } ++ if (line_end < to_parse.size()) { ++ to_parse.remove_prefix(line_end + 1); ++ } else { ++ break; ++ } ++ } ++ if (directives_.empty()) { ++ error_ = "No redirection directives in _redirects"; ++ LOG(ERROR) << error_; ++ } ++} ++ ++namespace { ++std::pair parse_status(std::string_view line, ++ std::size_t col); ++} ++bool r::File::parse_line(std::string_view line, int line_number) { ++ if (line.empty()) { ++ // empty line is not a directive ++ return false; ++ } ++ auto bpos = line.find_first_not_of(WHITESPACE); ++ if (bpos == std::string_view::npos) { ++ // effectively empty line ++ return false; ++ } else if (line[bpos] == '#') { ++ // https://specs.ipfs.tech/http-gateways/web-redirects-file/#comments ++ return false; ++ } ++ auto epos = line.find_first_of(WHITESPACE, bpos); ++ if (epos == std::string_view::npos) { ++ error_ = "Parsing _redirects file: line # " + std::to_string(line_number); ++ error_ ++ .append(" , expected at least 2 tokens (from and to) for directive: [") ++ .append(line) ++ .append("], but didn't even get whitespace to end from"); ++ return false; ++ } ++ auto from = line.substr(bpos, epos - bpos); ++ bpos = line.find_first_not_of(WHITESPACE, epos); ++ if (bpos == std::string_view::npos) { ++ error_ = "Parsing _redirects file: line # " + std::to_string(line_number); ++ error_ ++ .append(" , expected at least 2 tokens (from and to) for directive: [") ++ .append(line) ++ .append("], but didn't get a to"); ++ return false; ++ } ++ epos = line.find_first_of(WHITESPACE, bpos); ++ auto to = line.substr(bpos, epos - bpos); ++ auto [status, err] = parse_status(line, epos); ++ if (err.empty()) { ++ directives_.emplace_back(from, to, status); ++ return true; ++ } else { ++ error_ = err; ++ LOG(ERROR) << "Error parsing status on line #" << line_number << " [" ++ << line << "]."; ++ return false; ++ } ++} ++ ++namespace { ++ ++std::pair parse_status(std::string_view line, ++ std::size_t col) { ++ if (col >= line.size()) { ++ VLOG(2) << " No status specified, using default."; ++ return {DEFAULT_STATUS, ""}; ++ } ++ auto b = line.find_first_not_of(WHITESPACE, col); ++ if (b >= line.size()) { ++ VLOG(2) ++ << " No status specified (line ended in whitespace), using default."; ++ return {DEFAULT_STATUS, ""}; ++ } ++ auto status_str = line.substr(b); ++ if (status_str.size() < 3) { ++ return {PARSE_ERROR_STATUS, ++ " Not enough characters for a valid status string: [" + ++ std::string{status_str} + "]."}; ++ } ++ auto good = [](int i) { return std::make_pair(i, ""s); }; ++ auto unsupported = [status_str]() { ++ return std::make_pair( ++ PARSE_ERROR_STATUS, ++ "Unsupported status specified in directive:" + std::string{status_str}); ++ }; ++ /* ++ * 200 - OK treated as a rewrite, without changing the URL in the browser. ++ * 301 - Permanent Redirect (default) ++ * 302 - Found (commonly used for Temporary Redirect) ++ * 303 - See Other (replacing PUT and POST with GET) ++ * 307 - Temporary Redirect (explicitly preserving body and HTTP method) ++ * 308 - Permanent Redirect (preserving body & method of original request) ++ * 404 - Not Found (Useful for a pretty 404 page) ++ * 410 - Gone ++ * 451 - Unavailable For Legal Reasons ++ */ ++ switch (status_str[0]) { ++ case '2': ++ return status_str == "200" ? good(200) : unsupported(); ++ case '3': ++ if (status_str[1] != '0') { ++ return unsupported(); ++ } ++ return good(300 + status_str[2] - '0'); ++ case '4': ++ switch (status_str[1]) { ++ case '0': ++ return status_str[2] == '4' ? good(404) : unsupported(); ++ case '1': ++ return status_str[2] == '0' ? good(410) : unsupported(); ++ case '5': ++ return status_str[2] == '1' ? good(451) : unsupported(); ++ default: ++ return unsupported(); ++ } ++ default: ++ return unsupported(); ++ } ++} ++} // namespace +diff --git a/third_party/ipfs_client/src/ipfs_client/redirects.h b/third_party/ipfs_client/src/ipfs_client/redirects.h +new file mode 100644 +index 0000000000000..e0b333f1de2f1 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/redirects.h +@@ -0,0 +1,41 @@ ++#ifndef IPFS_REDIRECTS_H_ ++#define IPFS_REDIRECTS_H_ ++ ++#include ++ ++#include ++#include ++#include ++ ++namespace ipfs { ++namespace redirects { ++class Directive { ++ enum class ComponentType { LITERAL, PLACEHOLDER, SPLAT }; ++ std::vector> components_; ++ std::string to_; ++ int const status_; ++ ++ public: ++ Directive(std::string_view, std::string_view, int); ++ std::uint16_t rewrite(std::string&) const; ++ std::string error() const; ++ bool valid() const { return error().empty(); } ++}; ++class File { ++ std::vector directives_; ++ std::string error_; ++ ++ public: ++ File(std::string_view to_parse); ++ ++ bool valid() const { return error().empty(); } ++ std::string const& error() const { return error_; } ++ std::uint16_t rewrite(std::string& missing_path) const; ++ ++ private: ++ bool parse_line(std::string_view, int); ++}; ++} // namespace redirects ++} // namespace ipfs ++ ++#endif // IPFS_REDIRECTS_H_ +diff --git a/third_party/ipfs_client/src/ipfs_client/response.cc b/third_party/ipfs_client/src/ipfs_client/response.cc +new file mode 100644 +index 0000000000000..411d87d1354e4 +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/response.cc +@@ -0,0 +1,16 @@ ++#include "ipfs_client/response.h" ++ ++using Self = ipfs::Response; ++ ++Self Self::PLAIN_NOT_FOUND{"text/html", static_cast(404), ++ std::string{}, std::string{}}; ++Self Self::IMMUTABLY_GONE{"text/plain", 410, ++ "Using immutable data it has been proven the " ++ "resource does not exist anywhere.", ++ std::string{}}; ++ ++Self Self::HOST_NOT_FOUND{ ++ "text/plain", Self::HOST_NOT_FOUND_STATUS, ++ "either a hostname didn't resolve a DNS TXT records for dnslink=, or we " ++ "can't find a gateway with the necessary IPNS record", ++ std::string{}}; +diff --git a/third_party/ipfs_client/src/ipfs_client/signing_key_type.cc b/third_party/ipfs_client/src/ipfs_client/signing_key_type.cc +new file mode 100644 +index 0000000000000..b6489a47b130f +--- /dev/null ++++ b/third_party/ipfs_client/src/ipfs_client/signing_key_type.cc +@@ -0,0 +1,15 @@ ++#include ++ ++#include ++ ++using T = ipfs::SigningKeyType; ++namespace n = ipfs::ipns; ++ ++// It is critically important that these 2 enumerations remain in-synch. ++// However, some headers that reference SigningKeyType need to be able to ++// compile without access to protobuf. ++static_assert(static_cast(T::RSA) == n::RSA); ++static_assert(static_cast(T::Ed25519) == n::Ed25519); ++static_assert(static_cast(T::Secp256k1) == n::Secp256k1); ++static_assert(static_cast(T::ECDSA) == n::ECDSA); ++static_assert(static_cast(T::KeyTypeCount) == n::KeyType_ARRAYSIZE); +diff --git a/third_party/ipfs_client/src/libp2p/crypto/protobuf_key.hpp b/third_party/ipfs_client/src/libp2p/crypto/protobuf_key.hpp +new file mode 100644 +index 0000000000000..459426f8c58a2 +--- /dev/null ++++ b/third_party/ipfs_client/src/libp2p/crypto/protobuf_key.hpp +@@ -0,0 +1,29 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#ifndef KAGOME_PROTOBUF_KEY_HPP ++#define KAGOME_PROTOBUF_KEY_HPP ++ ++#include ++#include ++ ++#include ++ ++namespace libp2p::crypto { ++ /** ++ * Strict type for key, which is encoded into Protobuf format ++ */ ++ struct ProtobufKey : public boost::equality_comparable { ++ explicit ProtobufKey(std::vector key) : key{std::move(key)} {} ++ ++ std::vector key; ++ ++ bool operator==(const ProtobufKey &other) const { ++ return key == other.key; ++ } ++ }; ++} // namespace libp2p::crypto ++ ++#endif // KAGOME_PROTOBUF_KEY_HPP +diff --git a/third_party/ipfs_client/src/libp2p/multi/multibase_codec/codecs/base16.cc b/third_party/ipfs_client/src/libp2p/multi/multibase_codec/codecs/base16.cc +new file mode 100644 +index 0000000000000..b032cccbbdc1c +--- /dev/null ++++ b/third_party/ipfs_client/src/libp2p/multi/multibase_codec/codecs/base16.cc +@@ -0,0 +1,104 @@ ++#include ++ ++namespace b16 = ipfs::base16; ++ ++namespace { ++std::uint8_t to_i(char c); ++template ++char to_c(std::uint8_t n) { ++ if (n < 10) { ++ return n + '0'; ++ } else { ++ return n - 10 + a; ++ } ++} ++template ++std::string encode(ipfs::ByteView bytes) { ++ std::string result; ++ result.reserve(bytes.size() * 2); ++ for (auto b : bytes) { ++ auto i = to_integer(b); ++ result.push_back(to_c(i >> 4)); ++ result.push_back(to_c(i & 0xF)); ++ } ++ return result; ++} ++} // namespace ++ ++std::string b16::encodeLower(ByteView bytes) { ++ return encode<'a'>(bytes); ++} ++std::string b16::encodeUpper(ByteView bytes) { ++ return encode<'A'>(bytes); ++} ++auto b16::decode(std::string_view s) -> Decoded { ++ ByteArray result(s.size() / 2, ipfs::Byte{}); ++ for (auto i = 0U; i + 1U < s.size(); i += 2U) { ++ auto a = to_i(s[i]); ++ auto b = to_i(s[i + 1]); ++ if (a > 0xF || b > 0xF) { ++ return ipfs::unexpected{BaseError::INVALID_BASE16_INPUT}; ++ } ++ result[i / 2] = ipfs::Byte{static_cast((a << 4) | b)}; ++ } ++ if (s.size() % 2) { ++ auto a = to_i(s.back()); ++ if (a <= 0xF) { ++ result.push_back(ipfs::Byte{a}); ++ } ++ } ++ return result; ++} ++ ++namespace { ++std::uint8_t to_i(char c) { ++ switch (c) { ++ case '0': ++ return 0; ++ case '1': ++ return 1; ++ case '2': ++ return 2; ++ case '3': ++ return 3; ++ case '4': ++ return 4; ++ case '5': ++ return 5; ++ case '6': ++ return 6; ++ case '7': ++ return 7; ++ case '8': ++ return 8; ++ case '9': ++ return 9; ++ case 'a': ++ return 10; ++ case 'b': ++ return 11; ++ case 'c': ++ return 12; ++ case 'd': ++ return 13; ++ case 'e': ++ return 14; ++ case 'f': ++ return 15; ++ case 'A': ++ return 10; ++ case 'B': ++ return 11; ++ case 'C': ++ return 12; ++ case 'D': ++ return 13; ++ case 'E': ++ return 14; ++ case 'F': ++ return 15; ++ default: ++ return 0xFF; ++ } ++} ++} // namespace +diff --git a/third_party/ipfs_client/src/libp2p/multi/multibase_codec/codecs/base32.cc b/third_party/ipfs_client/src/libp2p/multi/multibase_codec/codecs/base32.cc +new file mode 100644 +index 0000000000000..36030d0b445fb +--- /dev/null ++++ b/third_party/ipfs_client/src/libp2p/multi/multibase_codec/codecs/base32.cc +@@ -0,0 +1,200 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++/** ++ * base32 (de)coder implementation as specified by RFC4648. ++ * ++ * Copyright (c) 2010 Adrien Kunysz ++ * ++ * Permission is hereby granted, free of charge, to any person obtaining a copy ++ * of this software and associated documentation files (the "Software"), to deal ++ * in the Software without restriction, including without limitation the rights ++ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ++ * copies of the Software, and to permit persons to whom the Software is ++ * furnished to do so, subject to the following conditions: ++ * ++ * The above copyright notice and this permission notice shall be included in ++ * all copies or substantial portions of the Software. ++ * ++ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ++ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ++ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ++ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ++ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ++ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ++ * THE SOFTWARE. ++ **/ ++ ++#include "libp2p/multi/multibase_codec/codecs/base32.hpp" ++#include "libp2p/multi/multibase_codec/codecs/base_error.hpp" ++ ++#include ++ ++namespace libp2p::multi::detail { ++const std::string kUpperBase32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; ++const std::string kLowerBase32Alphabet = "abcdefghijklmnopqrstuvwxyz234567"; ++ ++enum Base32Mode { ++ LOWER, ++ UPPER, ++}; ++ ++int get_byte(int block) { ++ return block * 5 / 8; ++} ++ ++int get_bit(int block) { ++ return 8 - 5 - block * 5 % 8; ++} ++ ++char encode_char(unsigned char c, Base32Mode mode) { ++ if (mode == Base32Mode::UPPER) { ++ return kUpperBase32Alphabet[c & 0x1F]; // 0001 1111 ++ } ++ return kLowerBase32Alphabet[c & 0x1F]; ++} ++ ++unsigned char shift_right(uint8_t byte, int8_t offset) { ++ if (offset > 0) { ++ return byte >> offset; ++ } ++ ++ return byte << -offset; ++} ++ ++unsigned char shift_left(uint8_t byte, int8_t offset) { ++ return shift_right(byte, -offset); ++} ++ ++int encode_sequence(ipfs::span plain, ++ ipfs::span coded, ++ Base32Mode mode) { ++ for (int block = 0; block < 8; block++) { ++ int byte = get_byte(block); ++ int bit = get_bit(block); ++ ++ if (byte >= static_cast(plain.size())) { ++ return block; ++ } ++ ++ unsigned char c = shift_right(plain[byte], bit); ++ ++ if (bit < 0 && byte < static_cast(plain.size()) - 1L) { ++ c |= shift_right(plain[byte + 1], 8 + bit); ++ } ++ coded[block] = encode_char(c, mode); ++ } ++ return 8; ++} ++ ++std::string encodeBase32(ipfs::ByteView bytes, Base32Mode mode) { ++ std::string result; ++ if (bytes.size() % 5 == 0) { ++ result = std::string(bytes.size() / 5 * 8, ' '); ++ } else { ++ result = std::string((bytes.size() / 5 + 1) * 8, ' '); ++ } ++ ++ for (size_t i = 0, j = 0; i < bytes.size(); i += 5, j += 8) { ++ int n = encode_sequence( ++ ipfs::span(reinterpret_cast(&bytes[i]), ++ std::min(bytes.size() - i, 5)), ++ ipfs::span(&result[j], 8U), mode); ++ if (n < 8) { ++ result.erase(result.end() - (8 - n), result.end()); ++ } ++ } ++ ++ return result; ++} ++ ++std::string encodeBase32Upper(ipfs::ByteView bytes) { ++ return encodeBase32(bytes, Base32Mode::UPPER); ++} ++ ++std::string encodeBase32Lower(ipfs::ByteView bytes) { ++ return encodeBase32(bytes, Base32Mode::LOWER); ++} ++ ++int decode_char(unsigned char c, Base32Mode mode) { ++ char decoded_ch = -1; ++ ++ if (mode == Base32Mode::UPPER) { ++ if (c >= 'A' && c <= 'Z') { ++ decoded_ch = c - 'A'; // NOLINT ++ } ++ } else { ++ if (c >= 'a' && c <= 'z') { ++ decoded_ch = c - 'a'; // NOLINT ++ } ++ } ++ if (c >= '2' && c <= '7') { ++ decoded_ch = c - '2' + 26; // NOLINT ++ } ++ ++ return decoded_ch; ++} ++ ++ipfs::expected decode_sequence(ipfs::span coded, ++ ipfs::span plain, ++ Base32Mode mode) { ++ plain[0] = 0; ++ for (int block = 0; block < 8; block++) { ++ int bit = get_bit(block); ++ int byte = get_byte(block); ++ ++ if (block >= static_cast(coded.size())) { ++ return byte; ++ } ++ int c = decode_char(coded[block], mode); ++ if (c < 0) { ++ // return absl::InvalidArgumentError("INVALID_BASE32_INPUT"); ++ return ipfs::unexpected{BaseError::INVALID_BASE32_INPUT}; ++ } ++ ++ plain[byte] |= shift_left(c, bit); ++ if (bit < 0) { ++ plain[byte + 1] = shift_left(c, 8 + bit); ++ } ++ } ++ return 5; ++} ++ ++ipfs::expected decodeBase32( ++ std::string_view string, ++ Base32Mode mode) { ++ common::ByteArray result; ++ if (string.size() % 8 == 0) { ++ result = common::ByteArray(string.size() / 8 * 5, ipfs::Byte{0}); ++ } else { ++ result = common::ByteArray((string.size() / 8 + 1) * 5, ipfs::Byte{0}); ++ } ++ ++ for (size_t i = 0, j = 0; i < string.size(); i += 8, j += 5) { ++ auto n = decode_sequence( ++ ipfs::span(&string[i], ++ std::min(string.size() - i, 8)), ++ ipfs::span(reinterpret_cast(&result[j]), 5U), mode); ++ if (!n.has_value()) { ++ return ipfs::unexpected{n.error()}; ++ } ++ if (n.value() < 5) { ++ result.erase(result.end() - (5 - n.value()), result.end()); ++ } ++ } ++ return result; ++} ++ ++ipfs::expected decodeBase32Upper( ++ std::string_view string) { ++ return decodeBase32(string, Base32Mode::UPPER); ++} ++ ++ipfs::expected decodeBase32Lower( ++ std::string_view string) { ++ return decodeBase32(string, Base32Mode::LOWER); ++} ++ ++} // namespace libp2p::multi::detail +diff --git a/third_party/ipfs_client/src/libp2p/multi/multibase_codec/codecs/base36.cc b/third_party/ipfs_client/src/libp2p/multi/multibase_codec/codecs/base36.cc +new file mode 100644 +index 0000000000000..2f508979a004c +--- /dev/null ++++ b/third_party/ipfs_client/src/libp2p/multi/multibase_codec/codecs/base36.cc +@@ -0,0 +1,58 @@ ++#include ++ ++#include ++ ++#include ++ ++#include ++ ++namespace det = libp2p::multi::detail; ++ ++namespace { ++constexpr double kLengthRatio = 0.646240625; // log(36)/log(256) ++ ++std::int_least16_t digit_value(char digit) { ++ if (digit < '0') { ++ return -1; ++ } else if (digit <= '9') { ++ return digit - '0'; ++ } else if (digit < 'A') { ++ return -2; ++ } else if (digit <= 'Z') { ++ return (digit - 'A') + 10; ++ } else if (digit < 'a') { ++ return -3; ++ } else if (digit <= 'z') { ++ return (digit - 'a') + 10; ++ } else { ++ return -4; ++ } ++} ++int operator*(int a, ipfs::Byte b) { ++ return a * static_cast(b); ++} ++} // namespace ++ ++std::string det::encodeBase36Lower(ipfs::ByteView) { ++ std::abort(); ++} ++ ++auto det::decodeBase36(std::string_view str_b36) ++ -> ipfs::expected { ++ common::ByteArray out; ++ out.resize(std::ceil(static_cast(str_b36.size()) * kLengthRatio), ++ ipfs::Byte{}); ++ for (auto digit : str_b36) { // chunk) { ++ int val = digit_value(digit); ++ if (val < 0) { ++ return ipfs::unexpected{BaseError::INVALID_BASE36_INPUT}; ++ } ++ auto mod_byte = [&val](auto& b) { ++ val += 36 * b; ++ b = static_cast(val & 0xFF); ++ val >>= 8; ++ }; ++ std::for_each(out.rbegin(), out.rend(), mod_byte); ++ } ++ return out; ++} +diff --git a/third_party/ipfs_client/src/libp2p/multi/uvarint.cc b/third_party/ipfs_client/src/libp2p/multi/uvarint.cc +new file mode 100644 +index 0000000000000..2e6ed6eb0bada +--- /dev/null ++++ b/third_party/ipfs_client/src/libp2p/multi/uvarint.cc +@@ -0,0 +1,107 @@ ++/** ++ * Copyright Soramitsu Co., Ltd. All Rights Reserved. ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++#include ++ ++namespace libp2p::multi { ++ ++UVarint::UVarint(UVarint const& rhs) : bytes_(rhs.bytes_) {} ++UVarint::UVarint(uint64_t number) { ++ do { ++ auto byte = static_cast(number) & ipfs::Byte{0x7f}; ++ number >>= 7; ++ if (number != 0) { ++ byte |= ipfs::Byte{0x80}; ++ } ++ bytes_.push_back(byte); ++ } while (number != 0); ++} ++ ++UVarint::UVarint(ipfs::ByteView varint_bytes) { ++ auto size = calculateSize(varint_bytes); ++ if (size <= varint_bytes.size()) { ++ bytes_.assign(varint_bytes.begin(), varint_bytes.begin() + size); ++ } ++} ++ ++UVarint::UVarint(ipfs::ByteView varint_bytes, size_t varint_size) ++ : bytes_(varint_bytes.begin(), varint_bytes.begin() + varint_size) {} ++ ++std::optional UVarint::create(ipfs::ByteView varint_bytes) { ++ size_t size = calculateSize(varint_bytes); ++ if (size > 0 && size <= varint_bytes.size()) { ++ return UVarint{varint_bytes, size}; ++ } ++ return {}; ++} ++ ++uint64_t UVarint::toUInt64() const { ++ uint64_t res = 0; ++ size_t index = 0; ++ for (const auto& byte : bytes_) { ++ res += static_cast((byte & ipfs::Byte{0x7f})) << index; ++ index += 7; ++ } ++ return res; ++} ++ ++ipfs::ByteView UVarint::toBytes() const { ++ return ipfs::ByteView{bytes_.data(), bytes_.size()}; ++} ++ ++std::vector const& UVarint::toVector() const { ++ return bytes_; ++} ++ ++size_t UVarint::size() const { ++ return bytes_.size(); ++} ++ ++UVarint& UVarint::operator=(UVarint const& rhs) { ++ bytes_ = rhs.bytes_; // actually OK even if &rhs == this ++ return *this; ++} ++UVarint& UVarint::operator=(uint64_t n) { ++ *this = UVarint(n); ++ return *this; ++} ++ ++bool UVarint::operator==(const UVarint& r) const { ++ return std::equal(bytes_.begin(), bytes_.end(), r.bytes_.begin(), ++ r.bytes_.end()); ++} ++ ++bool UVarint::operator!=(const UVarint& r) const { ++ return !(*this == r); ++} ++ ++bool UVarint::operator<(const UVarint& r) const { ++ return toUInt64() < r.toUInt64(); ++} ++ ++size_t UVarint::calculateSize(ipfs::ByteView varint_bytes) { ++ size_t size = 0; ++ size_t shift = 0; ++ constexpr size_t capacity = sizeof(uint64_t) * 8; ++ bool last_byte_found = false; ++ for (const auto& byte : varint_bytes) { ++ ++size; ++ std::uint_least64_t slice = to_integer(byte) & 0x7f; ++ if (shift >= capacity || ((slice << shift) >> shift) != slice) { ++ size = 0; ++ break; ++ } ++ if ((byte & ipfs::Byte{0x80}) == ipfs::Byte{0}) { ++ last_byte_found = true; ++ break; ++ } ++ shift += 7; ++ } ++ return last_byte_found ? size : 0; ++} ++ ++UVarint::~UVarint() noexcept {} ++ ++} // namespace libp2p::multi +diff --git a/third_party/ipfs_client/src/log_macros.h b/third_party/ipfs_client/src/log_macros.h +new file mode 100644 +index 0000000000000..e406429d0f280 +--- /dev/null ++++ b/third_party/ipfs_client/src/log_macros.h +@@ -0,0 +1,53 @@ ++#ifndef IPFS_LOG_MACROS_H_ ++#define IPFS_LOG_MACROS_H_ ++ ++#include ++ ++#if __has_include("base/logging.h") //In Chromium ++ ++#include "base/logging.h" ++#include "base/check_op.h" ++ ++#else // Not in Chromium ++ ++#include ++ ++#include ++ ++#define DCHECK_EQ GOOGLE_DCHECK_EQ ++#define DCHECK_GT GOOGLE_DCHECK_GT ++#define DCHECK GOOGLE_DCHECK ++#define LOG GOOGLE_LOG ++ ++#define VLOG(X) \ ++ ::google::protobuf::internal::LogFinisher() = \ ++ ::google::protobuf::internal::LogMessage( \ ++ static_cast<::google::protobuf::LogLevel>( \ ++ ::google::protobuf::LOGLEVEL_INFO - X), \ ++ __FILE__, __LINE__) ++ ++#pragma GCC diagnostic push ++#pragma GCC diagnostic ignored "-Wunused-variable" ++namespace { ++static bool is_logging_initialized = ::ipfs::log::IsInitialized(); ++} ++#pragma GCC diagnostic pop ++ ++#endif //Chromium in-tree check ++ ++#define L_VAR(X) LOG(INFO) << "VAR " << #X << "='" << (X) << '\''; ++ ++inline bool starts_with(std::string_view full_text, std::string_view prefix) { ++ if (prefix.size() > full_text.size()) { ++ return false; ++ } ++ return full_text.substr(0UL, prefix.size()) == prefix; ++} ++inline bool ends_with(std::string_view full_text, std::string_view suffix) { ++ if (suffix.size() > full_text.size()) { ++ return false; ++ } ++ return full_text.substr(full_text.size() - suffix.size()) == suffix; ++} ++ ++#endif // IPFS_LOG_MACROS_H_ +diff --git a/third_party/ipfs_client/src/smhasher/MurmurHash3.cc b/third_party/ipfs_client/src/smhasher/MurmurHash3.cc +new file mode 100644 +index 0000000000000..677aedf1d7a55 +--- /dev/null ++++ b/third_party/ipfs_client/src/smhasher/MurmurHash3.cc +@@ -0,0 +1,424 @@ ++//----------------------------------------------------------------------------- ++// MurmurHash3 was written by Austin Appleby, and is placed in the public ++// domain. The author hereby disclaims copyright to this source code. ++ ++// Note - The x86 and x64 versions do _not_ produce the same results, as the ++// algorithms are optimized for their respective platforms. You can still ++// compile and run any of them on any platform, but your performance with the ++// non-native version will be less than optimal. ++ ++#include "smhasher/MurmurHash3.h" ++#ifdef __GNUG__ ++#pragma GCC diagnostic ignored "-Wimplicit-fallthrough" ++#endif ++ ++#ifdef __clang__ ++#pragma clang diagnostic ignored "-Wimplicit-fallthrough" ++#endif ++//----------------------------------------------------------------------------- ++// Platform-specific functions and macros ++ ++// Microsoft Visual Studio ++ ++#if defined(_MSC_VER) ++ ++#define FORCE_INLINE __forceinline ++ ++#include ++ ++#define ROTL32(x, y) _rotl(x, y) ++#define ROTL64(x, y) _rotl64(x, y) ++ ++#define BIG_CONSTANT(x) (x) ++ ++// Other compilers ++ ++#else // defined(_MSC_VER) ++ ++#define FORCE_INLINE inline __attribute__((always_inline)) ++ ++inline uint32_t rotl32(uint32_t x, int8_t r) { ++ return (x << r) | (x >> (32 - r)); ++} ++ ++inline uint64_t rotl64(uint64_t x, int8_t r) { ++ return (x << r) | (x >> (64 - r)); ++} ++ ++#define ROTL32(x, y) rotl32(x, y) ++#define ROTL64(x, y) rotl64(x, y) ++ ++#define BIG_CONSTANT(x) (x##LLU) ++ ++#endif // !defined(_MSC_VER) ++ ++//----------------------------------------------------------------------------- ++// Block read - if your platform needs to do endian-swapping or can only ++// handle aligned reads, do the conversion here ++ ++FORCE_INLINE uint32_t getblock32(const uint32_t* p, int i) { ++ return p[i]; ++} ++ ++FORCE_INLINE uint64_t getblock64(const uint64_t* p, int i) { ++ return p[i]; ++} ++ ++//----------------------------------------------------------------------------- ++// Finalization mix - force all bits of a hash block to avalanche ++ ++FORCE_INLINE uint32_t fmix32(uint32_t h) { ++ h ^= h >> 16; ++ h *= 0x85ebca6b; ++ h ^= h >> 13; ++ h *= 0xc2b2ae35; ++ h ^= h >> 16; ++ ++ return h; ++} ++ ++//---------- ++ ++FORCE_INLINE uint64_t fmix64(uint64_t k) { ++ k ^= k >> 33; ++ k *= BIG_CONSTANT(0xff51afd7ed558ccd); ++ k ^= k >> 33; ++ k *= BIG_CONSTANT(0xc4ceb9fe1a85ec53); ++ k ^= k >> 33; ++ ++ return k; ++} ++ ++//----------------------------------------------------------------------------- ++ ++void MurmurHash3_x86_32(const void* key, int len, uint32_t seed, void* out) { ++ const uint8_t* data = (const uint8_t*)key; ++ const int nblocks = len / 4; ++ ++ uint32_t h1 = seed; ++ ++ const uint32_t c1 = 0xcc9e2d51; ++ const uint32_t c2 = 0x1b873593; ++ ++ //---------- ++ // body ++ ++ const uint32_t* blocks = (const uint32_t*)(data + nblocks * 4); ++ ++ for (int i = -nblocks; i; i++) { ++ uint32_t k1 = getblock32(blocks, i); ++ ++ k1 *= c1; ++ k1 = ROTL32(k1, 15); ++ k1 *= c2; ++ ++ h1 ^= k1; ++ h1 = ROTL32(h1, 13); ++ h1 = h1 * 5 + 0xe6546b64; ++ } ++ ++ //---------- ++ // tail ++ ++ const uint8_t* tail = (const uint8_t*)(data + nblocks * 4); ++ ++ uint32_t k1 = 0; ++ ++ switch (len & 3) { ++ case 3: ++ k1 ^= tail[2] << 16; ++ case 2: ++ k1 ^= tail[1] << 8; ++ case 1: ++ k1 ^= tail[0]; ++ k1 *= c1; ++ k1 = ROTL32(k1, 15); ++ k1 *= c2; ++ h1 ^= k1; ++ }; ++ ++ //---------- ++ // finalization ++ ++ h1 ^= len; ++ ++ h1 = fmix32(h1); ++ ++ *(uint32_t*)out = h1; ++} ++ ++//----------------------------------------------------------------------------- ++ ++void MurmurHash3_x86_128(const void* key, ++ const int len, ++ uint32_t seed, ++ void* out) { ++ const uint8_t* data = (const uint8_t*)key; ++ const int nblocks = len / 16; ++ ++ uint32_t h1 = seed; ++ uint32_t h2 = seed; ++ uint32_t h3 = seed; ++ uint32_t h4 = seed; ++ ++ const uint32_t c1 = 0x239b961b; ++ const uint32_t c2 = 0xab0e9789; ++ const uint32_t c3 = 0x38b34ae5; ++ const uint32_t c4 = 0xa1e38b93; ++ ++ //---------- ++ // body ++ ++ const uint32_t* blocks = (const uint32_t*)(data + nblocks * 16); ++ ++ for (int i = -nblocks; i; i++) { ++ uint32_t k1 = getblock32(blocks, i * 4 + 0); ++ uint32_t k2 = getblock32(blocks, i * 4 + 1); ++ uint32_t k3 = getblock32(blocks, i * 4 + 2); ++ uint32_t k4 = getblock32(blocks, i * 4 + 3); ++ ++ k1 *= c1; ++ k1 = ROTL32(k1, 15); ++ k1 *= c2; ++ h1 ^= k1; ++ ++ h1 = ROTL32(h1, 19); ++ h1 += h2; ++ h1 = h1 * 5 + 0x561ccd1b; ++ ++ k2 *= c2; ++ k2 = ROTL32(k2, 16); ++ k2 *= c3; ++ h2 ^= k2; ++ ++ h2 = ROTL32(h2, 17); ++ h2 += h3; ++ h2 = h2 * 5 + 0x0bcaa747; ++ ++ k3 *= c3; ++ k3 = ROTL32(k3, 17); ++ k3 *= c4; ++ h3 ^= k3; ++ ++ h3 = ROTL32(h3, 15); ++ h3 += h4; ++ h3 = h3 * 5 + 0x96cd1c35; ++ ++ k4 *= c4; ++ k4 = ROTL32(k4, 18); ++ k4 *= c1; ++ h4 ^= k4; ++ ++ h4 = ROTL32(h4, 13); ++ h4 += h1; ++ h4 = h4 * 5 + 0x32ac3b17; ++ } ++ ++ //---------- ++ // tail ++ ++ const uint8_t* tail = (const uint8_t*)(data + nblocks * 16); ++ ++ uint32_t k1 = 0; ++ uint32_t k2 = 0; ++ uint32_t k3 = 0; ++ uint32_t k4 = 0; ++ ++ switch (len & 15) { ++ case 15: ++ k4 ^= tail[14] << 16; ++ case 14: ++ k4 ^= tail[13] << 8; ++ case 13: ++ k4 ^= tail[12] << 0; ++ k4 *= c4; ++ k4 = ROTL32(k4, 18); ++ k4 *= c1; ++ h4 ^= k4; ++ ++ case 12: ++ k3 ^= tail[11] << 24; ++ case 11: ++ k3 ^= tail[10] << 16; ++ case 10: ++ k3 ^= tail[9] << 8; ++ case 9: ++ k3 ^= tail[8] << 0; ++ k3 *= c3; ++ k3 = ROTL32(k3, 17); ++ k3 *= c4; ++ h3 ^= k3; ++ ++ case 8: ++ k2 ^= tail[7] << 24; ++ case 7: ++ k2 ^= tail[6] << 16; ++ case 6: ++ k2 ^= tail[5] << 8; ++ case 5: ++ k2 ^= tail[4] << 0; ++ k2 *= c2; ++ k2 = ROTL32(k2, 16); ++ k2 *= c3; ++ h2 ^= k2; ++ ++ case 4: ++ k1 ^= tail[3] << 24; ++ case 3: ++ k1 ^= tail[2] << 16; ++ case 2: ++ k1 ^= tail[1] << 8; ++ case 1: ++ k1 ^= tail[0] << 0; ++ k1 *= c1; ++ k1 = ROTL32(k1, 15); ++ k1 *= c2; ++ h1 ^= k1; ++ }; ++ ++ //---------- ++ // finalization ++ ++ h1 ^= len; ++ h2 ^= len; ++ h3 ^= len; ++ h4 ^= len; ++ ++ h1 += h2; ++ h1 += h3; ++ h1 += h4; ++ h2 += h1; ++ h3 += h1; ++ h4 += h1; ++ ++ h1 = fmix32(h1); ++ h2 = fmix32(h2); ++ h3 = fmix32(h3); ++ h4 = fmix32(h4); ++ ++ h1 += h2; ++ h1 += h3; ++ h1 += h4; ++ h2 += h1; ++ h3 += h1; ++ h4 += h1; ++ ++ ((uint32_t*)out)[0] = h1; ++ ((uint32_t*)out)[1] = h2; ++ ((uint32_t*)out)[2] = h3; ++ ((uint32_t*)out)[3] = h4; ++} ++ ++//----------------------------------------------------------------------------- ++ ++void MurmurHash3_x64_128(const void* key, ++ const int len, ++ const uint32_t seed, ++ void* out) { ++ const uint8_t* data = (const uint8_t*)key; ++ const int nblocks = len / 16; ++ ++ uint64_t h1 = seed; ++ uint64_t h2 = seed; ++ ++ const uint64_t c1 = BIG_CONSTANT(0x87c37b91114253d5); ++ const uint64_t c2 = BIG_CONSTANT(0x4cf5ad432745937f); ++ ++ //---------- ++ // body ++ ++ const uint64_t* blocks = (const uint64_t*)(data); ++ ++ for (int i = 0; i < nblocks; i++) { ++ uint64_t k1 = getblock64(blocks, i * 2 + 0); ++ uint64_t k2 = getblock64(blocks, i * 2 + 1); ++ ++ k1 *= c1; ++ k1 = ROTL64(k1, 31); ++ k1 *= c2; ++ h1 ^= k1; ++ ++ h1 = ROTL64(h1, 27); ++ h1 += h2; ++ h1 = h1 * 5 + 0x52dce729; ++ ++ k2 *= c2; ++ k2 = ROTL64(k2, 33); ++ k2 *= c1; ++ h2 ^= k2; ++ ++ h2 = ROTL64(h2, 31); ++ h2 += h1; ++ h2 = h2 * 5 + 0x38495ab5; ++ } ++ ++ //---------- ++ // tail ++ ++ const uint8_t* tail = (const uint8_t*)(data + nblocks * 16); ++ ++ uint64_t k1 = 0; ++ uint64_t k2 = 0; ++ ++ switch (len & 15) { ++ case 15: ++ k2 ^= ((uint64_t)tail[14]) << 48; ++ case 14: ++ k2 ^= ((uint64_t)tail[13]) << 40; ++ case 13: ++ k2 ^= ((uint64_t)tail[12]) << 32; ++ case 12: ++ k2 ^= ((uint64_t)tail[11]) << 24; ++ case 11: ++ k2 ^= ((uint64_t)tail[10]) << 16; ++ case 10: ++ k2 ^= ((uint64_t)tail[9]) << 8; ++ case 9: ++ k2 ^= ((uint64_t)tail[8]) << 0; ++ k2 *= c2; ++ k2 = ROTL64(k2, 33); ++ k2 *= c1; ++ h2 ^= k2; ++ ++ case 8: ++ k1 ^= ((uint64_t)tail[7]) << 56; ++ case 7: ++ k1 ^= ((uint64_t)tail[6]) << 48; ++ case 6: ++ k1 ^= ((uint64_t)tail[5]) << 40; ++ case 5: ++ k1 ^= ((uint64_t)tail[4]) << 32; ++ case 4: ++ k1 ^= ((uint64_t)tail[3]) << 24; ++ case 3: ++ k1 ^= ((uint64_t)tail[2]) << 16; ++ case 2: ++ k1 ^= ((uint64_t)tail[1]) << 8; ++ case 1: ++ k1 ^= ((uint64_t)tail[0]) << 0; ++ k1 *= c1; ++ k1 = ROTL64(k1, 31); ++ k1 *= c2; ++ h1 ^= k1; ++ }; ++ ++ //---------- ++ // finalization ++ ++ h1 ^= len; ++ h2 ^= len; ++ ++ h1 += h2; ++ h2 += h1; ++ ++ h1 = fmix64(h1); ++ h2 = fmix64(h2); ++ ++ h1 += h2; ++ h2 += h1; ++ ++ ((uint64_t*)out)[0] = h1; ++ ((uint64_t*)out)[1] = h2; ++} ++ ++//----------------------------------------------------------------------------- +diff --git a/third_party/ipfs_client/src/vocab/byte_view.cc b/third_party/ipfs_client/src/vocab/byte_view.cc +new file mode 100644 +index 0000000000000..f71dcaa0181f1 +--- /dev/null ++++ b/third_party/ipfs_client/src/vocab/byte_view.cc +@@ -0,0 +1,2 @@ ++#include "vocab/byte_view.h" ++ +diff --git a/third_party/ipfs_client/src/vocab/slash_delimited.cc b/third_party/ipfs_client/src/vocab/slash_delimited.cc +new file mode 100644 +index 0000000000000..c81ae5823c867 +--- /dev/null ++++ b/third_party/ipfs_client/src/vocab/slash_delimited.cc +@@ -0,0 +1,117 @@ ++#include ++ ++#include "log_macros.h" ++ ++#include ++ ++#if __has_include() ++#include ++#define HAS_STRINGPIECE 1 ++#endif ++ ++using Self = ipfs::SlashDelimited; ++ ++Self::SlashDelimited(std::string_view unowned) : remainder_{unowned} {} ++ ++Self::operator bool() const { ++ return remainder_.find_first_not_of("/") < remainder_.size(); ++} ++std::string_view Self::pop() { ++ if (remainder_.empty()) { ++ return remainder_; ++ } ++ auto slash = remainder_.find('/'); ++ if (slash == std::string_view::npos) { ++ return pop_all(); ++ } ++ auto result = remainder_.substr(0UL, slash); ++ remainder_.remove_prefix(slash + 1); ++ if (result.empty()) { ++ return pop(); ++ } else { ++ return result; ++ } ++} ++std::string_view Self::pop_all() { ++ auto result = remainder_; ++ remainder_ = ""; ++ return result; ++} ++std::string_view Self::pop_n(std::size_t n) { ++ std::size_t a = 0UL; ++ while (n) { ++ auto slash = remainder_.find('/', a); ++ auto non_slash = remainder_.find_first_not_of("/", a); ++ if (slash == std::string_view::npos) { ++ auto result = remainder_; ++ remainder_ = ""; ++ return result; ++ } ++ if (non_slash < slash) { ++ --n; ++ } ++ a = slash + 1UL; ++ } ++ auto result = remainder_.substr(0UL, a - 1); ++ remainder_.remove_prefix(a); ++ return result; ++} ++std::string_view Self::peek_back() const { ++ auto s = remainder_; ++ while (!s.empty() && s.back() == '/') { ++ s.remove_suffix(1); ++ } ++ if (s.empty()) { ++ return s; ++ } ++ auto last_slash = s.find_last_of('/'); ++ if (last_slash < remainder_.size()) { ++ return remainder_.substr(last_slash + 1); ++ } else { ++ return s; ++ } ++} ++std::string Self::pop_back() { ++ auto non_slash = remainder_.find_last_not_of('/'); ++ if (non_slash == std::string_view::npos) { ++ return ""; ++ } ++ auto slash = remainder_.find_last_of('/', non_slash); ++ std::string rv; ++ if (slash == std::string_view::npos) { ++ rv = remainder_; ++ remainder_ = ""; ++ } else { ++ rv = remainder_.substr(slash + 1, non_slash - slash); ++ remainder_ = remainder_.substr(0UL, slash); ++ } ++ return rv; ++} ++ ++std::ostream& operator<<(std::ostream& str, ipfs::SlashDelimited const& sd) { ++ return str << sd.to_view(); ++} ++ ++#if __has_include() ++#include ++ ++using namespace google::protobuf::internal; ++using namespace google::protobuf; ++ ++#if PROTOBUF_VERSION >= 3020000 ++#include ++LogMessage& operator<<(LogMessage& str, ipfs::SlashDelimited const& sd) { ++ return str << sd.to_view(); ++} ++#elif __has_include() ++#include ++LogMessage& operator<<(LogMessage& str, ipfs::SlashDelimited const& sd) { ++ return str << StringPiece{sd.to_view()}; ++} ++#else ++LogMessage& operator<<(LogMessage& str, ipfs::SlashDelimited const& sd) { ++ return str << std::string{sd.to_view()}; ++} ++#endif ++ ++#endif +diff --git a/third_party/ipfs_client/unix_fs.proto b/third_party/ipfs_client/unix_fs.proto +new file mode 100644 +index 0000000000000..9d117a4d66bdf +--- /dev/null ++++ b/third_party/ipfs_client/unix_fs.proto +@@ -0,0 +1,32 @@ ++syntax = "proto2"; ++option optimize_for = LITE_RUNTIME; ++package ipfs.unix_fs; ++ ++message Data { ++ enum DataType { ++ Raw = 0; ++ Directory = 1; ++ File = 2; ++ Metadata = 3; ++ Symlink = 4; ++ HAMTShard = 5; ++ } ++ ++ required DataType Type = 1; ++ optional bytes Data = 2; ++ optional uint64 filesize = 3; ++ repeated uint64 blocksizes = 4; ++ optional uint64 hashType = 5; ++ optional uint64 fanout = 6; ++ optional uint32 mode = 7; ++ optional UnixTime mtime = 8; ++} ++ ++message Metadata { ++ optional string MimeType = 1; ++} ++ ++message UnixTime { ++ required int64 Seconds = 1; ++ optional fixed32 FractionalNanoseconds = 2; ++} +diff --git a/url/BUILD.gn b/url/BUILD.gn +index c525c166979d6..ce2b1ae43c0a7 100644 +--- a/url/BUILD.gn ++++ b/url/BUILD.gn +@@ -5,6 +5,7 @@ + import("//build/buildflag_header.gni") + import("//testing/libfuzzer/fuzzer_test.gni") + import("//testing/test.gni") ++import("//third_party/ipfs_client/args.gni") + import("features.gni") + + import("//build/config/cronet/config.gni") +@@ -67,6 +68,7 @@ component("url") { + public_deps = [ + "//base", + "//build:robolectric_buildflags", ++ "//third_party/ipfs_client:ipfs_buildflags", + ] + + configs += [ "//build/config/compiler:wexit_time_destructors" ] +@@ -89,6 +91,11 @@ component("url") { + public_configs = [ "//third_party/jdk" ] + } + ++ if (enable_ipfs) { ++ sources += [ "url_canon_ipfs.cc" ] ++ deps += [ "//third_party/ipfs_client:ipfs_client" ] ++ } ++ + if (is_win) { + # Don't conflict with Windows' "url.dll". + output_name = "url_lib" +diff --git a/url/url_canon.h b/url/url_canon.h +index 913b3685c6fec..3c3c55e580564 100644 +--- a/url/url_canon.h ++++ b/url/url_canon.h +@@ -792,6 +792,23 @@ bool CanonicalizeMailtoURL(const char16_t* spec, + CanonOutput* output, + Parsed* new_parsed); + ++COMPONENT_EXPORT(URL) ++bool CanonicalizeIpfsURL(const char* spec, ++ int spec_len, ++ const Parsed& parsed, ++ SchemeType scheme_type, ++ CharsetConverter* query_converter, ++ CanonOutput* output, ++ Parsed* new_parsed); ++COMPONENT_EXPORT(URL) ++bool CanonicalizeIpfsURL(const char16_t* spec, ++ int spec_len, ++ const Parsed& parsed, ++ SchemeType scheme_type, ++ CharsetConverter* query_converter, ++ CanonOutput* output, ++ Parsed* new_parsed); ++ + // Part replacer -------------------------------------------------------------- + + // Internal structure used for storing separate strings for each component. +diff --git a/url/url_canon_ipfs.cc b/url/url_canon_ipfs.cc +new file mode 100644 +index 0000000000000..9511e3f5e6f5c +--- /dev/null ++++ b/url/url_canon_ipfs.cc +@@ -0,0 +1,55 @@ ++#include "url_canon_internal.h" ++ ++#include ++#include ++ ++#include ++ ++bool url::CanonicalizeIpfsURL(const char* spec, ++ int spec_len, ++ const Parsed& parsed, ++ SchemeType scheme_type, ++ CharsetConverter* charset_converter, ++ CanonOutput* output, ++ Parsed* output_parsed) { ++ if ( spec_len < 1 || !spec ) { ++ return false; ++ } ++ if ( parsed.host.len < 1 ) { ++ return false; ++ } ++ std::string_view cid_str{ spec + parsed.host.begin, static_cast(parsed.host.len) }; ++ auto cid = ipfs::Cid(cid_str); ++ if ( !cid.valid() ) { ++ cid = ipfs::id_cid::forText( std::string{cid_str} + " is not a valid CID." ); ++ } ++ auto as_str = cid.to_string(); ++ if ( as_str.empty() ) { ++ return false; ++ } ++ std::string stdurl{ spec, static_cast(parsed.host.begin) }; ++ stdurl.append( as_str ); ++ stdurl.append( spec + parsed.host.end(), spec_len - parsed.host.end() ); ++ spec = stdurl.data(); ++ spec_len = static_cast(stdurl.size()); ++ Parsed parsed_input; ++ ParseStandardURL(spec, spec_len, &parsed_input); ++ return CanonicalizeStandardURL( ++ spec, spec_len, ++ parsed_input, ++ scheme_type, ++ charset_converter, ++ output, output_parsed ++ ); ++} ++bool url::CanonicalizeIpfsURL(const char16_t* spec, ++ int spec_len, ++ const Parsed& parsed, ++ SchemeType scheme_type, ++ CharsetConverter* query_converter, ++ CanonOutput* output, ++ Parsed* new_parsed) { ++ RawCanonOutput<2048> as8; ++ ConvertUTF16ToUTF8(spec, spec_len, &as8); ++ return CanonicalizeIpfsURL(as8.data(), as8.length(), parsed, scheme_type, query_converter, output, new_parsed); ++} +diff --git a/url/url_util.cc b/url/url_util.cc +index 9258cfcfada47..daf10e4c3b741 100644 +--- a/url/url_util.cc ++++ b/url/url_util.cc +@@ -277,6 +277,12 @@ bool DoCanonicalize(const CHAR* spec, + charset_converter, output, + output_parsed); + ++ } else if (DoCompareSchemeComponent(spec, scheme, "ipfs")) { ++ // Switch multibase away from case-sensitive ones before continuing canonicalization. ++ ParseStandardURL(spec, spec_len, &parsed_input); ++ success = CanonicalizeIpfsURL(spec, spec_len, parsed_input, scheme_type, ++ charset_converter, output, output_parsed); ++ + } else if (DoIsStandard(spec, scheme, &scheme_type)) { + // All "normal" URLs. + ParseStandardURL(spec, spec_len, &parsed_input); + diff --git a/component/preferences.cc b/component/preferences.cc new file mode 100644 index 00000000..60bb0f67 --- /dev/null +++ b/component/preferences.cc @@ -0,0 +1,89 @@ +#include "preferences.h" + +#include + +#include +#include +#include + +#include + +namespace { +std::string const kRateLimit{"ipfs.gateways.rate_limits"}; +} + +void ipfs::RegisterPreferences(PrefRegistrySimple* service) { + base::Value::Dict vals; + for (auto& gw : Gateways::DefaultGateways()) { + vals.Set(gw.prefix, static_cast(gw.rate)); + } + LOG(WARNING) << "Registering ipfs.gateways preference with a default value " + "that contains " + << vals.size() << " entries."; + for (auto [k, v] : vals) { + DCHECK(v.is_int()); + } + service->RegisterDictionaryPref(kRateLimit, std::move(vals)); +} +using Rates = ipfs::GatewayRates; +Rates::GatewayRates(PrefService* prefs) : prefs_{prefs} { + if (prefs) { + last_ = prefs->GetDict(kRateLimit).Clone(); + for (auto [k, v] : last_) { + auto i = v.GetInt(); + curr_.Set(k, std::max(i, 1)); + } + LOG(INFO) << "Initialized with " << curr_.size() << " gateways."; + } else { + LOG(ERROR) + << "Reading preferences without a preferences service is not great."; + } +} + +std::pair Rates::at(std::size_t index) const { + if (index >= curr_.size()) { + return {nullptr, 0U}; + } + auto it = std::next(curr_.cbegin(), index); + auto* p_k = &(it->first); + auto v = static_cast(std::max(0, it->second.GetInt())); + return {p_k, v}; +} +unsigned Rates::GetRate(std::string_view k) const { + auto i = std::max(0, curr_.FindInt(k).value_or(0)); + return static_cast(i); +} +void Rates::SetRate(std::string_view k, unsigned val) { + auto i = static_cast(std::min(val, static_cast(INT_MAX))); + auto old = curr_.contains(k); + curr_.Set(k, i); + if (!old) { + LOG(INFO) << "Added new gateway: " << k << '@' << val; + // TODO - I believe the calls to save here need to be sent to UI thread + save(); + } else if (++changes > update_thresh) { + LOG(INFO) << "Changing rate for gateway " << k << " to " << val; + auto d = delta(); + if (d > update_thresh) { + save(); + } else { + changes = d / 2; + } + } +} +std::size_t Rates::delta() const { + std::size_t rv = 0; + for (auto [k, v] : curr_) { + auto d = std::abs(v.GetInt() - last_.FindInt(k).value_or(0)); + rv += static_cast(d); + } + return rv; +} +void Rates::save() { + LOG(INFO) << "Saving " << changes + << " dynamic updates to gateway rates to disk."; + changes = 0; + last_ = curr_.Clone(); + update_thresh++; + prefs_->SetDict(kRateLimit, last_.Clone()); +} diff --git a/component/preferences.h b/component/preferences.h new file mode 100644 index 00000000..ee616cb0 --- /dev/null +++ b/component/preferences.h @@ -0,0 +1,36 @@ +#ifndef IPFS_PREFERENCES_H_INCLUDED +#define IPFS_PREFERENCES_H_INCLUDED 1 + +#include "export.h" + +#include +#include +#include + +#include + +class PrefRegistrySimple; +class PrefService; + +namespace ipfs { +COMPONENT_EXPORT(IPFS) void RegisterPreferences(PrefRegistrySimple*); +class GatewayRates { + raw_ptr prefs_; + base::Value::Dict last_; + base::Value::Dict curr_; + std::size_t changes = 0; + std::size_t update_thresh = 1; + + void save(); + std::size_t delta() const; + + public: + GatewayRates(PrefService*); + + unsigned GetRate(std::string_view) const; + void SetRate(std::string_view, unsigned); + std::pair at(std::size_t index) const; +}; +} + +#endif diff --git a/component/url_loader_factory.cc b/component/url_loader_factory.cc index a514fcaf..9a802840 100644 --- a/component/url_loader_factory.cc +++ b/component/url_loader_factory.cc @@ -7,11 +7,12 @@ void ipfs::IpfsURLLoaderFactory::Create( NonNetworkURLLoaderFactoryMap* in_out, content::BrowserContext* context, URLLoaderFactory* default_factory, - network::mojom::NetworkContext* net_ctxt) { + network::mojom::NetworkContext* net_ctxt, + PrefService* pref_svc) { for (char const* scheme : {"ipfs", "ipns"}) { mojo::PendingRemote pending; new IpfsURLLoaderFactory(scheme, pending.InitWithNewPipeAndPassReceiver(), - context, default_factory, net_ctxt); + context, default_factory, net_ctxt, pref_svc); in_out->emplace(scheme, std::move(pending)); } } @@ -21,14 +22,20 @@ ipfs::IpfsURLLoaderFactory::IpfsURLLoaderFactory( mojo::PendingReceiver factory_receiver, content::BrowserContext* context, URLLoaderFactory* default_factory, - network::mojom::NetworkContext* net_ctxt) + network::mojom::NetworkContext* net_ctxt, + PrefService* pref_svc) : network::SelfDeletingURLLoaderFactory(std::move(factory_receiver)), scheme_{scheme}, context_{context}, default_factory_{default_factory}, - network_context_{net_ctxt} {} + network_context_{net_ctxt}, + pref_svc_{pref_svc} {} -ipfs::IpfsURLLoaderFactory::~IpfsURLLoaderFactory() noexcept {} +ipfs::IpfsURLLoaderFactory::~IpfsURLLoaderFactory() noexcept { + context_ = nullptr; + default_factory_ = nullptr; + network_context_ = nullptr; +} void ipfs::IpfsURLLoaderFactory::CreateLoaderAndStart( mojo::PendingReceiver loader, @@ -45,15 +52,5 @@ void ipfs::IpfsURLLoaderFactory::CreateLoaderAndStart( auto ptr = std::make_shared( *default_factory_, InterRequestState::FromBrowserContext(context_)); ptr->StartRequest(ptr, request, std::move(loader), std::move(client)); - - } /* else if (scheme_ == "ipns") { - auto ptr = std::make_shared( - InterRequestState::FromBrowserContext(context_), request.url.host(), - network_context_, *default_factory_); - ptr->StartHandling(ptr, request, std::move(loader), std::move(client)); - - } else { - NOTREACHED(); - } - */ + } } diff --git a/component/url_loader_factory.h b/component/url_loader_factory.h index 2ae56afa..01cd66ea 100644 --- a/component/url_loader_factory.h +++ b/component/url_loader_factory.h @@ -6,6 +6,7 @@ #include +class PrefService; namespace content { class BrowserContext; } @@ -26,14 +27,16 @@ class COMPONENT_EXPORT(IPFS) IpfsURLLoaderFactory static void Create(NonNetworkURLLoaderFactoryMap* in_out, content::BrowserContext*, URLLoaderFactory*, - network::mojom::NetworkContext*); + network::mojom::NetworkContext*, + PrefService*); private: IpfsURLLoaderFactory(std::string, mojo::PendingReceiver, content::BrowserContext*, network::mojom::URLLoaderFactory*, - network::mojom::NetworkContext*); + network::mojom::NetworkContext*, + PrefService*); ~IpfsURLLoaderFactory() noexcept override; void CreateLoaderAndStart( mojo::PendingReceiver loader, @@ -48,6 +51,7 @@ class COMPONENT_EXPORT(IPFS) IpfsURLLoaderFactory raw_ptr context_; raw_ptr default_factory_; raw_ptr network_context_; + raw_ptr pref_svc_; }; } // namespace ipfs diff --git a/library/conanfile.py b/library/conanfile.py index 289e3b48..75cb4f2f 100644 --- a/library/conanfile.py +++ b/library/conanfile.py @@ -68,9 +68,10 @@ def package(self): def package_info(self): self.cpp_info.libs = ["ipfs_client"] - def build_requirements(self): - if not which("doxygen"): - self.tool_requires("doxygen/1.9.4") + # def build_requirements(self): + # if not which("doxygen"): + # self.tool_requires("doxygen/1.9.4") + def layout(self): cmake_layout(self) diff --git a/library/include/ipfs_client/context_api.h b/library/include/ipfs_client/context_api.h index da524bb9..dc46f903 100644 --- a/library/include/ipfs_client/context_api.h +++ b/library/include/ipfs_client/context_api.h @@ -3,6 +3,7 @@ #include "crypto/hasher.h" #include "dag_cbor_value.h" +#include "gateway_spec.h" #include "http_request_description.h" #include "ipns_cbor_entry.h" #include "multi_hash.h" @@ -77,6 +78,10 @@ class ContextApi : public std::enable_shared_from_this { std::optional> Hash(HashType, ByteView data); + virtual std::optional GetGateway(std::size_t index) const = 0; + virtual unsigned GetGatewayRate(std::string_view); + virtual void SetGatewayRate(std::string_view, unsigned); + protected: std::unordered_map> hashers_; }; diff --git a/library/include/ipfs_client/gateway_spec.h b/library/include/ipfs_client/gateway_spec.h new file mode 100644 index 00000000..159e68d4 --- /dev/null +++ b/library/include/ipfs_client/gateway_spec.h @@ -0,0 +1,19 @@ +#ifndef IPFS_GATEWAY_SPEC_H_ +#define IPFS_GATEWAY_SPEC_H_ + +#include + +namespace ipfs { +struct GatewaySpec { + std::string prefix; + unsigned rate; + bool operator<(GatewaySpec const& r) const { + if (rate == r.rate) { + return prefix < r.prefix; + } + return rate > r.rate; + } +}; +} // namespace ipfs + +#endif // IPFS_GATEWAY_SPEC_H_ diff --git a/library/include/ipfs_client/gateways.h b/library/include/ipfs_client/gateways.h index 0063b525..9852971b 100644 --- a/library/include/ipfs_client/gateways.h +++ b/library/include/ipfs_client/gateways.h @@ -1,6 +1,7 @@ #ifndef CHROMIUM_IPFS_GATEWAYS_H_ #define CHROMIUM_IPFS_GATEWAYS_H_ +#include "gateway_spec.h" #include "vocab/flat_mapset.h" #include @@ -11,16 +12,6 @@ #include namespace ipfs { -struct GatewaySpec { - std::string prefix; - unsigned strength; - bool operator<(GatewaySpec const& r) const { - if (strength == r.strength) { - return prefix < r.prefix; - } - return strength > r.strength; - } -}; using GatewayList = std::vector; class ContextApi; diff --git a/library/include/ipfs_client/gw/gateway_request.h b/library/include/ipfs_client/gw/gateway_request.h index efded265..f1d7d795 100644 --- a/library/include/ipfs_client/gw/gateway_request.h +++ b/library/include/ipfs_client/gw/gateway_request.h @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -42,13 +43,14 @@ class GatewayRequest { void ParseNodes(std::string_view, ContextApi* api); public: - Type type; + Type type = Type::Zombie; std::string main_param; ///< CID, IPNS name, hostname std::string path; ///< For CAR requests std::shared_ptr dependent; std::optional cid; short parallel = 0; std::string affinity; + flat_set failures; std::string url_suffix() const; std::string_view accept() const; @@ -56,7 +58,7 @@ class GatewayRequest { short timeout_seconds() const; bool is_http() const; std::optional max_response_size() const; - std::optional describe_http() const; + std::optional describe_http(std::string_view) const; std::string debug_string() const; void orchestrator(std::shared_ptr const&); diff --git a/library/include/ipfs_client/ipld/dag_node.h b/library/include/ipfs_client/ipld/dag_node.h index 1c66f4fd..f91532e3 100644 --- a/library/include/ipfs_client/ipld/dag_node.h +++ b/library/include/ipfs_client/ipld/dag_node.h @@ -51,7 +51,6 @@ using ResolveResult = */ class DagNode : public std::enable_shared_from_this { Link* FindChild(std::string_view); - static void Descend(ResolutionState&); protected: std::vector> links_; diff --git a/library/include/ipfs_client/ipld/resolution_state.h b/library/include/ipfs_client/ipld/resolution_state.h index 82e330ce..56e4e529 100644 --- a/library/include/ipfs_client/ipld/resolution_state.h +++ b/library/include/ipfs_client/ipld/resolution_state.h @@ -16,12 +16,14 @@ using NodePtr = std::shared_ptr; using BlockLookup = std::function; class ResolutionState { - friend class DagNode; std::string resolved_path_components; SlashDelimited unresolved_path; BlockLookup get_available_block; public: + ResolutionState(std::string_view path_to_resolve, BlockLookup); + ResolutionState(SlashDelimited path_to_resolve, BlockLookup); + SlashDelimited MyPath() const; SlashDelimited PathToResolve() const; bool IsFinalComponent() const; @@ -30,6 +32,8 @@ class ResolutionState { ResolutionState WithPath(std::string_view) const; ResolutionState RestartResolvedPath() const; + + void Descend(); }; } // namespace ipfs::ipld diff --git a/library/include/ipfs_client/orchestrator.h b/library/include/ipfs_client/orchestrator.h index f204dde7..7222c23c 100644 --- a/library/include/ipfs_client/orchestrator.h +++ b/library/include/ipfs_client/orchestrator.h @@ -24,10 +24,10 @@ class Orchestrator : public std::enable_shared_from_this { void build_response(std::shared_ptr); bool add_node(std::string key, ipld::NodePtr); bool has_key(std::string const& k) const; + std::size_t Stored() const { return dags_.size(); } private: flat_map dags_; - // GatewayAccess gw_requestor_; std::shared_ptr api_; std::shared_ptr requestor_; diff --git a/library/include/ipfs_client/pb_dag.h b/library/include/ipfs_client/pb_dag.h index 97b4a855..d6cb392f 100644 --- a/library/include/ipfs_client/pb_dag.h +++ b/library/include/ipfs_client/pb_dag.h @@ -11,8 +11,6 @@ #include "cid.h" -#include - #include #include diff --git a/library/include/ipfs_client/test_context.h b/library/include/ipfs_client/test_context.h index c85b8792..9216fb0c 100644 --- a/library/include/ipfs_client/test_context.h +++ b/library/include/ipfs_client/test_context.h @@ -49,7 +49,7 @@ namespace ipfs { // LCOV_EXCL_START -class AllInclusiveContext final : public ContextApi { +class TestContext final : public ContextApi { void SendHttpRequest(HttpRequestDescription, HttpCompleteCallback) const override; struct DnsCbs { @@ -121,6 +121,9 @@ class AllInclusiveContext final : public ContextApi { GOOGLE_LOG(ERROR) << "TODO\n"; return true; } + std::optional GetGateway(std::size_t) const; + + std::vector gateways_; boost::asio::io_context& io_; boost::asio::ssl::context mutable ssl_ctx_ = boost::asio::ssl::context{boost::asio::ssl::context::tls_client}; @@ -129,13 +132,13 @@ class AllInclusiveContext final : public ContextApi { void CAresProcess(); public: - AllInclusiveContext(boost::asio::io_context& io); - ~AllInclusiveContext() noexcept override; + TestContext(boost::asio::io_context& io); + ~TestContext() noexcept override; void DnsResults(std::string&, ares_txt_reply&); }; inline std::shared_ptr start_default( boost::asio::io_context& io) { - auto api = std::make_shared(io); + auto api = std::make_shared(io); auto gl = Gateways::DefaultGateways(); auto rtor = gw::default_requestor(gl, {}, api); auto orc = std::make_shared(rtor, api); diff --git a/library/include/libp2p/common/types.hpp b/library/include/libp2p/common/types.hpp deleted file mode 100644 index a112d1bf..00000000 --- a/library/include/libp2p/common/types.hpp +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright Soramitsu Co., Ltd. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef LIBP2P_P2P_COMMON_TYPES_HPP -#define LIBP2P_P2P_COMMON_TYPES_HPP - -#include "vocab/byte_view.h" - -#include -#include -#include -#include - -namespace libp2p::common { -/** - * Sequence of bytes - */ -using ByteArray = std::vector; -// using ByteArray = std::string; - -template -void append(Collection& c, Item&& g) { - c.insert(c.end(), g.begin(), g.end()); -} - -template -void append(Collection& c, char g) { - c.push_back(g); -} - -/// Hash256 as a sequence of 32 bytes -using Hash256 = std::array; -/// Hash512 as a sequence of 64 bytes -using Hash512 = std::array; -} // namespace libp2p::common - -#endif // LIBP2P_P2P_COMMON_TYPES_HPP diff --git a/library/include/libp2p/crypto/key.h b/library/include/libp2p/crypto/key.h deleted file mode 100644 index 8198e411..00000000 --- a/library/include/libp2p/crypto/key.h +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright Soramitsu Co., Ltd. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef LIBP2P_LIBP2P_CRYPTO_KEY_HPP -#define LIBP2P_LIBP2P_CRYPTO_KEY_HPP - -#include - -#include "libp2p/common/types.hpp" - -namespace libp2p::crypto { - -using Buffer = libp2p::common::ByteArray; - -struct Key { - /** - * Supported types of all keys - */ - enum class Type { - UNSPECIFIED = 100, - RSA = 0, - Ed25519 = 1, - Secp256k1 = 2, - ECDSA = 3 - }; - - Key(Type, std::vector); - ~Key() noexcept; - Type type = Type::UNSPECIFIED; ///< key type - std::vector data{}; ///< key content -}; - -inline bool operator==(const Key& lhs, const Key& rhs) { - return lhs.type == rhs.type && lhs.data == rhs.data; -} - -inline bool operator!=(const Key& lhs, const Key& rhs) { - return !(lhs == rhs); -} - -struct PublicKey : public Key {}; - -struct PrivateKey : public Key {}; - -struct KeyPair { - PublicKey publicKey; - PrivateKey privateKey; -}; - -using Signature = std::vector; - -inline bool operator==(const KeyPair& a, const KeyPair& b) { - return a.publicKey == b.publicKey && a.privateKey == b.privateKey; -} - -/** - * Result of ephemeral key generation - * -struct EphemeralKeyPair { - Buffer ephemeral_public_key; - std::function(Buffer)> shared_secret_generator; -}; -*/ - -/** - * Type of the stretched key - * -struct StretchedKey { - Buffer iv; - Buffer cipher_key; - Buffer mac_key; -}; -*/ -} // namespace libp2p::crypto - -namespace std { -template <> -struct hash { - size_t operator()(const libp2p::crypto::Key& x) const; -}; - -template <> -struct hash { - size_t operator()(const libp2p::crypto::PrivateKey& x) const; -}; - -template <> -struct hash { - size_t operator()(const libp2p::crypto::PublicKey& x) const; -}; - -template <> -struct hash { - size_t operator()(const libp2p::crypto::KeyPair& x) const; -}; -} // namespace std - -#endif // LIBP2P_LIBP2P_CRYPTO_KEY_HPP diff --git a/library/include/libp2p/crypto/protobuf/protobuf_key.hpp b/library/include/libp2p/crypto/protobuf/protobuf_key.hpp deleted file mode 100644 index 1a0d7ae7..00000000 --- a/library/include/libp2p/crypto/protobuf/protobuf_key.hpp +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright Soramitsu Co., Ltd. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef KAGOME_PROTOBUF_KEY_HPP -#define KAGOME_PROTOBUF_KEY_HPP - -// #include - -#include - -#include - -namespace libp2p::crypto { -/** - * Strict type for key, which is encoded into Protobuf format - */ -struct ProtobufKey { //: public boost::equality_comparable { - explicit ProtobufKey(std::vector key); - ~ProtobufKey() noexcept; - - std::vector key; - - bool operator==(const ProtobufKey& other) const { return key == other.key; } -}; -} // namespace libp2p::crypto - -#endif // KAGOME_PROTOBUF_KEY_HPP diff --git a/library/include/libp2p/multi/multibase_codec.hpp b/library/include/libp2p/multi/multibase_codec.hpp deleted file mode 100644 index c7b9cbd1..00000000 --- a/library/include/libp2p/multi/multibase_codec.hpp +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright Soramitsu Co., Ltd. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef LIBP2P_MULTIBASE_HPP -#define LIBP2P_MULTIBASE_HPP - -#include "vocab/expected.h" - -#include -#include -#include - -#include - -namespace libp2p::multi { -/** - * Allows to distinguish between different base-encoded binaries - * See more: https://github.com/multiformats/multibase - */ -class MultibaseCodec { - public: - enum class Error { UNSUPPORTED_BASE = 1, INPUT_TOO_SHORT, BASE_CODEC_ERROR }; - - using ByteBuffer = common::ByteArray; - using FactoryResult = ipfs::expected; - - virtual ~MultibaseCodec() = default; - /** - * Encodings, supported by this Multibase - * @sa https://github.com/multiformats/multibase#multibase-table - */ - enum class Encoding : char { - BASE16_LOWER = 'f', - BASE16_UPPER = 'F', - BASE32_LOWER = 'b', - BASE32_UPPER = 'B', - BASE36 = 'k', - BASE58 = 'z', - BASE64 = 'm' - }; - - /** - * Encode the incoming bytes - * @param bytes to be encoded - * @param encoding - base of the desired encoding - * @return encoded string WITH an encoding prefix - */ - virtual std::string encode(const ByteBuffer& bytes, - Encoding encoding) const = 0; - - /** - * Decode the incoming string - * @param string to be decoded - * @return bytes, if decoding was successful, error otherwise - */ - virtual FactoryResult decode(std::string_view string) const = 0; -}; - -bool case_critical(MultibaseCodec::Encoding); - -} // namespace libp2p::multi - -#endif // LIBP2P_MULTIBASE_HPP diff --git a/library/include/libp2p/multi/multibase_codec/codecs/base16.h b/library/include/libp2p/multi/multibase_codec/codecs/base16.h deleted file mode 100644 index 72a74237..00000000 --- a/library/include/libp2p/multi/multibase_codec/codecs/base16.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef IPFS_BASE32_H_ -#define IPFS_BASE32_H_ - -#include "base_error.hpp" - -#include -#include - -#include - -#include - -namespace ipfs::base16 { -std::string encodeLower(ByteView bytes); -std::string encodeUpper(ByteView bytes); - -using libp2p::common::ByteArray; -using libp2p::multi::detail::BaseError; -using Decoded = ipfs::expected; -Decoded decode(std::string_view string); - -} // namespace ipfs::base16 - -#endif // IPFS_BASE32_H_ diff --git a/library/include/libp2p/multi/multibase_codec/codecs/base32.hpp b/library/include/libp2p/multi/multibase_codec/codecs/base32.hpp deleted file mode 100644 index c24dc59d..00000000 --- a/library/include/libp2p/multi/multibase_codec/codecs/base32.hpp +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright Soramitsu Co., Ltd. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef LIBP2P_BASE32_HPP -#define LIBP2P_BASE32_HPP - -#include "base_error.hpp" - -#include -#include - -/** - * Encode/decode to/from base32 format - * Implementation is taken from - * https://github.com/mjg59/tpmtotp/blob/master/base32.c - */ -namespace libp2p::multi::detail { - -/** - * Encode bytes to base32 uppercase string - * @param bytes to be encoded - * @return encoded string - */ -std::string encodeBase32Upper(ipfs::ByteView bytes); -/** - * Encode bytes to base32 lowercase string - * @param bytes to be encoded - * @return encoded string - */ -std::string encodeBase32Lower(ipfs::ByteView bytes); - -/** - * Decode base32 uppercase to bytes - * @param string to be decoded - * @return decoded bytes in case of success - */ -ipfs::expected decodeBase32Upper( - std::string_view string); - -/** - * Decode base32 lowercase string to bytes - * @param string to be decoded - * @return decoded bytes in case of success - */ -ipfs::expected decodeBase32Lower( - std::string_view string); - -} // namespace libp2p::multi::detail - -#endif // LIBP2P_BASE32_HPP diff --git a/library/include/libp2p/multi/multibase_codec/codecs/base36.hpp b/library/include/libp2p/multi/multibase_codec/codecs/base36.hpp deleted file mode 100644 index 20006df2..00000000 --- a/library/include/libp2p/multi/multibase_codec/codecs/base36.hpp +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright Soramitsu Co., Ltd. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef LIBP2P_BASE36_HPP -#define LIBP2P_BASE36_HPP - -#include "base_error.hpp" - -#include -#include - -/** - * Encode/decode to/from base36 format - */ -namespace libp2p::multi::detail { - -/** - * Encode bytes to base36 uppercase string - * @param bytes to be encoded - * @return encoded string - */ -std::string encodeBase36Upper(ipfs::ByteView bytes); -/** - * Encode bytes to base36 lowercase string - * @param bytes to be encoded - * @return encoded string - */ -std::string encodeBase36Lower(ipfs::ByteView bytes); - -/** - * Decode base36 (case-insensitively) to bytes - * @param string to be decoded - * @return decoded bytes in case of success - */ -ipfs::expected decodeBase36( - std::string_view string); - -} // namespace libp2p::multi::detail - -#endif // LIBP2P_BASE36_HPP diff --git a/library/include/libp2p/multi/multibase_codec/codecs/base58.hpp b/library/include/libp2p/multi/multibase_codec/codecs/base58.hpp deleted file mode 100644 index 2cfb4576..00000000 --- a/library/include/libp2p/multi/multibase_codec/codecs/base58.hpp +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright Soramitsu Co., Ltd. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef LIBP2P_BASE58_HPP -#define LIBP2P_BASE58_HPP - -#include "base_error.hpp" - -#include - -#include -#include - -#include -#include - -/** - * Encode/decode to/from base58 format - * Implementation is taken from - * https://github.com/bitcoin/bitcoin/blob/master/src/base58.h - */ -namespace libp2p::multi::detail { - -/** - * Encode bytes to base58 string - * @param bytes to be encoded - * @return encoded string - */ -std::string encodeBase58(ipfs::ByteView bytes); - -/** - * Decode base58 string to bytes - * @param string to be decoded - * @return decoded bytes in case of success - */ -ipfs::expected decodeBase58( - std::string_view string); -} // namespace libp2p::multi::detail - -#endif // LIBP2P_BASE58_HPP diff --git a/library/include/libp2p/multi/multibase_codec/codecs/base_error.hpp b/library/include/libp2p/multi/multibase_codec/codecs/base_error.hpp deleted file mode 100644 index a0ab1b6c..00000000 --- a/library/include/libp2p/multi/multibase_codec/codecs/base_error.hpp +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright Soramitsu Co., Ltd. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef LIBP2P_BASE_ERROR_HPP -#define LIBP2P_BASE_ERROR_HPP - -namespace libp2p::multi::detail { - -enum class BaseError { - INVALID_BASE58_INPUT = 1, - INVALID_BASE64_INPUT, - INVALID_BASE32_INPUT, - INVALID_BASE36_INPUT, - NON_UPPERCASE_INPUT, - NON_LOWERCASE_INPUT, - UNIMPLEMENTED_MULTIBASE, - INVALID_BASE16_INPUT -}; - -} - -#endif // LIBP2P_BASE_ERROR_HPP diff --git a/library/include/libp2p/multi/multicodec_type.hpp b/library/include/libp2p/multi/multicodec_type.hpp deleted file mode 100644 index bda027bb..00000000 --- a/library/include/libp2p/multi/multicodec_type.hpp +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright Soramitsu Co., Ltd. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef LIBP2P_MULTICODECTYPE_HPP -#define LIBP2P_MULTICODECTYPE_HPP - -#include - -namespace libp2p::multi { - -/** - * LibP2P uses "protocol tables" to agree upon the mapping from one multicodec - * code. These tables can be application specific, though, like with other - * multiformats, there is a globally agreed upon table with common protocols - * and formats. - */ -class MulticodecType { - public: - enum class Code { - IDENTITY = 0x00, - SHA1 = 0x11, - SHA2_256 = 0x12, - SHA2_512 = 0x13, - SHA3_512 = 0x14, - SHA3_384 = 0x15, - SHA3_256 = 0x16, - SHA3_224 = 0x17, - RAW = 0x55, - DAG_PB = 0x70, - DAG_CBOR = 0x71, - LIBP2P_KEY = 0x72, - DAG_JSON = 0x0129, - FILECOIN_COMMITMENT_UNSEALED = 0xf101, - FILECOIN_COMMITMENT_SEALED = 0xf102, - }; - - constexpr static std::string_view getName(Code code) { - switch (code) { - case Code::IDENTITY: - return "identity"; - case Code::SHA1: - return "sha1"; - case Code::SHA2_256: - return "sha2-256"; - case Code::SHA2_512: - return "sha2-512"; - case Code::SHA3_224: - return "sha3-224"; - case Code::SHA3_256: - return "sha3-256"; - case Code::SHA3_384: - return "sha3-384"; - case Code::SHA3_512: - return "sha3-512"; - case Code::RAW: - return "raw"; - case Code::DAG_PB: - return "dag-pb"; - case Code::DAG_CBOR: - return "dag-cbor"; - case Code::DAG_JSON: - return "dag-json"; - case Code::LIBP2P_KEY: - return "libp2p-key"; - case Code::FILECOIN_COMMITMENT_UNSEALED: - return "fil-commitment-unsealed"; - case Code::FILECOIN_COMMITMENT_SEALED: - return "fil-commitment-sealed"; - } - return "unknown"; - } -}; - -} // namespace libp2p::multi - -#endif // LIBP2P_MULTICODECTYPE_HPP diff --git a/library/src/ipfs_client/car.cc b/library/src/ipfs_client/car.cc index e3644234..b231e8d0 100644 --- a/library/src/ipfs_client/car.cc +++ b/library/src/ipfs_client/car.cc @@ -26,7 +26,7 @@ Self::Car(ByteView bytes, ContextApi const& api) { LOG(ERROR) << "Problem parsing CAR header."; break; case 1: - LOG(INFO) << "Reading CARv1"; + VLOG(1) << "Reading CARv1"; data_ = after_header; break; case 2: { @@ -59,7 +59,6 @@ auto Self::NextBlock() -> std::optional { rv.bytes = data_.subspan(0U, len->toUInt64()); data_ = data_.subspan(len->toUInt64()); if (rv.cid.ReadStart(rv.bytes)) { - // TODO : check hash return rv; } return std::nullopt; diff --git a/library/src/ipfs_client/cid.cc b/library/src/ipfs_client/cid.cc index b2068608..da2a693d 100644 --- a/library/src/ipfs_client/cid.cc +++ b/library/src/ipfs_client/cid.cc @@ -25,8 +25,6 @@ Self::Cid(std::string_view s) { auto view = ByteView{bytes.value()}; ReadStart(view); } - } else { - LOG(WARNING) << "Failed to decode the multibase for a CID: " << s; } } diff --git a/library/src/ipfs_client/context_api.cc b/library/src/ipfs_client/context_api.cc index 972e3df2..f58a062d 100644 --- a/library/src/ipfs_client/context_api.cc +++ b/library/src/ipfs_client/context_api.cc @@ -19,3 +19,8 @@ auto Self::Hash(HashType ht, ByteView data) } return it->second->hash(data); } + +unsigned int Self::GetGatewayRate(std::string_view) { + return 120; +} +void Self::SetGatewayRate(std::string_view, unsigned int) {} diff --git a/library/src/ipfs_client/gateways.cc b/library/src/ipfs_client/gateways.cc index d118846f..68eaa7c3 100644 --- a/library/src/ipfs_client/gateways.cc +++ b/library/src/ipfs_client/gateways.cc @@ -95,27 +95,27 @@ auto ipfs::Gateways::DefaultGateways() -> GatewayList { auto N = static_cast(result.size()); for (auto i = 0; i < N; ++i) { auto& r = result[i]; - r.strength = N - i; - LOG(INFO) << "User-specified gateway: " << r.prefix << '=' << r.strength; + r.rate = N - i; + LOG(INFO) << "User-specified gateway: " << r.prefix << '=' << r.rate; } return result; } - return {{"http://localhost:8080/"s, 932}, - {"https://jcsl.hopto.org/"s, 920}, - {"https://human.mypinata.cloud/"s, 883}, - {"https://ipfs.io/"s, 847}, - {"https://gateway.ipfs.io/"s, 741}, - {"https://dweb.link/"s, 619}, - {"https://gateway.pinata.cloud/"s, 497}, - {"https://ipfs.joaoleitao.org/"s, 438}, - {"https://ipfs.runfission.com/"s, 376}, - {"https://nftstorage.link/"s, 311}, - {"https://w3s.link/"s, 246}, - {"https://ipfs.fleek.co/"s, 207}, - {"https://ipfs.jpu.jp/"s, 173}, - {"https://permaweb.eu.org/"s, 126}, - {"https://jorropo.net/"s, 79}, - {"https://hardbin.com/"s, 41}, + return {{"http://localhost:8080/"s, 885}, + {"https://jcsl.hopto.org/"s, 833}, + {"https://human.mypinata.cloud/"s, 737}, + {"https://ipfs.io/"s, 731}, + {"https://gateway.ipfs.io/"s, 628}, + {"https://dweb.link/"s, 512}, + {"https://gateway.pinata.cloud/"s, 455}, + {"https://ipfs.joaoleitao.org/"s, 355}, + {"https://ipfs.runfission.com/"s, 341}, + {"https://nftstorage.link/"s, 239}, + {"https://w3s.link/"s, 166}, + {"https://ipfs.fleek.co/"s, 133}, + {"https://permaweb.eu.org/"s, 96}, + {"https://hardbin.com/"s, 4}, + {"https://jorropo.net/"s, 3}, + {"https://ipfs.jpu.jp/"s, 2}, {"https://ipfs.soul-network.com/"s, 1}, {"https://storry.tv/"s, 0}}; } diff --git a/library/src/ipfs_client/gateways_unittest.cc b/library/src/ipfs_client/gateways_unittest.cc index 16743d60..1786f812 100644 --- a/library/src/ipfs_client/gateways_unittest.cc +++ b/library/src/ipfs_client/gateways_unittest.cc @@ -12,7 +12,7 @@ TEST(GatewaysTest, DefaultListMeetsBasicGuidelines) { EXPECT_EQ(dg.at(i).prefix.back(), '/'); // I considered asserting the number of /s and :s, but that _is_ allowed to // change - EXPECT_LE(dg.at(i).strength, dg.at(i - 1).strength); + EXPECT_LE(dg.at(i).rate, dg.at(i - 1).rate); } } @@ -24,7 +24,7 @@ TEST(GatewaysTest, OverriddenListEndsEntriesInSlash) { EXPECT_EQ(dg.at(0).prefix, "a/"); for (auto i = 1U; i < dg.size(); ++i) { EXPECT_EQ(dg.at(i).prefix.back(), '/'); - EXPECT_LE(dg.at(i).strength, dg.at(i - 1).strength); + EXPECT_LE(dg.at(i).rate, dg.at(i - 1).rate); } } diff --git a/library/src/ipfs_client/gw/default_requestor.cc b/library/src/ipfs_client/gw/default_requestor.cc index 46dbf880..3a3da2d8 100644 --- a/library/src/ipfs_client/gw/default_requestor.cc +++ b/library/src/ipfs_client/gw/default_requestor.cc @@ -4,10 +4,10 @@ #include #include #include -#include +#include #include -auto ipfs::gw::default_requestor(ipfs::GatewayList gws, +auto ipfs::gw::default_requestor(ipfs::GatewayList /* gws */, std::shared_ptr early, std::shared_ptr api) -> std::shared_ptr { @@ -17,14 +17,13 @@ auto ipfs::gw::default_requestor(ipfs::GatewayList gws, result->or_else(early); early->api(api); } - auto pool = std::make_shared(); + // auto pool = std::make_shared(); result->or_else(std::make_shared(api)) - .or_else(pool) + .or_else(std::make_shared()) .or_else(std::make_shared()); - for (auto& gw : gws) { - auto gwr = - std::make_shared(gw.prefix, gw.strength, api); - pool->add(gwr); - } + // for (auto& gw : gws) { + // auto gwr = std::make_shared(gw.prefix, gw.rate, + // api); pool->add(gwr); + // } return result; } diff --git a/library/src/ipfs_client/gw/default_requestor_unittest.cc b/library/src/ipfs_client/gw/default_requestor_unittest.cc index 007c5174..83162ad0 100644 --- a/library/src/ipfs_client/gw/default_requestor_unittest.cc +++ b/library/src/ipfs_client/gw/default_requestor_unittest.cc @@ -34,7 +34,8 @@ TEST(DefaultRequestorTest, name_check) { EXPECT_EQ(a.name, "DNSLink requestor"); EXPECT_TRUE(a.next); a.next->TestAccess(&a); - EXPECT_EQ(a.name, "requestor pool"); + EXPECT_EQ(a.name, "multi-gateway requestor"); + // EXPECT_EQ(a.name, "requestor pool"); EXPECT_TRUE(a.next); a.next->TestAccess(&a); EXPECT_EQ(a.name, "Terminating requestor"); diff --git a/library/src/ipfs_client/gw/gateway_http_requestor.cc b/library/src/ipfs_client/gw/gateway_http_requestor.cc index ecac0bf0..5fd1d91e 100644 --- a/library/src/ipfs_client/gw/gateway_http_requestor.cc +++ b/library/src/ipfs_client/gw/gateway_http_requestor.cc @@ -6,7 +6,6 @@ #include #include #include -#include #include "log_macros.h" @@ -31,18 +30,13 @@ auto Self::handle(ipfs::gw::RequestPtr r) -> HandleOutcome { if (target(*r) <= r->parallel + pending_ + seen_[req_key]) { return HandleOutcome::MAYBE_LATER; } - auto desc = r->describe_http(); + auto desc = r->describe_http(prefix_); if (!desc.has_value() || desc.value().url.empty()) { LOG(ERROR) << r->debug_string() << " is HTTP but can't describe the HTTP request that would happen?"; return HandleOutcome::NOT_HANDLED; } - if (prefix_.back() == '/' && desc.value().url[0] == '/') { - desc.value().url.insert(0, prefix_, 0UL, prefix_.size() - 1UL); - } else { - desc.value().url.insert(0, prefix_); - } desc.value().timeout_seconds += extra_seconds_; auto cb = [this, r, desc, req_key](std::int16_t status, std::string_view body, ContextApi::HeaderAccess ha) { @@ -121,54 +115,6 @@ Self::GatewayHttpRequestor(std::string gateway_prefix, } Self::~GatewayHttpRequestor() {} -ipfs::ipld::NodePtr Self::node_from_type(std::optional const& cid, - ReqTyp t, - std::string_view body) const { - switch (t) { - case ReqTyp::Block: { - if (cid.has_value()) { - ipfs::PbDag blk{cid.value(), as_bytes(body)}; - if (blk.cid_matches_data(*api_)) { - return ipfs::ipld::DagNode::fromBlock(blk); - } - } else { - LOG(ERROR) << "Block request on an invalid CID."; - } - return {}; - } - case ReqTyp::Ipns: { - if (cid.has_value()) { - auto byte_ptr = reinterpret_cast(body.data()); - auto rec = ipfs::ValidateIpnsRecord({byte_ptr, body.size()}, - cid.value(), *api_); - if (rec.has_value()) { - return ipfs::ipld::DagNode::fromIpnsRecord(rec.value()); - } else { - LOG(ERROR) << "IPNS record failed to validate!"; - } - } - return {}; - } - case ReqTyp::Identity: - LOG(ERROR) << "An HTTP response from a gateway received for an identity " - "(inlined) CID"; - return {}; - case ReqTyp::DnsLink: - LOG(WARNING) << "HTTP responses to DnsLink requests not yet implemented, " - "and it's not clear they will be."; - return {}; - case ReqTyp::Car: - LOG(INFO) << "TODO responses to Car requests not yet implemented."; - return {}; - case ReqTyp::Providers: - LOG(INFO) << "TODO responses to Car requests not yet implemented: " - << body; - return {}; - case ReqTyp::Zombie: - return {}; - } - return {}; // TODO -} int Self::target(GatewayRequest const& r) const { int result = (strength_ - pending_) / 2; if (!pending_) { diff --git a/library/src/ipfs_client/gw/gateway_http_requestor.h b/library/src/ipfs_client/gw/gateway_http_requestor.h index 6d68b79c..8c61bef8 100644 --- a/library/src/ipfs_client/gw/gateway_http_requestor.h +++ b/library/src/ipfs_client/gw/gateway_http_requestor.h @@ -21,9 +21,6 @@ class GatewayHttpRequestor final : public Requestor { HandleOutcome handle(RequestPtr) override; std::string_view name() const override; - ipfs::ipld::NodePtr node_from_type(std::optional const& cid, - ipfs::gw::Type, - std::string_view body) const; int target(GatewayRequest const&) const; public: diff --git a/library/src/ipfs_client/gw/gateway_request.cc b/library/src/ipfs_client/gw/gateway_request.cc index 1b251b1f..8149b7cc 100644 --- a/library/src/ipfs_client/gw/gateway_request.cc +++ b/library/src/ipfs_client/gw/gateway_request.cc @@ -106,15 +106,15 @@ std::string_view Self::accept() const { short Self::timeout_seconds() const { switch (type) { case Type::DnsLink: - return 16; + return 32; case Type::Block: - return 39; - case Type::Providers: return 64; - case Type::Car: + case Type::Providers: return 128; - case Type::Ipns: + case Type::Car: return 256; + case Type::Ipns: + return 512; case Type::Identity: case Type::Zombie: return 0; @@ -134,14 +134,33 @@ auto Self::identity_data() const -> std::string_view { } bool Self::is_http() const { - return type != Type::DnsLink && type != Type::Identity; + switch (type) { + case Type::Ipns: + case Type::Car: + case Type::Block: + case Type::Providers: + return true; + case Type::Identity: + case Type::DnsLink: + case Type::Zombie: + return false; + } + return true; } -auto Self::describe_http() const -> std::optional { +auto Self::describe_http(std::string_view prefix) const + -> std::optional { if (!is_http()) { return {}; } - return HttpRequestDescription{url_suffix(), timeout_seconds(), - std::string{accept()}, max_response_size()}; + DCHECK(!prefix.empty()); + auto url = url_suffix(); + if (url.front() == '/' && prefix.back() == '/') { + prefix.remove_suffix(1UL); + } else if (url.front() != '/' && prefix.back() != '/') { + url.insert(0UL, 1UL, '/'); + } + url.insert(0UL, prefix); + return HttpRequestDescription{url, timeout_seconds(), std::string{accept()}, max_response_size()}; } std::optional Self::max_response_size() const { switch (type) { @@ -269,7 +288,7 @@ bool Self::RespondSuccessfully(std::string_view bytes, LOG(INFO) << "Did not add node from CAR: " << cid_s; } } - LOG(INFO) << "Added " << added << " nodes from a CAR."; + VLOG(1) << "Added " << added << " nodes from a CAR."; success = added > 0; break; } @@ -289,6 +308,7 @@ bool Self::RespondSuccessfully(std::string_view bytes, bytes_received_hooks.clear(); orchestrator_->build_response(dependent); } + type = Type::Zombie; return success; } void Self::Hook(std::function f) { diff --git a/library/src/ipfs_client/gw/gateway_state.cc b/library/src/ipfs_client/gw/gateway_state.cc new file mode 100644 index 00000000..6e0f5951 --- /dev/null +++ b/library/src/ipfs_client/gw/gateway_state.cc @@ -0,0 +1,52 @@ +#include "gateway_state.h" + +#include + +#include "log_macros.h" + +using Self = ipfs::gw::GatewayState; + +Self::GatewayState() { + request_type_success.fill(0L); + last_hist_update = std::time({}); + sent_counts.fill(0U); +} +long Self::score(GatewayRequest const& req, unsigned baseline) const { + auto result = static_cast(baseline); + result += 2 * request_type_success.at(static_cast(req.type)); + auto i = affinity_success.find(req.affinity); + if (i != affinity_success.end()) { + result += 3 * i->second; + } + return result; +} +bool Self::over_rate(unsigned req_per_min) { + return total_sent + current_bucket() > req_per_min * MinutesTracked; +} +bool Self::bored() const { + return total_sent == 0UL; +} +void Self::just_sent_one() { + current_bucket()++; + ++total_sent; +} +unsigned int& Self::current_bucket() { + auto now = std::time({}); + while (last_hist_update < now) { + ++last_hist_update; + auto& c = sent_counts[last_hist_update % sent_counts.size()]; + total_sent -= c; + c = 0; + } + return sent_counts[last_hist_update % sent_counts.size()]; +} +void Self::hit(GatewayRequest const& req) { + std::clog << "GatewayState::hit " << static_cast(this) << ' ' + << static_cast(&req) << std::endl; + request_type_success.at(static_cast(req.type))++; + affinity_success[req.affinity]++; +} +bool Self::miss(GatewayRequest const& req) { + request_type_success.at(static_cast(req.type))--; + return affinity_success[req.affinity]-- >= 0; +} diff --git a/library/src/ipfs_client/gw/gateway_state.h b/library/src/ipfs_client/gw/gateway_state.h new file mode 100644 index 00000000..7adf7072 --- /dev/null +++ b/library/src/ipfs_client/gw/gateway_state.h @@ -0,0 +1,34 @@ +#ifndef IPFS_GATEWAY_STATE_H_ +#define IPFS_GATEWAY_STATE_H_ + +#include + +#include + +#include +#include + +namespace ipfs::gw { +class GatewayRequest; +class GatewayState { + flat_map affinity_success; + std::array request_type_success; + static constexpr short MinutesTracked = 4; + std::array sent_counts; + std::size_t total_sent = 0UL; + std::time_t last_hist_update; + unsigned& current_bucket(); + + public: + GatewayState(); + long score(GatewayRequest const&, unsigned) const; + bool over_rate(unsigned req_per_min); + bool bored() const; + + void just_sent_one(); + void hit(GatewayRequest const&); + bool miss(GatewayRequest const&); +}; +} // namespace ipfs::gw + +#endif // IPFS_GATEWAY_STATE_H_ diff --git a/library/src/ipfs_client/gw/gateway_state_unittest.cc b/library/src/ipfs_client/gw/gateway_state_unittest.cc new file mode 100644 index 00000000..705ead8d --- /dev/null +++ b/library/src/ipfs_client/gw/gateway_state_unittest.cc @@ -0,0 +1,20 @@ +#include "gateway_state.h" + +#include + +#include + +namespace i = ipfs; +namespace ig = i::gw; +using T = ig::GatewayState; +using R = ig::GatewayRequest; + +TEST(GatewayStateTest, InitialValues) { + T t; + R req; + EXPECT_TRUE(t.bored()); + for (auto r = 0U; r < 99U; ++r) { + EXPECT_FALSE(t.over_rate(r)); + EXPECT_EQ(t.score(req, r), r); + } +} \ No newline at end of file diff --git a/library/src/ipfs_client/gw/multi_gateway_requestor.cc b/library/src/ipfs_client/gw/multi_gateway_requestor.cc new file mode 100644 index 00000000..97af40d6 --- /dev/null +++ b/library/src/ipfs_client/gw/multi_gateway_requestor.cc @@ -0,0 +1,158 @@ +#include "multi_gateway_requestor.h" + +#include + +#include "log_macros.h" + +#include +#include + +using Self = ipfs::gw::MultiGatewayRequestor; + +namespace ch = std::chrono; + +std::string_view Self::name() const { + return "multi-gateway requestor"; +} +auto Self::handle(RequestPtr r) -> HandleOutcome { + VLOG(2) << name() << " handle(" << r->debug_string() << ")"; + if (!r->is_http()) { + LOG(INFO) << r->debug_string() << " is not an HTTP request."; + return HandleOutcome::NOT_HANDLED; + } + if (!q.empty()) { + auto popped = q.front(); + q.pop_front(); + Process(popped); + } + Process(r); + return HandleOutcome::PENDING; +} +bool Self::Process(RequestPtr const& req) { + if (!req->is_http()) { + return false; + } + auto state_iter = state_.begin(); + auto config_idx = 0UL; + std::vector> candidates; + auto bored = 0U; + while (auto gw = api_->GetGateway(config_idx++)) { + if (state_iter == state_.end() || state_iter->first > gw->prefix) { + VLOG(1) << "A new gateway has entered the chat: " << gw->prefix << '=' + << gw->rate; + // One can insert like this because state_ is std::map w/ stable iterators + state_iter = state_.insert({gw->prefix, GatewayState{}}).first; + candidates.emplace_back(LONG_MAX, gw->prefix, &(state_iter->second)); + } else if (state_iter->first < gw->prefix) { + LOG(INFO) << "Gateway has disappeared: " << state_iter->first + << " (would've come before " << gw->prefix << ')'; + // TODO remove permanently, but without crashing any existing request that + // will respond to this gateway + // auto to_rm = state_iter; + std::advance(state_iter, 1); + // state_.erase(to_rm); + continue; + } else if (req->failures.contains(gw->prefix)) { + // VLOG(2) << "Not going to resend " << req->debug_string() << " to " + // << gw->prefix << " as it has already failed us."; + } else if (state_iter->second.over_rate(gw->rate)) { + // VLOG(2) << "Not considering " << gw->prefix + // << " at the moment as it's over its rate limit " << + // gw->rate; + } else { + candidates.push_back({state_iter->second.score(*req, gw->rate), + gw->prefix, &(state_iter->second)}); + if (state_iter->second.bored()) { + ++bored; + } + } + std::advance(state_iter, 1); + } + if (candidates.empty() && config_idx <= req->failures.size()) { + LOG(ERROR) << "Request has failed on every gateway I have:" + << req->debug_string(); + forward(req); + return false; + } + auto to_send = std::max(bored / 2, 3U); + std::sort(candidates.begin(), candidates.end(), std::greater{}); + for (auto& [score, prefix, state] : candidates) { + DoSend(req, prefix, *state); + if (!--to_send) { + return true; + } + } + q.push_back(req); + return false; +} +void Self::DoSend(RequestPtr req, std::string const& gw, GatewayState& state) { + auto desc = req->describe_http(gw); + if (!desc.has_value()) { + LOG(ERROR) << "A request that has no HTTP description got pretty far " + "toward doing an HTTP request: " + << req->debug_string(); + return; + } + auto timeout_threshold = + ch::system_clock::now() + + ch::seconds(desc->timeout_seconds ? desc->timeout_seconds : 300) - + ch::milliseconds(1); + auto hold_alive = shared_from_this(); + auto cb = [this, hold_alive, req, gw, timeout_threshold, desc]( + std::int16_t s, std::string_view b, auto h) { + auto timed_out = ch::system_clock::now() >= timeout_threshold; + HandleResponse(*desc, req, gw, s, b, h, timed_out); + }; + state.just_sent_one(); + api_->SendHttpRequest(*desc, cb); +} +void Self::HandleResponse(HttpRequestDescription const& desc, + RequestPtr req, + std::string const& gw, + std::int16_t status, + std::string_view body, + ContextApi::HeaderAccess hdrs, + bool timed_out) { + if (req->type == Type::Zombie || + (req->PartiallyRedundant() && req->type == Type::Block)) { + return; + } + auto i = state_.find(gw); + if (status == 200) { + auto ct = hdrs("content-type"); + std::transform(ct.begin(), ct.end(), ct.begin(), ::tolower); + if (ct.empty()) { + LOG(ERROR) << "No content-type header?"; + } else if (desc.accept.size() && + ct.find(desc.accept) == std::string::npos) { + LOG(WARNING) << "Requested with Accept: " << desc.accept + << " but received response with content-type: " << ct; + } else if (!req->RespondSuccessfully(body, api_)) { + LOG(ERROR) << "Got an unuseful response from " << gw + << " forwarding request " << req->debug_string() + << " to next requestor."; + } else { + if (state_.end() != i) { + i->second.hit(*req); + } + auto rpm = api_->GetGatewayRate(gw); + api_->SetGatewayRate(gw, rpm + 3); + return; + } + } + auto rpm = api_->GetGatewayRate(gw); + if (status == 408 || status == 504 || status == 429 || status == 110 || + timed_out) { + LOG(INFO) << gw << " timed out."; + if (rpm > 9) { + api_->SetGatewayRate(gw, rpm - 4); + } else if (rpm) { + api_->SetGatewayRate(gw, 0U); + } + } + req->failures.insert(gw); + if (state_.end() != i && i->second.miss(*req) && rpm) { + api_->SetGatewayRate(gw, rpm - 1); + } + Process(req); +} diff --git a/library/src/ipfs_client/gw/multi_gateway_requestor.h b/library/src/ipfs_client/gw/multi_gateway_requestor.h new file mode 100644 index 00000000..8a7b70c3 --- /dev/null +++ b/library/src/ipfs_client/gw/multi_gateway_requestor.h @@ -0,0 +1,33 @@ +#ifndef IPFS_MULTI_GATEWAY_REQUESTOR_H_ +#define IPFS_MULTI_GATEWAY_REQUESTOR_H_ + +#include "gateway_state.h" + +#include + +#include + +#include +#include + +namespace ipfs::gw { +class MultiGatewayRequestor : public Requestor { + std::map state_; + std::deque q; + bool Process(RequestPtr const&); + void DoSend(RequestPtr, std::string const&, GatewayState&); + void HandleResponse(HttpRequestDescription const&, + RequestPtr, + std::string const&, + std::int16_t, + std::string_view, + ContextApi::HeaderAccess, + bool); + + public: + std::string_view name() const override; + HandleOutcome handle(RequestPtr) override; +}; +} // namespace ipfs::gw + +#endif // IPFS_MULTI_GATEWAY_REQUESTOR_H_ diff --git a/library/src/ipfs_client/gw/requestor_pool.cc b/library/src/ipfs_client/gw/requestor_pool.cc deleted file mode 100644 index b337dda1..00000000 --- a/library/src/ipfs_client/gw/requestor_pool.cc +++ /dev/null @@ -1,73 +0,0 @@ -#include "requestor_pool.h" - -#include - -#include "log_macros.h" - -using Self = ipfs::gw::RequestorPool; - -std::string_view Self::name() const { - return "requestor pool"; -} -Self& Self::add(std::shared_ptr r) { - if (api_ && !(r->api_)) { - r->api_ = api_; - } - pool_.push_back(r); - r->or_else(shared_from_this()); - return *this; -} -auto Self::handle(ipfs::gw::RequestPtr req) -> HandleOutcome { - auto now = std::time(nullptr); - for (auto i = 0UL; i * 2 < waiting_.size(); ++i) { - auto& t = waiting_.front().when; - if (t != now) { - auto to_pop = waiting_.front(); - waiting_.pop(); - check(to_pop); - } - } - return check({req, 0UL, 0L}); -} -auto Self::check(Waiting w) -> HandleOutcome { - using O = HandleOutcome; - auto next_retry = pool_.size(); - auto req = w.req; - if (req->PartiallyRedundant()) { - return O::DONE; - } - for (auto i = w.at_idx; i < pool_.size(); ++i) { - if (req->type == Type::Zombie) { - return O::DONE; - } - auto& tor = pool_[i]; - switch (tor->handle(req)) { - case O::DONE: - LOG(INFO) << "RequestorPool::handle returning DONE because a member of " - "the pool's handle returned DONE."; - return O::DONE; - case O::PENDING: - case O::PARALLEL: - req->parallel++; - break; - case O::MAYBE_LATER: - if (next_retry == pool_.size()) { - next_retry = i; - } - break; - case O::NOT_HANDLED: - break; - } - } - if (req->parallel > 0) { - return O::PENDING; - } - if (next_retry < pool_.size()) { - w.when = std::time(nullptr); - waiting_.emplace(w); - return O::PENDING; - } - VLOG(1) << "Have exhausted all requestors in pool looking for " - << req->debug_string(); - return O::NOT_HANDLED; -} diff --git a/library/src/ipfs_client/gw/requestor_pool_unittest.cc b/library/src/ipfs_client/gw/requestor_pool_unittest.cc deleted file mode 100644 index 185e67dd..00000000 --- a/library/src/ipfs_client/gw/requestor_pool_unittest.cc +++ /dev/null @@ -1,46 +0,0 @@ -#include - -#include - -#include -#include - -namespace { -struct RequestorPoolTest : public ::testing::Test { - std::vector> members; - std::shared_ptr api = std::make_shared(); - std::shared_ptr tested = - std::make_shared(); - std::shared_ptr req = - ig::GatewayRequest::fromIpfsPath(i::SlashDelimited{"/ipns/ipfs.io"}); - void add() { - auto p = std::make_shared(); - members.push_back(p); - tested->add(p); - } - void set_api() { - auto p = std::make_shared(); - p->api(api); - p->or_else(tested); - } -}; -} // namespace - -TEST_F(RequestorPoolTest, add_with_api_sets_member_api) { - add(); - EXPECT_EQ(members.size(), 1U); - set_api(); - add(); - EXPECT_EQ(members.size(), 2U); - EXPECT_TRUE(members.at(0)); - EXPECT_TRUE(members.at(1)); - EXPECT_FALSE(members.at(0)->api()); - EXPECT_TRUE(members.at(1)->api()); -} -TEST_F(RequestorPoolTest, pending_doesnt_stop_parallel_requests) { - add(); - add(); - members.at(0)->outcomes.push_back(MockRequestor::O::PENDING); - members.at(1)->outcomes.push_back(MockRequestor::O::PENDING); - tested->request(req); -} \ No newline at end of file diff --git a/library/src/ipfs_client/gw/requestor_unittest.cc b/library/src/ipfs_client/gw/requestor_unittest.cc index 70775d4c..03aefee9 100644 --- a/library/src/ipfs_client/gw/requestor_unittest.cc +++ b/library/src/ipfs_client/gw/requestor_unittest.cc @@ -27,6 +27,7 @@ struct RequestorTest : public ::testing::Test { std::shared_ptr b = std::make_shared(); std::shared_ptr req_ = std::make_shared(); + RequestorTest() { req_->type = g::Type::Block; } }; } // namespace diff --git a/library/src/ipfs_client/gw/terminating_requestor_unittest.cc b/library/src/ipfs_client/gw/terminating_requestor_unittest.cc index 1431c4fb..f6591e42 100644 --- a/library/src/ipfs_client/gw/terminating_requestor_unittest.cc +++ b/library/src/ipfs_client/gw/terminating_requestor_unittest.cc @@ -17,6 +17,7 @@ TEST(TerminatingRequestorTest, ZombieIsDone) { } TEST(TerminatingRequestorTest, BeingHandledInParallel) { auto req = std::make_shared(); + req->type = ig::Type::Block; req->parallel = 9; T tested; EXPECT_TRUE(tested.handle(req) == T::HandleOutcome::PENDING); diff --git a/library/src/ipfs_client/ipld/dag_node.cc b/library/src/ipfs_client/ipld/dag_node.cc index 4d11f6f2..0abe598b 100644 --- a/library/src/ipfs_client/ipld/dag_node.cc +++ b/library/src/ipfs_client/ipld/dag_node.cc @@ -161,10 +161,7 @@ void Node::set_api(std::shared_ptr api) { } auto Node::resolve(SlashDelimited initial_path, BlockLookup blu) -> ResolveResult { - ResolutionState state; - state.resolved_path_components = ""; - state.unresolved_path = initial_path; - state.get_available_block = blu; + ResolutionState state{initial_path, blu}; return resolve(state); } auto Node::CallChild(ipfs::ipld::ResolutionState& state) -> ResolveResult { @@ -190,12 +187,12 @@ auto Node::CallChild(ResolutionState& state, std::string_view link_key) node = state.GetBlock(child->cid); } if (node) { - Descend(state); + state.Descend(); return node->resolve(state); } else { std::string needed{"/ipfs/"}; needed.append(child->cid); - auto more = state.unresolved_path.to_view(); + auto more = state.PathToResolve().to_view(); if (more.size()) { if (more.front() != '/') { needed.push_back('/'); @@ -221,7 +218,7 @@ auto Node::CallChild(ResolutionState& state, return ProvenAbsent{}; } } - Descend(state); + state.Descend(); return node->resolve(state); } auto Node::FindChild(std::string_view link_key) -> Link* { @@ -232,16 +229,6 @@ auto Node::FindChild(std::string_view link_key) -> Link* { } return nullptr; } -void Node::Descend(ResolutionState& state) { - auto next = state.unresolved_path.pop(); - if (next.empty()) { - return; - } - if (!state.resolved_path_components.ends_with('/')) { - state.resolved_path_components.push_back('/'); - } - state.resolved_path_components.append(next); -} std::ostream& operator<<(std::ostream& s, ipfs::ipld::PathChange const& c) { return s << "PathChange{" << c.new_path << '}'; diff --git a/library/src/ipfs_client/ipld/directory_shard.cc b/library/src/ipfs_client/ipld/directory_shard.cc index 7839e62b..3ac7e166 100644 --- a/library/src/ipfs_client/ipld/directory_shard.cc +++ b/library/src/ipfs_client/ipld/directory_shard.cc @@ -40,8 +40,6 @@ auto Self::resolve_internal(ipfs::ipld::DirShard::HashIter hash_b, continue; } if (ends_with(name, human_name)) { - VLOG(2) << "Found " << human_name << ", leaving HAMT sharded directory " - << name << "->" << link.cid; return CallChild(parms, name); } auto node = parms.GetBlock(link.cid); @@ -60,7 +58,7 @@ auto Self::resolve_internal(ipfs::ipld::DirShard::HashIter hash_b, } VLOG(2) << "Found hash chunk, continuing to next level of HAMT sharded " "directory " - << name << "->" << link.cid; + << name << "->" << link.cid; return downcast->resolve_internal(std::next(hash_b), hash_e, human_name, parms); } else { diff --git a/library/src/ipfs_client/ipld/ipns_name.cc b/library/src/ipfs_client/ipld/ipns_name.cc index f3820092..e3ad0271 100644 --- a/library/src/ipfs_client/ipld/ipns_name.cc +++ b/library/src/ipfs_client/ipld/ipns_name.cc @@ -4,22 +4,31 @@ using Self = ipfs::ipld::IpnsName; -Self::IpnsName(std::string_view target_abs_path) - : target_path_{target_abs_path} {} +Self::IpnsName(std::string_view target_abs_path) { + SlashDelimited target{target_abs_path}; + target_namespace_ = target.pop(); + target_root_ = target.pop(); + links_.emplace_back("", Link{target_root_, nullptr}); + target_path_.assign(target.to_string()); +} auto Self::resolve(ResolutionState& params) -> ResolveResult { - // Can't use PathChange, as the target is truly absolute (rootless) - SlashDelimited t{target_path_}; - t.pop(); // Discard namespace, though realistically it's going to be ipfs - // basically all the time - auto name = t.pop(); - if (t) { - LOG(WARNING) << "Odd case: name points at /ns/root/MORE/PATH (" - << target_path_ << "): " << params.MyPath(); - auto path = t.to_string() + "/" + params.PathToResolve().to_string(); - auto altered = params.WithPath(path); - return CallChild(altered, "", name); - } else { - return CallChild(params, "", name); + auto& node = links_.at(0).second.node; + if (!node) { + node = params.GetBlock(target_root_); + } + if (!node) { + return MoreDataNeeded(target_namespace_ + "/" + target_root_); + } + if (target_path_.empty()) { + return node->resolve(params); } + auto path = target_path_; + path.append("/").append(params.PathToResolve().to_view()); + auto altered = params.WithPath(path); + LOG(WARNING) << "Odd case: name points at /ns/root/MORE/PATH (" + << target_namespace_ << '/' << target_root_ << '/' + << target_path_ << "): " << params.MyPath() + << " will be resolved as " << path; + return node->resolve(params); } diff --git a/library/src/ipfs_client/ipld/ipns_name.h b/library/src/ipfs_client/ipld/ipns_name.h index 8b50d6e8..b27025bd 100644 --- a/library/src/ipfs_client/ipld/ipns_name.h +++ b/library/src/ipfs_client/ipld/ipns_name.h @@ -5,7 +5,9 @@ namespace ipfs::ipld { class IpnsName : public DagNode { - std::string const target_path_; + std::string target_namespace_; + std::string target_root_; + std::string target_path_; ResolveResult resolve(ResolutionState& params) override; diff --git a/library/src/ipfs_client/ipld/resolution_state.cc b/library/src/ipfs_client/ipld/resolution_state.cc index 7b51513d..bf8f907b 100644 --- a/library/src/ipfs_client/ipld/resolution_state.cc +++ b/library/src/ipfs_client/ipld/resolution_state.cc @@ -4,6 +4,14 @@ using Self = ipfs::ipld::ResolutionState; +Self::ResolutionState(std::string_view path, ipfs::ipld::BlockLookup blu) + : ResolutionState(SlashDelimited{path}, blu) {} +Self::ResolutionState(SlashDelimited path_to_resolve, + ipfs::ipld::BlockLookup blu) + : resolved_path_components{}, + unresolved_path(path_to_resolve), + get_available_block(blu) {} + bool Self::IsFinalComponent() const { return !unresolved_path; } @@ -33,4 +41,15 @@ auto Self::RestartResolvedPath() const -> ResolutionState { auto rv = *this; rv.resolved_path_components.clear(); return rv; -} \ No newline at end of file +} + +void Self::Descend() { + auto next = unresolved_path.pop(); + if (next.empty()) { + return; + } + if (!resolved_path_components.ends_with('/')) { + resolved_path_components.push_back('/'); + } + resolved_path_components.append(next); +} diff --git a/library/src/ipfs_client/ipld/root.cc b/library/src/ipfs_client/ipld/root.cc index fd5af1b2..e08afc54 100644 --- a/library/src/ipfs_client/ipld/root.cc +++ b/library/src/ipfs_client/ipld/root.cc @@ -21,6 +21,7 @@ Ptr Self::rooted() { auto Self::resolve(ResolutionState& params) -> ResolveResult { auto location = params.PathToResolve().to_string(); + VLOG(2) << "Root " << params.MyPath() << " resolving " << location; auto result = deroot()->resolve(params); if (auto pc = std::get_if(&result)) { auto lower = params.WithPath(pc->new_path); diff --git a/library/src/ipfs_client/ipld/small_directory.cc b/library/src/ipfs_client/ipld/small_directory.cc index b8613663..bae13675 100644 --- a/library/src/ipfs_client/ipld/small_directory.cc +++ b/library/src/ipfs_client/ipld/small_directory.cc @@ -13,6 +13,8 @@ using namespace std::literals; using Self = ipfs::ipld::SmallDirectory; auto Self::resolve(ResolutionState& params) -> ResolveResult { + VLOG(1) << "dir(" << params.MyPath() << " // " << params.PathToResolve() + << ")"; if (params.IsFinalComponent()) { LOG(INFO) << "Directory listing requested for " << params.MyPath(); auto result = CallChild(params, "index.html"); diff --git a/library/src/ipfs_client/ipld/symlink.cc b/library/src/ipfs_client/ipld/symlink.cc index b35725ad..141f06c0 100644 --- a/library/src/ipfs_client/ipld/symlink.cc +++ b/library/src/ipfs_client/ipld/symlink.cc @@ -12,7 +12,6 @@ auto Self::resolve(ResolutionState& params) -> ResolveResult { std::string result; if (!is_absolute()) { auto left_path = params.MyPath(); - left_path.pop_n(2); // Returning a path relative to content root. left_path.pop_back(); // Because the final component refers to this // symlink, which is getting replaced with target result.assign(left_path.to_view()); diff --git a/library/src/ipfs_client/ipld/symlink_unittest.cc b/library/src/ipfs_client/ipld/symlink_unittest.cc index f1451682..b0e7c21e 100644 --- a/library/src/ipfs_client/ipld/symlink_unittest.cc +++ b/library/src/ipfs_client/ipld/symlink_unittest.cc @@ -46,3 +46,14 @@ TEST(SymlinkTest, rooted) { EXPECT_TRUE(std::holds_alternative(res)); EXPECT_EQ(std::get(res).new_path, target); } +TEST(SymlinkTest, relative) { + auto sub = std::make_shared("c"); + ii::DagNode& super = *sub; + auto blu = [sub](std::string const& block_key) -> ii::NodePtr { return {}; }; + ii::ResolutionState state{"/a/b", blu}; + state.Descend(); + state.Descend(); + auto res = super.resolve(state); + EXPECT_TRUE(std::holds_alternative(res)); + EXPECT_EQ(std::get(res).new_path, "/a/c"); +} diff --git a/library/src/ipfs_client/ipns_record_unittest.cc b/library/src/ipfs_client/ipns_record_unittest.cc index 73bcf919..182d9803 100644 --- a/library/src/ipfs_client/ipns_record_unittest.cc +++ b/library/src/ipfs_client/ipns_record_unittest.cc @@ -73,6 +73,10 @@ struct Api final : public i::ContextApi { }; } } + std::optional GetGateway(std::size_t) const { + return std::nullopt; + } + virtual ~Api() noexcept {} }; } // namespace diff --git a/library/src/ipfs_client/orchestrator.cc b/library/src/ipfs_client/orchestrator.cc index 018fc810..b66cfb15 100644 --- a/library/src/ipfs_client/orchestrator.cc +++ b/library/src/ipfs_client/orchestrator.cc @@ -23,7 +23,7 @@ void Self::build_response(std::shared_ptr req) { return; } auto req_path = req->path(); - VLOG(2) << "build_response(" << req_path.to_string() << ')'; + VLOG(1) << "build_response(" << req_path.to_string() << ')'; req_path.pop(); // namespace std::string affinity{req_path.pop()}; auto it = dags_.find(affinity); @@ -32,7 +32,7 @@ void Self::build_response(std::shared_ptr req) { build_response(req); } } else { - VLOG(2) << "Requesting root " << affinity << " resolve path " + VLOG(1) << "Requesting root " << affinity << " resolve path " << req_path.to_string(); auto root = it->second->rooted(); if (root != it->second) { @@ -54,10 +54,11 @@ void Self::from_tree(std::shared_ptr req, auto result = root->resolve(relative_path, block_look_up); auto response = std::get_if(&result); if (response) { - VLOG(1) << "Tree gave us a response: status=" << response->status_ - << " mime=" << response->mime_ - << " location=" << response->location_ << " body is " - << response->body_.size() << " bytes."; + VLOG(1) << "Tree gave us a response to '" << req->path() + << "' : status=" << response->status_ + << " mime=" << response->mime_ + << " location=" << response->location_ << " body is " + << response->body_.size() << " bytes."; if (response->mime_.empty() && !response->body_.empty()) { if (response->location_.empty()) { LOG(INFO) << "Request for " << req->path() @@ -72,26 +73,30 @@ void Self::from_tree(std::shared_ptr req, hit_path.push_back('/'); } hit_path.append(response->location_); - LOG(INFO) << "Request for " << req->path() << " returned a location of " - << response->location_ << " and a body of " + VLOG(1) << "Request for " << req->path() << " returned a location of " + << response->location_ << " and a body of " << response->body_.size() << " bytes, sniffing mime from " << hit_path; response->mime_ = sniff(SlashDelimited{hit_path}, response->body_); } } + if (response->status_ / 100 != 3) { + response->location_.clear(); + } req->finish(*response); - } else if (std::holds_alternative(result)) { - auto& np = std::get(result); - LOG(INFO) << "Symlink converts request to " << req->path().to_string() - << " into " << np.new_path - << ". TODO - check for infinite loops."; - req->new_path(np.new_path); - build_response(req); + } else if (auto* pc = std::get_if(&result)) { + LOG(ERROR) << "Should not be getting a PathChange in orchestrator - " + "should've been handled in the root, but got " + << pc->new_path << " for " << req->path(); } else if (std::get_if(&result)) { req->finish(Response::IMMUTABLY_GONE); } else { auto& mps = std::get(result).ipfs_abs_paths_; req->till_next(mps.size()); + for (auto& mp : mps) { + VLOG(1) << "Attempt to resolve " << relative_path << " for " + << req->path() << " leads to request for " << mp; + } if (std::any_of(mps.begin(), mps.end(), [this, &req, &affinity](auto& p) { return gw_request(req, SlashDelimited{p}, affinity); })) { @@ -102,8 +107,8 @@ void Self::from_tree(std::shared_ptr req, bool Self::gw_request(std::shared_ptr ir, ipfs::SlashDelimited path, std::string const& aff) { - LOG(INFO) << "Seeking " << path.to_string(); auto req = gw::GatewayRequest::fromIpfsPath(path); + VLOG(1) << "Seeking " << path.to_string() << " -> " << req->debug_string(); if (req) { req->dependent = ir; req->orchestrator(shared_from_this()); @@ -136,8 +141,8 @@ std::string Self::sniff(ipfs::SlashDelimited p, std::string const& body) const { ext.assign(file_name, dot + 1); } auto result = api_->MimeType(ext, body, fake_url); - LOG(INFO) << "Deduced mime from (ext=" << ext << " body of " << body.size() - << " bytes, 'url'=" << fake_url << ")=" << result; + VLOG(1) << "Deduced mime from (ext=" << ext << " body of " << body.size() + << " bytes, 'url'=" << fake_url << ")=" << result; return result; } diff --git a/library/src/ipfs_client/orchestrator_unittest.cc b/library/src/ipfs_client/orchestrator_unittest.cc index b8abb2b4..a0578c03 100644 --- a/library/src/ipfs_client/orchestrator_unittest.cc +++ b/library/src/ipfs_client/orchestrator_unittest.cc @@ -49,6 +49,9 @@ struct MockApi final : public ipfs::ContextApi { std::unique_ptr ParseJson(std::string_view) const { return {}; } + auto GetGateway(std::size_t index) const -> std::optional { + return std::nullopt; + } void Discover(std::function)> cb) {} struct DnsInvocation { std::string host; diff --git a/library/src/ipfs_client/pb_dag.cc b/library/src/ipfs_client/pb_dag.cc index 6f72bcf2..86cbb42d 100644 --- a/library/src/ipfs_client/pb_dag.cc +++ b/library/src/ipfs_client/pb_dag.cc @@ -139,7 +139,6 @@ std::string ipfs::PbDag::LinkCid(ipfs::ByteView binary_link_hash) const { bool ipfs::PbDag::cid_matches_data(ContextApi& api) const { if (!cid_) { - // TODO - probably remove those constructors and make cid_ not optional return true; } if (type() == Type::Invalid) { diff --git a/library/src/ipfs_client/pb_dag_unittest.cc b/library/src/ipfs_client/pb_dag_unittest.cc index 37e7090b..418e4c0f 100644 --- a/library/src/ipfs_client/pb_dag_unittest.cc +++ b/library/src/ipfs_client/pb_dag_unittest.cc @@ -41,7 +41,7 @@ TEST(BlockTest, IdentityBlockValidates) { EXPECT_TRUE(block.valid()); EXPECT_TRUE(block.is_file()); MockApi api; - // TODO: if this fails, awesome. Change the block_bytes to "Ipsum lorem" + // if this fails, awesome. Change the block_bytes to "Ipsum lorem" EXPECT_TRUE(block.cid_matches_data(api)); } TEST(BlockTest, DirectoryCopiedIsStillDirectory) { diff --git a/library/src/ipfs_client/test_context.cc b/library/src/ipfs_client/test_context.cc index 5b416235..56c885ea 100644 --- a/library/src/ipfs_client/test_context.cc +++ b/library/src/ipfs_client/test_context.cc @@ -10,7 +10,9 @@ #include "log_macros.h" -using Self = ipfs::AllInclusiveContext; +using Self = ipfs::TestContext; + +// LCOV_EXCL_START namespace { struct CallbackCallback { @@ -38,7 +40,10 @@ static void c_ares_c_callback(void* vp, } } -Self::AllInclusiveContext(boost::asio::io_context& io) : io_{io} { +Self::TestContext(boost::asio::io_context& io) + : gateways_{Gateways::DefaultGateways()}, io_{io} { + std::sort(gateways_.begin(), gateways_.end(), + [](auto& a, auto& b) { return a.prefix < b.prefix; }); if (ares_library_init(ARES_LIB_INIT_ALL)) { throw std::runtime_error("Failed to initialize c-ares library."); } @@ -46,7 +51,7 @@ Self::AllInclusiveContext(boost::asio::io_context& io) : io_{io} { throw std::runtime_error("Failed to initialize c-ares channel."); } } -Self::~AllInclusiveContext() { +Self::~TestContext() { pending_dns_.clear(); ares_destroy(ares_channel_); ares_library_cleanup(); @@ -117,7 +122,7 @@ class HttpSession : public std::enable_shared_from_this { int expiry_seconds_ = 91; std::string host_, port_, target_; ipfs::HttpRequestDescription desc_; - tcp::resolver::results_type resolution_; + static std::map resolutions_; std::string parsed_host_; boost::beast::http::response_parser response_parser_; @@ -125,9 +130,9 @@ class HttpSession : public std::enable_shared_from_this { res_; void fail(boost::beast::error_code ec, char const* what) { - GOOGLE_LOG(ERROR) << what << ": " << ec.value() << ' ' << ec.message() - << "\n URL:" << desc_.url << "\n HOST:" << host_ - << "\n PORT:" << port_ << "\n TARGET:" << target_; + GOOGLE_LOG(WARNING) << what << ": " << ec.value() << ' ' << ec.message() + << " URL:" << desc_.url << " HOST:" << host_ + << " PORT:" << port_ << " TARGET:" << target_; cb_(500, "", [](auto) { return std::string{}; }); } std::string parse_url() { @@ -169,7 +174,9 @@ class HttpSession : public std::enable_shared_from_this { response_parser_.body_limit(boost::none); } } - + tcp::resolver::results_type& resolution() { + return resolutions_[host_ + port_]; + } // Start the asynchronous operation void run() { auto parsed_host_ = parse_url(); @@ -192,26 +199,32 @@ class HttpSession : public std::enable_shared_from_this { // std::clog << "Setting Accept: " << desc_.accept << '\n'; req_.set("Accept", desc_.accept); } - extend_time(); - GOOGLE_LOG(DEBUG) << "Starting " << desc_.url - << " with a host resolution of " << host_ << ':' << port_; - // Look up the domain name - resolver_.async_resolve(host_, port_, - boost::beast::bind_front_handler( - &HttpSession::on_resolve, shared_from_this())); + if (resolution().empty()) { + GOOGLE_LOG(DEBUG) << "Starting " << desc_.url + << " with a host resolution of " << host_ << ':' + << port_; + extend_time(); + resolver_.async_resolve( + host_, port_, + boost::beast::bind_front_handler(&HttpSession::on_resolve, + shared_from_this())); + } else { + on_resolve({}, resolution()); + } } - void on_resolve(boost::beast::error_code ec, tcp::resolver::results_type results) { if (ec) return fail(ec, "resolve"); - resolution_ = results; + resolution() = results; + for (auto& ep : results) { + GOOGLE_LOG(DEBUG) << desc_.url << " Resolved " << host_ + << ", now connecting to " + << req_[boost::beast::http::field::host] << " aka " + << ep.host_name() << ':' << ep.service_name() << " for " + << target_; + } extend_time(); - GOOGLE_LOG(DEBUG) << desc_.url << " Resolved " << host_ - << ", now connecting to " - << req_[boost::beast::http::field::host] << " for " - << target_; - // Make the connection on the IP address we get from a lookup boost::beast::get_lowest_layer(stream_).async_connect( results, boost::beast::bind_front_handler(&HttpSession::on_connect, shared_from_this())); @@ -222,7 +235,7 @@ class HttpSession : public std::enable_shared_from_this { if (ec) return fail(ec, "connect"); extend_time(); - GOOGLE_LOG(TRACE) << desc_.url << " connected."; + GOOGLE_LOG(INFO) << desc_.url << " connected."; if (use_ssl()) { GOOGLE_LOG(DEBUG) << "Perform the SSL handshake because port=" << port_; stream_.async_handshake( @@ -239,8 +252,8 @@ class HttpSession : public std::enable_shared_from_this { } bool use_ssl() const { return port_ == "443" || port_ == "https"; } void extend_time() { - expiry_seconds_ += desc_.timeout_seconds; - GOOGLE_LOG(DEBUG) << "expiry_seconds_ = " << expiry_seconds_ << '\n'; + expiry_seconds_ += desc_.timeout_seconds + 1; + GOOGLE_LOG(TRACE) << "expiry_seconds_ = " << expiry_seconds_ << '\n'; boost::beast::get_lowest_layer(stream_).expires_after( std::chrono::seconds(expiry_seconds_)); } @@ -300,11 +313,12 @@ class HttpSession : public std::enable_shared_from_this { auto loc = (*res_)[boost::beast::http::field::location]; if (loc.size()) { desc_.url = loc; + close(); GOOGLE_LOG(WARNING) << "Redirecting to " << loc << " aka " << desc_.url; res_ = boost::beast::http::response{}; req_.set(boost::beast::http::field::host, parse_url()); req_.target(target_); - on_resolve({}, resolution_); + on_resolve({}, resolution()); return; } } @@ -317,32 +331,42 @@ class HttpSession : public std::enable_shared_from_this { << " response incorrect content type: " << content_type << " != " << desc_.accept; } + close(); + } + void close() { if (use_ssl()) { stream_.async_shutdown(boost::beast::bind_front_handler( &HttpSession::on_shutdown, shared_from_this())); } else { boost::beast::get_lowest_layer(stream_).close(); - if (ec && ec != boost::beast::errc::not_connected) - return fail(ec, "shutdown"); } } void on_shutdown(boost::beast::error_code ec) { - if (ec == boost::asio::error::eof) { - // Rationale: - // http://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error - ec = {}; + namespace E = boost::asio::error; + switch (ec.value()) { + case 0: + case 2: + case ENOTCONN: + return; + default: + return fail(ec, "shutdown"); } - if (ec) - return fail(ec, "shutdown"); - - // If we get here then the connection is closed gracefully } }; +std::map + HttpSession::resolutions_; + void Self::SendHttpRequest(HttpRequestDescription desc, HttpCompleteCallback cb) const { auto sess = std::make_shared(io_, ssl_ctx_, desc, cb); sess->run(); } +std::optional Self::GetGateway(std::size_t index) const { + if (index < gateways_.size()) { + return gateways_.at(index); + } + return std::nullopt; +} #endif diff --git a/library/src/libp2p/crypto/protobuf_key.hpp b/library/src/libp2p/crypto/protobuf_key.hpp deleted file mode 100644 index 459426f8..00000000 --- a/library/src/libp2p/crypto/protobuf_key.hpp +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright Soramitsu Co., Ltd. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef KAGOME_PROTOBUF_KEY_HPP -#define KAGOME_PROTOBUF_KEY_HPP - -#include -#include - -#include - -namespace libp2p::crypto { - /** - * Strict type for key, which is encoded into Protobuf format - */ - struct ProtobufKey : public boost::equality_comparable { - explicit ProtobufKey(std::vector key) : key{std::move(key)} {} - - std::vector key; - - bool operator==(const ProtobufKey &other) const { - return key == other.key; - } - }; -} // namespace libp2p::crypto - -#endif // KAGOME_PROTOBUF_KEY_HPP diff --git a/library/src/libp2p/multi/multibase_codec/codecs/base16.cc b/library/src/libp2p/multi/multibase_codec/codecs/base16.cc deleted file mode 100644 index b032cccb..00000000 --- a/library/src/libp2p/multi/multibase_codec/codecs/base16.cc +++ /dev/null @@ -1,104 +0,0 @@ -#include - -namespace b16 = ipfs::base16; - -namespace { -std::uint8_t to_i(char c); -template -char to_c(std::uint8_t n) { - if (n < 10) { - return n + '0'; - } else { - return n - 10 + a; - } -} -template -std::string encode(ipfs::ByteView bytes) { - std::string result; - result.reserve(bytes.size() * 2); - for (auto b : bytes) { - auto i = to_integer(b); - result.push_back(to_c(i >> 4)); - result.push_back(to_c(i & 0xF)); - } - return result; -} -} // namespace - -std::string b16::encodeLower(ByteView bytes) { - return encode<'a'>(bytes); -} -std::string b16::encodeUpper(ByteView bytes) { - return encode<'A'>(bytes); -} -auto b16::decode(std::string_view s) -> Decoded { - ByteArray result(s.size() / 2, ipfs::Byte{}); - for (auto i = 0U; i + 1U < s.size(); i += 2U) { - auto a = to_i(s[i]); - auto b = to_i(s[i + 1]); - if (a > 0xF || b > 0xF) { - return ipfs::unexpected{BaseError::INVALID_BASE16_INPUT}; - } - result[i / 2] = ipfs::Byte{static_cast((a << 4) | b)}; - } - if (s.size() % 2) { - auto a = to_i(s.back()); - if (a <= 0xF) { - result.push_back(ipfs::Byte{a}); - } - } - return result; -} - -namespace { -std::uint8_t to_i(char c) { - switch (c) { - case '0': - return 0; - case '1': - return 1; - case '2': - return 2; - case '3': - return 3; - case '4': - return 4; - case '5': - return 5; - case '6': - return 6; - case '7': - return 7; - case '8': - return 8; - case '9': - return 9; - case 'a': - return 10; - case 'b': - return 11; - case 'c': - return 12; - case 'd': - return 13; - case 'e': - return 14; - case 'f': - return 15; - case 'A': - return 10; - case 'B': - return 11; - case 'C': - return 12; - case 'D': - return 13; - case 'E': - return 14; - case 'F': - return 15; - default: - return 0xFF; - } -} -} // namespace diff --git a/library/src/libp2p/multi/multibase_codec/codecs/base16_unittest.cc b/library/src/libp2p/multi/multibase_codec/codecs/base16_unittest.cc deleted file mode 100644 index e43b4091..00000000 --- a/library/src/libp2p/multi/multibase_codec/codecs/base16_unittest.cc +++ /dev/null @@ -1,21 +0,0 @@ -#include -#include - -namespace b16 = ipfs::base16; - -TEST(Base16Test, EncodeLower) { - auto xyz = reinterpret_cast("xyz"); - auto actual = b16::encodeLower({xyz, 3}); - EXPECT_EQ(actual, "78797a"); -} -TEST(Base16Test, DecodeUpper) { - std::string s = "78797A"; - auto expected = reinterpret_cast("xyz"); - auto result = b16::decode(s); - EXPECT_TRUE(result.has_value()); - auto actual = result.value(); - EXPECT_EQ(3, actual.size()); - for (auto i : {0, 1, 2}) { - EXPECT_EQ((short)(actual.at(i)), (short)(expected[i])); - } -} diff --git a/library/src/libp2p/multi/multibase_codec/codecs/base32.cc b/library/src/libp2p/multi/multibase_codec/codecs/base32.cc deleted file mode 100644 index 36030d0b..00000000 --- a/library/src/libp2p/multi/multibase_codec/codecs/base32.cc +++ /dev/null @@ -1,200 +0,0 @@ -/** - * Copyright Soramitsu Co., Ltd. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * base32 (de)coder implementation as specified by RFC4648. - * - * Copyright (c) 2010 Adrien Kunysz - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - **/ - -#include "libp2p/multi/multibase_codec/codecs/base32.hpp" -#include "libp2p/multi/multibase_codec/codecs/base_error.hpp" - -#include - -namespace libp2p::multi::detail { -const std::string kUpperBase32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; -const std::string kLowerBase32Alphabet = "abcdefghijklmnopqrstuvwxyz234567"; - -enum Base32Mode { - LOWER, - UPPER, -}; - -int get_byte(int block) { - return block * 5 / 8; -} - -int get_bit(int block) { - return 8 - 5 - block * 5 % 8; -} - -char encode_char(unsigned char c, Base32Mode mode) { - if (mode == Base32Mode::UPPER) { - return kUpperBase32Alphabet[c & 0x1F]; // 0001 1111 - } - return kLowerBase32Alphabet[c & 0x1F]; -} - -unsigned char shift_right(uint8_t byte, int8_t offset) { - if (offset > 0) { - return byte >> offset; - } - - return byte << -offset; -} - -unsigned char shift_left(uint8_t byte, int8_t offset) { - return shift_right(byte, -offset); -} - -int encode_sequence(ipfs::span plain, - ipfs::span coded, - Base32Mode mode) { - for (int block = 0; block < 8; block++) { - int byte = get_byte(block); - int bit = get_bit(block); - - if (byte >= static_cast(plain.size())) { - return block; - } - - unsigned char c = shift_right(plain[byte], bit); - - if (bit < 0 && byte < static_cast(plain.size()) - 1L) { - c |= shift_right(plain[byte + 1], 8 + bit); - } - coded[block] = encode_char(c, mode); - } - return 8; -} - -std::string encodeBase32(ipfs::ByteView bytes, Base32Mode mode) { - std::string result; - if (bytes.size() % 5 == 0) { - result = std::string(bytes.size() / 5 * 8, ' '); - } else { - result = std::string((bytes.size() / 5 + 1) * 8, ' '); - } - - for (size_t i = 0, j = 0; i < bytes.size(); i += 5, j += 8) { - int n = encode_sequence( - ipfs::span(reinterpret_cast(&bytes[i]), - std::min(bytes.size() - i, 5)), - ipfs::span(&result[j], 8U), mode); - if (n < 8) { - result.erase(result.end() - (8 - n), result.end()); - } - } - - return result; -} - -std::string encodeBase32Upper(ipfs::ByteView bytes) { - return encodeBase32(bytes, Base32Mode::UPPER); -} - -std::string encodeBase32Lower(ipfs::ByteView bytes) { - return encodeBase32(bytes, Base32Mode::LOWER); -} - -int decode_char(unsigned char c, Base32Mode mode) { - char decoded_ch = -1; - - if (mode == Base32Mode::UPPER) { - if (c >= 'A' && c <= 'Z') { - decoded_ch = c - 'A'; // NOLINT - } - } else { - if (c >= 'a' && c <= 'z') { - decoded_ch = c - 'a'; // NOLINT - } - } - if (c >= '2' && c <= '7') { - decoded_ch = c - '2' + 26; // NOLINT - } - - return decoded_ch; -} - -ipfs::expected decode_sequence(ipfs::span coded, - ipfs::span plain, - Base32Mode mode) { - plain[0] = 0; - for (int block = 0; block < 8; block++) { - int bit = get_bit(block); - int byte = get_byte(block); - - if (block >= static_cast(coded.size())) { - return byte; - } - int c = decode_char(coded[block], mode); - if (c < 0) { - // return absl::InvalidArgumentError("INVALID_BASE32_INPUT"); - return ipfs::unexpected{BaseError::INVALID_BASE32_INPUT}; - } - - plain[byte] |= shift_left(c, bit); - if (bit < 0) { - plain[byte + 1] = shift_left(c, 8 + bit); - } - } - return 5; -} - -ipfs::expected decodeBase32( - std::string_view string, - Base32Mode mode) { - common::ByteArray result; - if (string.size() % 8 == 0) { - result = common::ByteArray(string.size() / 8 * 5, ipfs::Byte{0}); - } else { - result = common::ByteArray((string.size() / 8 + 1) * 5, ipfs::Byte{0}); - } - - for (size_t i = 0, j = 0; i < string.size(); i += 8, j += 5) { - auto n = decode_sequence( - ipfs::span(&string[i], - std::min(string.size() - i, 8)), - ipfs::span(reinterpret_cast(&result[j]), 5U), mode); - if (!n.has_value()) { - return ipfs::unexpected{n.error()}; - } - if (n.value() < 5) { - result.erase(result.end() - (5 - n.value()), result.end()); - } - } - return result; -} - -ipfs::expected decodeBase32Upper( - std::string_view string) { - return decodeBase32(string, Base32Mode::UPPER); -} - -ipfs::expected decodeBase32Lower( - std::string_view string) { - return decodeBase32(string, Base32Mode::LOWER); -} - -} // namespace libp2p::multi::detail diff --git a/library/src/libp2p/multi/multibase_codec/codecs/base36.cc b/library/src/libp2p/multi/multibase_codec/codecs/base36.cc deleted file mode 100644 index 2f508979..00000000 --- a/library/src/libp2p/multi/multibase_codec/codecs/base36.cc +++ /dev/null @@ -1,58 +0,0 @@ -#include - -#include - -#include - -#include - -namespace det = libp2p::multi::detail; - -namespace { -constexpr double kLengthRatio = 0.646240625; // log(36)/log(256) - -std::int_least16_t digit_value(char digit) { - if (digit < '0') { - return -1; - } else if (digit <= '9') { - return digit - '0'; - } else if (digit < 'A') { - return -2; - } else if (digit <= 'Z') { - return (digit - 'A') + 10; - } else if (digit < 'a') { - return -3; - } else if (digit <= 'z') { - return (digit - 'a') + 10; - } else { - return -4; - } -} -int operator*(int a, ipfs::Byte b) { - return a * static_cast(b); -} -} // namespace - -std::string det::encodeBase36Lower(ipfs::ByteView) { - std::abort(); -} - -auto det::decodeBase36(std::string_view str_b36) - -> ipfs::expected { - common::ByteArray out; - out.resize(std::ceil(static_cast(str_b36.size()) * kLengthRatio), - ipfs::Byte{}); - for (auto digit : str_b36) { // chunk) { - int val = digit_value(digit); - if (val < 0) { - return ipfs::unexpected{BaseError::INVALID_BASE36_INPUT}; - } - auto mod_byte = [&val](auto& b) { - val += 36 * b; - b = static_cast(val & 0xFF); - val >>= 8; - }; - std::for_each(out.rbegin(), out.rend(), mod_byte); - } - return out; -} diff --git a/library/src/libp2p/multi/multibase_codec/codecs/base36_unittest.cc b/library/src/libp2p/multi/multibase_codec/codecs/base36_unittest.cc deleted file mode 100644 index c339f2d0..00000000 --- a/library/src/libp2p/multi/multibase_codec/codecs/base36_unittest.cc +++ /dev/null @@ -1,28 +0,0 @@ -#include -#include - -#include - -TEST(Base36Tests, KnownConversionFrom32) { - /* - std::array expected{ - 0x01, 0x72, 0x00, 0x24, 0x08, 0x01, 0x12, 0x20, 0x5f, 0x0a, - 0x3b, 0x0d, 0x2c, 0xe9, 0x87, 0x90, 0x97, 0x43, 0xb3, 0xc1, - 0x52, 0xd0, 0x80, 0x82, 0x73, 0xc0, 0x34, 0xbd, 0x2d, 0x63, - 0xfc, 0x85, 0x08, 0xfb, 0x82, 0xe2, 0x1a, 0x4d, 0x53, 0xe2}; - */ - auto result = libp2p::multi::detail::decodeBase32Lower( - "afzaajaiaejcaxykhmgsz2mhscluhm6bkliibattya2l2lld7scqr64c4ine2u7c"); - EXPECT_TRUE(result.has_value()); - auto expected = result.value(); - result = libp2p::multi::detail::decodeBase36( - "51qzi5uqu5dijv526o4z2z10ejylnel0bfvrtw53itcmsecffo8yf0zb4g9gi"); - EXPECT_TRUE(result.has_value()); - auto actual = result.value(); - EXPECT_EQ(expected.size(), actual.size()); - for (auto i = 0U; i < actual.size() && i < expected.size(); ++i) { - ASSERT_EQ(static_cast(expected[i]), - static_cast(actual[i])) - << " @ " << i; - } -} diff --git a/library/src/libp2p/multi/muticodec_type_unittest.cc b/library/src/libp2p/multi/muticodec_type_unittest.cc deleted file mode 100644 index c03ce58d..00000000 --- a/library/src/libp2p/multi/muticodec_type_unittest.cc +++ /dev/null @@ -1,42 +0,0 @@ -#include - -#include - -using T = libp2p::multi::MulticodecType; -using C = T::Code; - -TEST(MultiCodecType, Codes) { - EXPECT_EQ(static_cast(C::IDENTITY), 0x00); - EXPECT_EQ(static_cast(C::SHA1), 0x11); - EXPECT_EQ(static_cast(C::SHA2_256), 0x12); - EXPECT_EQ(static_cast(C::SHA2_512), 0x13); - EXPECT_EQ(static_cast(C::SHA3_512), 0x14); - EXPECT_EQ(static_cast(C::SHA3_384), 0x15); - EXPECT_EQ(static_cast(C::SHA3_256), 0x16); - EXPECT_EQ(static_cast(C::SHA3_224), 0x17); - EXPECT_EQ(static_cast(C::RAW), 0x55); - EXPECT_EQ(static_cast(C::DAG_PB), 0x70); - EXPECT_EQ(static_cast(C::DAG_CBOR), 0x71); - EXPECT_EQ(static_cast(C::LIBP2P_KEY), 0x72); - EXPECT_EQ(static_cast(C::DAG_JSON), 0x0129); - EXPECT_EQ(static_cast(C::FILECOIN_COMMITMENT_UNSEALED), 0xf101); - EXPECT_EQ(static_cast(C::FILECOIN_COMMITMENT_SEALED), 0xf102); -} -TEST(MultiCodecType, Names) { - EXPECT_EQ(T::getName(C::IDENTITY), "identity"); - EXPECT_EQ(T::getName(C::SHA1), "sha1"); - EXPECT_EQ(T::getName(C::SHA2_256), "sha2-256"); - EXPECT_EQ(T::getName(C::SHA2_512), "sha2-512"); - EXPECT_EQ(T::getName(C::SHA3_512), "sha3-512"); - EXPECT_EQ(T::getName(C::SHA3_384), "sha3-384"); - EXPECT_EQ(T::getName(C::SHA3_256), "sha3-256"); - EXPECT_EQ(T::getName(C::SHA3_224), "sha3-224"); - EXPECT_EQ(T::getName(C::RAW), "raw"); - EXPECT_EQ(T::getName(C::DAG_PB), "dag-pb"); - EXPECT_EQ(T::getName(C::DAG_CBOR), "dag-cbor"); - EXPECT_EQ(T::getName(C::LIBP2P_KEY), "libp2p-key"); - EXPECT_EQ(T::getName(C::DAG_JSON), "dag-json"); - EXPECT_EQ(T::getName(C::FILECOIN_COMMITMENT_UNSEALED), - "fil-commitment-unsealed"); - EXPECT_EQ(T::getName(C::FILECOIN_COMMITMENT_SEALED), "fil-commitment-sealed"); -} \ No newline at end of file diff --git a/library/src/vocab/html_escape_unittest.cc b/library/src/vocab/html_escape_unittest.cc new file mode 100644 index 00000000..151fb60c --- /dev/null +++ b/library/src/vocab/html_escape_unittest.cc @@ -0,0 +1,16 @@ +#include + +#include + +using namespace std::literals; + +TEST(HTMLEscapeTest, CommonForbiddenCharacters) { + std::string actual; + for (auto c : "&"sv) { + actual.append(html_escape(c)); + } + auto expected = + "<a href='favicon.ico' " + "class="icon">&</a>"; + EXPECT_EQ(expected, actual); +} diff --git a/test_data/blocks/QmNcvPciWi2srZusBPH6nzrmYenpQKaBS1jL6PXTA1yN6w b/test_data/blocks/QmNcvPciWi2srZusBPH6nzrmYenpQKaBS1jL6PXTA1yN6w deleted file mode 100644 index 86a28536..00000000 --- a/test_data/blocks/QmNcvPciWi2srZusBPH6nzrmYenpQKaBS1jL6PXTA1yN6w +++ /dev/null @@ -1,2 +0,0 @@ - -˜Bu8 Bu9 Bv0 Bv1 Bv2 Bv3 Bv4 Bv5 Bv6 Bv7 Bv8 Bv9 Bw0 Bw1 Bw2 Bw3 Bw4 Bw5 Bw6 Bw7 Bw8 Bw9 Bx0 Bx1 Bx2 Bx3 Bx4 Bx5 Bx6 Bx7 Bx8 Bx9 By0 By1 By2 By3  \ No newline at end of file diff --git a/test_data/blocks/QmPLxynXoBvGDfRbQx1PSLufk3X5juVqm8KwxeMbUG1N6J b/test_data/blocks/QmPLxynXoBvGDfRbQx1PSLufk3X5juVqm8KwxeMbUG1N6J deleted file mode 100644 index 7e547e66..00000000 --- a/test_data/blocks/QmPLxynXoBvGDfRbQx1PSLufk3X5juVqm8KwxeMbUG1N6J +++ /dev/null @@ -1,2 +0,0 @@ - -˜Gg0 Gg1 Gg2 Gg3 Gg4 Gg5 Gg6 Gg7 Gg8 Gg9 Gh0 Gh1 Gh2 Gh3 Gh4 Gh5 Gh6 Gh7 Gh8 Gh9 Gi0 Gi1 Gi2 Gi3 Gi4 Gi5 Gi6 Gi7 Gi8 Gi9 Gj0 Gj1 Gj2 Gj3 Gj4 Gj5  \ No newline at end of file diff --git a/test_data/blocks/QmPRW81waexXEK7FxVyfePLH5fvb2b4M6xaRHMR3yM7VeZ b/test_data/blocks/QmPRW81waexXEK7FxVyfePLH5fvb2b4M6xaRHMR3yM7VeZ deleted file mode 100644 index 58a84201..00000000 --- a/test_data/blocks/QmPRW81waexXEK7FxVyfePLH5fvb2b4M6xaRHMR3yM7VeZ +++ /dev/null @@ -1,2 +0,0 @@ - -˜Fr6 Fr7 Fr8 Fr9 Fs0 Fs1 Fs2 Fs3 Fs4 Fs5 Fs6 Fs7 Fs8 Fs9 Ft0 Ft1 Ft2 Ft3 Ft4 Ft5 Ft6 Ft7 Ft8 Ft9 Fu0 Fu1 Fu2 Fu3 Fu4 Fu5 Fu6 Fu7 Fu8 Fu9 Fv0 Fv1  \ No newline at end of file diff --git a/test_data/blocks/QmPRqSSVfKSP6b2gb3HQM4yiZ3netkpGUp8zmto5dTyg8R b/test_data/blocks/QmPRqSSVfKSP6b2gb3HQM4yiZ3netkpGUp8zmto5dTyg8R deleted file mode 100644 index 6524964f..00000000 --- a/test_data/blocks/QmPRqSSVfKSP6b2gb3HQM4yiZ3netkpGUp8zmto5dTyg8R +++ /dev/null @@ -1,2 +0,0 @@ - -˜Kk0 Kk1 Kk2 Kk3 Kk4 Kk5 Kk6 Kk7 Kk8 Kk9 Kl0 Kl1 Kl2 Kl3 Kl4 Kl5 Kl6 Kl7 Kl8 Kl9 Km0 Km1 Km2 Km3 Km4 Km5 Km6 Km7 Km8 Km9 Kn0 Kn1 Kn2 Kn3 Kn4 Kn5  \ No newline at end of file diff --git a/test_data/blocks/QmPdoYcjn5pa8qWQfBNH58NomjePyFSZDsFhAV895u5Bdq b/test_data/blocks/QmPdoYcjn5pa8qWQfBNH58NomjePyFSZDsFhAV895u5Bdq deleted file mode 100644 index 49454552..00000000 --- a/test_data/blocks/QmPdoYcjn5pa8qWQfBNH58NomjePyFSZDsFhAV895u5Bdq +++ /dev/null @@ -1,2 +0,0 @@ - -˜Kn6 Kn7 Kn8 Kn9 Ko0 Ko1 Ko2 Ko3 Ko4 Ko5 Ko6 Ko7 Ko8 Ko9 Kp0 Kp1 Kp2 Kp3 Kp4 Kp5 Kp6 Kp7 Kp8 Kp9 Kq0 Kq1 Kq2 Kq3 Kq4 Kq5 Kq6 Kq7 Kq8 Kq9 Kr0 Kr1  \ No newline at end of file diff --git a/test_data/blocks/QmPe8ymecEzgNiNbcmecv9qvDwmiUms2yH3JsjEh4GSoda b/test_data/blocks/QmPe8ymecEzgNiNbcmecv9qvDwmiUms2yH3JsjEh4GSoda deleted file mode 100644 index e2bdd820..00000000 --- a/test_data/blocks/QmPe8ymecEzgNiNbcmecv9qvDwmiUms2yH3JsjEh4GSoda +++ /dev/null @@ -1,2 +0,0 @@ - -˜Zf2 Zf3 Zf4 Zf5 Zf6 Zf7 Zf8 Zf9 Zg0 Zg1 Zg2 Zg3 Zg4 Zg5 Zg6 Zg7 Zg8 Zg9 Zh0 Zh1 Zh2 Zh3 Zh4 Zh5 Zh6 Zh7 Zh8 Zh9 Zi0 Zi1 Zi2 Zi3 Zi4 Zi5 Zi6 Zi7  \ No newline at end of file diff --git a/test_data/blocks/QmPeixRgsHY1xgpQ3DxeTRrrF5EsNnuRbCkhP4A1XX7iLr b/test_data/blocks/QmPeixRgsHY1xgpQ3DxeTRrrF5EsNnuRbCkhP4A1XX7iLr deleted file mode 100644 index 1dbd93b2..00000000 --- a/test_data/blocks/QmPeixRgsHY1xgpQ3DxeTRrrF5EsNnuRbCkhP4A1XX7iLr +++ /dev/null @@ -1,2 +0,0 @@ - -˜Fg8 Fg9 Fh0 Fh1 Fh2 Fh3 Fh4 Fh5 Fh6 Fh7 Fh8 Fh9 Fi0 Fi1 Fi2 Fi3 Fi4 Fi5 Fi6 Fi7 Fi8 Fi9 Fj0 Fj1 Fj2 Fj3 Fj4 Fj5 Fj6 Fj7 Fj8 Fj9 Fk0 Fk1 Fk2 Fk3  \ No newline at end of file diff --git a/test_data/blocks/QmPepid8j9yeQo9ibqcL6meRmS6QaR9b713xzSbUTYD3DW b/test_data/blocks/QmPepid8j9yeQo9ibqcL6meRmS6QaR9b713xzSbUTYD3DW deleted file mode 100644 index 6ec43e23..00000000 --- a/test_data/blocks/QmPepid8j9yeQo9ibqcL6meRmS6QaR9b713xzSbUTYD3DW +++ /dev/null @@ -1,2 +0,0 @@ - -˜Yj6 Yj7 Yj8 Yj9 Yk0 Yk1 Yk2 Yk3 Yk4 Yk5 Yk6 Yk7 Yk8 Yk9 Yl0 Yl1 Yl2 Yl3 Yl4 Yl5 Yl6 Yl7 Yl8 Yl9 Ym0 Ym1 Ym2 Ym3 Ym4 Ym5 Ym6 Ym7 Ym8 Ym9 Yn0 Yn1  \ No newline at end of file diff --git a/test_data/blocks/QmPqqzX9DBztn4Y8LNWfiKSJZQdgsjajZxwoGGrwo8FyWT b/test_data/blocks/QmPqqzX9DBztn4Y8LNWfiKSJZQdgsjajZxwoGGrwo8FyWT deleted file mode 100644 index 829b66c8..00000000 --- a/test_data/blocks/QmPqqzX9DBztn4Y8LNWfiKSJZQdgsjajZxwoGGrwo8FyWT +++ /dev/null @@ -1,2 +0,0 @@ - -˜Tu8 Tu9 Tv0 Tv1 Tv2 Tv3 Tv4 Tv5 Tv6 Tv7 Tv8 Tv9 Tw0 Tw1 Tw2 Tw3 Tw4 Tw5 Tw6 Tw7 Tw8 Tw9 Tx0 Tx1 Tx2 Tx3 Tx4 Tx5 Tx6 Tx7 Tx8 Tx9 Ty0 Ty1 Ty2 Ty3  \ No newline at end of file diff --git a/test_data/blocks/QmQ8nFioaJbQLCoz7jW4LKiVPwDHLLPMo7sKYUfbxgBTEJ b/test_data/blocks/QmQ8nFioaJbQLCoz7jW4LKiVPwDHLLPMo7sKYUfbxgBTEJ deleted file mode 100644 index d380e106..00000000 --- a/test_data/blocks/QmQ8nFioaJbQLCoz7jW4LKiVPwDHLLPMo7sKYUfbxgBTEJ +++ /dev/null @@ -1,2 +0,0 @@ - -˜Ee0 Ee1 Ee2 Ee3 Ee4 Ee5 Ee6 Ee7 Ee8 Ee9 Ef0 Ef1 Ef2 Ef3 Ef4 Ef5 Ef6 Ef7 Ef8 Ef9 Eg0 Eg1 Eg2 Eg3 Eg4 Eg5 Eg6 Eg7 Eg8 Eg9 Eh0 Eh1 Eh2 Eh3 Eh4 Eh5  \ No newline at end of file diff --git a/test_data/blocks/QmQLZCYjU3S8D9fnoSKdZa16iayHqBRN2Rasc6kZbp1z4i b/test_data/blocks/QmQLZCYjU3S8D9fnoSKdZa16iayHqBRN2Rasc6kZbp1z4i deleted file mode 100644 index 1022b688..00000000 --- a/test_data/blocks/QmQLZCYjU3S8D9fnoSKdZa16iayHqBRN2Rasc6kZbp1z4i +++ /dev/null @@ -1,2 +0,0 @@ - -˜Di4 Di5 Di6 Di7 Di8 Di9 Dj0 Dj1 Dj2 Dj3 Dj4 Dj5 Dj6 Dj7 Dj8 Dj9 Dk0 Dk1 Dk2 Dk3 Dk4 Dk5 Dk6 Dk7 Dk8 Dk9 Dl0 Dl1 Dl2 Dl3 Dl4 Dl5 Dl6 Dl7 Dl8 Dl9  \ No newline at end of file diff --git a/test_data/blocks/QmQTLkAPTfKD686jh6eoECRmB2jYDaNAWXYGHzbvfFiUKy b/test_data/blocks/QmQTLkAPTfKD686jh6eoECRmB2jYDaNAWXYGHzbvfFiUKy deleted file mode 100644 index bf9cf374..00000000 --- a/test_data/blocks/QmQTLkAPTfKD686jh6eoECRmB2jYDaNAWXYGHzbvfFiUKy +++ /dev/null @@ -1,2 +0,0 @@ - -˜Hf2 Hf3 Hf4 Hf5 Hf6 Hf7 Hf8 Hf9 Hg0 Hg1 Hg2 Hg3 Hg4 Hg5 Hg6 Hg7 Hg8 Hg9 Hh0 Hh1 Hh2 Hh3 Hh4 Hh5 Hh6 Hh7 Hh8 Hh9 Hi0 Hi1 Hi2 Hi3 Hi4 Hi5 Hi6 Hi7  \ No newline at end of file diff --git a/test_data/blocks/QmQbNeD31QypqpeAaRZmtYALTEuRMmVvB2V1TKAdczdMjE b/test_data/blocks/QmQbNeD31QypqpeAaRZmtYALTEuRMmVvB2V1TKAdczdMjE deleted file mode 100644 index 23a4749e..00000000 --- a/test_data/blocks/QmQbNeD31QypqpeAaRZmtYALTEuRMmVvB2V1TKAdczdMjE +++ /dev/null @@ -1,2 +0,0 @@ - -˜Gn2 Gn3 Gn4 Gn5 Gn6 Gn7 Gn8 Gn9 Go0 Go1 Go2 Go3 Go4 Go5 Go6 Go7 Go8 Go9 Gp0 Gp1 Gp2 Gp3 Gp4 Gp5 Gp6 Gp7 Gp8 Gp9 Gq0 Gq1 Gq2 Gq3 Gq4 Gq5 Gq6 Gq7  \ No newline at end of file diff --git a/test_data/blocks/QmQbdyMqFsgWdjrU6Q7e9RqCopd8uwdCfJ6Vg4ZGXCEPVG b/test_data/blocks/QmQbdyMqFsgWdjrU6Q7e9RqCopd8uwdCfJ6Vg4ZGXCEPVG deleted file mode 100644 index 0e6ec23e..00000000 --- a/test_data/blocks/QmQbdyMqFsgWdjrU6Q7e9RqCopd8uwdCfJ6Vg4ZGXCEPVG +++ /dev/null @@ -1,2 +0,0 @@ - -˜Wh6 Wh7 Wh8 Wh9 Wi0 Wi1 Wi2 Wi3 Wi4 Wi5 Wi6 Wi7 Wi8 Wi9 Wj0 Wj1 Wj2 Wj3 Wj4 Wj5 Wj6 Wj7 Wj8 Wj9 Wk0 Wk1 Wk2 Wk3 Wk4 Wk5 Wk6 Wk7 Wk8 Wk9 Wl0 Wl1  \ No newline at end of file diff --git a/test_data/blocks/QmQfvc1RaX4f3AMWrMwBJ4fHvBzQLAkpe7jA7scuzAGPgm b/test_data/blocks/QmQfvc1RaX4f3AMWrMwBJ4fHvBzQLAkpe7jA7scuzAGPgm deleted file mode 100644 index daf82e8c..00000000 --- a/test_data/blocks/QmQfvc1RaX4f3AMWrMwBJ4fHvBzQLAkpe7jA7scuzAGPgm +++ /dev/null @@ -1,2 +0,0 @@ - -˜Bg4 Bg5 Bg6 Bg7 Bg8 Bg9 Bh0 Bh1 Bh2 Bh3 Bh4 Bh5 Bh6 Bh7 Bh8 Bh9 Bi0 Bi1 Bi2 Bi3 Bi4 Bi5 Bi6 Bi7 Bi8 Bi9 Bj0 Bj1 Bj2 Bj3 Bj4 Bj5 Bj6 Bj7 Bj8 Bj9  \ No newline at end of file diff --git a/test_data/blocks/QmQfyrjYXauvrg5pg4FdBWQmGVQojGHkBLosWqCrEtfL6a b/test_data/blocks/QmQfyrjYXauvrg5pg4FdBWQmGVQojGHkBLosWqCrEtfL6a deleted file mode 100644 index fa5892ca..00000000 --- a/test_data/blocks/QmQfyrjYXauvrg5pg4FdBWQmGVQojGHkBLosWqCrEtfL6a +++ /dev/null @@ -1,2 +0,0 @@ - -˜El2 El3 El4 El5 El6 El7 El8 El9 Em0 Em1 Em2 Em3 Em4 Em5 Em6 Em7 Em8 Em9 En0 En1 En2 En3 En4 En5 En6 En7 En8 En9 Eo0 Eo1 Eo2 Eo3 Eo4 Eo5 Eo6 Eo7  \ No newline at end of file diff --git a/test_data/blocks/QmQhrF1PnZ76U9FFmBBWNdxL3DC8L2hLt89DxoDJiMwzyq b/test_data/blocks/QmQhrF1PnZ76U9FFmBBWNdxL3DC8L2hLt89DxoDJiMwzyq deleted file mode 100644 index 33d04f84..00000000 --- a/test_data/blocks/QmQhrF1PnZ76U9FFmBBWNdxL3DC8L2hLt89DxoDJiMwzyq +++ /dev/null @@ -1,2 +0,0 @@ - -˜Gj6 Gj7 Gj8 Gj9 Gk0 Gk1 Gk2 Gk3 Gk4 Gk5 Gk6 Gk7 Gk8 Gk9 Gl0 Gl1 Gl2 Gl3 Gl4 Gl5 Gl6 Gl7 Gl8 Gl9 Gm0 Gm1 Gm2 Gm3 Gm4 Gm5 Gm6 Gm7 Gm8 Gm9 Gn0 Gn1  \ No newline at end of file diff --git a/test_data/blocks/QmQijzkbe5yP8NM1WUvPq5LRToMuvZECSrW7QtVdrTeWZD b/test_data/blocks/QmQijzkbe5yP8NM1WUvPq5LRToMuvZECSrW7QtVdrTeWZD deleted file mode 100644 index f9e25e15..00000000 --- a/test_data/blocks/QmQijzkbe5yP8NM1WUvPq5LRToMuvZECSrW7QtVdrTeWZD +++ /dev/null @@ -1,2 +0,0 @@ - -˜Mp6 Mp7 Mp8 Mp9 Mq0 Mq1 Mq2 Mq3 Mq4 Mq5 Mq6 Mq7 Mq8 Mq9 Mr0 Mr1 Mr2 Mr3 Mr4 Mr5 Mr6 Mr7 Mr8 Mr9 Ms0 Ms1 Ms2 Ms3 Ms4 Ms5 Ms6 Ms7 Ms8 Ms9 Mt0 Mt1  \ No newline at end of file diff --git a/test_data/blocks/QmQj5ARTnu85pJ26Mxmhhiddvkkg7cQDZc6wszYMWetkcL b/test_data/blocks/QmQj5ARTnu85pJ26Mxmhhiddvkkg7cQDZc6wszYMWetkcL deleted file mode 100644 index 80ea606a..00000000 --- a/test_data/blocks/QmQj5ARTnu85pJ26Mxmhhiddvkkg7cQDZc6wszYMWetkcL +++ /dev/null @@ -1,2 +0,0 @@ - -˜Wa4 Wa5 Wa6 Wa7 Wa8 Wa9 Wb0 Wb1 Wb2 Wb3 Wb4 Wb5 Wb6 Wb7 Wb8 Wb9 Wc0 Wc1 Wc2 Wc3 Wc4 Wc5 Wc6 Wc7 Wc8 Wc9 Wd0 Wd1 Wd2 Wd3 Wd4 Wd5 Wd6 Wd7 Wd8 Wd9  \ No newline at end of file diff --git a/test_data/blocks/QmQx24y7nKXUYFX2NLWUUTw5oK2zCAgwNjyq6nSUzin76J b/test_data/blocks/QmQx24y7nKXUYFX2NLWUUTw5oK2zCAgwNjyq6nSUzin76J deleted file mode 100644 index 71f84a78..00000000 --- a/test_data/blocks/QmQx24y7nKXUYFX2NLWUUTw5oK2zCAgwNjyq6nSUzin76J +++ /dev/null @@ -1,2 +0,0 @@ - -˜Lc0 Lc1 Lc2 Lc3 Lc4 Lc5 Lc6 Lc7 Lc8 Lc9 Ld0 Ld1 Ld2 Ld3 Ld4 Ld5 Ld6 Ld7 Ld8 Ld9 Le0 Le1 Le2 Le3 Le4 Le5 Le6 Le7 Le8 Le9 Lf0 Lf1 Lf2 Lf3 Lf4 Lf5  \ No newline at end of file diff --git a/test_data/blocks/QmR1u7SZtyYU22pANU4WivHyqWShVMPeigJd7aeEgffEvV b/test_data/blocks/QmR1u7SZtyYU22pANU4WivHyqWShVMPeigJd7aeEgffEvV deleted file mode 100644 index ef39ec4f..00000000 --- a/test_data/blocks/QmR1u7SZtyYU22pANU4WivHyqWShVMPeigJd7aeEgffEvV +++ /dev/null @@ -1,2 +0,0 @@ - -˜Vp6 Vp7 Vp8 Vp9 Vq0 Vq1 Vq2 Vq3 Vq4 Vq5 Vq6 Vq7 Vq8 Vq9 Vr0 Vr1 Vr2 Vr3 Vr4 Vr5 Vr6 Vr7 Vr8 Vr9 Vs0 Vs1 Vs2 Vs3 Vs4 Vs5 Vs6 Vs7 Vs8 Vs9 Vt0 Vt1  \ No newline at end of file diff --git a/test_data/blocks/QmR3gXz9KWc3gioJECyKmC5y89a1AN27ZPzC8MSa9HGb8h b/test_data/blocks/QmR3gXz9KWc3gioJECyKmC5y89a1AN27ZPzC8MSa9HGb8h deleted file mode 100644 index e6726734..00000000 --- a/test_data/blocks/QmR3gXz9KWc3gioJECyKmC5y89a1AN27ZPzC8MSa9HGb8h +++ /dev/null @@ -1,2 +0,0 @@ - -˜Uj2 Uj3 Uj4 Uj5 Uj6 Uj7 Uj8 Uj9 Uk0 Uk1 Uk2 Uk3 Uk4 Uk5 Uk6 Uk7 Uk8 Uk9 Ul0 Ul1 Ul2 Ul3 Ul4 Ul5 Ul6 Ul7 Ul8 Ul9 Um0 Um1 Um2 Um3 Um4 Um5 Um6 Um7  \ No newline at end of file diff --git a/test_data/blocks/QmR4SptPpdiA5yfWwdkFxFWGZN4pHAx2qGE1qzAkL1ZK2P b/test_data/blocks/QmR4SptPpdiA5yfWwdkFxFWGZN4pHAx2qGE1qzAkL1ZK2P deleted file mode 100644 index 2d09b0ae..00000000 --- a/test_data/blocks/QmR4SptPpdiA5yfWwdkFxFWGZN4pHAx2qGE1qzAkL1ZK2P +++ /dev/null @@ -1,2 +0,0 @@ - -˜Pc4 Pc5 Pc6 Pc7 Pc8 Pc9 Pd0 Pd1 Pd2 Pd3 Pd4 Pd5 Pd6 Pd7 Pd8 Pd9 Pe0 Pe1 Pe2 Pe3 Pe4 Pe5 Pe6 Pe7 Pe8 Pe9 Pf0 Pf1 Pf2 Pf3 Pf4 Pf5 Pf6 Pf7 Pf8 Pf9  \ No newline at end of file diff --git a/test_data/blocks/bafkreidzbjhm6fubixfarfyd4ovykuhm3ghbbpbywknw7wljtvvipcwb7e b/test_data/blocks/bafkreidzbjhm6fubixfarfyd4ovykuhm3ghbbpbywknw7wljtvvipcwb7e new file mode 100644 index 0000000000000000000000000000000000000000..84ac48771e5a767127f83bbf2ec2e838a9d501d0 GIT binary patch literal 2130 zcmV-Y2(9;0Nk&FW2mky1Pr*hpRf4c<12fP1FT{EE@D~3e!pE05F*)xx-zCSS;Tk=1fd#N4jz6TZH8|< zwmshKoESld%^k9#4z8S|Ewg{6wfqFS^GQjSN>Rgcq$fE*J|wCIJPFLbUlzwmhLNsr z*J9E;eXAlAs2D^D7SbG;7t|024q4Z{rt%$H^(yi>gt|6(f;#WSZuo0h+3ZbBqN8)d zHB4A_Z^vX8oM4?AAsnSe3|R?nWs+O@5Aui_O$1C(*$cAN>pB{?X@}_G8^)~tZRaHt zn&dz`+$EgBcv=O9jLre;=SiZWNk0QK-ZDj4zT`IPvVxe28;rEGhg_n3(p)n_0 z&#f^o@#1nZZM;-&8v<45oq%S61DuO^*M?f^uh7B#zLBZB>ks*^?cTy>;2#K`6l-;{ z)b+1#9qSgQale7DxfS}-hMUU+)T&zT@gt@?lT<>;UQ zKMpfmvN_hiE(6b?qgOJ6G0$wH%;n4uWI}|qGh#3)ms;Xk1N7?g|4!7F0ld6BmSy=F z25AOZzQ`NGAZ!}I)+4a_0Zg<$q;{Q7YSk+4e!7)0hAKf9U*qu1e{jGB7NeYi`mFBV zb(|_q#y#0^Pi;FA*j$ zSdo@iM_nxUM%p0$c`Llgo^en{W)S*Q0YVtJ0*zm;=vnAI8cZpcgSqEFOxeZ`fNI&d z!{vcV!bs?hZ4znfe#nbDt?2CW)@|#J705wdDZ+61XKqby%v;b~{;RqA>A8vw@ZqyL z)_su(v%k)TLxUu?`+ojq%0;`;<@VK-;eGCfkp$_|8e$iJTB(2o5BX@ zb&45j`M&$A|0UCikIAp7r!Q-#qFv1D%BA?9@}JA+Y)g(H9`d_faWxGul6#4CCnDh( zf{F>X`6AX9=6&SvkwTAu>+gf1gW;OnXxy38nB1E;H`8)D7|&_1vNJgyCwnam*(~8k z-5sgB&}G;Rw1Tw^p~;V2dgVnKEx74P#XIy*>if=!$@MGx<9+!YoHDFh$Pu0na|uGe zY5$JT|eFYb*UtvLDGs8mK~Rp*H0Gevj_ z#e`QI%g~@_(zD++F*Ier6mLX&KwFpjIzR#&hv9IU zuq^l{Qb&;>_r+sj*>o_0cIk4=g^)ST8>#%_)FA*QHT*-*M#(qx9$Q4MZEpZ2_nF?h zxtqw2`U1pTFc3x{QWfns>7&+m9>?jkU7Viv%s51MhDa}ol~)^&JmVEa+a3ddI00g& z^g`HLPNFAnn)Q_uIu?d-2qGi@rHk|Kj<|%3TLW8i!lYdYbsOAd?zY8qf(0xkmF!EP zUk-NMaAOk{b_yc!xiCoEr`+4N8;d3&=JffSXh5JH7^l_>%Pl2yUnf?ccEV7 zod^syC65)87~jh_k7^aD?Ma51Pw15nYwTPpK;WT!V19rlJ!@5?1w{_Ezo3t7u#U?- z1@`AvpSS&12@M3;oWlUF8U#r|W@$yiWZQ^uAI(62RwV|P0AtO&P2&{=4X%EbXf3SU z8~QJ0-nblfsN#0hj00AsjS^hdd%selr#?0G`Fi)zfw)}8B z5+E*{Dq_2(IG4Ta`Nh~0hQIM!t`i|JYpn!3K+_IP3eBVf8{!S_vFCs-P;SuexjqP8 z;u zX~IuCOcuefN(~u&ccLZ8ZiY(@O5{UEO?-td0c-JjqKwJhk`qyf%;H5KHp+HElTz9C z$a!;ufTP>N*4w77-48W1@XZ-A=xE(@@5(ofBpJr|{;PgW>ox{4ym`Ily-bdd5-)!r)tjB-tO1YBp zRPREn+7EwZlhV!|n*2C&DNGqjLR{;ySNpYcO6NeFvAPgqnps~{y*6L6^SosWvmF#V ziuW2|p2air-?QPNNBv<2L4Xr-VH6kQbzSP_hSg@Ro+llz-XqCZf>1-GV2*vEFeq~^ I58E&R05F#x`v3p{ literal 0 HcmV?d00001 diff --git a/test_data/blocks/bafybeiag67bl2upfzjkvpfmrncgoky6klimtd65bniz5ovxe3ahepzjune b/test_data/blocks/bafybeiag67bl2upfzjkvpfmrncgoky6klimtd65bniz5ovxe3ahepzjune new file mode 100644 index 0000000000000000000000000000000000000000..46f684218c15ae8f389cfba65157e590cdff69b5 GIT binary patch literal 262158 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�)_B=?5qaA>3T=%Y(z;o5sp{d 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

6Av{Dl76{xt`Q}8smM>@{2;SAT#Not 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 ParseJson(std::string_view) const { return {}; } + std::optional GetGateway(std::size_t) const { + return std::nullopt; + } }; } // namespace