in path)
+func HasDNSLinkOrigin(gwURL string, path string) bool {
+ if gwURL != "" {
+ fqdn := stripPort(strings.TrimPrefix(gwURL, "//"))
+ return strings.HasPrefix(path, "/ipns/"+fqdn)
+ }
+ return false
+}
+
+func stripPort(hostname string) string {
+ host, _, err := net.SplitHostPort(hostname)
+ if err == nil {
+ return host
+ }
+ return hostname
+}
diff --git a/gateway/assets/build.sh b/gateway/assets/build.sh
new file mode 100755
index 0000000000..531bbfc024
--- /dev/null
+++ b/gateway/assets/build.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+set -euo pipefail
+
+function build() {
+ rm -f $1
+ sed '/ ./base-html.html
+ (echo "") > ./minified-wrapped-style.html
+ sed '/<\/title>/ r ./minified-wrapped-style.html' ./base-html.html > ./$1
+ rm ./base-html.html && rm ./minified-wrapped-style.html
+}
+
+build "directory-index.html"
+build "dag-index.html"
diff --git a/gateway/assets/dag-index.html b/gateway/assets/dag-index.html
new file mode 100644
index 0000000000..5bba8f5c0d
--- /dev/null
+++ b/gateway/assets/dag-index.html
@@ -0,0 +1,67 @@
+
+{{ $root := . }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{ .Path }}
+
+
+
+
+
+
+
+
+
+
+ Preview as JSON (application/json
)
+
+
+
+
+ Or download as:
+
+
+
+
+
+
+
+
+
diff --git a/gateway/assets/directory-index.html b/gateway/assets/directory-index.html
new file mode 100644
index 0000000000..d861cb6570
--- /dev/null
+++ b/gateway/assets/directory-index.html
@@ -0,0 +1,99 @@
+
+{{ $root := . }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{ .Path }}
+
+
+
+
+
+
+
+
+ {{ if .BackLink }}
+
+
+
+
+
+ ..
+
+
+
+
+ {{ end }}
+ {{ range .Listing }}
+
+
+
+
+
+ {{ .Name }}
+
+
+ {{ if .Hash }}
+
+ {{ .ShortHash }}
+
+ {{ end }}
+
+ {{ .Size }}
+
+ {{ end }}
+
+
+
+
+
diff --git a/gateway/assets/knownIcons.txt b/gateway/assets/knownIcons.txt
new file mode 100644
index 0000000000..c110530ea5
--- /dev/null
+++ b/gateway/assets/knownIcons.txt
@@ -0,0 +1,65 @@
+.aac
+.aiff
+.ai
+.avi
+.bmp
+.c
+.cpp
+.css
+.dat
+.dmg
+.doc
+.dotx
+.dwg
+.dxf
+.eps
+.exe
+.flv
+.gif
+.h
+.hpp
+.html
+.ics
+.iso
+.java
+.jpg
+.jpeg
+.js
+.key
+.less
+.mid
+.mkv
+.mov
+.mp3
+.mp4
+.mpg
+.odf
+.ods
+.odt
+.otp
+.ots
+.ott
+.pdf
+.php
+.png
+.ppt
+.psd
+.py
+.qt
+.rar
+.rb
+.rtf
+.sass
+.scss
+.sql
+.tga
+.tgz
+.tiff
+.txt
+.wav
+.wmv
+.xls
+.xlsx
+.xml
+.yml
+.zip
diff --git a/gateway/assets/src/dag-index.html b/gateway/assets/src/dag-index.html
new file mode 100644
index 0000000000..7a42ef6bed
--- /dev/null
+++ b/gateway/assets/src/dag-index.html
@@ -0,0 +1,66 @@
+
+{{ $root := . }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{ .Path }}
+
+
+
+
+
+
+
+
+
+ Preview as JSON (application/json
)
+
+
+
+
+ Or download as:
+
+
+
+
+
+
+
+
+
diff --git a/gateway/assets/src/directory-index.html b/gateway/assets/src/directory-index.html
new file mode 100644
index 0000000000..109c7afbf4
--- /dev/null
+++ b/gateway/assets/src/directory-index.html
@@ -0,0 +1,98 @@
+
+{{ $root := . }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{ .Path }}
+
+
+
+
+
+
+
+ {{ if .BackLink }}
+
+
+
+
+
+ ..
+
+
+
+
+ {{ end }}
+ {{ range .Listing }}
+
+
+
+
+
+ {{ .Name }}
+
+
+ {{ if .Hash }}
+
+ {{ .ShortHash }}
+
+ {{ end }}
+
+ {{ .Size }}
+
+ {{ end }}
+
+
+
+
+
diff --git a/gateway/assets/src/icons.css b/gateway/assets/src/icons.css
new file mode 100644
index 0000000000..dcdbd3cd9e
--- /dev/null
+++ b/gateway/assets/src/icons.css
@@ -0,0 +1,403 @@
+/* Source - fileicons.org */
+
+.ipfs-_blank {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAWBJREFUeNqEUj1LxEAQnd1MVA4lyIEWx6UIKEGUExGsbC3tLfwJ/hT/g7VlCnubqxXBwg/Q4hQP/LhKL5nZuBsvuGfW5MGyuzM7jzdvVuR5DgYnZ+f99ai7Vt5t9K9unu4HLweI3qWYxI6PDosdy0fhcntxO44CcOBzPA7mfEyuHwf7ntQk4jcnywOxIlfxOCNYaLVgb6cXbkTdhJXq2SIlNMC0xIqhHczDbi8OVzpLSUa0WebRfmigLHqj1EcPZnwf7gbDIrYVRyEinurj6jTBHyI7pqVrFQqEbt6TEmZ9v1NRAJNC1xTYxIQh/MmRUlmFQE3qWOW1nqB2TWk1/3tgJV0waVvkFIEeZbHq4ElyKzAmEXOx6gnEVJuWBzmkRJBRPYGZBDsVaOlpSgVJE2yVaAe/0kx/3azBRO0VsbMFZE3CDSZKweZfYIVg+DZ6v7h9GDVOwZPw/PoxKu/fAgwALbDAXf7DdQkAAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-_page {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmhJREFUeNpsUztv01AYPfdhOy/XTZ80VV1VoCqlA2zQqUgwMEErWBALv4GJDfEDmOEHsFTqVCTExAiiSI2QEKJKESVFFBWo04TESRzfy2c7LY/kLtf2d8+555zvM9NaI1ora5svby9OnbUEBxgDlIKiWjXQeLy19/X17sEtcPY2rtHS96/Hu0RvXXLz+cUzM87zShsI29DpHCYt4E6Box4IZzTnbDx7V74GjhOSfwgE0H2638K9h08A3iHGVbjTw7g6YmAyw/BgecHNGGJjvfQhIfmfIFDAXJpjuugi7djIFVI4P0plctgJQ0xnFe5eOO02OwEp2VkhSCnC8WOCdqgwnzFx4/IyppwRVN+XYXsecqZA1pB48ekAnw9/4GZx3L04N/GoTwEjX4cNH5vlPfjtAIYp8cWrQutxrC5Mod3VsXVTMFSqtaE+gl9dhaUxE2tXZiF7nYiiatJ3v5s8R/1yOCNLOuwjkELiTbmC9dJHpIaGASsDkoFQGJQwHWMcHWJYOmUj1OjvQotuytt5nHMLEGkCyx6QU384jwkUAd2sxJbS/QShZtg/8rHzzQOzSaFhxQrA6YgQMQHojCUlgnCAAvKFBoXXaHfArSCZDE0gyWJgFIKmvUFKO4MUNIk2a4+hODtDUVuJ/J732AKS6ZtImdTyAQQB3bZN8l9t75IFh0JMUdVKsohsUPqRgnka0tYgggYpCHkKGTsHI5NOMojB4iTICCepvX53AIEfQta1iUCmoTiBmdEri2RgddKFhuJoqb/af/yw/d3zTNM6UkaOfis62aUgddAbnz+rXuPY+Vnzjt9/CzAAbmLjCrfBiRgAAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-aac {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnhJREFUeNp0Uk1PE0EYftruVlvAUkhVEPoBcsEoLRJBY01MPHjCs3cvogcT/4qJJN5NvHhoohcOnPw4YEGIkCh+oLGBKm3Z7nZ3dme2vjOhTcjiJJvZzPvOM8/HG2q325Dr3kLp7Y1ibpIxjs4KhQBZfvV6s7K5Vb0bjeof5ZlcGysP1a51mifODybvzE8mzCbrAoTDIThMoGXZiZ4YSiurf+Z1XeuCqJ7Oj+sK3jQcNAmg8xkGQ71mYejcAB49vpmeuzJccl0+dUj6KIAvfHCPg3N+uAv4vg9BOxcCmfEzuP/genpmeqhEMgude10Jwm+DuUIyUdTlqu2byoMfX/dRermBeExHsTiWNi3+lMpzRwDki8zxCIATmzbevfmClukiP5NFhJgwkjeRTeLShdOoVJqnAgwkgCAZ6+UdLC9twjQZ8pdzioFkZBHY3q6B3l4dJEEEPOCeD4cYVH7Xsf15F+FImC775INAJBJSkVoWo0QY9YqgiR4ZZzRaGBkdwK3bFxGLRZUfB3Rm2x4x9CGtsUxH9QYkKICDFuLxKAozGZwdTqBRs2FbLlXbiPdECMCHadj/AaDXZNFqedCIvnRcS4UpRo7+hC5zUmw8Ope9wUFinvpmZ7NKt2RTmB4hKZo6n8qP4Oq1HBkKlVYAQBrUlziB0XQSif4YmQhksgNIJk9iaLhPaV9b/Um+uJSCdzyDbGZQRSkvjo+n4JNxubGUSsCj+ZCpODYjkGMAND2k7exUsfhkCd+29yguB88Wl7FW/o6tT7/gcXqAgGv7hhx1LWBireHVn79YP6ChQ3njb/eFlfWqGqT3H3ZlGIhGI2i2UO/U/wkwAAmoalcxlNA1AAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-ai {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAk5JREFUeNpsU01vElEUPTPzZqBAQaSFQiJYUmlKYhoTF41L3Tbu/Q/+AvsX3Bp/gPsuWLrqyqQ7TUxMtAvF1tYGoXwNw7wv7zwYgtKX3Lw379575p5z77O01ohW+/DVh8zj7aYKhflGdG9ZsGwLNydffgVfr19YHvsEa+Zu/nxndob5StQK+dyzvZzyw/gKlmMj7IygFM+xvNcanp4/t5dAomXHBy2UUBOO2MAl/B9/cPb6PULuoHx0WM0e3GvpUOxD3wZAJWutZqYUYmqpSg5OMgH3YQObL59W0/ullpryR3HegkKEqiWBSGV4R3vQ7sIhScTZFTpHx3A215B5sluVY/WWMg7+ATB/lcLsKpTonHzD+OMFEuTz8ikkt9Kwt9YJZB38cpBdoQAZJdLvCGByfoPB6Xdk90pYy6Xg3c/DaWwArg09DaG5lCsUFN0pckZAojdC8m4auBqaALuSgez7VB1RtDSUWOQvUaBLFUzJBMJ2DwmPgd1Jwm0WoSgJfjDvrTKxtwAIyEkAOQ5hU//Zdg5uowDlUNMnwZLW0sSuUuACYhwQRwFvJxupCjEYUUccOkoaKmdOlZnY1TkgAcXAhxhOwLsDsHoN3u4O5JTDfVCH6I9nfjId3gIgSUATFJk/hVevGtOMwS0XwQ3AzB/FrlKg8Q27I2javVoZrFgwD4qVipAEyMlnaFArzaj/D0DiMXlJAFQyK2r8fnMMRZp4lQ1MaSL5tU/1kqAkMCh2tYI+7+kh70cjPbr4bEZ51jZr8TJnB9PJXpz3V4ABAPOQVJn2Q60GAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-aiff {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAohJREFUeNpkU9tqE1EUXZmZpE3aTBLbJFPTtFURtSCthr7UCyKKFJ/9An3og6Ag/oXfoUj7og9asCBYKT6UIPHaWtpq7NU2aZK5z5wZ9xxMpMwZDuewz9prr32ZiO/7CNaDx3OLt6fOjBqGg/aKRCIInp8+KzfKH7fudnVF58nE16el+/yU2mBFSWZKpWJKVc0OgUBo02K4NDmU6o75Mx+Wdu9IUXFeiOA/pn1xHeYaugVDdzpbp91qGlAKGTx8dC19/Wpxhjnsxj/RRwk85hGJC9d1O6fneWAuoztDYSSLe9OT6SuXB2ccx73Z9uukwDwfls1g0xZIY/Ad/Gnyt/XVfbyYrSDRE8PExHB6/8B6QuaxIwRBFMt0iIAiMx+LCys8jfGJEUik2WpZOD2SQf9oDtVqQwopCAiY66FS/om3b75CVS2MlU7AJ2WiJBCZjZ2dJuRkDJZFwFAR7UCBja3fNfxY2YEoCtRCj9em3Tpds6FpJseGCBxS0GgYGBzqw62p84gnYnAI2CSbSbPhEpFAaE2zODaUAlWWwDoS5DheGqbWpVE/0CmqCY9qkEyINBceb2uADRNQ8bSWAVVzIFKomCQim+0luS4yKYlsHlRyZo7EsSEC23K5vAsXh/H92zZkuRvxeBS5nEx2yp2KqhxPoV5TYS/8CtdApylM9sZQKKSQzyeRTseRV2QoAzIYY8jme5DN9fI0dQoUIjANGydP9VM7PZw9p/AiBpNYrdbw/t0yTJqRtdU9UrfJCUMpSJIgbWzsYe51BcViHzLHeqCRqhZ1YX1tFwNfZBxS9O3NWkAcHqR606k/n/3coKAoV/Y7vQ/OYCZevlrmv3c0GsFh06u3/f4KMABvSWfDHmbK2gAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-avi {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAm1JREFUeNpsU8tu00AUPXZcN0nzTpq2KQ3pAwkIAnWHqCoeexBb+AQ+ABZ8A2s+AIkdm266QUJIFWKBkHg1KpRHi5omJGkbJ3bGHj+4M1EQrTvSyGPPueeec++1EgQBxHp+/9mbyuriRZdxjJaiKBD3W+u1+p9a856max+gDO8ebT+WT20Ezi9NZi/crqadvn2MQBAGfpCOpqNru2937vxPIpY6Onjccx3Twck9MBiSU0ncfHirXFmZX3Md9wqCUwiEVN/zaQfHt0vfbBe5uQyuPVgpl5Zn11ybL4/i/lkICOw5niQRGQShoiqI6Bo43W2ub8n3hRtLZT7gTynk6gkCX9gAOxpAnxhHZDwC1/aI1EViJolu/QhKRMHZ1UX0Gr1USIEn5FPWHy+/wTokkrQOq2vBaHZBN4hmY9Jwfr4An/teiEB45ZZDwDiMhoExT0N+sYDCuUkkplLIlXP4/XEXdo+RUhdhBSSfUwtVTUG8MIHK9QVqI7D/uY6vr2pwmCPrkz+Tk9gwARWQ9WxppbXZhNnpw+ya4A5HZi6L4lIR8WyCcL6sTZiAWjWgAmpxkn5+kqTamK6WkCwmERmLDLvjB0ML9ikWXPLFuozYOap3L8HYN6DHdbS/d5CeTVBndBz87FCBLYkNTyIjBQemnIEsSY5lYrK1+UoWcToLMjEHAyIQ2BCBSx/NVh+ZUhrqmEqBebS3WyhdLg0zt/ugAaIklsSGLHCLa6zDMGhZ2HjyGsnpFPqNHnY2fmHv3R5SMymYbROszSQ2ROAY9qHiofvlxSc5xsKKqqnY3diRE9h4X5d/pzg7lnM4ivsrwADe9Wg/CQJgFAAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-bmp {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmZJREFUeNp0U+1rUlEY/13v9YV0vq2wttI5CdpL9aEGBZUDv0df668I6n+or0UQ/RuuD0EgVDAZrsKF4AR1a6COKW5qXvXec27PuVeda3bgcF6e8/ye5/d7niMZhgExnK9fbTrm5pbBGMZDkgCyq+VyhTUaT6Eo2ZHJePPWXJXRhez3B1yxmM/QdctXUSCgtV4Py4CvY3cky4e1x5DlLCaGbbzjXDcousG5OQe5HPRSCQPK4PpsEM/XH4WvhS4noeu3JwHGGRiULhsMoKZS4I0GtEIB9mgULJGA0+9DPBpBT7sffvf1W/Lg6OgJufw8C0CRGEXWazUwiiyFQjA8bsjVKjaJzovMD/Q5gxyJhG2cvyeXe2cAuADQNGBmBvLaGuTFRaDfh31lBTWi9pumjbK0B4JQul3vOQpM8JdskOLrdCvDcDjAsjtg5TIkoiKLaokMNR2cnZbqNAMycqG7XbHKR2fMzwO/dsxSwu0BiBJsNsv2LwAJAJCI5ux2gXYbqNetcz5PoORI1cDS0n8AxGW7A+zvEYBKZ2ZlcsEtJLbedMjePBaCTQMghx45ulyWkzxMVUQ2RMQhLfFO16YAqCrixPnm6iqKrRb2W23EfF4cUNSrHg90cr7hDyB33MTnSmUKALVs4uIlROjxg+AsPhGVl3fuIl2tIOB0Ya91gkOi9mxhAal0ekork1ic/kGLBORMxy2K1qS9V1ZQbNThIj2EGh+2tsyOnSai8r1UxMNIBB+LRTTULr4Uds0K1tU/uOLxIrmbNz8XXSrnASSpubG9fbKRyVh1n/zSw29t9oC1b47MfwUYAAUsLiWr4QUJAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-c {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAcxJREFUeNqEUk1rE0EYfmZnkgoJCaGNCehuJTalhJZSUZB66a0HwXsP/Qn+FM+9+hty0LNYCr2I7UVLIW0Fc0hpQpSS7O7MrO9MspuvVV8YMnk/nn2e5x0WRRFMvP/w6WSz5jbi/9NxfP693Wp3DrJCnMW5d28P7a+IE15lufR8o1ZEStwPhkWHsWbrZ+eNEPxsuubEF6m0TBv2Q4liPofXuzveulttSqW2UwH+GjqC0horpSL2njU89+FyMwjlTlxOJMTa9ZQHzDQIjgwdom9zLzfXPc75kbnOAswBJTlC2XrqQRMLxhi442DgB4UFBhgPpm3B5pgBHNUUxQKAHs8pHf3TEuFMetM9IKr/i2mWMwC0SnuSFTG2YKyppwKYVdGO7TFhzBqGIenVeLCUtfURgErucx5ECKREKBU4d3B718PHz6cICGT/1Qs8qpQtGOdyhtGEARWDQFqQJSeDL98u4VbLaKw9IRAJPwjtoJGlVAoDQ800+fRFTTYXcjlcXN2g++s36p5Lzzlve1iEROa8BGH1EbrSAeqrjxEqicHQt8/YSDHMpaNs7wJAp9vvfb287idboAVkRAa5fBYXP9rxO4Mgf0xvPPdHgAEA8OoGd40i1j0AAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-cpp {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAfJJREFUeNqEUs9PE0EU/mZ2WgqpXX+QIDFdalVslh8NlAOQaOKFAwfvHvwT/FM8e/U/MOnBmwcj8WD0ACEGghIkbU0baaEthe3OTJ0ZWV26q37JZt68ee/b9733yGAwgMbL12/fz+azbnAPY2Nrt7Zfqz9JMrYZ+J4/e2pOFjiciRvXlgp5GzHonXk2o6S8V6k/TjBrM/xGA4MLyeOSPZ8jkx7D+uqCU3Amy1yIYizB36AlCSkwfjWDR4uu40yMl/s+XwjeWThQQ4Z6QNSnSkYykcDXasP4lmfvOZTSF9q8TDBEFPbN5bOqCglCCCxK0TvvZyIV4CIxbgpC+4gm/PUmFCIE8iJPyME/e8Lon9j4HvyHYLjKSwRCSEUgf9+15mFbx8QS6CZJMzJ9SlBCwX3fJDLG4PX7ykcwkmQmJtpEhWa7g1dvNlSwjwelebz7tAXLolh0p/Fxe9fErK2WDFGEgKjxfNjegX0lDTc/heNuF99/HGEslcKXwyoazWNDdlCr6+DoJgrBzdI0T9rYO6yg2zszMlaKM3Dv5OBzbuyZuzm1B16U4Nzz2f3cFOx0Gq12F9cztpExncsqYoaHpSIKtx0zJdVIFpHQ6py29muNk1uTN829o/6SHEnh80HFaE6NjmLnWxUJy1LyTltB3k8BBgBeEeQTiWRskAAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-css {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAk1JREFUeNpsUktvUlEQ/u5DoCLl/RAKKKUvWmIxjYntQtcu3LvwJ/hTXLt16coFC2PsojEaMKZtCqFaTdGmjbS0CG3By+vei3OOBSGXSU7uzNyZ78z3zRF6vR6YvXzzPrMUCyf68bB9zO+VfpROn5hkOdfPPX/2lH/lfiLidztX5mN2jLGG0rKLENIE8liWpdzwP7HvqJqujmvudFU4bFY8Wk1FZsOBtKppd8YCDNu77CZevd3gflfTUFcUhP0ePLibiIR9rjSBpgwAfe4dVcV6dhtep4PH5msylGYLrzeybErcT85FYiH/CyPAf74gObC2vMhzsiRhPhpC6eQUM+EA1pJzILEnjRSuJsju7MJqsUCSRei6Dp3yXqcdGlHZ/rLPazQWGCn8+6YW4pAkEW0SjzUzanWlCa/LgcR0lNfovTEi6lcIkzesnM/R8RlN0INGp3h4DHoDsE5YRvQyiKiRSMzikRAOS2WoqoZWu41K7RwzlOOAVDMMMHhIGvFlRxJFrKYW0ep0IYgC3SDh4b1lTJjNfENsrazOAMAw680mPuW+8lFno1P4XDigRhOiwQAyJK7TbsNS/PaA7giAIAhYz2yRgBIfsVA8wIetPG6FAqhdNrC5u0f+TUyHgyMTDDToEt/ftQsEvW4EPG5OZcrvw0mlimarTXkPfpXPcNlQoGtjACgpryQXsPNtH/nvRXqBJpoKHMzGNkNB0Odls7LNyAYKpUq1dt1iuvB7fRDp9kr9D1xOFwkpoksXusmXaZWFn0coV89r/b6/AgwAkUENaQaRxswAAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-dat {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAfVJREFUeNqMU01PE1EUPe/Na0uptmlASg3MoiZgCA3hQ8PHAjbqwsS9C3+CP8W1W/+BSReyYUPwI4QAVkAgUEgIbVIg1FZb2pl5b3zv2cHBjsaTTOa+e989OffcGeK6LhTevFv+OJoZHPHOfrz/sl86KpWfhxnLe7lXL1/oN/MSZqonOXU/k0AA6lfNhEFIrlAsP2PMyPtr1AscLpyg5pbtIHErhqez4+awmc45nI8FEvwNaiQuBHqTcSxMjJhmX0/Osp1xr878FxWEzwMinxAzEA4xFIpnOjedHTKpYbxW4U2CP4j8uWxmUKsghMCgFI2mFe9QgHZj0Ba4yhFF+KvGJToIRLuPC/efnjD6+26wB1Lq/xgbSCBXKeWJG/OTdky8cWTdT3C9RmWSGk2XCLlWo4xTNbfN5qh7PpXM72GjZeHt0gpq9QbmH4whGb+NpU/reDQ7hcWVVXxvXOHxzCQopQEKXKEbL6o1ZIcy+LC5g62DY2zsHeC0fA4zndIrHOjvg2XbAQRSfsuy9XxC2qzi/H5B6/68W0AsGkW0KyJPBLbDO0fg3JX/CUM81i0bD6WKe6j9qOPJ3EMcF0tSNsFA6g6alqW+VtZBUL78Vtk+Oqne7U9rs5qOQCjSheJFBeFIFOfVujSUYu3rIc4uqxWv76cAAwCwbvRb3SgYxQAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-dmg {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAn9JREFUeNpsU01rE1EUPe9lkk47yWTStCmtNhFSWxos2EXVhSsRcasuxYV05V8Qf4DgD/AvCK5EV1oFI7iUBqmCNdDvppq2mWSSzEzy3vPOpFFq+uDNfR/3nnvueXeYUgrBWH1/9/NE7k5BKRnuRcfF2qdnmJq9DeF9tQ+2isuMsxXGWHh/a1mEVsPJSI5fSU3OPEj291IIlN49RXz0KqzEQjIeZS/L5Y/3wPGhDxIM/i/A7fZWgVG0t5EaG0ZUa0JGM8gvPrZmLt58QYwv91mfAqCIE0sAqgumBFITGQzpUYhuF0KfRa7waDyXXXolpVrsh/0tgSLDr5I+wUZo1UHCSkAficPzY6juFSmbRPrC/azjq+fkcO00gAqoU7B0ETKkfWbuCTjTYeq5oESAauexcTScX+ZACWFm0YQSLZKhHdr67+/wW0e0dgjYo3sCEXXybYtBDVSHLp2es3IpsILS24c42lkBg6DzRjgRzCDZ/xr0GNRJwwYiWgzt+hYMawleu0V3wbkT+kUirOc7IGJAz68R/Qak1BAlx3hqASPGBJRXpXOv58dkz3eAgQoOm4hyj57NgZm0MHvpBmK6QdUdg/DAg9cRkhicBSDaKJdeo1bdxmR2DtWDDUxl51HZ+QHTysD3XdQO95Gfv06aeGcAdBrY3Chi8lwO3768QWX7J5q1XWyVSxgajiOXLyBG2hzurRKV9lmt7ISNkkjo6HhNyjoK+2gXRsKE57ZIE2ot10Z1fz0Ue4ABVw3NMjnW14rInh8jTYywoTg3EOFpOM4mXNfH9PQUfGlrAwBOs3I8ljbtuMWhRWzIIPrkn+GcYcgIWEowbZ+0qB334/4IMADESjqbnHbH0gAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-doc {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAppJREFUeNpsU79PFEEU/mZ39vZu77g7DokcP04BBSUmiEKCSCxs7Ei00JAYO2NlTKyMrX+CJhaGwopSQ0dMtFEsbDRBgiZEQIF4IHcg+2t2Z8eZ5QDlnM1mZ9+8973vfe8NEUJArfSNhzPG0VIfeIiDRSDkw1cWVt3N8rhG6SdSO2Gvn8dfuueqZwuNZqk3Jxg7iNcIfBbgXD6ZC8u5qffzX8eoYeyDxC77uygKhcouovgVUQj1H4YB2ovNuD9+tTTU0zMVBmG/+C8AIYh8F361DL/yE5HnADKYlVdg6MDAmW7cuz5WGuw+PsWDYGAvbL8ECFUt4K7/AHd/I9c7BLaxinD2Ld5Zo7g78RLuRhlBS2cpWbGfStfhfwCEpK0nUjCbWuGsLciSOELPhkq/YgdY3l6HsLfRcLYf+pHNbH0JigEPkLAyMsiEJ7NrqQzM1i7wyhoMZqOhvQs6Z0ovXgdAJACRoulEg5HOwrOroKk0zOY2BDtVpTF0CU6kLkQJXa+BNEoG0lMSsBBKQXWNQktmoGcaYeSaQCIVWOvUYQAiWZFQtk5mSMoSzEILtBrTfEcviC5bwVwQmoh96wA0ic5dB57ngeoaTIPCdb34zDITYNLOOIeVSsW+dQC+7+NSWx6jJ4tY/rWNV7PfcGv0tBoPTM7M4eKJVgx2FTE9u4QPS6x+kHzfw/mOAjarW2hJG3hy8zIceweuY+PRtREMdzbjzcd5WBqPB6xeRGUMGRzHjWvMmxQ7tiOF1JBN6FiTd6Sy9RuFbHpX7MMMqOD088Ii+op5OUAO7jyeRGfBwrF8Cg8mXuDL4neMXzgFwhwZz+hf7a9d5yu3Z6DTPjVQIY9k7erO7Y63Lvc8ErEeyq6JaM6efjai4v4IMABI0DEPqPKkigAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-dotx {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAndJREFUeNpsU01rE1EUPTPzJk0y+WhMStW2qdVWxUVEQUF0I+4ELQiC7lz4N9z0T+hG9wrdZKUgLqulhrbSag1CKpT0g7RpYjqZmffle5NEKdMHlzfvvXvPPffcO4aUEno9f3Vt4dTp+BXOe+fB0u/NbVpv7h89NU1j1TCM8H7+xY9wJwPHZMbOjRadLAvE/2gToJTiTPx89k+OlVd/LT+0TPIPpO/SzyQk40xCMxBSZ9Z3CoAx5DOjeHT7SbE0XSpzwa8OWB9jINELolQg8AR0EgUKn1PIlIWpkUt4cPNxkTOU12trs8p95RiAXpqaztqou8q6SKQJJmZSqGwsodFsIJk1kcyLYv7IeafcLx4HUNkFF4jFTExMZ0B9DrfD4HUEusYhWs4GPEJg5wly/tBYRIOeDhpEwlS34xcyajdQr3UwOT2MlJOEBRuGNHWp9AQRVXDfQiFV/U5GBSiQ5p6ngBEa5z3fiIhC6g6IMDBwOdoHPkYnHPVyhN0tF7E4QSpr94CEOKELffq+y9Bq+DCJ7rWBoQQBVbPR2O6G4OlsLASJMtCZfQqm0NP5IVWnamdAkUxbyuIYtD7wWegb0YAzAVMkkI6NwPM9xEwHloyDGAmk7AKS9rAS0FKOdugbYeAHPu7OPEM+MY7q3hIKqTFQHmC3XcONc/fxdfMDrk/ew/edzyhvvTmBAddocVRqH3Frahau56qpZDho7+PnTgXffi/gbHYmLEvPSIQBp5JU62sYz13G609zKBXvoOMdYn2zgm7Xg2MVML/4Eu3uPgxhk2gXmNl8v/i2pcXTP8tKdTEcbWLZqDQXwu/l6pfwbEnSGsT9FWAA4mdHv2/9YJ4AAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-dwg {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAoFJREFUeNpsU0tPE2EUPfOg006hD4rQh8WgbCSwkKgbF2owujaCiQsXxpX+D6MmbtXEsHCLmIAbE6NLo8YlGIxREIshIqVl+mQ6j8/zFVCb4UtuZua795577rl3FCEE5Bl79vPd5LHYiOP7cH1AUWi85ytmvlas1bJ9E5ryBntH3BpuP/X9i7ovkluuiE8N9SDepaLpCcRCCqa/VDCaMuIjSWP25Upl6n+QDoCz6Yh7KKzh3sI2LuUimPtRRyaqodj0MDloYiITSTi+mH29Wu0AUf9CsZPJoW5czJl48LmCc5kIKo5Al67B9gUGYxrun+5NnMlFZ+GKiQADj2a7AquseLIvjMv5KMaSBu4sWVir+3i8VIVKYSby0UTdFU8Znu8AYBHQgVOJEN5uOXi4UsdawwU0FSf6TaSoyw6DRvukPkgGWpDKy4F8a3jImCrqFDFn6rhKPR4VGnhvOTAY3WLcjifcQAsqRfhUc/Gq1MKNbBh9nIAMDjEppocxs9HCMktfGTCwP/oOBkUKNk/qF3pDYC6Ktk8RfWzyaaoKrqdDaBDwya8W1m0/CPCR3kFy7CcnmWQRUJqcRJFUKtTnPCeR71LwoeYF92CYyVnCFZpCTrRtCv5to2St8SOrKxiPqEEA4fkYT+mI0rdoeUiH1XZVuQPpsIKqw2QmfifTsnOABiWySlH9uU0Hh2MqjsZV5LtpPSoGeN9rKnhBX7ehoOSLIIPfnGONXGMMWN7xUfVldYDbjM3mrh5HCDgS17DhHgDQcIU+XbBxnDTn1x1UuQcJ9iv7l5Q5e1zLGri92EDJFnoAgHtcfr6wbbVXUqq193+0z97n3UJt1+d51n7aHwEGAAHXJoAuZNlzAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-dxf {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAo5JREFUeNpsU0trE1EYPfNMmtdoH2kDNmJbaVFcaBVFpAsREQpFwY0bu3HjQnTj1mVd+ANcuC3qQixmry6E0kWFVIQ+bKy2tbFJm3emyXTujGca+4DkwsedfLnn3POd77uS67rw1vC79ek7fZEzpu3AYUqS9tKQGZPLpa3VXP0uFCmJ/8t9OLC3q/uJbcs5bkIybvdHoMsSbLKENRmvU2WcNnTjRFD7ML1WGSPJHI6sA4KRWMAWVDPxLYex3iCmfpuIh1QsFSyMxQO4GvXHHwOJ6XWSyIck8v6HQsnjAxFc7vTj2VwBg4aG78VdBHQFCk+dbVcxMdwev9gTSEC455sIBOu2KLsoJFzqasP9vjCeDBlYqzn4VXXwarGKZN7Crd5QfLDT/7KpBM84c9fFUFjFp2wdk6smflRsKKqMa7EgfJJ3Ac2OKlit2pEmBTQfngdpnupoU7BUtRGiiTe7fXiRqmK+KuDn6TpvYogmBRJcrOwIJLIWxmM+dOsyLKryQAaJpjJ1/AxrGO3SqdZt7kKZJrzJWBg5piHENuY8vV6e0UOye1TyftvC5l+gZB8SHJTwpSx4q4JeTUKaxhXoR57h7Rn+3iFolJ3xvPhab6HgJG/pJ7jsNP4sUX+jZiCgEsWd/DjH5IrSYpBUAr0yHpzSoXKOP25a6OBhndh0zcX1qIYM2RIbu6i0KiHD5B/GTMHG03kTGpEL7H80wHFOWwhqDZ+SpkBOtCDYJDhZE4gRcKNbYynAqbCMbXpwpVPFbEng0aKJGbYzK1p4wIegLlcEPmdt+DjXbzcsxFlCynRwwVAwW6hjqeg0Zt521SYCWCJvbe0Un29UDx7Hgrs3IEitHXkw3jOv2fl92D8BBgAJeyqBh90ENQAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-eps {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmlJREFUeNp0U01vElEUPfMFCEVArdoSqEA0KV246UJdUJM2Lo2JK/9FjXu3utJqTNz4D9worrsQExbFpAFT0TYp0CZ8pIAiyMfMvBnvm2Foa9uX3Lw7c98979x77hNM0wRf7ufPsq7Z2SQYw2QJAkDxQalUZa3WI8hy3gmZr15bu+z8kILBkCeRCJi6bufKMji0NhwiCQR6iitdatTvQ5LyOLLEiWcYukm3m4Zhmbq1BX13FyoxuH7xAlbvpqKRK1fT0PWbRwEmDEyiy1QVg/V1GO02tO1tKLEY2PIy3KEAlmJRDLXb0TeZL+n9g4MHlLJ5HIBuYnSzXq+DlcsQLk/D9Hoh1WrIUjlPcpsYGQzS3LWoaBhvKeXWMQCDA1D9pt8PaXERUjwOjEZQFhZQp9L2yERiqYRCkPt/z58ogTGqHQLE1BLgUmC6XGD5AlipBIFKkbhanKHGYLBDqQ4ZED0OAbfLlo8OIxwGvhVgyTHlA3xkomjH/gegBgDURMv6faDbBZpN+/tHkUApkdTA/PwZAPxntwdUyjYA/+ZMqJHjLgM9iv/6zRt2GgMaIE21aVIjnSm0DGPfmhzyde0UAE2Dj+p7urKCPvkZku9eJILOSMUnkvVhIo7GYIB3xSKYdhoA1erXGVKXpvFxZwdBonnD68PQ7YEwM4O4xwMPxc8RYE87g4FIcz+kvfmnA0YzIJIy77/m0OCqsTkkCTysKPjJG3viLei63Gm3kCO6UWqcMejjxecMPmxsoFKtYop6UNirYL9Wtc5OHqzznIXHq1na7OfMJROcK8a6O7MjW7nfzZdrd7jzT4ABACh3NGsh3GcdAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-exe {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAo1JREFUeNp0k8tPE1EUxr+ZzvRJO62lUAQaKIQ0FVJFjBBdoIkrDDHuXJi4NnHtX+HCjW408Q/QmHTRaCRRohIJifgiiBICTQu29mHfnc7MHc+MlECKdxZz595zf+c737nD6boOYzxJLC6Nhwej7e/24HkO779s7G6mMjcEwfKZ21+/d+em+RbagaFev28qEpZwzKg3ZckqCPH1nfS8hScIdyhBe6JqTG3PfyTTeLrwFhvbKdy9/xi5QglXL0yGJsKDccZY7LDIAwWHpSferWBh+RN8ni4UylVER8MY6PHj0uSpUK0hxzfTmWsUtnoEwO3rer64jEyxim6/Hy67DXaHExvJX3jw7CX8XjfORUdDlOohhU4fAVjILCPbm9V1yIqK2FgYt+ZmsZcv4lH8Nb5upXD7+hVMjIRQa8qeDg8UTYPU5cTcxSk4nS709XTD53ZhpD+IYMAPj+TBz93fZiz5oHV4AP1fGdlyHZIkIZkrI7GyhnK9CZXy+Aig6p1+HQAY003AcF8AVtGGfLWG9XTO4MLZ5cL0WAixoT4zVmPHADSiMo3hzHA/xgeDWFjbNg8H3A7kKnX0koEcPdTu/ylgRGZgOjNv38zoSXC8BZJDRKOlwGEV0VJVGM0y4joAPO1spXbx6sNHeD1uRIYGUCxVSRlDt1fC8rfvcDnsmJ+dOaLgoAs6AVLZPJJ7WdhEkUyT8GJpBflSBcVKDTvpDBw2GzQqQT1OgaZqUOhtFQUTUKnVTVWNpgy51YLVKph7sqKYkA4A1ScEfT66vm5kC3+ofh6Xz59FQ5bpkvE4QW3M5Apoyorhl9ABIKnFgNdTOh2NkJG6WSf9eRBJtmFwLDJmriUzeaOkYvvcXwEGAIVNH6cDA1DkAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-flv {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmtJREFUeNpsUl1PE0EUPbssLYUCXdpaC9gWoSTgAyFigiRGY+KjvuuTr/4A44MP/gx/gMYfwIsan0RjIjGiJIZgSIGFIoXSD0t3Z3dnd70zpITazuZmJzP3nnvumaMEQQCx3jx69SV3a3KWMxetpSgKxP3m242Do43SQy2k/YRydvds67n8a63k+FRSn7l/bdg5tdsAuM3he/5weDC8vLdqPLgIIpba2niux52mg//DqlsYSg3iztO7mczN3DJ3+ByCLgCBH4hOFEF7cDpzPCRyOpaeLGXSc2PL3HbnW3XaRQCPEgWI2MsRVAVqrwbX9bHxbhOKpiJ/bzpDOr2k68V2BtRNzMtqDEqPejY/4zSGjb54BM0mQ8k4xsDoIMauXxnqYOD7PmwScP31d0SS/eAuh1lrolFpIBQNQw2pqJdqsAlIceB1AJCIkkE/FZskXDQVRXw6IYHiE0nBEcaPXSSvJnGwWkQXAE4acAhbxPMJpOdHweoMhc9b2F8zwKizbdlyPLVH7QLg+JKBYzoorxzjz3oRzUoToaEw9KyO8XQW5AE5jrFT6AbAYVVNxCZ0Ka3So+DSTAoDiej5ywTySbls1OEDobhFlMcXxrHw+AbINEjNXgb7y6BndLhk8cRkHHbD7g4gEhiJFxsdhrDqaamBaDKKerGGSKwPI9kR9EZCaNA5ubE7A5s8IFhsrxQkgJhZoa/06xC5xRz2v+3BOjFlbqcGlquxsondT9vY+2pAJdeZR6fI355CgQCN2A4O1w7gkQ7cdLUOAKdhV6uFSv3kd/n8mT68eC8dKWLnY4FsfeZQh7nVVt0/AQYAsf5g+SvepeQAAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-gif {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmVJREFUeNp0U0tPE1EU/trplAqlL0laiw40xASByEJIZFGVnSvj1j+gWxNXJq7VrbrwF7h10cSNhMRHojEuACVBKmH6SJQyJeXRxzzv9dyZPiCtN5lMe8853znf953xcc4hztDzZ1+C6fQMHAfd4/MBFG+p6h/n4OAeAoGNToi/eOm+A50LKRaLh6amoty2vVpZdotNXccMEK3LwZxa2bsDSdrAqePv/mLM5tSdMwYBYqyvw9zdhUn/L59P4OGtG8qlZCoH254/DdCdQBCxqZu+ugqnWoW9swN5ehp2NotgIo6bGQWGtaS8+vQ5V9a0u5S+1gfABEilAqdUgm98HDwUQkDT8JXoPPq+BoM5kCYmFT9jryn1+hkAt7heBx8dhbSwACmTAUwTgdlZ/CVKJaLnI1GD8TikZiPSR8Gxib8chH95mZTxgwWHwH7+gFMswqcokIRbjMO2HDCnZ1VvArpjEmnKZc8+cZJJYGsLsMiZ8AgwEqaY6Mb6RQR33JFhGECzCRyfAFXNu9v+RVNRZWIMuDJNuYMAaDycUFGhCOgtuAtFVDA83G5A8TrFDw+F5QMAxAKJJxz2xnW3RPJGbm+rCyjotZetH4DGzaSSeDA3h4Zl4R0JOEZWTpIzF4n/m995bNdqZwB6m0gFft3Ak6vz+KYWwFsGlqIxXItEcDt1ARMEtKdVgZb+fwA0G2C2hXM0ZTZNRcSf0b1pmXi7uYnjI+Lfanm5fRQsK8BIxKcrK7i/uIgP+Tw+FlREqHN5fx/vyU4uHBE6UO4gDWqk/JFaLuMxcXeFk6TuJ90V0HOk1in7J8AAjmgkPfjU+isAAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-h {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAbRJREFUeNqMUk1Lw0AQnf0woK0ttVqp0hwqVCl+UBERT94F7x78Cf4Uz179DT14F8WbYHtRkBYRLNqDtdaPZLObuLs1NGlXcWDJZGbey+x7QUEQgIqT07PL5WKhHL5H46J+22q22vsWpbWwdnR4oJ80LNiz2czGUjENhvj4ctIE4Wrj8XmPUlKL9nCYcOFzE9j1OKSTCdjdrtiLdr7KhVgzEvwW6krC92E6k4Kd9bJt57JV5vFK2KfRQRV+RAMkzxglYI1RaDy2dW1rpWRjQo5VGicYIorWVooFvQVCCAjG8Omw1MgG8AM0uSBUDSnCfk/IGCHwf3DCD/7UhOLBrFkDuep/hDUSSCv1iYo4rIfqGwmUSNJjfYbBcQKhZw0aBMA4B48LwBhBt/cON80HmM9NQ6fXg/Wlku4TwmNWDzaQqzHG+0PSKod5cH5Vh2RiAhYKc8DlV1UPSyuFMGygVlMg1/P6BC6DqXQK8jNZDXAYA1f21V34wMXYFaiyVw0rJyzLgs3VMkxOjGtix/V0XWChZ0cI2i/dzvXdfTd0Qf91BMPrhyNzgKfOmxaWypqaDXHfAgwAtCL8XOfF47gAAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-hpp {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAehJREFUeNqEUk1v00AUHK/XKf1yZdESVRBXjRSRFqMQVBA5Ic5I3DnwE/gpnLnyG3LgXglx4UDDLZS0RWkDLiRxSusk9u6GXSembmLgWZbX7+2bnZl92mg0goo3b3ffO/ncdvyfjHef6q2Dlvs8Q2ktzr16+SL60jhhZ69bO8X8ClLC7w9XdKJVG8fuM0r1WrJG4gXjgqU1D0MGc2kBTytl+7a9XmWcl1IB/hZKEhccq5aJJ/e3bTu7Wg1CVo7rNLlRhUh4oMnXoDoyhoHGyWmUe+QUbELIa7W8CjAFlMzdzeckCwFN06ATAn8QmDMMMGlMuwWucpoCHNe4jBkAMenjYvRPTyi53JvuwX8AplleAeBcRFrH6rXIxLim9I/pi3QA1RhKaYxdjkN8IwalCMIwWs9ljMkh0wzk+9M7w179C3LZNXxve2h+c3Hu91HeKmD/6zHOLnw83ilB1/V0CeqU3Q81LC/O41b2Btx2N2JVP2riR8eTUxmi0TzBwrKZMsqMoz8MsDh/DWuWhUBKURLKxQIeOMWoptYPnS1c+INZBkwISomOSsmBZS7B+3WOzZvrKGzkMAiGqNy7g+LmRkRfekBnANy2163PZXrSbrQ6vch19Xz8fPDHyL39QzkHBKedXjfu+y3AAGU37INBJto1AAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-html {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmBJREFUeNqEUktPE1EU/mY605a+hhZTBNKRDApNrWIRA4nEBUZdmCgLNi4MK5f+FNdu3bFv1J1EXODCR1JJSMTwpqUP6NiCpe10Zjz3hj5Mm3iSybl37jnf+c53jmDbNpi9eb+6Ftcisea909bWNzNb6dwzSXKkhIt/r14+515qBqmDA8HpqKagh53XaopblpIbe+knDpFAhPab2Dw0TKvRK7lmNODzePBgZlK9oUWSpmVNdpIU8T+jaMsyMaD4MDcZVa+NhJMN00w0n6V2nN3yQgdHWZag+LzYPTomIAtT0THVtPGanmb/BbjwLFkvn2IttYGYplKyDzsHh7gdmyAWfh5zVq0Guhg4RAHFUhmfvq3j134aXo8bd+ITnMFOOovU5jbGRoZwNxFn1cxuAIcDW/sZDjA/c4u+BNxOJyxqaenpI3z88gMfPn9Hv98HQZS6RazW6kjExvFi8TGdDSy/W0Emf4LS6R8sv11BmfzSwkPcm74Jo9Ei0GZgmkw8QCOao8OXcaz/5vSZnPdnp3ApqBBLkWJE0Ci7ASzbIhCLLQ1E0iOkBDh9NpUgiUejo8oNuJwyn0YPABtn51UYFFivG3yBGCNZkuDtc/MW+ZQI3OrYpBaARCKufk3B5XIiWyhiL5ODp8+FfFHH+KiKSqWKUL8fC/NznGlPBmz+24dZjKnD0CJDcMoyW0SqXuMtHBFw7rhIAD1ErNUNafxKBNevapwu65NpEQ4FqXIA+RMd6VwBP3cPSERb6gLIFIq61+UqGWaFdcrVt/lmAuWjAi2aiMFwmOYuIJ/N6M28vwIMAMoNDyg4rcU9AAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-ics {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAhRJREFUeNqEUkFPE0EU/mZ2dra7bLNpi2AxQFKalkJrohICiYkXPagXrx78Df4K48GDBzmQePLMhUODNxQ5ciEkJVqDtJGmMWrCATRbd2ecoS5u3aovmezsvu9973vfPiKlhI4XL7c2r5YL81LIELEghLA3u/udxmHnPmfGW/Wuv+LpwwdneRYBx7PeWK0wOYYhcXxyckGV1fdbnbuMsXcklqPRJQxFMKz4RxDCtVO4s3xlRjWoB0FYjlQPEEBieChwKCRGMx5uLtaKs1P5ei8IKlGa/YkXMXYtlTEDlsnw/mMXhBJcqxSK6vlcpa4PEpCooUyIqs5M6hG1o2CUwqA091cFcYLf/sjzcX75EiQIojI9779CTYR4jwTBf+r7GAwh0AxCiL6JMT/04vQ79u8aI2O/7Jzg69o6Go8ewycUahtBpADhHKLnK/eVbkMdtROWIv80NQ2sPhncA9Htwn+9hZG0rY6DzFwJl+7dhs0ZstUy8rduwPS/wd/ehmi3kwq4zTHiWUgXp+EuL8FvNvFl5Rn4xAS86iyI2kY3n0Mv48ByrOQmancdi8I0Kcj3U5iuA29xAelKCUHrEIayzltagG2E4IwkFaQgSC6lYI09iN0d8It5uNV5nG5sgJdKYC0G8WoTOZvBISFNEBxnsuzD3GX4vfDsszzqAu0jkJQDedCGbB6AWg54pYbPo+NGVPdTgAEAqQq70PytIL0AAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-iso {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAjlJREFUeNp0kstrU0EUxr/k5qbJzdPYpGkpsUJoA2q1oLjTdiGiIC5cuXHlxv9BEOrStTvBnQvRrSAIsejCrlqpsURq2hCJNQ+TNLm5uc/x3MmzJh34mDNnvvnNzOE4GGOwx8+t9XQkfn0VE0Y5/7Z+kHm+dvOhtd3P9c/xwNZh7nWaMYtNUmX/Fct/vlN7/8J5aRRgyzm8xzpRDjGE2aVH4VTqdnoUYg/XkEhmy+Cx3DhA5tMzdFolvg5Mx3Fx9SmH0JIg79Zo3j4GADMIokJTKtjbfAKXU4Y/2NvSfyH75TFOxa9Cmr0XnlPFl5ReOQ6wNMDsoFX6AElqQlNV1KsOuNwS/AGFjEUIDhmn5+/DMM16/9igBowAzFKIswPJr6MjlxFP3sV04gaP7RzMPe6xvWM1gNUBM2UKYlBau3QghGphg29J3gDlLLilWNdD3gkvIIDRhD9yGe2mCV0V4HFXuCxT5Dlv8Dz3sIkAs03FalDxBMQSt9BRBMhNncuO7dyU28c9tnf8C/Q0ZtR4GImeQSj8APLRH772BWcgiFODffCv/t8H9tO0v3RjV7VqkeeXLlzDfvYjj88uXhl4JwIsrYxmLY/M1gYclIvGE9jZfNPrSCD3/QgLyeWTADV6wW9AryIcCkB0u1Aq/oCPumlufoF72vIheaLDr4wCLIOqrYnULA14PSoqpSJEAUilZrD77Sv3LK+cI0+Be8cAbbmAOrob0agtD491LYfkoqvnyZLsWRkA/gkwABL4S3L78XYyAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-java {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAjxJREFUeNp8U01v00AUnNiOEyepQyhQobRBSlVIoRCBEPTAjQsSEneE+An8FM5cuXLNoQduIAE3qopKNJAIIppA2jrOR93aa6/N8yZuUxyxkrXr3ffmzczbTQRBgHC83nj3ca28dD36nx6fvnzrNNrdp4oibyUmey9fPBezEgWVFuYLdyvlPGaMY4fl1aRS+9pqP5ElAkmcnknRwuO+Nyt5u/ETYfyj9WrpZnmpxn2/Ok1Swn/GvtnH5k4TLue4kNfxoFoprRQv1TzOb8cAIu3+ZD7oD/Hm7XuxzqRUNDtdkuLiTmW5tFxceBXlnXgQTAORSMt2oGezUJJJrK9dFWdEH7Ik4dB29LiESeUEJXd7/dAT3L+1ivlCHr8NEzutXTBvbJPPSdO/AH5wysChwM/1HzCGlmAzOrKxu2eCud6Z2Jke2MwThpUXL6Nn2ZAVFTlNw70bK0iRnGAq9qwHtOmTRpsx1NsHyKRVnNPnoMoK9kc2BjbD4vk5JGV5NkBoEPM4FFnCteJFWOS4ntHEfphQyKaFTWFLw704AJ26ZFx/ZEEi3YyY0O1Dmr4EKTUHA8hUnS6siI0DEHLYog+b28RCRuNXR/iQUpPUEQ+NVht6Lodnjx+GXYgDSFRnq97Ed2pXSlXhUSeGhxYc5sKlNXM5DGLR2TMwfZVPAIi+otGNWy1fEZUKeo4qc4ysI+F8VksLIJfYcD9QYgB/DNPMptWBlsnBIS86xmDMTBo/PWd0LB6VZfdEbJT3V4ABAA5HIzlv9dtdAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-jpeg,
+.ipfs-jpg {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmlJREFUeNpsU8luUlEY/s4dmMpkWxRopGJNNbiwhk1tItbGtXHr0hcwmvgOdWld6Bu4coXumtREE3ZKu8FgOlC1kIoXtC3jPfdc/8PUIpzkBM7wf+f/hsts24YczuerGUc0moBlYTAYA+i8sbdXtAzjITRtq39kr73s/Gr9DTUYPOeamwvYnHdrdR0SnDebuCbswJGqpX+Uf92Hqm7hzFAG/4TgNr1uCwEJ0trcBC8U0Kb1/PQkHt9JxSLnL6TB+Y2zAIMOJBGLXmtsbEAYBsx8HnqCGKVScAX8uHf5EpqmGXv18VO6VDEe0PXsKABN8+AAgiabmYFNNJTDQ2RUFc8+Z9G0OPR4PKYwvKari0MAgiY/OQGCAajhMNR4nDZMaInrKBGl70SPMScck1NQG3X/CAWLE3/dAWV5hRRVIJxOWNksrP19sFgMqqAebUGYHMI6teq0A9oTVAhqu2sfbYYjsL7lCZ3683gA70T3TK7/B4BNoO020GwB9TpwfAz8LgMtWn/NkV8EHgoB81c7nYwCyBZlEVkHcqMTKFnkmehJTOPvEfCnKi0fAyADJKfXC/h83TaZTJjaa5lANLpOFqAXtlEAorAwO9u5syT5UxLfU0e3o1FMu1x4u7ODYq02BKAMAVSrSNLrK1MhLPj8mNF0vFm+C1ZvwKBwXXE4AGn1WAASazESwUW3BzUSMeJ2o1Aq4sPurvQYSRLwlhRR6mSaYyi0WlpAJrFRx3ouh5/lMt5lv8BLwXp0M4lSpYL17e2uK5wP6lj/c2ZPn2RI+YT8fDvqoyegVLyfG5kBKaQQOfvF2pLc+ifAABiQH3PEc1i/AAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-js {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RUQ5ODY5Q0NGMTE4MTFFMTlDRjlDN0VBQTY3QTk0MTEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RUQ5ODY5Q0RGMTE4MTFFMTlDRjlDN0VBQTY3QTk0MTEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpFRDk4NjlDQUYxMTgxMUUxOUNGOUM3RUFBNjdBOTQxMSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpFRDk4NjlDQkYxMTgxMUUxOUNGOUM3RUFBNjdBOTQxMSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PoT8zQ8AAAJdSURBVHjadFNbTxNREP52t7S0bktbKFAvTUVaw60YqkExUTD6oD74qC/yD/wp/gh885XEEI0RAyYQUiMpIBGMkYR6o23abi+73e2uc04v1LROMtnZPTPffvPNHMGyLDB7sbJ2ciUSli3U35smkK9t7x9v7n2dD/g8KUkUwWqeP3vKz23NxJGzgwOx0RC6mSgIo+WKuvP56MeUzy2nJEk8PWsGJVVTuhWbpgmHw47FB7d98Wg4mVWK52o1sxOg3Va3PmFp+Q2PdUquaFUM9/vw+O6cP3bxwm46Xwh1ALR3/vL1e+hGjcc9koScUsTSq3coVDQsXJ3wzo5HEs3clgZNMTVdx1T0Ep7cn6//QRQwMhzA6uZHLD5cIFEFSKIU+G8LK+tb0KsGZKcTJoEyP08AbpcLy6sbPKdQrigdAGaDwWxsDH1uGbliCYIgcM8WFPg8Mq5Pjzdyu4jYbCE44EepXMHuwXe+A8x3KKYxYsjvbUzmlPGpBmYdgI1oYjSMbL4Ao1YXMkcM2Dd2xnbAamPQAqg1GORLZdycmYTdJqFKk2DPR3fmwI4zBDrg9RADqxPAbPBif2WTSB584/3/TGegEOit+DRcvQ4OZJi1LgwIQKVCg2i6nb1I7H3Br3QWqT9pBAP9uDY5xjdSM3RqxeoUkfVnEOW8UkLykERTNXjkM7h3Iw6NNvHw6JjuhAhVrba0+QeALozcI9nQR0VvNxJc/ZmxCNGvIBQcpDG6udA22kyW29HC72wu8yG579ZoiSYuR/ly2+y9CA4NceWLmo717T1i5ULqJNtapL8CDACskxPFZRxLwQAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-key {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAlZJREFUeNpsU11PE0EUPbM7u/2AtJUWU6qiiSYYo5EmmPDCD9AH46sx8cEnja/+CB989z+Y+MKPgMiDsYQACcbaWBBogYD92t2Zud7ZlQZsbzKZ3bl3zj3n3IwgItjYeDO3MlWme0bjUth8e8/fO2tHzx3XqUEk50uft+Ndnhdmc3SlfNPkVZT8Cy600DoIISvVfKYtlvfX1p66XmoIYsMZdjJQWvEFbbsC/S5g2QhSkKUK7rx6OzvzqLpsovAhaAxA3DUBQn2TUFsl7KwTfm4Z9DoO5LW7uPXi9Wxpfn7ZKF09vyPxX2iWcNRkKGZz0mQWKoNs8AVB6x1yRY2pYnc2LLofuXTxMgAlmlXIfngCxNxEzM+DPv6NQa2BygLgZyX6JT83ngHTN5GAL0WSoUQkSQnXkyBh/k0GegTAaldM20sTKvet+yyhIZApECamL0jUSe3oFChx3TopM4TeEQP2gc6BgGIwb4KGNXRhCkMGxgg2kJeybRiZM45D8W61qEAknSmpHStBhywu0nFVupSCTAcM4ECwqapv+NQ6LS9JGALoMIIoPYDjZiEL1xHtbyO39AQUDaA7R1AH23DSeSA4hv5RG/VAhxomPYP8sw9A4TaC9iHkjUWmrtGvbyC18BLe3GP0m3WW4I5hEBEnPIStXzyuFIxb4EkMEJ79Qa/xHbKxCdM7xeCwzUZOjgEwnuzt7qLz6T3cySmQP43uzjeIiTJM6io6W19B/NLCKMVGCzkCoLR/0lrfOI2fNy/huKC1FTsK/rbGNeMRC8dHpHByfu+vAAMAL/0jvAVZQl0AAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-less {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RjZERjZENTJGMTE4MTFFMUIwOEVERjQ5MTZEMkVBREUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RjZERjZENTNGMTE4MTFFMUIwOEVERjQ5MTZEMkVBREUiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpGNkRGNkQ1MEYxMTgxMUUxQjA4RURGNDkxNkQyRUFERSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpGNkRGNkQ1MUYxMTgxMUUxQjA4RURGNDkxNkQyRUFERSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pl1w97IAAAJhSURBVHjahJNLbxJRFMf/wPAIMIxMkUI7tS0VYqlGDLGhjdKkqyZ24cJFN925de+XcONHaHRj4k7TND6SGo1VWwmp2kSLhlqMDbQ87gzPYcY7k4GgoJ6bmdw598zvnvM/95pUVYVma+svcovx8yMnFZHAMJPJBJfDzq5vpX6+/vD5qo/z7DOMBdo/d26t6jFMJ3iY51jBz4M+LP6wxEw40Gy23qYzB3HO7fpmpZCOmfEfa7Xb4NxOrC4lvbPToe2yKE3K1PdPwNOtHdx79ESfq4qKkijB5/XgevIyHxEC24USmewDqD2ABxubaLRkfW6zMqjWGlh7/ByyAtxYnOPnL0Q2+gGGmKRaw8zUBJaTiS5QOO1FJnuIAM8hciaIWHgi8NcSNt+loVDY8JBXh2ojJAR1HbTSNFMUpV8Dxcjg0nSYBrtBxdLbqI1iheCUh9XXNGurAwCdEkb9QyBSFam9TDfoPZ1LUg1BH28IiwEARTVAQOzcFKRaHZpLoa9avY6L1Gfs0c32t4PU6W2lWsV8LAorw0Cs1nXftYWE3qZGqwWHzYp2zzlgetuolVFvtiDLbRRKFTAWCxx2G/KlMtXFhWPqOzsWHJwBx7rxKv2R7mwFz3lw9/5DLC/M4Us2RwV0g3U58XJnF7dvrsBOoX0Abbej/DFKRMKI30fTVGC32WA2m5H9cQQvhYi0vE/7Wdgczn6ARA9QPBrBszcp/XvpyqxebzQ0Tlsq6llxLhe9bD4cFMr9XdjLHpLv+SLGBYHAYiVu1kNOpAaRTWbCejgiw0zGhFGSK1aw+zXbvfK/BBgAPwADAs5GpGsAAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-logo {
+ background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 553 235.3'%3E%3Cdefs%3E%3C/defs%3E%3Cpath fill='%23ffffff' d='M239 63h17.8v105H239V63zm35.6 0h36.3c7.9 0 14.5.9 19.6 2.6s9.2 4.1 12.1 7.1a24.45 24.45 0 0 1 6.2 10.2 40.75 40.75 0 0 1 1.8 12.1 45.69 45.69 0 0 1-1.8 12.9 26.58 26.58 0 0 1-6.2 10.8 30.59 30.59 0 0 1-12.1 7.3c-5.1 1.8-11.5 2.7-19.3 2.7h-19.1V168h-17.5V63zm36.2 51a38.37 38.37 0 0 0 11.1-1.3 16.3 16.3 0 0 0 6.8-3.7 13.34 13.34 0 0 0 3.5-5.8 29.75 29.75 0 0 0 1-7.6 25.68 25.68 0 0 0-1-7.7 12 12 0 0 0-3.6-5.5 17.15 17.15 0 0 0-6.9-3.4 41.58 41.58 0 0 0-10.9-1.2h-18.5V114h18.5zm119.9-51v15.3h-49.2V108h46.3v15.4h-46.3V168h-17.8V63h67zm26.2 72.9c.8 6.9 3.3 11.9 7.4 15s10.4 4.7 18.6 4.7a32.61 32.61 0 0 0 10.1-1.3 20.52 20.52 0 0 0 6.6-3.5 12 12 0 0 0 3.5-5.2 19.08 19.08 0 0 0 1-6.4 16.14 16.14 0 0 0-.7-4.9 12.87 12.87 0 0 0-2.6-4.5 16.59 16.59 0 0 0-5.1-3.6 35 35 0 0 0-8.2-2.4l-13.4-2.5a89.76 89.76 0 0 1-14.1-3.7 33.51 33.51 0 0 1-10.4-5.8 22.28 22.28 0 0 1-6.3-8.8 34.1 34.1 0 0 1-2.1-12.7 26 26 0 0 1 11.3-22.4 36.35 36.35 0 0 1 12.6-5.6 65.89 65.89 0 0 1 15.8-1.8c7.2 0 13.3.8 18.2 2.5a34.46 34.46 0 0 1 11.9 6.5 28.21 28.21 0 0 1 6.9 9.3 42.1 42.1 0 0 1 3.2 11l-16.8 2.6c-1.4-5.9-3.7-10.2-7.1-13.1s-8.7-4.3-16.1-4.3a43.9 43.9 0 0 0-10.5 1.1 19.47 19.47 0 0 0-6.8 3.1 11.63 11.63 0 0 0-3.7 4.6 14.08 14.08 0 0 0-1.1 5.4c0 4.6 1.2 8 3.7 10.3s6.9 4 13.2 5.3l14.5 2.8c11.1 2.1 19.2 5.6 24.4 10.5s7.8 12.1 7.8 21.4a31.37 31.37 0 0 1-2.4 12.3 25.27 25.27 0 0 1-7.4 9.8 36.58 36.58 0 0 1-12.4 6.6 56 56 0 0 1-17.3 2.4c-13.4 0-24-2.8-31.6-8.5s-11.9-14.4-12.6-26.2h18z'/%3E%3Cpath fill='%23469ea2' d='M30.3 164l84 48.5 84-48.5V67l-84-48.5-84 48.5v97z'/%3E%3Cpath fill='%236acad1' d='M105.7 30.1l-61 35.2a18.19 18.19 0 0 1 0 3.3l60.9 35.2a14.55 14.55 0 0 1 17.3 0l60.9-35.2a18.19 18.19 0 0 1 0-3.3L123 30.1a14.55 14.55 0 0 1-17.3 0zm84 48.2l-61 35.6a14.73 14.73 0 0 1-8.6 15l.1 70a15.57 15.57 0 0 1 2.8 1.6l60.9-35.2a14.73 14.73 0 0 1 8.6-15V79.9a20 20 0 0 1-2.8-1.6zm-150.8.4a15.57 15.57 0 0 1-2.8 1.6v70.4a14.38 14.38 0 0 1 8.6 15l60.9 35.2a15.57 15.57 0 0 1 2.8-1.6v-70.4a14.38 14.38 0 0 1-8.6-15L38.9 78.7z'/%3E%3Cpath fill='%23469ea2' d='M114.3 29l75.1 43.4v86.7l-75.1 43.4-75.1-43.4V72.3L114.3 29m0-10.3l-84 48.5v97l84 48.5 84-48.5v-97l-84-48.5z'/%3E%3Cpath fill='%23469ea2' d='M114.9 132h-1.2A15.66 15.66 0 0 1 98 116.3v-1.2a15.66 15.66 0 0 1 15.7-15.7h1.2a15.66 15.66 0 0 1 15.7 15.7v1.2a15.66 15.66 0 0 1-15.7 15.7zm0 64.5h-1.2a15.65 15.65 0 0 0-13.7 8l14.3 8.2 14.3-8.2a15.65 15.65 0 0 0-13.7-8zm83.5-48.5h-.6a15.66 15.66 0 0 0-15.7 15.7v1.2a15.13 15.13 0 0 0 2 7.6l14.3-8.3V148zm-14.3-89a15.4 15.4 0 0 0-2 7.6v1.2a15.66 15.66 0 0 0 15.7 15.7h.6V67.2L184.1 59zm-69.8-40.3L100 26.9a15.73 15.73 0 0 0 13.7 8.1h1.2a15.65 15.65 0 0 0 13.7-8l-14.3-8.3zM44.6 58.9l-14.3 8.3v16.3h.6a15.66 15.66 0 0 0 15.7-15.7v-1.2a16.63 16.63 0 0 0-2-7.7zM30.9 148h-.6v16.2l14.3 8.3a15.4 15.4 0 0 0 2-7.6v-1.2A15.66 15.66 0 0 0 30.9 148z'/%3E%3Cpath fill='%23083b54' fill-opacity='0.15' d='M114.3 213.2v-97.1l-84-48.5v97.1z'/%3E%3Cpath fill='%23083b54' fill-opacity='0.05' d='M198.4 163.8v-97l-84 48.5v97.1z'/%3E%3C/svg%3E%0A");
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-mid {
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-mkv {
+ background-image:url("data:image/svg+xml;charset=utf8,%3Csvg id='Layer_2' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 100'%3E%3Cstyle/%3E%3ClinearGradient id='SVGID_1_' gradientUnits='userSpaceOnUse' x1='36.2' y1='101' x2='36.2' y2='3.005' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23e2cde4'/%3E%3Cstop offset='.17' stop-color='%23e0cae2'/%3E%3Cstop offset='.313' stop-color='%23dbc0dd'/%3E%3Cstop offset='.447' stop-color='%23d2b1d4'/%3E%3Cstop offset='.575' stop-color='%23c79dc7'/%3E%3Cstop offset='.698' stop-color='%23ba84b9'/%3E%3Cstop offset='.819' stop-color='%23ab68a9'/%3E%3Cstop offset='.934' stop-color='%239c4598'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill='url(%23SVGID_1_)'/%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill-opacity='0' stroke='%23882383' stroke-width='2'/%3E%3Cpath d='M7.5 91.1V71.2h6.1l3.6 13.5 3.6-13.5h6.1V91h-3.8V75.4l-4 15.6h-3.9l-4-15.6V91H7.5zm23.5 0V71.2h4V80l8.2-8.8h5.4L41.1 79l8 12.1h-5.2l-5.5-9.3-3.4 3.3v6h-4zm25.2 0L49 71.3h4.4L58.5 86l4.9-14.7h4.3l-7.2 19.8h-4.3z' fill='%23fff'/%3E%3ClinearGradient id='SVGID_2_' gradientUnits='userSpaceOnUse' x1='18.2' y1='50.023' x2='18.2' y2='50.023' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3ClinearGradient id='SVGID_3_' gradientUnits='userSpaceOnUse' x1='11.511' y1='51.716' x2='65.211' y2='51.716' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3Cpath d='M64.3 55.5c-1.7-.2-3.4-.3-5.1-.3-7.3-.1-13.3 1.6-18.8 3.7S29.6 63.6 23.3 64c-3.4.2-7.3-.6-8.5-2.4-.8-1.3-.8-3.5-1-5.7-.6-5.7-1.6-11.7-2.4-17.3.8-.9 2.1-1.3 3.4-1.7.4 1.1.2 2.7.6 3.8 7.1.7 13.6-.4 20-1.5 6.3-1.1 12.4-2.2 19.4-2.6 3.4-.2 6.9-.2 10.3 0m-9.9 15.3c.5-.2 1.1-.3 1.9-.2.2-3.7.3-7.3.3-11.2-6.2.2-11.9.9-17 2.2.2 4 .4 7.8.3 12 4-1.1 7.7-2.5 12.6-2.7m2-12.1h1.1c.4-.4.2-1.2.2-1.9-1.5-.6-1.8 1-1.3 1.9zm3.9-.2h1.5V38h-1.3c0 .7-.4.9-.2 1.7zm4 0c.5-.1.8 0 1.1.2.4-.3.2-1.2.2-1.9h-1.3v1.7zm-11.5.3h.9c.4-.3.2-1.2.2-1.9-1.4-.4-1.6 1.2-1.1 1.9zm-4 .4c.7.2.8-.3 1.5-.2v-1.7c-1.5-.4-1.7.6-1.5 1.9zm-3.6-1.1c0 .6-.1 1.4.2 1.7.5.1.5-.4 1.1-.2-.2-.6.5-2-.4-1.9-.1.4-.8.1-.9.4zm-31.5.8c.4-.1 1.1.6 1.3 0-.5 0-.1-.8-.2-1.1-.7.2-1.3.3-1.1 1.1zm28.3-.4c-.3.3.2 1.1 0 1.9.6.2.6-.3 1.1-.2-.2-.6.5-2-.4-1.9-.1.3-.4.2-.7.2zm-3.5 2.8c.5-.1.9-.2 1.3-.4.2-.8-.4-.9-.2-1.7h-.9c-.3.3-.1 1.3-.2 2.1zm26.9-1.8c-2.1-.1-3.3-.2-5.5-.2-.5 3.4 0 7.8-.5 11.2 2.4 0 3.6.1 5.8.3M33.4 41.6c.5.2.1 1.2.2 1.7.5-.1 1.1-.2 1.5-.4.6-1.9-.9-2.4-1.7-1.3zm-4.7.6v1.9c.9.2 1.2-.2 1.9-.2-.1-.7.2-1.7-.2-2.1-.5.2-1.3.1-1.7.4zm-5.3.6c.3.5 0 1.6.4 2.1.7.1.8-.4 1.5-.2-.1-.7-.3-1.2-.2-2.1-.8-.2-.9.3-1.7.2zm-7.5 2H17c.2-.9-.4-1.2-.2-2.1-.4.1-1.2-.3-1.3.2.6.2-.1 1.7.4 1.9zm3.4 1c.1 4.1.9 9.3 1.4 13.7 8 .1 13.1-2.7 19.2-4.5-.5-3.9.1-8.7-.7-12.2-6.2 1.6-12.1 3.2-19.9 3zm.5-.8h1.1c.4-.5-.2-1.2 0-2.1h-1.5c.1.7.1 1.6.4 2.1zm-5.4 7.8c.2 0 .3.2.4.4-.4-.7-.7.5-.2.6.1-.2 0-.4.2-.4.3.5-.8.7-.2.8.7-.5 1.3-1.2 2.4-1.5-.1 1.5.4 2.4.4 3.8-.7.5-1.7.7-1.9 1.7 1.2.7 2.5 1.2 4.2 1.3-.7-4.9-1.1-8.8-1.6-13.7-2.2.3-4-.8-5.1-.9.9.8.6 2.5.8 3.6 0-.2 0-.4.2-.4-.1.7.1 1.7-.2 2.1.7.3.5-.2.4.9m44.6 3.2h1.1c.3-.3.2-1.1.2-1.7h-1.3v1.7zm-4-1.4v1.3c.4.4.7-.2 1.5 0v-1.5c-.6 0-1.2 0-1.5.2zm7.6 1.4h1.3v-1.5h-1.3c.1.5 0 1 0 1.5zm-11-1v1.3h1.1c.3-.3.4-1.7-.2-1.7-.1.4-.8.1-.9.4zm-3.6.4c.1.6-.3 1.7.4 1.7 0-.3.5-.2.9-.2-.2-.5.4-1.8-.4-1.7-.1.3-.6.2-.9.2zm-3.4 1v1.5c.7.2.6-.4 1.3-.2-.2-.5.4-1.8-.4-1.7-.1.3-.8.2-.9.4zM15 57c.7-.5 1.3-1.7.2-2.3-.7.4-.8 1.6-.2 2.3zm26.1-1.3c-.1.7.4.8.2 1.5.9 0 1.2-.6 1.1-1.7-.4-.5-.8.1-1.3.2zm-3 2.7c1 0 1.2-.8 1.1-1.9h-.9c-.3.4-.1 1.3-.2 1.9zm-3.6-.4v1.7c.6-.1 1.3-.2 1.5-.8-.6 0 .3-1.6-.6-1.3 0 .4-.7.1-.9.4zM16 60.8c-.4-.7-.2-2-1.3-1.9.2.7.2 2.7 1.3 1.9zm13.8-.9c.5 0 .1.9.2 1.3.8.1 1.2-.2 1.7-.4v-1.7c-.9-.1-1.6.1-1.9.8zm-4.7.6c0 .8-.1 1.7.4 1.9 0-.5.8-.1 1.1-.2.3-.3-.2-1.1 0-1.9-.7-.2-1 .1-1.5.2zM19 62.3v-1.7c-.5 0-.6-.4-1.3-.2-.1 1.1 0 2.1 1.3 1.9zm2.5.2h1.3c.2-.9-.3-1.1-.2-1.9h-1.3c-.1.9.2 1.2.2 1.9z' fill='url(%23SVGID_3_)'/%3E%3ClinearGradient id='SVGID_4_' gradientUnits='userSpaceOnUse' x1='45.269' y1='74.206' x2='58.769' y2='87.706' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23f9eff6'/%3E%3Cstop offset='.378' stop-color='%23f8edf5'/%3E%3Cstop offset='.515' stop-color='%23f3e6f1'/%3E%3Cstop offset='.612' stop-color='%23ecdbeb'/%3E%3Cstop offset='.69' stop-color='%23e3cce2'/%3E%3Cstop offset='.757' stop-color='%23d7b8d7'/%3E%3Cstop offset='.817' stop-color='%23caa1c9'/%3E%3Cstop offset='.871' stop-color='%23bc88bb'/%3E%3Cstop offset='.921' stop-color='%23ae6cab'/%3E%3Cstop offset='.965' stop-color='%239f4d9b'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill='url(%23SVGID_4_)'/%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill-opacity='0' stroke='%23882383' stroke-width='2' stroke-linejoin='bevel'/%3E%3C/svg%3E");
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-mov {
+ background-image:url("data:image/svg+xml;charset=utf8,%3Csvg id='Layer_2' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 100'%3E%3Cstyle/%3E%3ClinearGradient id='SVGID_1_' gradientUnits='userSpaceOnUse' x1='36.2' y1='101' x2='36.2' y2='3.005' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23e2cde4'/%3E%3Cstop offset='.17' stop-color='%23e0cae2'/%3E%3Cstop offset='.313' stop-color='%23dbc0dd'/%3E%3Cstop offset='.447' stop-color='%23d2b1d4'/%3E%3Cstop offset='.575' stop-color='%23c79dc7'/%3E%3Cstop offset='.698' stop-color='%23ba84b9'/%3E%3Cstop offset='.819' stop-color='%23ab68a9'/%3E%3Cstop offset='.934' stop-color='%239c4598'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill='url(%23SVGID_1_)'/%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill-opacity='0' stroke='%23882383' stroke-width='2'/%3E%3Cpath d='M6.1 91.1V71.2h6.1l3.6 13.5 3.6-13.5h6.1V91h-3.8V75.4l-4 15.6h-3.9l-4-15.6V91H6.1zm22.6-9.8c0-2 .3-3.7.9-5.1.5-1 1.1-1.9 1.9-2.7.8-.8 1.7-1.4 2.6-1.8 1.2-.5 2.7-.8 4.3-.8 3 0 5.3.9 7.1 2.7 1.8 1.8 2.7 4.3 2.7 7.6 0 3.2-.9 5.7-2.6 7.5-1.8 1.8-4.1 2.7-7.1 2.7s-5.4-.9-7.1-2.7c-1.8-1.8-2.7-4.3-2.7-7.4zm4.1-.2c0 2.2.5 4 1.6 5.1 1 1.2 2.4 1.7 4 1.7s2.9-.6 4-1.7c1-1.2 1.6-2.9 1.6-5.2 0-2.3-.5-4-1.5-5.1-1-1.1-2.3-1.7-4-1.7s-3 .6-4 1.7c-1.1 1.2-1.7 3-1.7 5.2zm23.6 10l-7.2-19.8h4.4L58.7 86l4.9-14.7h4.3l-7.2 19.8h-4.3z' fill='%23fff'/%3E%3ClinearGradient id='SVGID_2_' gradientUnits='userSpaceOnUse' x1='18.2' y1='50.023' x2='18.2' y2='50.023' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3ClinearGradient id='SVGID_3_' gradientUnits='userSpaceOnUse' x1='11.511' y1='51.716' x2='65.211' y2='51.716' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3Cpath d='M64.3 55.5c-1.7-.2-3.4-.3-5.1-.3-7.3-.1-13.3 1.6-18.8 3.7S29.6 63.6 23.3 64c-3.4.2-7.3-.6-8.5-2.4-.8-1.3-.8-3.5-1-5.7-.6-5.7-1.6-11.7-2.4-17.3.8-.9 2.1-1.3 3.4-1.7.4 1.1.2 2.7.6 3.8 7.1.7 13.6-.4 20-1.5 6.3-1.1 12.4-2.2 19.4-2.6 3.4-.2 6.9-.2 10.3 0m-9.9 15.3c.5-.2 1.1-.3 1.9-.2.2-3.7.3-7.3.3-11.2-6.2.2-11.9.9-17 2.2.2 4 .4 7.8.3 12 4-1.1 7.7-2.5 12.6-2.7m2-12.1h1.1c.4-.4.2-1.2.2-1.9-1.5-.6-1.8 1-1.3 1.9zm3.9-.2h1.5V38h-1.3c0 .7-.4.9-.2 1.7zm4 0c.5-.1.8 0 1.1.2.4-.3.2-1.2.2-1.9h-1.3v1.7zm-11.5.3h.9c.4-.3.2-1.2.2-1.9-1.4-.4-1.6 1.2-1.1 1.9zm-4 .4c.7.2.8-.3 1.5-.2v-1.7c-1.5-.4-1.7.6-1.5 1.9zm-3.6-1.1c0 .6-.1 1.4.2 1.7.5.1.5-.4 1.1-.2-.2-.6.5-2-.4-1.9-.1.4-.8.1-.9.4zm-31.5.8c.4-.1 1.1.6 1.3 0-.5 0-.1-.8-.2-1.1-.7.2-1.3.3-1.1 1.1zm28.3-.4c-.3.3.2 1.1 0 1.9.6.2.6-.3 1.1-.2-.2-.6.5-2-.4-1.9-.1.3-.4.2-.7.2zm-3.5 2.8c.5-.1.9-.2 1.3-.4.2-.8-.4-.9-.2-1.7h-.9c-.3.3-.1 1.3-.2 2.1zm26.9-1.8c-2.1-.1-3.3-.2-5.5-.2-.5 3.4 0 7.8-.5 11.2 2.4 0 3.6.1 5.8.3M33.4 41.6c.5.2.1 1.2.2 1.7.5-.1 1.1-.2 1.5-.4.6-1.9-.9-2.4-1.7-1.3zm-4.7.6v1.9c.9.2 1.2-.2 1.9-.2-.1-.7.2-1.7-.2-2.1-.5.2-1.3.1-1.7.4zm-5.3.6c.3.5 0 1.6.4 2.1.7.1.8-.4 1.5-.2-.1-.7-.3-1.2-.2-2.1-.8-.2-.9.3-1.7.2zm-7.5 2H17c.2-.9-.4-1.2-.2-2.1-.4.1-1.2-.3-1.3.2.6.2-.1 1.7.4 1.9zm3.4 1c.1 4.1.9 9.3 1.4 13.7 8 .1 13.1-2.7 19.2-4.5-.5-3.9.1-8.7-.7-12.2-6.2 1.6-12.1 3.2-19.9 3zm.5-.8h1.1c.4-.5-.2-1.2 0-2.1h-1.5c.1.7.1 1.6.4 2.1zm-5.4 7.8c.2 0 .3.2.4.4-.4-.7-.7.5-.2.6.1-.2 0-.4.2-.4.3.5-.8.7-.2.8.7-.5 1.3-1.2 2.4-1.5-.1 1.5.4 2.4.4 3.8-.7.5-1.7.7-1.9 1.7 1.2.7 2.5 1.2 4.2 1.3-.7-4.9-1.1-8.8-1.6-13.7-2.2.3-4-.8-5.1-.9.9.8.6 2.5.8 3.6 0-.2 0-.4.2-.4-.1.7.1 1.7-.2 2.1.7.3.5-.2.4.9m44.6 3.2h1.1c.3-.3.2-1.1.2-1.7h-1.3v1.7zm-4-1.4v1.3c.4.4.7-.2 1.5 0v-1.5c-.6 0-1.2 0-1.5.2zm7.6 1.4h1.3v-1.5h-1.3c.1.5 0 1 0 1.5zm-11-1v1.3h1.1c.3-.3.4-1.7-.2-1.7-.1.4-.8.1-.9.4zm-3.6.4c.1.6-.3 1.7.4 1.7 0-.3.5-.2.9-.2-.2-.5.4-1.8-.4-1.7-.1.3-.6.2-.9.2zm-3.4 1v1.5c.7.2.6-.4 1.3-.2-.2-.5.4-1.8-.4-1.7-.1.3-.8.2-.9.4zM15 57c.7-.5 1.3-1.7.2-2.3-.7.4-.8 1.6-.2 2.3zm26.1-1.3c-.1.7.4.8.2 1.5.9 0 1.2-.6 1.1-1.7-.4-.5-.8.1-1.3.2zm-3 2.7c1 0 1.2-.8 1.1-1.9h-.9c-.3.4-.1 1.3-.2 1.9zm-3.6-.4v1.7c.6-.1 1.3-.2 1.5-.8-.6 0 .3-1.6-.6-1.3 0 .4-.7.1-.9.4zM16 60.8c-.4-.7-.2-2-1.3-1.9.2.7.2 2.7 1.3 1.9zm13.8-.9c.5 0 .1.9.2 1.3.8.1 1.2-.2 1.7-.4v-1.7c-.9-.1-1.6.1-1.9.8zm-4.7.6c0 .8-.1 1.7.4 1.9 0-.5.8-.1 1.1-.2.3-.3-.2-1.1 0-1.9-.7-.2-1 .1-1.5.2zM19 62.3v-1.7c-.5 0-.6-.4-1.3-.2-.1 1.1 0 2.1 1.3 1.9zm2.5.2h1.3c.2-.9-.3-1.1-.2-1.9h-1.3c-.1.9.2 1.2.2 1.9z' fill='url(%23SVGID_3_)'/%3E%3ClinearGradient id='SVGID_4_' gradientUnits='userSpaceOnUse' x1='45.269' y1='74.206' x2='58.769' y2='87.706' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23f9eff6'/%3E%3Cstop offset='.378' stop-color='%23f8edf5'/%3E%3Cstop offset='.515' stop-color='%23f3e6f1'/%3E%3Cstop offset='.612' stop-color='%23ecdbeb'/%3E%3Cstop offset='.69' stop-color='%23e3cce2'/%3E%3Cstop offset='.757' stop-color='%23d7b8d7'/%3E%3Cstop offset='.817' stop-color='%23caa1c9'/%3E%3Cstop offset='.871' stop-color='%23bc88bb'/%3E%3Cstop offset='.921' stop-color='%23ae6cab'/%3E%3Cstop offset='.965' stop-color='%239f4d9b'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill='url(%23SVGID_4_)'/%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill-opacity='0' stroke='%23882383' stroke-width='2' stroke-linejoin='bevel'/%3E%3C/svg%3E");
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-mp3 {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnxJREFUeNp0U89PE0EU/ra7XWxpSsFYIbVQf9REFBHkYBRIPJh4wrN3DsZ4MPGP8b/wUCIHEw5EY0w04o9ILcREGmwVgaXbbXdnd2bXNxPahGyczebtzrz3ve99740WRRHkWn5cebu4cH6SMY7e0jRAHr9c3WxsVvcemmbys9yT6+uHJ8oaPefypdPDD5Ymh5w26wMkEho8JtDtuEOZFCrvN/4uJZNGH0T59D58X/C27aFNAL3Xthmsww5GCyN4+uzu+OLtQsUPxPQx6ZMAoQjBAw7O+bEVCMMQgqygs+LFs1h+dGd8bna0QmXO9OL6JYgwAvOFZKKoy3V44CgNfv7Yx8oLH+lUEgvzF8Ydhz+n41snAGRG5gUEwClzhHdvttFxfNyYK0EnJozKK5eGcf1qHo1GOxtjwI+pfvm4g/W1qtJgerYE2SXJSIL9+W0jk0mCShAxDXgQKgbNXxZq35vQKCiKQkSUXdc1+gcch1FHGPmKuIgBCdc66qJQHMG9+1NIpUylxxHtuW6gEiTIu+N4yjdWgty0yTmdNjFzcwKjY0MU7MLt+IjoSad16FoIx3b/A0DZ7FYXnsdpAjUMDOjI5zPgfoBsRodhhGhZHfBBU/nGAGRtxWIOg5lT2NtrI5dL0SB5KJzLodloqXaOEatPGztKq5gG3S5DNjuAK5NjKJfPYKI0okBkSdemCiSgS/rkQNLSePtxBj4LSCwfFtE0krqqX7ZVMnu9XlMXy2l7ME0dzA3iANQyY6vWxC61UY41zTyNcYh6/QCNXQvzi5dR39nHVq1BUyuMGAARsF6tbbe4iKD1r7Om5iFBdmW1SsDflLiuB6sX90+AAQDHAW7dW0YnzgAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-mp4 {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnBJREFUeNpsk99r01AUx79psrTrujVtbceabnZs4DYRHSoMh6Dgq77rn+AfoA/+If4Bok+C0CfxVRDBh+I2NqZzrpS1DVvbtU3SJPcm8SSlsJlecsn9dT73nO85V/B9H0H78OLdt/LDlQ1uMYybIAgI9n99OWxoe83nkiz9hDDae330JvxL48O51Xxm/enNtKPbVwAh0Ec6kYpXat9Pnl2GBC02HrjM5Y7h4P8+7FtIFVJ49OrxUnl7ucIdfhv+BIDv+fBcj7p/tXMPrs2RXVTw4OX2UnFTrXCbbY7tpMsA13FDSDAOQ4gJEGUJLs0PPh9CkESsPrmxxEz2lra3rnpAt3G6adgdQhBpmeLkFodNmsjpOPoXBrQTDcmFFNS7i3MRDzzPCw/vva8ikU+COQxm14BBhvJcHLGpGPTOAJxxeLbrRgAkYujBdH4G5oWJWXUW19YL4XqunAMFhnq1BqWYgaY1MAHASQOiU96zKzkU76mwehaOvx6h9uMv7KFN3RopL4oTAI4HRh4wSl399xla+00YbR3yrIzM9SzSqgJJnoKcklGrH08CcJjnBtLLCsSEGGpSWJvHtDKNoFippsJ0ulIsDDUCCATMlBQkNuahEyiZTcLsmFBKaQxaOk53TlHeKkM70AjAooCghBOk9sKtIvqtPqS4FBaRnJSRX8tj2DOh3lFB5Qw2ZNFK5LRo6w4sKt2ggAzywidAMN/9uIPSZglBLDO5FF3mRD3wHE9qVRvoHrUpfn+UEQK0/7ShtwboHJ6jdH8RZxSC57hSVETb7e5/2u0FxqPHJow+8iZ4lYY2QGu3idhIxO7Y7p8AAwALCGZKEPBGCgAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-mpg {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnxJREFUeNpsU0tPE1EU/ubRdlqmnUBboa0UeUQDiUGCC1+JmrhxoXt/gBvXJi74If4AV0Y3sNKF0YUaICqoIfjgVShEiGF4tDOdO/fOeOaSKtie5GZu7pzz3e/c7ztKGIaI4vn9p+/P3h4e4a6Pv6EoQBDiy7P5rc1P1Xt6XP8M5ejXo6UJ+dWbuemeTGdpvNdiNe9YvQLe4Bi4PmTpRmyq8m71rp74BxKF2twIHvAo+f/l1T2Yp0zceHizfOZa/xRnfBRhG4CQqAYioBWeXDyA8Di6ei1ceXC1XBwrTXHPH2vW6ccBBBMI6BsSUEQzakGL6xB0tvjyBxRNxdCtc2Xf8R9TyaWTDOg2TjfVdw6hqIoE9B2GxkEDWlLH7s4ette2kSp0oDRezrQwCIIA3oGHr0/mKMmE53qo23W4+w5S+Q5ohob9X3tgHgO8ULQACC7gMx9mKQP30EW6mEHpYi8xcJEdzMucjfkKcrTfmqmiFYBxCF/Id+gayKJwoQjHdrA5v4HK7Cq44KjZNWpagaqp7QACks0H9znW365ia24DzoEDozOJbH8eVtGShXHTwNracnsG7q6LzsEuaAlNPm9h7DSSVjLyCMkppDI+GS2StQWA1RlKo0X56n2X+6QHkmkDakxF9WMVqWyK+s/BrthYfvWz1Ug+zUDcjMPMm0h3pxEjFma3CbIuCud7oMc0LL1ZgmElpGJtW3B+15HIGNITrMYIlOH7i0U41NrInREylYbu4R5qQbQBaAh95fVKZCnpQCnb9DrWZyrRERS6NDeUw+yHaXh7rt4C4B8y+9vkwn7kwKNRpDoa9aiFKBYnF+RcREqQ2e1m3R8BBgAy9kz9ysCE6QAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-odf {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAi5JREFUeNp0UktrU0EU/mbu3FfE1KRRUpWYheALNBURUVy7cy9UkO6KW/+Lbt0IPsFui4gLBbUqFaUuXETUKCYa0jS5yZ2ZO557b5MmTXpgmDPnfOc7jznMGINYPi0de5UvmpORxpjE/kbNqW005DVu8TWw1H758ZfkFgNgJmtyxSPRjJIj0QTW/RDiYGXGb7Dl32/eXrVsd0gSCx9miqC0ooCdp69g5Q/h6OLN0ty5ynIkwzMwUwh2FwMdcbDiCZQXlkqFCpEoPT/wih1YjLInANcD+/Ua9bu3wJlGvrBZCmet2+S6ME5g4oGlZ9A/I70XCDhhDexPNTFmswJBwcnuXkF86VSNZxVu0ukLSGnBcqlnN4HoCQIaIuIv7LUooMOgQ7q75LAAb59B9gCBHSKgqemRr94mMKmD24CfM8nb7THYGQNLpAkUkcb66JyGBFFEWRVL57gFEH5qj8Lxwca2qS3EZaugmzAw24dR/XQgwtsCSBjPIdWbUoE2UJLBnV8Ac/ciWHsK9/glWLnD6K2vgPszsOdOQdfeQ1c/ThKoTgDn9A3KUED/52d45xchZsvorD6Bf/Z60riV3Q9Z/0bbGU1uopYGkfERSQ3VbsMwl0qlqoIARmSoPYXWy0dor79LfBMEEd8jGs/uQ3Yl7PJFNFbuEXiV2riCf88fovXhBbo/vqP3t02/ZYmJFqTkzY160Go9uEMbFK8hR/NrdXtFuUVmnmySVGgO4v4LMAAjRgmO+SJJiQAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-ods {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAetJREFUeNqMUj1IHEEU/i7u7Z23e8tGgneGQPw3hZDkkhQiSuwMQREba4uUgpVlCrvEQhurkCoWqcQQ0oTAaYKNqJygGEwgHCSB6Knn7eXcdX/GmdHVPWYFP3gw78173/vmvYkQQsAwNvckq96UnyIEh7/d4t7uUd/8y+85P+bXSX4grkhI6nJYPW7LrXpBK2YxiSoShhu4Buq1NPofDeqdrZ3Z4cl7D4J3UtA5VyVAlmJoru9Af2ZAp1lcCQ3nqgiuKmbY3l/BH+MnHM9GVLP0Ww3KNA33CQoQQnL834Fj74PUGkANEIkCSSsa8gQqgYTIcB0PVsXB318GInRiCVWCkpRFAs+j5gKlA4t29Ggh4d0t04FKt9PQqF4UFgumSEA8ApeaElilWbYRVy/lsns/N1QBkxtENF4jxPxcgcB1CZVOrvMteK5IQDtJJIGh++PcX9iYwWjXK37+vP0WdYk0Ht99jtX8JywWFkQChw4tc+cZcvlF7rMze+ubbxN40fMalRMDP/6twaiUeK7wlZ0TD0a5hLTWxo2d45KKprqHKJslTsy209s2wnMFBTYNZjc/oLt9gPvLOx+hxVJIKS2YW5pCbSyJTGMK775O8VyBwDJd2LTDl/X5i8v3S7NVw9vJb51tITDEUwEGANCx2/rXEEFFAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-odt {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAepJREFUeNqMkz1II1EQx/+7Ca6JkqyYiJ8cKEpAQbBQFDm0sVOsFBS9wt5KOTgEG5twxVlZ+XEnKNiIghYKxx5nwEpIIXaiSAgKGmMi0d23u8+3T7OaZJEMLG9mmPnN/w1vBUopLPNNhRWXHOyDg0nx82TiJtZPlPVoNpftc2cTotcHtxx06kdXpSQ/BvzKESZzIDmAz6y+NojOjpDMZiqRPIgNoFyWM8DrKUV7axO+gcp4g7AzmquAdVNqOgL2z2I4id1B0wgeygOyt/rLL5buLwAIDgA9dY+L+DkuDQOCrkMgBsRglcMOqAGwIstMg8AkGsuZMNUMRMkLqE+QGloglvlA7uIOAKvZajR0qJkUj/XHe0BTIclVKKlrfKsj9qA8gA6wqSJzPaXlr7ky//tdLEUfawsBjExUFGVWbT7AxSa42H2LMfODmvd3wKb7RAMLYwM8nts8xJ/pEe7/3PmP2eGv3D+9usb35W0bINoA7RmjXSHsH0f5Z/mUSZ0Ir2JmsBtD80s8/rGyzWsLFTD5yUQCbfUBHl9d38LvkdDTXIuHVBo0k+bbt06qO+yAPGXwe/cA4wO9PN44jKDG70GougIzi2tQ00ms7/3lpwnBBgjZ37Kkd1Shht5XzBIFl/ufFtniT/lFgAEAU//g6kvdGBMAAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-otp {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAcJJREFUeNqMkssvA1EUxr+ZjkdbrfFKVD12ErYSRELY2fkH+BMsLcQaSwsrSzZi47EjJEQkEhYkFlhYSVtFpdqOqpk717l3jKZmiC+5mZlzv/s795wzCuccQncz3YeRBj4KHz0/RrOZe2NsZPP20o255zQ3EAxzEAC+6uzTw13G4TFQAakA/CWtIYbY0KBOrx7IvwDQqlHV1o3YxKTOvyAUvfQCfqmA3e4ikyS/zRAKvOot7eoSHEgZIHrCfQAfBqBaKQQDKScQAExd8emBANg+2U2CvNMkkgSqBmrCxFB8mujeoJBWwEqARcssKTAJEGrmaGrjqK1zvNknH4BtyxKl2VUpRxmj5W+x73q9AEaZrR/ND1EJluIpS3i9JQiA+a+hSq8HwJjTsLrRaWitPTCOlhEZn5N75sM1qigmlN+dB3u++Qao5W4TtbEXXIsiszGL4PA00itTsu6XnQWo0TjMTAJqfMDx/ryBJcaVzSNSH4fW0Q+rkIf5rsjRiid7yyN7uoXS3Zn0egE0NiORAN9bQ017D1Lri7CLlP2EDr3Rf7C/itzV2bfXA/igLDaRixfngFhSCooH2xVPCWBlwKcAAwBX1suA6te+hAAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-ots {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAfZJREFUeNqMUk1rE1EUPS8zmabJdDKB2glEwY9ExJYiBUEQpV25qgtBXfgbpEtXuujKf+AfEKRddOdOGHClbYVCvyKWaijT2mhjphk7Sd7Me76ZONp0EsiBYWbOvfe88+69hHOOAE9f3zTVnDKNHvhlsfqPw/rM0ovyWsRFdXJEpDIyRnSlVz0KSkmvabaJeXSJBEhgAJzTDNybmtUnS5Pmg/lrN07H5NM/f13FoMgpXDSuhiIiK3Qi6LUugX7FAbaPPsJqfIHHKCStqRsXVFPQuZgD9BBxjikSiRq41AAkgCQBzVf0+BWEBX7GBm0xgHHUqk1UbBuEcIydzyCZlOI9YEGuDxwduCCitS3Xh3viCZ4jrcq4PJ6DLHd67tjtuAAXib54dCPVEfQ5XIcik/0/2iDeOYz3ceCxrisMi904y0XiMQFfkB7lg6xFHwFxEqUMV0anUNBLWKm8xd3i4zBWOzmASx0UsiW831mA59Xjm+h7HCOygduXHqJatzA7Poey9QnXjTuoVD/j/sRcmDOWLgqnLC5A2wwST+Pn8T629lahSCo291bwu9XA7vcy3m2+gTaUR14thrk9BXasbdiOjSe3nmPpwys0xSi/HpbDd3bIQC6dx/q3ZbRb/j8BEi3Po5cTJpHI9CBNDEa++GyDBN9/BBgAwfDlCVUQaNAAAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-ott {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAdFJREFUeNqMU89r02AYfpJ0iVm7EqhVOxw7dDBEdpiCE1RoEZRddvUgbIex/Rs7eehppyF4LOzQu4MxwYp0HgShIuwwUVSCVtl0s13afl+SzzcpyZYmyF74eN583/s+PO+PSEIIeJZdrtQVI19Cgmk/Ph39bpllXq82g7sgLxVcyKNZpIx8Uj5u5zSjc9Gov8ZihCRC8D+7On4JczevGeTGSEIC4ctKJtB1DTPXi1iCCEkIm1EFlC2Em0iwtWfinXkIzjiO0jljtDC5TtflGIGUQMB+mfja/oPv2Rx9MMjpMdJxOXyXTwkcwIkewfqQ1QtQNB385zcI14FrtQexsSb6SRysZ4Fbf+F6eHwATc9gJGNAm5iCTL5n/LCVRGADNoeaGoHqyaXj5gqQlTODovcwNk5Aj6wXqV8eCo7EDhMonEHpW+dZC7gUG98D3geo7vkb01h9cAvPdt76OGy1xntUd3bjUxAk3+l2sHJ/FgtrT0MUJNfDSm0bjQ/72Hzxxo+NK+h3B7XRNO4UrwymQtMIkdTBU0m+sBOayLsn8Ka78mQDjx/e87HXPkb1+UsfP37+AmZ1fP/suknBb6nefVQXjl06TxMlJfWKNWr+Kv8TYAAkUueexJF47QAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-pdf {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmhJREFUeNp0U0trU0EYPTP35qYxaW6TlDapNKWGbgo2FkF8rARB6rboXusf0F/hyq2U4krFqugqSBeuAyL4SERBstHa0iR9JKZJ7mvu+M0tqZGkH3x8987jzDnnm2FSSqh4ns0VU1ybFzj674Wa3uWiWbfsFQb+jrGj8Xvbm0HlvYVRxhJprpmTlGmum+OMm5uNPZNbtjk3l82ey8++8oW4Jv/H/wdA456g2kvH99FyHNiuAz2dwflbN8YW8zMK5Go/CMfQkAhpGsyQgRCtlpE4jIULyC9fHzu7MPPEl/5ib6WOE0JJNRiHHg6j86mMjw/2gG4bkbY4PW4Yj2j64skA5FTHdaEMPiAJszt1sK0d4suJmY4k0+IDDGRfqmh0u5gejQc+fG8eYCIahRQCEfgQnIuhEkgtONE+dGxYxEDj1DhiEycZ+1YXdUpHCqTMJIYyEES5aXXQsi2kYlGEia5GtHVKn+amPBeCutPgfLALPuVu+xDVPw2EQyFEjHDghbpYNm1yKVVnYjTOerepn4E6XQmLGSPkPkOXWATMSDcjQEkAaqOu6+i/rccALtFL53LI3r0Nq1ZD4/MXZJaWYFer+PXiJc6s3IEgY3+uPYZHTAcAHM+DTE8gnM1CSyaCulv+GrRy8uYyElcu4XfhLVpkpNtn/DGA5Uu0abFH36WnzzCayWAkmYJvWeCkfb9SwY+NDbSoOx4bYqJF8rZqVRRXV/HhzWtUSmWwmWl0RmN4v76OUqGASrmMOkntSHF8MOs954dT08W248wzYsJDOujRBAaqqikTpRo/qqd0/dv97c3Lat9fAQYA4z8bX9nTsb8AAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-php {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAhNJREFUeNqMkltrE0EUx//ZbDaXNrvZzdIkbYOXGgxYQlCK2IIY6EufxGdB8Av44AdR8AP44JOPBR+Ego0PClUKTTXQSmkTYtOkmubSJrQ1e3H2yJSEJNIDs3PmP+f89pyZcdm2DcdWvn7LzkxFHmCIra7nm9ulg8yLZ09yXON55Dgjt1PM2iPs0+aW/frdh8bzV2/SvQBnCLiEqcFxLKSSodlrU9leiGPihWePBkgeEZO6ShC2dCAZNuf6ADb+ldQ5PUPx4BCFcgXfdwq4Ph1Dtd5CZi4Nw7SQiMdCXkl6yVIy/QBWgcU+yx/XsLK2cdHndqlK/lZxH/OpJO7fnsWY3z/YAq+g0TmHpoUH2vB5PXi8RD9Fo10aAmDJTgWyIuOupmK38rsPcOvqJO33XWEvwLJsmKxHRVEwf/MKWl/yUMf8mIloWN8rw+sP0D6PHQmYuzGNgCRiMZVA17IQV4OIaTI8buH/AJMFd02Tkp05PO4jnWvc57EDAINt7u1X8Pb9KgI+Lxbv3cFR8xjx6AQ+b+Txs/qL9KePlih2CMBCq92hg2qzt1AoV7H5YxdhdqhHzRbgcpFeqdUplpvQW4FhmAixZ/sws4BoWCM/qmsE5XqE3dDQCrqGAYWdejqZgK6GUD8+IV9VghBFN1RZJv3sT5diBwC15gncggCPJKF0WCPN8dun55jQdVpz3Ynl9leAAQAJhiGatD9AOgAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-png {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmtJREFUeNpsU9tOE1EUXXPp0CAUWmJbC04xBANNTF+kKhG8fID6aqL/gPEj9E0lIf6Dj30HL03wxQtVIC0QKrWxNG1Dk9Z2Oj1zxn1m0oIZTnIyZ8/ee+211z5Hsm0bYg29fLGpxWIJWBYGS5IA8ncKhT9Wvf4Yqprtu+w3q85X7f9QxseD/pmZMZsxN9fnc5JNw0ACGGv6tPSvyvEDKEoWZ5Y8OHHObKpucw4B0t3agnl4CJPs2YkQVu4s61ORaBqMJc8CDBiIRhhVM9bXYdVqYAcH8M3NgS0tQQsFcfdKHEbvlr6WyaR/V6uPKPy7B4DT7lUq4MUipMlJ2MPDUKtVfKZ2nn/5BoNbkONxXeb8LYXe/A9AJLNWCxgdhZJagDI9DZg9qIkEytRSkdqTSFQtGILSbgc8LViM+tc0yPfukzIyOJ359k9YR0eQdB2KmBbpwXoM3Dod1SkD+scpEapCI5DdpsJhIJcjajQZagcjI+5oLe4VkeQnyiZgdIH2X6BJ7dSqQLfrggjw0AQwP+/GegCIHppNoFAgEMO1RZKo7BQgRi3yN05cnwdA0BQMAgF3C6pnbuNg92M9AFT1diSCh6kb+FGvo2MxnBB9ocZxp4Mns1cde213B81e7xwAcl4jkaa0IUSjUdLJwkL0Ej6VSvArCt7l81iku6GrKnYEU89VJlSJRmR0Dax+fI9suYxSo4HlWIw6M3FBlnD9YhiXabyOsOeIqG7TzDeIYo6EDGp+ZPb2kKKqH8h+mkxiI5/D1/19J3bwYPvPWXq2skkiJVxesqt0XzghpKM8nRVV2Lv2q9eLIvSfAAMAaacnllcFBmYAAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-ppt {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAkhJREFUeNpsU11rE0EUPTM7ySZpmzT9DNamWAtFfSiCigr+AxF9zKtv/hvf/Aki+FEi6ov4ItWHPGiwiBUKoUqqTUJImmR3M7Mz3t0kNe1m4LIwc+65595zlxljEJzdR5uf5nLmsvZx6gSvtd9W9bjhF7jg5dH9nRc/wq8YXaTSJptb0xklx7IZoKUEz1zJ2DUU69/37vFYrDxegJ9U0lC+AoIIVGg9CL+vIObP48KDQn7x0sWiVnJrnEDg7KGk+i/Ac4iUM/R7BsmrSSxtXMfa3X7el8+Kjf3KfUJ+iRJQw4w0Tc8BRyWGRAZY3rBR/VlC+XED2ayDhZyXl03+hNA3TxNQshlGLAnE44zCIL1goXZwiMNvB1i6zbC0KuAsxNITWwgNMYPeLVJiFEO9ArjHAivrAjNzBr4f4vwIgdGD4YUACsZCE8AtYGWT5jCsGQw5wEYJzP/pj5RwYTA1b07eQmfZ8P0sgdaM2FlYwWkMgMpl6NQAO33GKM0wsQWflkh1uqGVmVWblsiDkQyqxwfag35SqcktaEWTUTHYNx4iGU/C29+BvX4Lpu/C7zYgFjegSY63WySsHyXwpYHU00ieu0bAOuJbBTArBkiXKiaAmTzcvRJUV9E8rOgqBwqlY8ASs/AadbRLb8CzeTjVClqft6FdB17tL7yeCbFRBYoLr6vR/PiSEl5BZJaBD0/R2nkOZqfQ2fsKt+0SEQ+GLSIEUvJm+6jbah2+pS2aon+4g/afd4SYJVuA7vvXdC/IHQtSoTnK+yfAAIEaId1m+vudAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-psd {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAqxJREFUeNpsU01ME0EYfbtdKKWGtoItRWgJHApCBE2I0YuoiSaaeDJeOJh41YN3TfTixcRwMfEk8eDJGA+Eg0YTTRRMg02KKFooCBbTlkJLS7f7P+u3K9Xo8iWT3Zn55s173/uGM00TVlwZfzJztD92iKO5ouvQGQPHcQDN380vlDPr65fdLj4Oa41i9sFt+ytgN7o7woGOrqgvvpLBaF8vWj1NUAwGTVNRM3mf5vU/zaU+XySQuTqIFXz9hxmGLkoS7r+YxvVnrzGzlgXPDOzUZPT4m3Dt/KlIuH9oUjXYEHZZ/wOgGQZi4TZcGI5hLb+FO++TSOSKcLtcMA0dI0EPrp4+HtnfG5skiUecDGwQE2MjAwiGWlFVNDz+tIyCokJhPKYSX7Gdz2I01hOJdnY9rJ/7UwPGTEiqjtbmJtw4MYx78S/4Wa3h5UoOYwPdIOp2Xi/t18rlFgcDw6o+ydiWVRwOBnCpL0oOAMmNEhLZIgSeoxwGSWcERon/M9DoBknTIdNQNAMnO4PIVGpIFXcwndlA2OtGc4MAxml27p4AIulWSIa9QVadiYSoJxhqBJivKgh5ad3k9gaw6JdlDaqq7q5wINY4F22HaLHSDZQkBW72O9cBYFEviBIURQH7a7MN0uDisUW12ZZcaGlmdq4DwCqeTo1zNtZuW7hUqGIw7MNqSUS2ImNsKEpSdEwt5lGhfQdAkQBEoub3NNrDJfAIeBuRrcrY5xGQ2RFJAjl00I8PCckJUCB9q1URBnk38XEJEuk41tmGwZAf66s1VOh2keqwoUnYpFxHH4iKIixkN3HzVQKP3iQR/5GDKMuYmE3h+fx3MHqh1sMafztHLuiCg0FAk0uFdLqcpGY5QEXbTC/j7mIaVjc18DxufUtBJ/vcggs+3ijVz/0SYABsJHPUtu/OYwAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-py {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAlVJREFUeNpsUktvEmEUPTPzTUFmgJK2UqXQFG3pA6OBLrQxamJcaYwuu3Dp0l9iXLvVtRuDpgt3JIYaTVSaxtRHsJq2xEJBHgXmifebMhECXzKZme+ee+65516h2+2Cn2cb2VwyHl12//vP2/zOQaF4uD7GWN69e/LogfNm7kUsPBFaXYwHMeK0OlpQEJApHJTuykzK98dE98O0bLM/UNgr4v32Dj1fwSQRt9dSsfmZcMa0rIv9ODaqYrPVxuPnL1Cu1aEbJu7fvIZUIo4bqeVYRzcyv/8c3SPYpwECt/dmu4ON3Ed4TymI+hQc1ZqoE+F+uQLDsnHlwkKMscJTgl4eJOi9fxZLePNhGx6ZQRRFqH4VjZaGSv0Y6cQcJLpra0ZguIWegqDiw7lYBBZV6xiGk9DQDLzK5bEyF4Hi9VLMsoYI7J6Es5PjeHjnOl5ubqHaaJGBEkzbxplQAKIgDmBHekDTgI+qKKqKLvNApgmEgyquLs1CoFn2Y4cIeLJpkjoCLkWnUSIF3JxISIUsCjAoxhWNJLBIJs3YeXj/08oYZkOKY65HllE/bkMmY504YUd40HUq2JSSyW6iVPmLiXE/ZMYQCU+hXK3h1toqdNN0sEObyKtqtDQ6kXDwcadDS2TBryp4nX2HxXjsJK6bDnZIAZem6Tp5YMMmicn5OC4lztNWtvB9cg+hQABtWjKL2jH/T3GgBcYDXEE6mcDM6SlaJAGMWkivLBC54ZgniZaDHSI4rNSqn7/t1vgkGJPwZXffSeCjk2iUWz9+nSTQN8e6ef8EGAClUi/qoiOc3wAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-qt {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnVJREFUeNpsU8tu00AUPU5sp41NkzRxpfSZqi0VIIQqEEJUZYXECvbwCWxYsuBD+ABUFrDrCnWBQEJdIWigBSr6pqRJ1ebhxrE9M7aZmSrQ4o505fHMnXPPPWdGiaIIYrx89GKpNDdxmXkU3aEoCsT+z8W1Sm21+jCpJctQTvaerj+TX7WbnJ+0cpfuX8mQtn8GgJ4AZtIFY2Hz3foDVRcgyt+cRHcS0IARh+D/8G0PpmVi7smd0dLs+AIjwTVEiANEYYQwCHlEZyJgIQKfoX84g9uPZ0cHZ4YWmE9nuufU0wABCSSImMsWEgqSuoqA/39/swZFTWLy7vQo7dDnfPvWWQa8GuOV3IYLJXmyzDzG2/ChZ3pwbHdQ267BKJoYuj7SF2MQhiF8LuDK/Gf0DKTBKINz1IbTbEMzU1ANDW7LAfEIQKIgBsBFlAx6LYOz6MAcvoDCtAVGGPKlAiIu/F55F33FDA6W93EOAOMaMOl7biKPwRtD8Foetj5sYPfTDtxjl1f3Ubo5jkQieQ4ACSUD2iE4XDpAdbUiW9D7UsiN9WNkZgxajwbd0LGzt3keAJPUc1N5SVeENT0Ao2BKV6QzwlZeRBSKAYhe3aYHcZWn7l1EfjyPypcK9LQGa8qCvW9j9+MvaasQOHaRhGWdhsNLR8hwodYWf6B4tYjDjSOovRqq32rSYq/lytw4A77o1V2ERiAtzY5kkUrrsH+3QF2KY87ArTtQuQ6nAf4x6FCV1D001+vYersBM2vA4y1Rm2D7/Rac/TZIw4d/6MrcGAPf9htN0miJh7Lyuoyvr8rQeP9iVJcrSKgJ+TrFcyYebXTP/RFgAFQobmIOBxbsAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-rar {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnpJREFUeNpsUktPE1EU/u68OgylZXi0hZACQU1LEKKCMcat7jTRnQsXxsQtv4E/4M74P1iriUaNCw1FgxpjCJQKKAU60+m8mJnrmSll4XCTc8+959zz3e88GOcc8aq9evChOHl/lvMoubvWX/z4+BwTlbvw7bXdg8b7h6LE1gGW+O88CRMt4XTlR6/rYxce5Xv3jlHH19fPkBu+gWy5mlcFb3Wn/umeKOEMJF5C7xCFbtA9dRXjFoYKGiTRAlPGUV1aKU9O3VwNQ74A8DQAIZxqAuAhBPIMFYpQVAVB4CPSZjEzv1weH5tbDQN+JQ2Abu488mnzIbAAA3o/VK2PwDJo7r5Fy7ZRuvi4PFS6+qIXdVYD8Jg6BUcuOD8BozSLlRWyicgVKkTMQWwUlFF0Ooe5FIPk57BD7G0SiywyjD8bCDyHsOkeeeR3SUxEkROmU6BfQYFJMHfhWXV8efkUrb13VPMTsrcTQSzxZ/+n0GVA6EGbSGdgG9vo15fg2nFgbO8k70SRdd+mahDT81vUxTZRlJBRMsjq89C0EXCvSf7TIBZ136YZUJEiE7LgJ2dN01BZuE0dkIhxE7KcQTK1QUj+cwAEyrPZ+IydzRoyah+mLy2isbWBweESJEnB9q+1RM9Ub9GQOWkABg8HjRr2d9Yh0hTlBlRsfn+D4vg0BvUC9rZqECUJuk7Tzr1zahCYlB6HJAREPwfbbMBzLBzsbUKVI0qBgQkc+SxgWUYaIAqOpKwKXJ6bgGlaaDV/YvHaFNrtDsKTfVSrJeqIg/bRNwjclFIALeP3saybhu8SC4VBHwnhBXXIKocYRXD9QzBi4Xgchmkd9+L+CTAAMqwy+ZzluBgAAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-rb {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAixJREFUeNqEUktvElEU/mag5f2yJhXLwxIt0kiqsVEXujP+A925cu1Pce3WtXVtYuJCF7KtTY0NrVQIpRVKeXTkMcO9F8+9ZVooJJ5kcmbmfOe733fO1YbDIWS8+/g1dycVX7W/xyO3vdsuVKqvnE7HZ230783rlyo7bVBicSGyfjsVwozomVbIPe/c+FmsPHfoRKJd1HT7hXHBZjVbA4aA14NnD9bC2VR8gwuxPi5Sx39Cp+M0XUP0ahhP1jLhW7HFD4zze3b93ILtXYyyVKlR8/5hFbnvO9gtlrGSjOF+OpXkYviWyo8mCS4R6bqO4p86vm3v4fC4DrPfw4unj1XN6JvBaQtjChzUXK43sVU4wNFJA43Tv/B73edQwTmfIhAjCVL6UdPAj1IVFSKhCdAcAI9rnjBiAjtBYEu3GEeh1sKJ0YXR68sVIujzIhzwY8DEBHZqiLRKkicQDfvABxaiQTc4Y/C65pCOXwcjcmlvJgHtlwi4epYifiQWgmoLZwPW6HQG07LgcOgKO0UglAKOTt/E+09fwAiUWU7QAE9xUK3jbvomsispZVHMVEDSZdHo9rCZ/4VIMKAu0XGjpU7d2S8hk0pCELHEzrjKnCQOYJoD+Dxu1RyiwUm5LaMDo9NFt2cqDLvY4oQFp/QpfT/MrmI5FkWebt+NpWto0j2QmQkOjZ9hpwhqjXZzM/+7LU+cc7lRrjXh8/lVLRK5ovLWXglOsiOxdt8/AQYAzv8qbmu6vgEAAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-rtf {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAe5JREFUeNqEU01PE0EYfnZmd5FSvgLYFuwWt9EgHyEaox68eDJevHvwJ/hTPHv1N/QgZ2NC4g3kUAQKFKGhjVKqRrvbnRlnht262FHfy+y8877PPM8z71pCCKh4/ebt+rJfXEz26Vjf2mnsN5rPKKWbVpx7+eK5Xu2kyMtNTd5d8MdhiJ9BOO7atFI9ajy1UyAqSPIRMR6ZmoNehNHMMB7fX/UWvEKFMbYKE8DfQnAhwRmmJkbx6M6S5+WmK2Evup2c9yUk2nnKA0XVcSiGXAe1k5beP1i+4RFCXqnPywB/AKVzK34RjHNYlgVKCH50w7EBBogbTa/AVM5SgBdn0gc2AMDjPsbFPz2xye9asweS6n+NTbG8BCCfUtLjff2WoVnVpAH6z6hMUtJE3EykYfpF4vUiL3QNS7FMeSAQRBHW3r1Hq91B+VoBQRji4+ExFsvz6Hz7jm7Yw5OH92AcJKW9G4SoHhzhy/lXbB98Qmm2oCXN5WawsV2TACEoJXqwTKOsb3BtR2ucmZxANpPB8JUhyPnHWDaDpfJ1eZFALzJJ4MKO5MEtv4TSXB7V/br8iQLMz+almRZWbvoo5q9qRlxwewCgeXbe3qrVO5ZkUD/9jJGRLPaOm6COi92TU1DbxYe9umRD0DrrtJO+XwIMABWp9nS+FgaoAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-sass {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDNDMTBBM0JGMTE5MTFFMTg3N0NFOTIyMTQ2QzhBNkQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MDNDMTBBM0NGMTE5MTFFMTg3N0NFOTIyMTQ2QzhBNkQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowM0MxMEEzOUYxMTkxMUUxODc3Q0U5MjIxNDZDOEE2RCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowM0MxMEEzQUYxMTkxMUUxODc3Q0U5MjIxNDZDOEE2RCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Po72XUcAAAJcSURBVHjahFJdTxNBFD1bykc/ttvdtttWGgI0bYrUgDZoNYqRJ014kMRXHvwB/hQTH/wFhMREJfFBQxBjhMRIFEQSCAlQxKYGggiU3e3HbnfX2bFt1EU9k9m9mblz5p4zlzFNExYmpue/jmTSZw5PZAl1MAwDT0c7O72wvPdudeNakPNtOZ0tsM7cvzdOc5yN5LDAsTFRAJks/kC2PxFRVe39Si6f4byez62EpAEH/gNN18F53Ri/Ocxf7OtdLMpKT42s/ZPg1cISJp/P0tg0TBzLCoK8D7eHh4RkLLJ4cCz12AjMXwgez8yhqtVo3NbqRKlcxcSL16gZwJ2Ry8KVc8kZO0HdTKlURn+8G6PD2SZhLMQj96WAiMAh2RXFYKI78lcJcx9WYBCycICnpNbojUWpD5Y0C4Zh2D0w6hWc70uQZC+IWfQZrXF0IsHvY+meBd08haAhoVMMQFJKWF7PNZM+klhRyogGhbqxOIXAMOtEwGAqDqVcgbVkkE+5UsEAWavf0az2t0ZqvK2qabh6IU3joizDwTgwej1LdVfJXkdbK8mt2QkayO99A0/0trQ46I1lVcX+UREhnsP34yLp1AD1xibBMuntpzU8mJyi3Tc1O4+l9U06n7x8Q/8PHz1DrrALt8tlr0CrkbJMHTop9Sk5sLa1g8L+ARJdnShKClY3tunN69t5iGLYTlCtakjFY7gxNABdN3B37BaqqoYT8pyX0in4ORbRkIA46YlDRbUTbBZ2Jb/Pw4qiKFnapcpPo9pdbrg8DjAOBsFgELJmsGs7eWkkc5bu/xBgAHkWC6UPADTOAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-scss {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RkM4QjYyNDVGMTE4MTFFMTlBREZCNDNEM0ExMTk0MUIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RkM4QjYyNDZGMTE4MTFFMTlBREZCNDNEM0ExMTk0MUIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpGQzhCNjI0M0YxMTgxMUUxOUFERkI0M0QzQTExOTQxQiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpGQzhCNjI0NEYxMTgxMUUxOUFERkI0M0QzQTExOTQxQiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pkf1yeMAAAJbSURBVHjahFNdTxNBFD0tLULpB91uodVWPmorUIxo0VSiNSExMYYHE33l0Ud/in+C+OSjYgjRGDBRCKJIUkIEWi0WKlja0ul22+5219lJ26gLeiezuXvn7rnnnrlrUFUVms3Mvd2bjIyezRVLBA0zGAzo6jhjm1te+7EU37rFO+w7JlMbtG+ePJ5mOaZmci/nsPl6ONBtw18WDQc9tZq0sp7YjTisXV/NFKRpRvzHpHodDqsF03djzuvDg6vHJWFAprF/Arxe/oins6+YryoqCiUBvNOO+7FrXMjnWc0WyIAOQP0N4Nn8IqqSzPx2swllsYqZl28gK8DDyRvcxKXQvB6gISYpiwgH+jEVi7YAfW4nEqk0PJwDofNejAX7Pae2sPhhHQoF63U5Gai2Bn1epoPWmmaKoug1UBoMrgwHabIVVCx2jdrKFwm67TZ2plldPQGg2cK5HheIUMbaZqKV9In6giDCy3MNYXECgKI2gICxoQAEsQItpNCHWKngMo01arTY/jFIzbutShJuXh1Fm9FImYiM7tTtKOtbO+toN9Nc+fQ5SGUOIVYl7HzPIH2YRZ0y2KZ+sVzBHn2v1mpMGx0DTaR3nzfwfGEJdybGkdo/wEigDyvxLzg4yiESvojZhfd49OAeLJ2degaSLIPOO6vwgiYaaRErTRREEdn8MeJbSVZ5M7nLdNExqFLaQwEfFfACQn1+HBWKSKb3MT4Sgstuh9vVDa+bQ4DORE6o6RlspzMk9TOPfr+fiLJCLFYr3TZSKNcI7+aJwWQmPM+TkqRg49tu65f/JcAAMwMas6WUKd8AAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-sql {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAh5JREFUeNp8kctrE1EUxr+ZyXMkoa1NBROaSkpTBE23PhZ25cql2y5duvAPUdGFS1FxIRRBXZlFQ9GVdDENIhGJxkDsw2mneZnM83ruNZlOmNoDhzlzz3d/9zv3Sowx8Ch/qlYK2XM3cEJsbH0+qjV/rd6/u6aN18b7RMFT+9aosP/Ex+0ae/puw7j36PlKEMAzctKJ3aGFamMHjV0d+wcGitkMrpWWp6hVIciEk2MAOwbUWjosx0UiFoWqJpGMx5DNzODq5aIPoa82AWBg/lyKLMH1PMp/a9XvLXLzG1cuFlBaWpiKxaIPSLY6CaC93ggQjyiQZRkeQSzLRovGaPciWLt5faSWEBoh6KBvOhiaNga0+Y9pwaFxvu7rfp8F5pWDt+qNMp2IijHGwddWCvN+33/CoAOP5nVdT9SdoQ1JkggiQ6Yvr7V60+9z7akA2gfH9cRF8hO5F5Ve4lQAF9uuK+qFsylkzsQxrcaQm04hdWkR83Mzfp9rQ3fAFzu9Ph6+WMfjl6/pGBdb2jbKmx8QlRjWy5vkyhUZBPgOeGNHN9AbDLGUz6He2hVj3Ll9C8/evsdgaMK0HV8bcmDTU0UUBYXcedR+NLGnH0I3jvDk1Rsy46FP4C/1BtrdntCGHNiOAzWZgEKQ5Qt5lIqLojbaXSQTcRy2OwT4SZqk0IYAOgkVWUE+lxX/zb0DpFNpkTzmZmfFtzewhHYcfwUYAMZmVaZQlLFHAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-tga {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnxJREFUeNp0U89PE0EU/ra725K22ILRGipb22pMG6JcSEQTbUIwnozxpBcvepeEP0KPogcT/wlNT17kIKbEmChFUYKGVtL0R2gLtNCl3Z1Z3+zSAlonmezOe/O+973vvZEsy4JYnqdPMu6RkSQYQ29JEkB+PZcrslrtPhQl23VZc8/tr9I1yMHg0EA8HrBM04lVFAhoY38fSSDQVN3pfKV8G7KcxZHl6v1xblqU3eLc3p2VFZjr6+gQgwsnhzGTuq6Nhs6kYZqXjwL0GFhEl3U60OfnwWs1GGtrUKNRsKkpeIIBpKIRtI1J7cX7hXRhc/MOhXw5DkCZGG2zXAajzFIoBMvng1ypIKOqmP30GW3OIEcimovzlxRy5RgAFwDEAIODkCcmIMdiQLsNdWwMZdJlg8pzEUt1aBhKq3XinxKYqF9yQbqRIqsMy+0Gyy47bKgUWXSLtDENE5wdtuqQATm50F1VnPbRGeEw8HXZbiV8fsDvI9ldju9vADAyihLEbrWAZhOoVp3z6iqBUiB1A4nEfwCEsbkL/M4TgE5n5jDx+oTEzp1d8m9tC8H6MaAB0imzx0NU/WKUYE+loEyawDBo2ui6TGfT6ANAxrvx87gYCGCxXEKVJvCWFsG3eh1vN/J4OD6Od4UC8o0G3TX7TGLHwI9iEQmvF9X6Fh7F4/iYy+GcLOMSlfEgGsP0qdNOmX0BiGKpVkV1bw/1nW2b/gCpf1PTcI+Y7eg6ps+G4bG4PR99SjAVo9HE4q+fKNE0vl5awuSohjeijbRefVjAtUgEQRK7Yhi9OKn7nKWZxxlSPWl3QwgnaIrW8QMhD542vUbx/W49m7sq4v4IMABOqi3Ej7bAEAAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-tgz {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnhJREFUeNpsU1trE0EYPbMzSTfdtInFtkkpiaXVWou2FRUEn/so6JugL/oH/Af+B1988if40jcFERQURNBSQdDWlLQN2lsue8neZsZvc7FoOrDszM75znfOmVmmtUYyvry++36yfOeS1qqzDtvH2P76ApPlW3Drb2sHex/uccHWAdbZX30kO2+B3siN3zhTnHuQ66+95i423jzFzOVljBdKOZNHazvVT7e5wF+SZBj9iZJ+3J11mbW2kR8T4LwFli5i4fqTUvnczTUp9RLtDhKgJx0q4dEwWAxrREKICHEsoYYXMXvlcWmquLgmY71yCkG/c0AkARgLMZpnMDMpGNzEYe0dGp6HwvmHpbHC1Wf9MnFCkHQOyYEPzSJwQ2B65Tm5NZG3Fshim6wbMNJn4bpHowMKtIqo2COgR2IcAptwjvcgo6i77igjEmVDqbY8xQJ1VwRULhiBI6+G9Zf3cbTziuzIDkmHSNqECTFgQScEcYuc2NA8TcdYwXD+GkK/TYVN+u72WrIudiAD8o6oAR2RRCmQMjis3CIy1iSpPySCXhFTXeyAgh4BR+JVw8pauLi0Cp4yCX9A90FQhnSBYtnF/k+Q+HYam9itfIZB3QvT8zj8XSW5EhNTs9ivbSLwPUzPLNPJBIMEKnaQYg6aB9+RGR5F5VsNgnNKXMI1NdJGG5WfHzFVLJ7k8c8xUngpVodlDSGbFYj8Y4yMpOG09lHf3yIFPzA3fwHZTAQVtU4JUTeFDrdgDdlI8wAz5Qy2KxswReI7QODZcOr0ZH3q2hIDBI7zq16tuk3FNPxAI4wN+pkoccYoE4YJU5EdUtM4Qst26v26PwIMAKj3P/2YUKgYAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-tiff {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmRJREFUeNp0UktPE1EU/qYzHWstlrYJNcWUElyUJsaNGh9B0g1Lo0v9Ey78EbrVxBhXuHShm25YGBJRQpAYBDEWpaEPEhksdVpbyjzveO4MfZDCTWbauefc736PIziOA77OPH2yJCcSGdg2uksQAKofFou/7VrtASRpvVNynj13f6XOhjg8HAlMTIQdy/LO+v3uYUPTkAHCTb+cK+0pdyGK6+hbvu4/xiyHbncYAwfR19ZgbG/DoO9LsSgeTd9JXoxfyMG2rvQDdBlwIZauQ5ufh12twioU4E+nYU1NIRCNIDs+Bt28mXzx8VNuZ796j9q/DgAwomwqClilAmF0FE4wCInAlkjO4y+r0JgNX2os6XPYS2q/cQyAcQatFjA0BPH6NYipccAwIGUy2CVJFZInkKlyJAqx3T4/IMGmJkeWIWSz5KgI5pdhb3yDXS5DSCYh8rTID8s0wexeVD0GtMd85KkkefFxUfE47M1NokbJkByEQl6tL+ouAI+MUwbFhnYbaJKc/Sqg0x4H4eDRGDA56fUOABA9/GsCpaIHwr8FOhQ823O5RfW66tUGADhNy3RNRDjcN41HLxdQ8J6jYTsOQLfOJBK4f+s2/uoathoNGKT1MtFeVHZxdWTEZfEq/wMKl3rCJOIzTV6ADs2R5ulYDDNkYjp0DhrF+zCVgkw31+v1UxjQZkNV0SADd2o1MIuc9gmY+/kLxb0/UFoHePd9A1qzeUoKpilx9xcLWzgg+u/zeVfuQqkM9bCN1ysrWKXxdtPgvScwUAm58XZ52W16QyPtifRUzi588GbEi1ztHPsvwAC4uC9qhnsZvwAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-txt {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAeJJREFUeNp8UrtOG1EQPfsyXiyzBguIJSyChZBBEFCKpKHLo6egpErNn8CHgH8gkZIiTSIXLhJAWCgkoMgRMSiRBSK29z4y9+I1d/HCrFb3MTPnnjkzlpQSynY+fP70fGF2gQuByCz6lfdd9Uurfvrrjes6762eb3tzQ69uFJwPsqOPC+MBEmxxphi4tlU5OGmsOzaBWLc+O9oIIVhScidkyGZ8vH62nHtSKlaI4cse6TjAfSaFBBcco0EWqyvzubmpyQrj/FXk75cQaSEMeMXU8xykPA/Hjd/6/LRcyjEpt2i7HAe4A2TeLZWKUOJaVLxj27j813EHGKCXaAJExu/4BOdiAED08riQD2riOrexyRoYc3CvsAbLGAAjZga7vgZG23WMCdBvoxKJc36TRBlMiaa2JByjNqqD8qkYc1pjDK7abey+/YhrWlfKswhpiCR96aEU9o5+QE3g2ovVWDm2Sc22bBQm8vrVpbkS9r+doPr1EOWZaQ0yFoxg2PcREosEAI4uvZhJpzFMP+cSXRbq+043RManez+tNWKMI6GN0g0Z04HFR+NoNC/0yx717efZOSbzY3AcR4Op2AGA5p/W31r9e0vNgSrh9OwCrpeCkqvZuqTybnpRqx/r2CjvvwADAJC/7lzAzQmwAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-wav {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAApFJREFUeNpsU1tPE0EYPXtpKbX0wqUQKVQMFdIXQBNCQBs06KP+B8ODGh+Mf4b/4IsGE54kxhcMBrkp7YOQgBRvSKG73fvsrt8Otoask0xmd+b7zpxzvm8E3/cRjPkniyulW0NFy2JoDkEAguOlpXJ9p3L8MBqVl4O9YHxae8pXuRlcGO7KPLhfTDVUqwUgigJMy4Whm6lEXHjxYf3XnByRN0QB/2KaH7btMlUxoRJAcyqKhdOaht7+DJ49n+2cvTnwynXcsb+kLwJ4rgfmMDDGWqvneXCZS9ND7mov5h9ND85M9y86Dpto5rUkuJ4Py3YDJpy6QGJPayqB+Njf+43XL220t0cwOZkfrNXsBUqZugDA6CbLdAiAwaek1ZU9LmP8Rh6S78GsGxjOp9FdzKJaVZIhBgGASzK21w/wbrnCk8euX+EMAjaaZuPHdwUdHVFYluuGPGCORwwYjg5rqOwccRk+3Ux0IEvntmsNG4ZmUayL/wAwKHUNfZfTKN0ZRaw9Cof8qJ/pMAyHy5KkAMTksSEJtnMenM7EMVMawbejMzJRh67bXEYiIXEAVTW50SEAhzqwfqrBcXx4VOhYm4RsNgHbsJFOyZTsQ1MN+hcohoUlkFiMT+TQFpMwXOjGpXgE+XwGk1N5pFJtKNCequgYGupCRBbCDOp0KBJc4VoP3dyBONW8uydBgBHUThqQKCk3mEZ/LoUG+RBioJO7VarAwEAntjYPiUUW9Hh4b2R7k9j98hN37xWx8fGAt3eIAdVMLn+uUv+b2KReSCZjZJiB9bV9jIz2ofr1BKvvd7G9dRC80lae0HzOt+cWVnrSKDrMJykifwNBpCgE/UAllEXufmDu8Zlffvvm8XSQ90eAAQA0pF7c08o4PAAAAABJRU5ErkJggg==);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-wmv {
+ background-image:url("data:image/svg+xml;charset=utf8,%3Csvg id='Layer_2' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 100'%3E%3Cstyle/%3E%3ClinearGradient id='SVGID_1_' gradientUnits='userSpaceOnUse' x1='36.2' y1='101' x2='36.2' y2='3.005' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23e2cde4'/%3E%3Cstop offset='.17' stop-color='%23e0cae2'/%3E%3Cstop offset='.313' stop-color='%23dbc0dd'/%3E%3Cstop offset='.447' stop-color='%23d2b1d4'/%3E%3Cstop offset='.575' stop-color='%23c79dc7'/%3E%3Cstop offset='.698' stop-color='%23ba84b9'/%3E%3Cstop offset='.819' stop-color='%23ab68a9'/%3E%3Cstop offset='.934' stop-color='%239c4598'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill='url(%23SVGID_1_)'/%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill-opacity='0' stroke='%23882383' stroke-width='2'/%3E%3Cpath d='M9.1 91.1L4.7 72.5h3.9l2.8 12.8 3.4-12.8h4.5l3.3 13 2.9-13h3.8l-4.6 18.6h-4L17 77.2l-3.7 13.9H9.1zm22.1 0V72.5h5.7l3.4 12.7 3.4-12.7h5.7v18.6h-3.5V76.4l-3.7 14.7h-3.7l-3.7-14.7v14.7h-3.6zm26.7 0l-6.7-18.6h4.1l4.8 13.8 4.6-13.8h4L62 91.1h-4.1z' fill='%23fff'/%3E%3ClinearGradient id='SVGID_2_' gradientUnits='userSpaceOnUse' x1='18.2' y1='50.023' x2='18.2' y2='50.023' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3ClinearGradient id='SVGID_3_' gradientUnits='userSpaceOnUse' x1='11.511' y1='51.716' x2='65.211' y2='51.716' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3Cpath d='M64.3 55.5c-1.7-.2-3.4-.3-5.1-.3-7.3-.1-13.3 1.6-18.8 3.7S29.6 63.6 23.3 64c-3.4.2-7.3-.6-8.5-2.4-.8-1.3-.8-3.5-1-5.7-.6-5.7-1.6-11.7-2.4-17.3.8-.9 2.1-1.3 3.4-1.7.4 1.1.2 2.7.6 3.8 7.1.7 13.6-.4 20-1.5 6.3-1.1 12.4-2.2 19.4-2.6 3.4-.2 6.9-.2 10.3 0m-9.9 15.3c.5-.2 1.1-.3 1.9-.2.2-3.7.3-7.3.3-11.2-6.2.2-11.9.9-17 2.2.2 4 .4 7.8.3 12 4-1.1 7.7-2.5 12.6-2.7m2-12.1h1.1c.4-.4.2-1.2.2-1.9-1.5-.6-1.8 1-1.3 1.9zm3.9-.2h1.5V38h-1.3c0 .7-.4.9-.2 1.7zm4 0c.5-.1.8 0 1.1.2.4-.3.2-1.2.2-1.9h-1.3v1.7zm-11.5.3h.9c.4-.3.2-1.2.2-1.9-1.4-.4-1.6 1.2-1.1 1.9zm-4 .4c.7.2.8-.3 1.5-.2v-1.7c-1.5-.4-1.7.6-1.5 1.9zm-3.6-1.1c0 .6-.1 1.4.2 1.7.5.1.5-.4 1.1-.2-.2-.6.5-2-.4-1.9-.1.4-.8.1-.9.4zm-31.5.8c.4-.1 1.1.6 1.3 0-.5 0-.1-.8-.2-1.1-.7.2-1.3.3-1.1 1.1zm28.3-.4c-.3.3.2 1.1 0 1.9.6.2.6-.3 1.1-.2-.2-.6.5-2-.4-1.9-.1.3-.4.2-.7.2zm-3.5 2.8c.5-.1.9-.2 1.3-.4.2-.8-.4-.9-.2-1.7h-.9c-.3.3-.1 1.3-.2 2.1zm26.9-1.8c-2.1-.1-3.3-.2-5.5-.2-.5 3.4 0 7.8-.5 11.2 2.4 0 3.6.1 5.8.3M33.4 41.6c.5.2.1 1.2.2 1.7.5-.1 1.1-.2 1.5-.4.6-1.9-.9-2.4-1.7-1.3zm-4.7.6v1.9c.9.2 1.2-.2 1.9-.2-.1-.7.2-1.7-.2-2.1-.5.2-1.3.1-1.7.4zm-5.3.6c.3.5 0 1.6.4 2.1.7.1.8-.4 1.5-.2-.1-.7-.3-1.2-.2-2.1-.8-.2-.9.3-1.7.2zm-7.5 2H17c.2-.9-.4-1.2-.2-2.1-.4.1-1.2-.3-1.3.2.6.2-.1 1.7.4 1.9zm3.4 1c.1 4.1.9 9.3 1.4 13.7 8 .1 13.1-2.7 19.2-4.5-.5-3.9.1-8.7-.7-12.2-6.2 1.6-12.1 3.2-19.9 3zm.5-.8h1.1c.4-.5-.2-1.2 0-2.1h-1.5c.1.7.1 1.6.4 2.1zm-5.4 7.8c.2 0 .3.2.4.4-.4-.7-.7.5-.2.6.1-.2 0-.4.2-.4.3.5-.8.7-.2.8.7-.5 1.3-1.2 2.4-1.5-.1 1.5.4 2.4.4 3.8-.7.5-1.7.7-1.9 1.7 1.2.7 2.5 1.2 4.2 1.3-.7-4.9-1.1-8.8-1.6-13.7-2.2.3-4-.8-5.1-.9.9.8.6 2.5.8 3.6 0-.2 0-.4.2-.4-.1.7.1 1.7-.2 2.1.7.3.5-.2.4.9m44.6 3.2h1.1c.3-.3.2-1.1.2-1.7h-1.3v1.7zm-4-1.4v1.3c.4.4.7-.2 1.5 0v-1.5c-.6 0-1.2 0-1.5.2zm7.6 1.4h1.3v-1.5h-1.3c.1.5 0 1 0 1.5zm-11-1v1.3h1.1c.3-.3.4-1.7-.2-1.7-.1.4-.8.1-.9.4zm-3.6.4c.1.6-.3 1.7.4 1.7 0-.3.5-.2.9-.2-.2-.5.4-1.8-.4-1.7-.1.3-.6.2-.9.2zm-3.4 1v1.5c.7.2.6-.4 1.3-.2-.2-.5.4-1.8-.4-1.7-.1.3-.8.2-.9.4zM15 57c.7-.5 1.3-1.7.2-2.3-.7.4-.8 1.6-.2 2.3zm26.1-1.3c-.1.7.4.8.2 1.5.9 0 1.2-.6 1.1-1.7-.4-.5-.8.1-1.3.2zm-3 2.7c1 0 1.2-.8 1.1-1.9h-.9c-.3.4-.1 1.3-.2 1.9zm-3.6-.4v1.7c.6-.1 1.3-.2 1.5-.8-.6 0 .3-1.6-.6-1.3 0 .4-.7.1-.9.4zM16 60.8c-.4-.7-.2-2-1.3-1.9.2.7.2 2.7 1.3 1.9zm13.8-.9c.5 0 .1.9.2 1.3.8.1 1.2-.2 1.7-.4v-1.7c-.9-.1-1.6.1-1.9.8zm-4.7.6c0 .8-.1 1.7.4 1.9 0-.5.8-.1 1.1-.2.3-.3-.2-1.1 0-1.9-.7-.2-1 .1-1.5.2zM19 62.3v-1.7c-.5 0-.6-.4-1.3-.2-.1 1.1 0 2.1 1.3 1.9zm2.5.2h1.3c.2-.9-.3-1.1-.2-1.9h-1.3c-.1.9.2 1.2.2 1.9z' fill='url(%23SVGID_3_)'/%3E%3ClinearGradient id='SVGID_4_' gradientUnits='userSpaceOnUse' x1='45.269' y1='74.206' x2='58.769' y2='87.706' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23f9eff6'/%3E%3Cstop offset='.378' stop-color='%23f8edf5'/%3E%3Cstop offset='.515' stop-color='%23f3e6f1'/%3E%3Cstop offset='.612' stop-color='%23ecdbeb'/%3E%3Cstop offset='.69' stop-color='%23e3cce2'/%3E%3Cstop offset='.757' stop-color='%23d7b8d7'/%3E%3Cstop offset='.817' stop-color='%23caa1c9'/%3E%3Cstop offset='.871' stop-color='%23bc88bb'/%3E%3Cstop offset='.921' stop-color='%23ae6cab'/%3E%3Cstop offset='.965' stop-color='%239f4d9b'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill='url(%23SVGID_4_)'/%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill-opacity='0' stroke='%23882383' stroke-width='2' stroke-linejoin='bevel'/%3E%3C/svg%3E");
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-xls {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmxJREFUeNpsU0trFEEQ/mamZ3Y2+0zIC2MmITEkUYgERFQErx5E8KTi1b/h79A/4SW3nCNeYggBYZVEMU/y3N3Z7M7OTD/G6lk2ruw20zRdU/XV91VVG0mSQK/3n1a/jky6d6Xs3G8WXS+Pw5N6LXjLLGuna/78oZKerGsYKtrDE16uJGL1L9gEOOcYd2dL1fNwrbL//aXN7J1efPMmkUqEFAk0A0VZNbFEaQCBscIkXj975y3NLq9xye8PBkAniHOFph+j2eC4rsdoB4LsFubGl/Hq8RtvYWpxTQi52o1jvWiGYaRZL0/auDgOkC/Z8BYL2Pqxidp1FZkhoDxpeaXA/Ujuj/4HoOxKKjiOiek7RUShRNQWaNYFQuMafrYCxiw4ozZKfqbYJ0EvRdl1DQyyTs8XCNTA6UELMwvDyLpZWIZNNlNLlQOK2LMJRJ+5AkuZ1S7CFFzJzk56GnUjQWlYkqCoBWFbonEVYcLLA4dNnB624GQsDBWIgfZJEgxkoChzSFWvn4VpQemDm2VwXQsXJwF1h6c+gxlQ5jgSiEUEt0wdIe7tMES+nEG2aCLiJMOIIWIr9e0DEELAMUrwRuchVAyTKimUwO75Jm6VF3Bv7imOaj+xd7UFKVS/BPJF1b/E4tgTrE49J60O5kceoNqowiuuYKa8ghHXA48U9MT2AQgyRvTThE30bQiaSGa4yLMJNFo+Dq/2cHt4CYlwyFf2S6BHwwrMw/avDbR5C1k7h1YQ4KH3Amf+AcZyEbZPv9CItzQD1l9EbtYOjv74v/d3O9RMPTDrsEwGIWN8q2yk7XNYRs9JrRv3V4ABADSGR6eQ0/NQAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-xlsx {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmlJREFUeNpsU8tqFEEUPVXdPY/ueWZIoiYZiSYKYhJc6EbduHOhgijo3t/wH1z6B0JAhOyMILhxo4kJGk1ASTAxwWF0Mpp5dHc9vFUzYwidaoqmq+8959xzbzGtNcx69PTS26ETmQtS9r4Hy/xv7MW7jV+th5yzVcaYPX/++It9u4NAv+CVR6tBUUTqMJsDcRzjZOZM8W9ZLKx+/XDb4e5/kH5In0lpIYWGUaC0YTZnBCAEKoVR3L36oDo7NbsglZwbqD6iQKOXFMcKUVfBkBAoQhlD5xxMDp/HrSv3q1JgYW3z0x0KXzkCYJaRZljru23aHWTzLiamAyytv0O9UYdf5PArqlppBfMUfu4oALErqZBKcUxMFRCHEp0DgW5Lo4N9NIN1dF0XXsVFOUyPJTzo+WBANDidjp8tgHGG3c0DnJ4uIRf4cOCBaW5KjY8xkZL72xpJ9QcFz5bVqHUJGHZL2YtNmKi06YCyiVFb4s/vEKMTAf1p4edOG6mMi1zR6wEpdUwX+vLDtkCzHoK7ptcM6ayLmGajvtex4PliyoIkFRjmUEASelB2rXQRSfjUCT9PlWpmW21iTGzCAyEkUixPRqXhe2V4zKczbdmybgkpJ0cGOuA6Y2MTCsKoi5HsNK7N3MN+uwYaWbxYfoLLkzdxcew6lrYWaZhm8PHHG3zffp1UwJSHz9vvkU8PodbcQYYYS5lxYkxTkGdVDQdV1Js1qPgYD6JIuIE7gsXVefIhIuM05k7dwMbeMmh87a18ufIMaVYyprrJLgje2Nr+1tzYXANnDnr3zRhHj37Vvy2wpXHtNAd5/wQYAD6WMuT2CwoVAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-xml {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAilJREFUeNqMks1PE0EYxh+g3W2t1G0sEqyISynUFJsSOShNwCamiYZED3LgIkcuxoN/iCZePZiYGD2aGD+i0F5KMChxlVaakAK2ykcAt+WzdLu7zkxo3WZL4pu8mXfmeeY3885ug67roPFh5nvc62m9hjoR+5LMp7MrkYf370qVtco+VtCUFpbj+jGR+JbWn76OyQ8ePwsZATQb8R/hanZgINgj9IqeuBFCw1Kt9OMBnNWCs24XwkG/QKYUEiGjVAPQof/rq0783pShET3ULQo8xz0iS5FaANmrHQH2DoqY+DSLSz6RzecWlnD9ymU47LYjd4O5BXqDTG4FM3NpTEkpdJ5rw0AowLRMbhUfp58gTOaD/UHmNQPI6YmvKWRX1zESHUJ/oBs2nmPa+Mgw0ZIM3tZyGoJwygzQNB2jNyJIZX7iB0lpPoM70UGmPX8zCU+rG8NDVxHwdiC5mKsPUFUN/gvtLLf39sFzVqaN3YrC6TjBauqhXhNA1TQoqloV7Da+pjZq1FsXUCamF29j6LvYhf3iISamZ3Fv9DZevouhRzzPfOG+3hpA9U9UyioOlTJ7pFeTCQS6RGzIebyf+oz5pSzWtmSW1EO9phvQ00slBRt/8qR3DoWdXbiczUiTzd52D+tdLmyTB14mx1rMAKVcRpEATjrsuElee/HXGmnFRyBOGD30C/nEDjNgs7CDpsYmnHG3YPegBCvHs9oYfm8nG9dJa5X4K8AAQzQX4KSN3wcAAAAASUVORK5CYII=);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-yml {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAdxJREFUeNqMUl1rE0EUPbM7m5Y0Zptu21AwWwhYpfSDFh+kvvRd8N0Hf4I/xWdf/Q158F0QoQ+CVsFKaLSQpt/dpmvztTOzzky6cetOpWcZZvbO3MO5514SxzEU3r57/3GpWllM/tP4sL3TarROXuSo/SWJvX71Uu80Cfhlr/T4UdWFAVfdnmsTUtvdP35OUyQKVnJgXDBTcj9icAsTeLax7j/052qM81UjwW1QJXEhMF0qYnN90fdnvdogYmvJPU0/VBApD4hcDrWRcyikfB17srzgW7b9Rh1vEvxDlI4tVytaBSEEtmWh0xsUMwpwnWjqAlcxogiHd1wiQyCu87iI/+sJtf6+NXsgpd7FWCMB50KvkYMGMbLdZgLlfj+K9K4+FnFQ2x7WntIs50AbmiGwLILt+k+EvzvSNIHzdigdJ/AmXQRhiHv5POSwYmG+cqPVo0HqDxj8uTK2vn1Hfa+JmdIkvtZ/4fOPXU3WPDpFeNWVyUKryCiIGMN4zsH98gym3CIcOTwT+XHdXrdQQHAZotE8kBPpSqPNHtBOr48HUmLOcXRJT9dWNMGYJFby91pHOAvaykSaITg+bwefdhrteDRTMSwyrFCgI88E056Hy+4Ah2cXQZL3R4ABALUe7fqXWFN6AAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
+
+.ipfs-zip {
+ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAm9JREFUeNpsk0tv00AUhc+MY6dOmgeFJg1FoVVpUWlFC0s2IFF1jxBbhKj4BSxYdscPYcEmQmIDq0gsERIViy4TpD7VFzF1Ho5je2a4thOqNhlp5Mz4zudzzp0wpRTC8fPrk0/TC6+fDtYicLH97T1Kc2vQDcs+rH3eUAxVznn0fn1DRM8E+iOdv5ct3XmZG6yVlNj6solUbgVTt0q5FGtX6vXqC6VklTE+KAO/OODHSIQPRQpsXC+kkEz2ELA0ystv84tLzyucsbWByisAGf+QAS2CCDRRLMJMmxC+i8C4jdLCm/zM7OOKFGptcO6/BTpJ0yeQB0Y+mfKQuZZG0jQgeRbW8Xdomobs9LN8scc+UPHNy4Dwq8IljotIIQEm59/RoSyM1CKkXKZNBm7kIVgyM6wgAnSgRK9vqQfHPiMFDHqyFVsLR9Cm0o4YzoAASrSjCelQfRPb1Vc4qn0EY5L2W9GEaBLcxQgFHpGbkMIDJ69e+wjJ8VXqRgKid0r7ftQdxkRs9SqA2kgAm14SSIQh9uhuLGPMnKJs/5KquL1x0N0RCsizigoDaLqBdHoMiyvrlBsHVx1wphD4BCewoqxGKKDwAgtOy8JufYuk+5golGGaGZwc1sIGoDz3AOPZSVLaHgVwydoJDM1H4DbQODughB3YpOD44HfoHgnu4e7So0uAi0stHLJ3Aud8B9bpHu6vPoSu9TtDl6tUuoFiIYOgu0+158MKmOxomtyD3Qi/3MTR7i8K0EDG1GHO5DE3X4DvNahZlJOwEkOATvdPc2//hx3mXJ5lFJaF8K8bStd0YGfnOJbMGex21x6c+yfAAOlIPDJzr7cLAAAAAElFTkSuQmCC);
+ background-repeat:no-repeat;
+ background-size:contain
+}
diff --git a/gateway/assets/src/style.css b/gateway/assets/src/style.css
new file mode 100644
index 0000000000..3e7b8a734b
--- /dev/null
+++ b/gateway/assets/src/style.css
@@ -0,0 +1,212 @@
+body {
+ color:#34373f;
+ font-family:"Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size:14px;
+ line-height:1.43;
+ margin:0;
+ word-break:break-all;
+ -webkit-text-size-adjust:100%;
+ -ms-text-size-adjust:100%;
+ -webkit-tap-highlight-color:transparent
+}
+
+a {
+ color:#117eb3;
+ text-decoration:none
+}
+
+a:hover {
+ color:#00b0e9;
+ text-decoration:underline
+}
+
+a:active,
+a:visited {
+ color:#00b0e9
+}
+
+strong {
+ font-weight:700
+}
+
+table {
+ border-collapse:collapse;
+ border-spacing:0;
+ max-width:100%;
+ width:100%
+}
+
+table:last-child {
+ border-bottom-left-radius:3px;
+ border-bottom-right-radius:3px
+}
+
+tr:first-child td {
+ border-top:0
+}
+
+tr:nth-of-type(even) {
+ background-color:#f7f8fa
+}
+
+td {
+ border-top:1px solid #d9dbe2;
+ padding:.65em;
+ vertical-align:top
+}
+
+#page-header {
+ align-items:center;
+ background:#0b3a53;
+ border-bottom:4px solid #69c4cd;
+ color:#fff;
+ display:flex;
+ font-size:1.12em;
+ font-weight:500;
+ justify-content:space-between;
+ padding:0 1em
+}
+
+#page-header a {
+ color:#69c4cd
+}
+
+#page-header a:active {
+ color:#9ad4db
+}
+
+#page-header a:hover {
+ color:#fff
+}
+
+#page-header-logo {
+ height:2.25em;
+ margin:.7em .7em .7em 0;
+ width:7.15em
+}
+
+#page-header-menu {
+ align-items:center;
+ display:flex;
+ margin:.65em 0
+}
+
+#page-header-menu div {
+ margin:0 .6em
+}
+
+#page-header-menu div:last-child {
+ margin:0 0 0 .6em
+}
+
+#page-header-menu svg {
+ fill:#69c4cd;
+ height:1.8em;
+ margin-top:.125em
+}
+
+#page-header-menu svg:hover {
+ fill:#fff
+}
+
+.menu-item-narrow {
+ display:none
+}
+
+#content {
+ border:1px solid #d9dbe2;
+ border-radius:4px;
+ margin:1em
+}
+
+#content-header {
+ background-color:#edf0f4;
+ border-bottom:1px solid #d9dbe2;
+ border-top-left-radius:3px;
+ border-top-right-radius:3px;
+ padding:.7em 1em
+}
+
+.type-icon,
+.type-icon>* {
+ width:1.15em
+}
+
+.no-linebreak {
+ white-space:nowrap
+}
+
+.ipfs-hash {
+ color:#7f8491;
+ font-family:monospace
+}
+
+@media only screen and (max-width:500px) {
+ .menu-item-narrow {
+ display:inline
+ }
+ .menu-item-wide {
+ display:none
+ }
+}
+
+@media print {
+ #page-header {
+ display:none
+ }
+ #content-header,
+ .ipfs-hash,
+ body {
+ color:#000
+ }
+ #content-header {
+ border-bottom:1px solid #000
+ }
+ #content {
+ border:1px solid #000
+ }
+ a,
+ a:visited {
+ color:#000;
+ text-decoration:underline
+ }
+ a[href]:after {
+ content:" (" attr(href) ")"
+ }
+ tr {
+ page-break-inside:avoid
+ }
+ tr:nth-of-type(even) {
+ background-color:transparent
+ }
+ td {
+ border-top:1px solid #000
+ }
+}
+
+@-ms-viewport {
+ width:device-width
+}
+
+.d-flex {
+ display:flex
+}
+
+.flex-wrap {
+ flex-flow:wrap
+}
+
+.flex-shrink-1 {
+ flex-shrink:1
+}
+
+.ml-auto {
+ margin-left:auto
+}
+
+.table-responsive {
+ display:block;
+ width:100%;
+ overflow-x:auto;
+ -webkit-overflow-scrolling:touch
+}
diff --git a/gateway/assets/test/go.mod b/gateway/assets/test/go.mod
new file mode 100644
index 0000000000..8980d9a71e
--- /dev/null
+++ b/gateway/assets/test/go.mod
@@ -0,0 +1,3 @@
+module gateway-test
+
+go 1.19
diff --git a/gateway/assets/test/main.go b/gateway/assets/test/main.go
new file mode 100644
index 0000000000..96d940496e
--- /dev/null
+++ b/gateway/assets/test/main.go
@@ -0,0 +1,156 @@
+package main
+
+import (
+ "fmt"
+ "html/template"
+ "net/http"
+ "net/url"
+ "os"
+)
+
+const (
+ directoryTemplateFile = "../directory-index.html"
+ dagTemplateFile = "../dag-index.html"
+
+ testPath = "/ipfs/QmFooBarQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7/a/b/c"
+)
+
+var directoryTestData = DirectoryTemplateData{
+ GatewayURL: "//localhost:3000",
+ DNSLink: true,
+ Listing: []DirectoryItem{{
+ Size: "25 MiB",
+ Name: "short-film.mov",
+ Path: testPath + "/short-film.mov",
+ Hash: "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR",
+ ShortHash: "QmbW\u2026sMnR",
+ }, {
+ Size: "23 KiB",
+ Name: "250pxيوسف_الوزاني_صورة_ملتقطة_بواسطة_مرصد_هابل_الفضائي_توضح_سديم_السرطان،_وهو_بقايا_مستعر_أعظم._.jpg",
+ Path: testPath + "/250pxيوسف_الوزاني_صورة_ملتقطة_بواسطة_مرصد_هابل_الفضائي_توضح_سديم_السرطان،_وهو_بقايا_مستعر_أعظم._.jpg",
+ Hash: "QmUwrKrMTrNv8QjWGKMMH5QV9FMPUtRCoQ6zxTdgxATQW6",
+ ShortHash: "QmUw\u2026TQW6",
+ }, {
+ Size: "1 KiB",
+ Name: "this-piece-of-papers-got-47-words-37-sentences-58-words-we-wanna-know.txt",
+ Path: testPath + "/this-piece-of-papers-got-47-words-37-sentences-58-words-we-wanna-know.txt",
+ Hash: "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
+ ShortHash: "bafy\u2026bzdi",
+ }},
+ Size: "25 MiB",
+ Path: testPath,
+ Breadcrumbs: []Breadcrumb{{
+ Name: "ipfs",
+ }, {
+ Name: "QmFooBarQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7",
+ Path: testPath + "/../../..",
+ }, {
+ Name: "a",
+ Path: testPath + "/../..",
+ }, {
+ Name: "b",
+ Path: testPath + "/..",
+ }, {
+ Name: "c",
+ Path: testPath,
+ }},
+ BackLink: testPath + "/..",
+ Hash: "QmFooBazBar2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7",
+}
+
+var dagTestData = DagTemplateData{
+ Path: "/ipfs/baguqeerabn4wonmz6icnk7dfckuizcsf4e4igua2ohdboecku225xxmujepa",
+ CID: "baguqeerabn4wonmz6icnk7dfckuizcsf4e4igua2ohdboecku225xxmujepa",
+ CodecName: "dag-json",
+ CodecHex: "0x129",
+}
+
+func main() {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/dag":
+ dagTemplate, err := template.New("dag-index.html").ParseFiles(dagTemplateFile)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("failed to parse template file: %s", err), http.StatusInternalServerError)
+ return
+ }
+ err = dagTemplate.Execute(w, &dagTestData)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("failed to execute template: %s", err), http.StatusInternalServerError)
+ return
+ }
+ case "/directory":
+ directoryTemplate, err := template.New("directory-index.html").Funcs(template.FuncMap{
+ "iconFromExt": func(name string) string {
+ return "ipfs-_blank" // place-holder
+ },
+ "urlEscape": func(rawUrl string) string {
+ pathURL := url.URL{Path: rawUrl}
+ return pathURL.String()
+ },
+ }).ParseFiles(directoryTemplateFile)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("failed to parse template file: %s", err), http.StatusInternalServerError)
+ return
+ }
+ err = directoryTemplate.Execute(w, &directoryTestData)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("failed to execute template: %s", err), http.StatusInternalServerError)
+ return
+ }
+ case "/":
+ html := `Test paths: DAG , Directory .`
+ _, _ = w.Write([]byte(html))
+ default:
+ http.Redirect(w, r, "/", http.StatusSeeOther)
+ }
+ })
+
+ if _, err := os.Stat(directoryTemplateFile); err != nil {
+ wd, _ := os.Getwd()
+ fmt.Printf("could not open template file %q, relative to %q: %s\n", directoryTemplateFile, wd, err)
+ os.Exit(1)
+ }
+
+ if _, err := os.Stat(dagTemplateFile); err != nil {
+ wd, _ := os.Getwd()
+ fmt.Printf("could not open template file %q, relative to %q: %s\n", dagTemplateFile, wd, err)
+ os.Exit(1)
+ }
+
+ fmt.Printf("listening on localhost:3000\n")
+ _ = http.ListenAndServe("localhost:3000", mux)
+}
+
+// Copied from ../assets.go
+type DagTemplateData struct {
+ Path string
+ CID string
+ CodecName string
+ CodecHex string
+}
+
+type DirectoryTemplateData struct {
+ GatewayURL string
+ DNSLink bool
+ Listing []DirectoryItem
+ Size string
+ Path string
+ Breadcrumbs []Breadcrumb
+ BackLink string
+ Hash string
+}
+
+type DirectoryItem struct {
+ Size string
+ Name string
+ Path string
+ Hash string
+ ShortHash string
+}
+
+type Breadcrumb struct {
+ Name string
+ Path string
+}
diff --git a/gateway/errors.go b/gateway/errors.go
new file mode 100644
index 0000000000..191426742d
--- /dev/null
+++ b/gateway/errors.go
@@ -0,0 +1,181 @@
+package gateway
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/ipfs/boxo/path/resolver"
+ "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+var (
+ ErrInternalServerError = NewErrorResponseForCode(http.StatusInternalServerError)
+ ErrGatewayTimeout = NewErrorResponseForCode(http.StatusGatewayTimeout)
+ ErrBadGateway = NewErrorResponseForCode(http.StatusBadGateway)
+ ErrServiceUnavailable = NewErrorResponseForCode(http.StatusServiceUnavailable)
+ ErrTooManyRequests = NewErrorResponseForCode(http.StatusTooManyRequests)
+)
+
+type ErrorRetryAfter struct {
+ Err error
+ RetryAfter time.Duration
+}
+
+// NewErrorWithRetryAfter wraps any error in RetryAfter hint that
+// gets passed to HTTP clients in Retry-After HTTP header.
+func NewErrorRetryAfter(err error, retryAfter time.Duration) *ErrorRetryAfter {
+ if err == nil {
+ err = ErrServiceUnavailable
+ }
+ if retryAfter < 0 {
+ retryAfter = 0
+ }
+ return &ErrorRetryAfter{
+ RetryAfter: retryAfter,
+ Err: err,
+ }
+}
+
+func (e *ErrorRetryAfter) Error() string {
+ var text string
+ if e.Err != nil {
+ text = e.Err.Error()
+ }
+ if e.RetryAfter != 0 {
+ text += fmt.Sprintf(", retry after %s", e.Humanized())
+ }
+ return text
+}
+
+func (e *ErrorRetryAfter) Unwrap() error {
+ return e.Err
+}
+
+func (e *ErrorRetryAfter) Is(err error) bool {
+ switch err.(type) {
+ case *ErrorRetryAfter:
+ return true
+ default:
+ return false
+ }
+}
+
+func (e *ErrorRetryAfter) RoundSeconds() time.Duration {
+ return e.RetryAfter.Round(time.Second)
+}
+
+func (e *ErrorRetryAfter) Humanized() string {
+ return e.RoundSeconds().String()
+}
+
+// HTTPHeaderValue returns the Retry-After header value as a string, representing the number
+// of seconds to wait before making a new request, rounded to the nearest second.
+// This function follows the Retry-After header definition as specified in RFC 9110.
+func (e *ErrorRetryAfter) HTTPHeaderValue() string {
+ return strconv.Itoa(int(e.RoundSeconds().Seconds()))
+}
+
+// Custom type for collecting error details to be handled by `webError`. When an error
+// of this type is returned to the gateway handler, the StatusCode will be used for
+// the response status.
+type ErrorResponse struct {
+ StatusCode int
+ Err error
+}
+
+func NewErrorResponseForCode(statusCode int) *ErrorResponse {
+ return NewErrorResponse(errors.New(http.StatusText(statusCode)), statusCode)
+}
+
+func NewErrorResponse(err error, statusCode int) *ErrorResponse {
+ return &ErrorResponse{
+ Err: err,
+ StatusCode: statusCode,
+ }
+}
+
+func (e *ErrorResponse) Is(err error) bool {
+ switch err.(type) {
+ case *ErrorResponse:
+ return true
+ default:
+ return false
+ }
+}
+
+func (e *ErrorResponse) Error() string {
+ var text string
+ if e.Err != nil {
+ text = e.Err.Error()
+ }
+ return text
+}
+
+func (e *ErrorResponse) Unwrap() error {
+ return e.Err
+}
+
+func webError(w http.ResponseWriter, err error, defaultCode int) {
+ code := defaultCode
+
+ // Pass Retry-After hint to the client
+ var era *ErrorRetryAfter
+ if errors.As(err, &era) {
+ if era.RetryAfter > 0 {
+ w.Header().Set("Retry-After", era.HTTPHeaderValue())
+ // Adjust defaultCode if needed
+ if code != http.StatusTooManyRequests && code != http.StatusServiceUnavailable {
+ code = http.StatusTooManyRequests
+ }
+ }
+ err = era.Unwrap()
+ }
+
+ // Handle status code
+ switch {
+ case errors.Is(err, &cid.ErrInvalidCid{}):
+ code = http.StatusBadRequest
+ case isErrNotFound(err):
+ code = http.StatusNotFound
+ case errors.Is(err, context.DeadlineExceeded):
+ code = http.StatusGatewayTimeout
+ }
+
+ // Handle explicit code in ErrorResponse
+ var gwErr *ErrorResponse
+ if errors.As(err, &gwErr) {
+ code = gwErr.StatusCode
+ }
+
+ http.Error(w, err.Error(), code)
+}
+
+func isErrNotFound(err error) bool {
+ if ipld.IsNotFound(err) {
+ return true
+ }
+
+ // Checks if err is a resolver.ErrNoLink. resolver.ErrNoLink does not implement
+ // the .Is interface and cannot be directly compared to. Therefore, errors.Is
+ // always returns false with it.
+ for {
+ _, ok := err.(resolver.ErrNoLink)
+ if ok {
+ return true
+ }
+
+ err = errors.Unwrap(err)
+ if err == nil {
+ return false
+ }
+ }
+}
+
+func webRequestError(w http.ResponseWriter, err *ErrorResponse) {
+ webError(w, err.Err, err.StatusCode)
+}
diff --git a/gateway/errors_test.go b/gateway/errors_test.go
new file mode 100644
index 0000000000..05e6ca887b
--- /dev/null
+++ b/gateway/errors_test.go
@@ -0,0 +1,65 @@
+package gateway
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestErrRetryAfterIs(t *testing.T) {
+ var err error
+
+ err = NewErrorRetryAfter(errors.New("test"), 10*time.Second)
+ assert.True(t, errors.Is(err, &ErrorRetryAfter{}), "pointer to error must be error")
+
+ err = fmt.Errorf("wrapped: %w", err)
+ assert.True(t, errors.Is(err, &ErrorRetryAfter{}), "wrapped pointer to error must be error")
+}
+
+func TestErrRetryAfterAs(t *testing.T) {
+ var (
+ err error
+ errRA *ErrorRetryAfter
+ )
+
+ err = NewErrorRetryAfter(errors.New("test"), 25*time.Second)
+ assert.True(t, errors.As(err, &errRA), "pointer to error must be error")
+ assert.EqualValues(t, errRA.RetryAfter, 25*time.Second)
+
+ err = fmt.Errorf("wrapped: %w", err)
+ assert.True(t, errors.As(err, &errRA), "wrapped pointer to error must be error")
+ assert.EqualValues(t, errRA.RetryAfter, 25*time.Second)
+}
+
+func TestWebError(t *testing.T) {
+ t.Parallel()
+
+ t.Run("429 Too Many Requests", func(t *testing.T) {
+ err := fmt.Errorf("wrapped for testing: %w", NewErrorRetryAfter(ErrTooManyRequests, 0))
+ w := httptest.NewRecorder()
+ webError(w, err, http.StatusInternalServerError)
+ assert.Equal(t, http.StatusTooManyRequests, w.Result().StatusCode)
+ assert.Zero(t, len(w.Result().Header.Values("Retry-After")))
+ })
+
+ t.Run("429 Too Many Requests with Retry-After header", func(t *testing.T) {
+ err := NewErrorRetryAfter(ErrTooManyRequests, 25*time.Second)
+ w := httptest.NewRecorder()
+ webError(w, err, http.StatusInternalServerError)
+ assert.Equal(t, http.StatusTooManyRequests, w.Result().StatusCode)
+ assert.Equal(t, "25", w.Result().Header.Get("Retry-After"))
+ })
+
+ t.Run("503 Service Unavailable with Retry-After header", func(t *testing.T) {
+ err := NewErrorRetryAfter(ErrServiceUnavailable, 50*time.Second)
+ w := httptest.NewRecorder()
+ webError(w, err, http.StatusInternalServerError)
+ assert.Equal(t, http.StatusServiceUnavailable, w.Result().StatusCode)
+ assert.Equal(t, "50", w.Result().Header.Get("Retry-After"))
+ })
+}
diff --git a/gateway/gateway.go b/gateway/gateway.go
new file mode 100644
index 0000000000..f32e0646f9
--- /dev/null
+++ b/gateway/gateway.go
@@ -0,0 +1,120 @@
+package gateway
+
+import (
+ "context"
+ "net/http"
+ "sort"
+
+ "github.com/ipfs/boxo/blocks"
+ iface "github.com/ipfs/boxo/coreiface"
+ "github.com/ipfs/boxo/coreiface/path"
+ "github.com/ipfs/boxo/files"
+ cid "github.com/ipfs/go-cid"
+)
+
+// Config is the configuration used when creating a new gateway handler.
+type Config struct {
+ Headers map[string][]string
+}
+
+// API defines the minimal set of API services required for a gateway handler.
+type API interface {
+ // GetUnixFsNode returns a read-only handle to a file tree referenced by a path.
+ GetUnixFsNode(context.Context, path.Resolved) (files.Node, error)
+
+ // LsUnixFsDir returns the list of links in a directory.
+ LsUnixFsDir(context.Context, path.Resolved) (<-chan iface.DirEntry, error)
+
+ // GetBlock return a block from a certain CID.
+ GetBlock(context.Context, cid.Cid) (blocks.Block, error)
+
+ // GetIPNSRecord retrieves the best IPNS record for a given CID (libp2p-key)
+ // from the routing system.
+ GetIPNSRecord(context.Context, cid.Cid) ([]byte, error)
+
+ // GetDNSLinkRecord returns the DNSLink TXT record for the provided FQDN.
+ // Unlike ResolvePath, it does not perform recursive resolution. It only
+ // checks for the existence of a DNSLink TXT record with path starting with
+ // /ipfs/ or /ipns/ and returns the path as-is.
+ GetDNSLinkRecord(context.Context, string) (path.Path, error)
+
+ // IsCached returns whether or not the path exists locally.
+ IsCached(context.Context, path.Path) bool
+
+ // ResolvePath resolves the path using UnixFS resolver. If the path does not
+ // exist due to a missing link, it should return an error of type:
+ // https://pkg.go.dev/github.com/ipfs/go-path@v0.3.0/resolver#ErrNoLink
+ ResolvePath(context.Context, path.Path) (path.Resolved, error)
+}
+
+// A helper function to clean up a set of headers:
+// 1. Canonicalizes.
+// 2. Deduplicates.
+// 3. Sorts.
+func cleanHeaderSet(headers []string) []string {
+ // Deduplicate and canonicalize.
+ m := make(map[string]struct{}, len(headers))
+ for _, h := range headers {
+ m[http.CanonicalHeaderKey(h)] = struct{}{}
+ }
+ result := make([]string, 0, len(m))
+ for k := range m {
+ result = append(result, k)
+ }
+
+ // Sort
+ sort.Strings(result)
+ return result
+}
+
+// AddAccessControlHeaders adds default headers used for controlling
+// cross-origin requests. This function adds several values to the
+// Access-Control-Allow-Headers and Access-Control-Expose-Headers entries.
+// If the Access-Control-Allow-Origin entry is missing a value of '*' is
+// added, indicating that browsers should allow requesting code from any
+// origin to access the resource.
+// If the Access-Control-Allow-Methods entry is missing a value of 'GET' is
+// added, indicating that browsers may use the GET method when issuing cross
+// origin requests.
+func AddAccessControlHeaders(headers map[string][]string) {
+ // Hard-coded headers.
+ const ACAHeadersName = "Access-Control-Allow-Headers"
+ const ACEHeadersName = "Access-Control-Expose-Headers"
+ const ACAOriginName = "Access-Control-Allow-Origin"
+ const ACAMethodsName = "Access-Control-Allow-Methods"
+
+ if _, ok := headers[ACAOriginName]; !ok {
+ // Default to *all*
+ headers[ACAOriginName] = []string{"*"}
+ }
+ if _, ok := headers[ACAMethodsName]; !ok {
+ // Default to GET
+ headers[ACAMethodsName] = []string{http.MethodGet}
+ }
+
+ headers[ACAHeadersName] = cleanHeaderSet(
+ append([]string{
+ "Content-Type",
+ "User-Agent",
+ "Range",
+ "X-Requested-With",
+ }, headers[ACAHeadersName]...))
+
+ headers[ACEHeadersName] = cleanHeaderSet(
+ append([]string{
+ "Content-Length",
+ "Content-Range",
+ "X-Chunked-Output",
+ "X-Stream-Output",
+ "X-Ipfs-Path",
+ "X-Ipfs-Roots",
+ }, headers[ACEHeadersName]...))
+}
+
+type RequestContextKey string
+
+const (
+ DNSLinkHostnameKey RequestContextKey = "dnslink-hostname"
+ GatewayHostnameKey RequestContextKey = "gw-hostname"
+ ContentPathKey RequestContextKey = "content-path"
+)
diff --git a/gateway/gateway_test.go b/gateway/gateway_test.go
new file mode 100644
index 0000000000..b78f95e455
--- /dev/null
+++ b/gateway/gateway_test.go
@@ -0,0 +1,637 @@
+package gateway
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ gopath "path"
+ "regexp"
+ "strings"
+ "testing"
+
+ "github.com/ipfs/boxo/blocks"
+ "github.com/ipfs/boxo/blockservice"
+ blockstore "github.com/ipfs/boxo/blockstore"
+ iface "github.com/ipfs/boxo/coreiface"
+ nsopts "github.com/ipfs/boxo/coreiface/options/namesys"
+ ipath "github.com/ipfs/boxo/coreiface/path"
+ offline "github.com/ipfs/boxo/exchange/offline"
+ bsfetcher "github.com/ipfs/boxo/fetcher/impl/blockservice"
+ "github.com/ipfs/boxo/files"
+ carblockstore "github.com/ipfs/boxo/ipld/car/v2/blockstore"
+ "github.com/ipfs/boxo/ipld/merkledag"
+ "github.com/ipfs/boxo/namesys"
+ "github.com/ipfs/boxo/namesys/resolve"
+ path "github.com/ipfs/boxo/path"
+ "github.com/ipfs/boxo/path/resolver"
+ "github.com/ipfs/boxo/unixfs"
+ ufile "github.com/ipfs/boxo/unixfs/file"
+ uio "github.com/ipfs/boxo/unixfs/io"
+ "github.com/ipfs/go-cid"
+ format "github.com/ipfs/go-ipld-format"
+ "github.com/ipfs/go-unixfsnode"
+ dagpb "github.com/ipld/go-codec-dagpb"
+ "github.com/ipld/go-ipld-prime"
+ "github.com/ipld/go-ipld-prime/node/basicnode"
+ "github.com/ipld/go-ipld-prime/schema"
+ "github.com/libp2p/go-libp2p/core/crypto"
+ "github.com/libp2p/go-libp2p/core/routing"
+ "github.com/stretchr/testify/assert"
+)
+
+type mockNamesys map[string]path.Path
+
+func (m mockNamesys) Resolve(ctx context.Context, name string, opts ...nsopts.ResolveOpt) (value path.Path, err error) {
+ cfg := nsopts.DefaultResolveOpts()
+ for _, o := range opts {
+ o(&cfg)
+ }
+ depth := cfg.Depth
+ if depth == nsopts.UnlimitedDepth {
+ // max uint
+ depth = ^uint(0)
+ }
+ for strings.HasPrefix(name, "/ipns/") {
+ if depth == 0 {
+ return value, namesys.ErrResolveRecursion
+ }
+ depth--
+
+ var ok bool
+ value, ok = m[name]
+ if !ok {
+ return "", namesys.ErrResolveFailed
+ }
+ name = value.String()
+ }
+ return value, nil
+}
+
+func (m mockNamesys) ResolveAsync(ctx context.Context, name string, opts ...nsopts.ResolveOpt) <-chan namesys.Result {
+ out := make(chan namesys.Result, 1)
+ v, err := m.Resolve(ctx, name, opts...)
+ out <- namesys.Result{Path: v, Err: err}
+ close(out)
+ return out
+}
+
+func (m mockNamesys) Publish(ctx context.Context, name crypto.PrivKey, value path.Path, opts ...nsopts.PublishOption) error {
+ return errors.New("not implemented for mockNamesys")
+}
+
+func (m mockNamesys) GetResolver(subs string) (namesys.Resolver, bool) {
+ return nil, false
+}
+
+type mockAPI struct {
+ blockStore blockstore.Blockstore
+ blockService blockservice.BlockService
+ dagService format.DAGService
+ resolver resolver.Resolver
+ namesys mockNamesys
+}
+
+func newMockAPI(t *testing.T) (*mockAPI, cid.Cid) {
+ r, err := os.Open("./testdata/fixtures.car")
+ assert.Nil(t, err)
+
+ blockStore, err := carblockstore.NewReadOnly(r, nil)
+ assert.Nil(t, err)
+
+ t.Cleanup(func() {
+ blockStore.Close()
+ r.Close()
+ })
+
+ cids, err := blockStore.Roots()
+ assert.Nil(t, err)
+ assert.Len(t, cids, 1)
+
+ blockService := blockservice.New(blockStore, offline.Exchange(blockStore))
+ dagService := merkledag.NewDAGService(blockService)
+
+ fetcherConfig := bsfetcher.NewFetcherConfig(blockService)
+ fetcherConfig.PrototypeChooser = dagpb.AddSupportToChooser(func(lnk ipld.Link, lnkCtx ipld.LinkContext) (ipld.NodePrototype, error) {
+ if tlnkNd, ok := lnkCtx.LinkNode.(schema.TypedLinkNode); ok {
+ return tlnkNd.LinkTargetNodePrototype(), nil
+ }
+ return basicnode.Prototype.Any, nil
+ })
+ fetcher := fetcherConfig.WithReifier(unixfsnode.Reify)
+ resolver := resolver.NewBasicResolver(fetcher)
+
+ return &mockAPI{
+ blockStore: blockService.Blockstore(),
+ blockService: blockService,
+ dagService: dagService,
+ resolver: resolver,
+ namesys: mockNamesys{},
+ }, cids[0]
+}
+
+func (api *mockAPI) GetUnixFsNode(ctx context.Context, p ipath.Resolved) (files.Node, error) {
+ nd, err := api.resolveNode(ctx, p)
+ if err != nil {
+ return nil, err
+ }
+
+ return ufile.NewUnixfsFile(ctx, api.dagService, nd)
+}
+
+func (api *mockAPI) LsUnixFsDir(ctx context.Context, p ipath.Resolved) (<-chan iface.DirEntry, error) {
+ node, err := api.resolveNode(ctx, p)
+ if err != nil {
+ return nil, err
+ }
+
+ dir, err := uio.NewDirectoryFromNode(api.dagService, node)
+ if err != nil {
+ return nil, err
+ }
+
+ out := make(chan iface.DirEntry, uio.DefaultShardWidth)
+
+ go func() {
+ defer close(out)
+ for l := range dir.EnumLinksAsync(ctx) {
+ select {
+ case out <- api.processLink(ctx, l):
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+
+ return out, nil
+}
+
+func (api *mockAPI) GetBlock(ctx context.Context, c cid.Cid) (blocks.Block, error) {
+ return api.blockService.GetBlock(ctx, c)
+}
+
+func (api *mockAPI) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, error) {
+ return nil, routing.ErrNotSupported
+}
+
+func (api *mockAPI) GetDNSLinkRecord(ctx context.Context, hostname string) (ipath.Path, error) {
+ if api.namesys != nil {
+ p, err := api.namesys.Resolve(ctx, "/ipns/"+hostname, nsopts.Depth(1))
+ if err == namesys.ErrResolveRecursion {
+ err = nil
+ }
+ return ipath.New(p.String()), err
+ }
+
+ return nil, errors.New("not implemented")
+}
+
+func (api *mockAPI) IsCached(ctx context.Context, p ipath.Path) bool {
+ rp, err := api.ResolvePath(ctx, p)
+ if err != nil {
+ return false
+ }
+
+ has, _ := api.blockStore.Has(ctx, rp.Cid())
+ return has
+}
+
+func (api *mockAPI) ResolvePath(ctx context.Context, ip ipath.Path) (ipath.Resolved, error) {
+ if _, ok := ip.(ipath.Resolved); ok {
+ return ip.(ipath.Resolved), nil
+ }
+
+ err := ip.IsValid()
+ if err != nil {
+ return nil, err
+ }
+
+ p := path.Path(ip.String())
+ if p.Segments()[0] == "ipns" {
+ p, err = resolve.ResolveIPNS(ctx, api.namesys, p)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if p.Segments()[0] != "ipfs" {
+ return nil, fmt.Errorf("unsupported path namespace: %s", ip.Namespace())
+ }
+
+ node, rest, err := api.resolver.ResolveToLastNode(ctx, p)
+ if err != nil {
+ return nil, err
+ }
+
+ root, err := cid.Parse(p.Segments()[1])
+ if err != nil {
+ return nil, err
+ }
+
+ return ipath.NewResolvedPath(p, node, root, gopath.Join(rest...)), nil
+}
+
+func (api *mockAPI) resolveNode(ctx context.Context, p ipath.Path) (format.Node, error) {
+ rp, err := api.ResolvePath(ctx, p)
+ if err != nil {
+ return nil, err
+ }
+
+ node, err := api.dagService.Get(ctx, rp.Cid())
+ if err != nil {
+ return nil, fmt.Errorf("get node: %w", err)
+ }
+ return node, nil
+}
+
+func (api *mockAPI) processLink(ctx context.Context, result unixfs.LinkResult) iface.DirEntry {
+ if result.Err != nil {
+ return iface.DirEntry{Err: result.Err}
+ }
+
+ link := iface.DirEntry{
+ Name: result.Link.Name,
+ Cid: result.Link.Cid,
+ }
+
+ switch link.Cid.Type() {
+ case cid.Raw:
+ link.Type = iface.TFile
+ link.Size = result.Link.Size
+ case cid.DagProtobuf:
+ link.Size = result.Link.Size
+ }
+
+ return link
+}
+
+func doWithoutRedirect(req *http.Request) (*http.Response, error) {
+ tag := "without-redirect"
+ c := &http.Client{
+ CheckRedirect: func(req *http.Request, via []*http.Request) error {
+ return errors.New(tag)
+ },
+ }
+ res, err := c.Do(req)
+ if err != nil && !strings.Contains(err.Error(), tag) {
+ return nil, err
+ }
+ return res, nil
+}
+
+func newTestServerAndNode(t *testing.T, ns mockNamesys) (*httptest.Server, *mockAPI, cid.Cid) {
+ api, root := newMockAPI(t)
+ ts := newTestServer(t, api)
+ return ts, api, root
+}
+
+func newTestServer(t *testing.T, api API) *httptest.Server {
+ config := Config{Headers: map[string][]string{}}
+ AddAccessControlHeaders(config.Headers)
+
+ handler := NewHandler(config, api)
+ mux := http.NewServeMux()
+ mux.Handle("/ipfs/", handler)
+ mux.Handle("/ipns/", handler)
+ handler = WithHostname(mux, api, map[string]*Specification{}, false)
+
+ ts := httptest.NewServer(handler)
+ t.Cleanup(func() { ts.Close() })
+
+ return ts
+}
+
+func matchPathOrBreadcrumbs(s string, expected string) bool {
+ matched, _ := regexp.MatchString("Index of(\n|\r\n)[\t ]*"+regexp.QuoteMeta(expected), s)
+ return matched
+}
+
+func TestGatewayGet(t *testing.T) {
+ ts, api, root := newTestServerAndNode(t, nil)
+ t.Logf("test server url: %s", ts.URL)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ k, err := api.ResolvePath(ctx, ipath.Join(ipath.IpfsPath(root), t.Name(), "fnord"))
+ assert.Nil(t, err)
+
+ api.namesys["/ipns/example.com"] = path.FromCid(k.Cid())
+ api.namesys["/ipns/working.example.com"] = path.FromString(k.String())
+ api.namesys["/ipns/double.example.com"] = path.FromString("/ipns/working.example.com")
+ api.namesys["/ipns/triple.example.com"] = path.FromString("/ipns/double.example.com")
+ api.namesys["/ipns/broken.example.com"] = path.FromString("/ipns/" + k.Cid().String())
+ // We picked .man because:
+ // 1. It's a valid TLD.
+ // 2. Go treats it as the file extension for "man" files (even though
+ // nobody actually *uses* this extension, AFAIK).
+ //
+ // Unfortunately, this may not work on all platforms as file type
+ // detection is platform dependent.
+ api.namesys["/ipns/example.man"] = path.FromString(k.String())
+
+ t.Log(ts.URL)
+ for _, test := range []struct {
+ host string
+ path string
+ status int
+ text string
+ }{
+ {"127.0.0.1:8080", "/", http.StatusNotFound, "404 page not found\n"},
+ {"127.0.0.1:8080", "/" + k.Cid().String(), http.StatusNotFound, "404 page not found\n"},
+ {"127.0.0.1:8080", "/ipfs/this-is-not-a-cid", http.StatusBadRequest, "failed to resolve /ipfs/this-is-not-a-cid: invalid path \"/ipfs/this-is-not-a-cid\": invalid CID: invalid cid: illegal base32 data at input byte 3\n"},
+ {"127.0.0.1:8080", k.String(), http.StatusOK, "fnord"},
+ {"127.0.0.1:8080", "/ipns/nxdomain.example.com", http.StatusInternalServerError, "failed to resolve /ipns/nxdomain.example.com: " + namesys.ErrResolveFailed.Error() + "\n"},
+ {"127.0.0.1:8080", "/ipns/%0D%0A%0D%0Ahello", http.StatusInternalServerError, "failed to resolve /ipns/\\r\\n\\r\\nhello: " + namesys.ErrResolveFailed.Error() + "\n"},
+ {"127.0.0.1:8080", "/ipns/k51qzi5uqu5djucgtwlxrbfiyfez1nb0ct58q5s4owg6se02evza05dfgi6tw5", http.StatusInternalServerError, "failed to resolve /ipns/k51qzi5uqu5djucgtwlxrbfiyfez1nb0ct58q5s4owg6se02evza05dfgi6tw5: " + namesys.ErrResolveFailed.Error() + "\n"},
+ {"127.0.0.1:8080", "/ipns/example.com", http.StatusOK, "fnord"},
+ {"example.com", "/", http.StatusOK, "fnord"},
+
+ {"working.example.com", "/", http.StatusOK, "fnord"},
+ {"double.example.com", "/", http.StatusOK, "fnord"},
+ {"triple.example.com", "/", http.StatusOK, "fnord"},
+ {"working.example.com", k.String(), http.StatusNotFound, "failed to resolve /ipns/working.example.com" + k.String() + ": no link named \"ipfs\" under " + k.Cid().String() + "\n"},
+ {"broken.example.com", "/", http.StatusInternalServerError, "failed to resolve /ipns/broken.example.com/: " + namesys.ErrResolveFailed.Error() + "\n"},
+ {"broken.example.com", k.String(), http.StatusInternalServerError, "failed to resolve /ipns/broken.example.com" + k.String() + ": " + namesys.ErrResolveFailed.Error() + "\n"},
+ // This test case ensures we don't treat the TLD as a file extension.
+ {"example.man", "/", http.StatusOK, "fnord"},
+ } {
+ testName := "http://" + test.host + test.path
+ t.Run(testName, func(t *testing.T) {
+ var c http.Client
+ r, err := http.NewRequest(http.MethodGet, ts.URL+test.path, nil)
+ assert.Nil(t, err)
+ r.Host = test.host
+ resp, err := c.Do(r)
+ assert.Nil(t, err)
+ defer resp.Body.Close()
+ assert.Equal(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type"))
+ body, err := io.ReadAll(resp.Body)
+ assert.Nil(t, err)
+ assert.Equal(t, test.status, resp.StatusCode, "body", body)
+ assert.Equal(t, test.text, string(body))
+ })
+ }
+}
+
+func TestUriQueryRedirect(t *testing.T) {
+ ts, _, _ := newTestServerAndNode(t, mockNamesys{})
+
+ cid := "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"
+ for _, test := range []struct {
+ path string
+ status int
+ location string
+ }{
+ // - Browsers will send original URI in URL-escaped form
+ // - We expect query parameters to be persisted
+ // - We drop fragments, as those should not be sent by a browser
+ {"/ipfs/?uri=ipfs%3A%2F%2FQmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco%2Fwiki%2FFoo_%C4%85%C4%99.html%3Ffilename%3Dtest-%C4%99.html%23header-%C4%85", http.StatusMovedPermanently, "/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/wiki/Foo_%c4%85%c4%99.html?filename=test-%c4%99.html"},
+ {"/ipfs/?uri=ipns%3A%2F%2Fexample.com%2Fwiki%2FFoo_%C4%85%C4%99.html%3Ffilename%3Dtest-%C4%99.html", http.StatusMovedPermanently, "/ipns/example.com/wiki/Foo_%c4%85%c4%99.html?filename=test-%c4%99.html"},
+ {"/ipfs/?uri=ipfs://" + cid, http.StatusMovedPermanently, "/ipfs/" + cid},
+ {"/ipfs?uri=ipfs://" + cid, http.StatusMovedPermanently, "/ipfs/?uri=ipfs://" + cid},
+ {"/ipfs/?uri=ipns://" + cid, http.StatusMovedPermanently, "/ipns/" + cid},
+ {"/ipns/?uri=ipfs%3A%2F%2FQmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco%2Fwiki%2FFoo_%C4%85%C4%99.html%3Ffilename%3Dtest-%C4%99.html%23header-%C4%85", http.StatusMovedPermanently, "/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/wiki/Foo_%c4%85%c4%99.html?filename=test-%c4%99.html"},
+ {"/ipns/?uri=ipns%3A%2F%2Fexample.com%2Fwiki%2FFoo_%C4%85%C4%99.html%3Ffilename%3Dtest-%C4%99.html", http.StatusMovedPermanently, "/ipns/example.com/wiki/Foo_%c4%85%c4%99.html?filename=test-%c4%99.html"},
+ {"/ipns?uri=ipns://" + cid, http.StatusMovedPermanently, "/ipns/?uri=ipns://" + cid},
+ {"/ipns/?uri=ipns://" + cid, http.StatusMovedPermanently, "/ipns/" + cid},
+ {"/ipns/?uri=ipfs://" + cid, http.StatusMovedPermanently, "/ipfs/" + cid},
+ {"/ipfs/?uri=unsupported://" + cid, http.StatusBadRequest, ""},
+ {"/ipfs/?uri=invaliduri", http.StatusBadRequest, ""},
+ {"/ipfs/?uri=" + cid, http.StatusBadRequest, ""},
+ } {
+ testName := ts.URL + test.path
+ t.Run(testName, func(t *testing.T) {
+ r, err := http.NewRequest(http.MethodGet, ts.URL+test.path, nil)
+ assert.Nil(t, err)
+ resp, err := doWithoutRedirect(r)
+ assert.Nil(t, err)
+ defer resp.Body.Close()
+ assert.Equal(t, test.status, resp.StatusCode)
+ assert.Equal(t, test.location, resp.Header.Get("Location"))
+ })
+ }
+}
+
+func TestIPNSHostnameRedirect(t *testing.T) {
+ ts, api, root := newTestServerAndNode(t, nil)
+ t.Logf("test server url: %s", ts.URL)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ k, err := api.ResolvePath(ctx, ipath.Join(ipath.IpfsPath(root), t.Name()))
+ assert.Nil(t, err)
+
+ t.Logf("k: %s\n", k)
+ api.namesys["/ipns/example.net"] = path.FromString(k.String())
+
+ // make request to directory containing index.html
+ req, err := http.NewRequest(http.MethodGet, ts.URL+"/foo", nil)
+ assert.Nil(t, err)
+ req.Host = "example.net"
+
+ res, err := doWithoutRedirect(req)
+ assert.Nil(t, err)
+
+ // expect 301 redirect to same path, but with trailing slash
+ assert.Equal(t, http.StatusMovedPermanently, res.StatusCode)
+ hdr := res.Header["Location"]
+ assert.Positive(t, len(hdr), "location header not present")
+ assert.Equal(t, hdr[0], "/foo/")
+
+ // make request with prefix to directory containing index.html
+ req, err = http.NewRequest(http.MethodGet, ts.URL+"/foo", nil)
+ assert.Nil(t, err)
+ req.Host = "example.net"
+
+ res, err = doWithoutRedirect(req)
+ assert.Nil(t, err)
+ // expect 301 redirect to same path, but with prefix and trailing slash
+ assert.Equal(t, http.StatusMovedPermanently, res.StatusCode)
+
+ hdr = res.Header["Location"]
+ assert.Positive(t, len(hdr), "location header not present")
+ assert.Equal(t, hdr[0], "/foo/")
+
+ // make sure /version isn't exposed
+ req, err = http.NewRequest(http.MethodGet, ts.URL+"/version", nil)
+ assert.Nil(t, err)
+ req.Host = "example.net"
+
+ res, err = doWithoutRedirect(req)
+ assert.Nil(t, err)
+ assert.Equal(t, http.StatusNotFound, res.StatusCode)
+}
+
+// Test directory listing on DNSLink website
+// (scenario when Host header is the same as URL hostname)
+// This is basic regression test: additional end-to-end tests
+// can be found in test/sharness/t0115-gateway-dir-listing.sh
+func TestIPNSHostnameBacklinks(t *testing.T) {
+ ts, api, root := newTestServerAndNode(t, nil)
+ t.Logf("test server url: %s", ts.URL)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ k, err := api.ResolvePath(ctx, ipath.Join(ipath.IpfsPath(root), t.Name()))
+ assert.Nil(t, err)
+
+ // create /ipns/example.net/foo/
+ k2, err := api.ResolvePath(ctx, ipath.Join(k, "foo? #<'"))
+ assert.Nil(t, err)
+
+ k3, err := api.ResolvePath(ctx, ipath.Join(k, "foo? #<'/bar"))
+ assert.Nil(t, err)
+
+ t.Logf("k: %s\n", k)
+ api.namesys["/ipns/example.net"] = path.FromString(k.String())
+
+ // make request to directory listing
+ req, err := http.NewRequest(http.MethodGet, ts.URL+"/foo%3F%20%23%3C%27/", nil)
+ assert.Nil(t, err)
+ req.Host = "example.net"
+
+ res, err := doWithoutRedirect(req)
+ assert.Nil(t, err)
+
+ // expect correct links
+ body, err := io.ReadAll(res.Body)
+ assert.Nil(t, err)
+ s := string(body)
+ t.Logf("body: %s\n", string(body))
+
+ assert.True(t, matchPathOrBreadcrumbs(s, "/ipns/example.net /foo? #<' "), "expected a path in directory listing")
+ // https://github.com/ipfs/dir-index-html/issues/42
+ assert.Contains(t, s, "", "expected backlink in directory listing")
+ assert.Contains(t, s, " ", "expected file in directory listing")
+ assert.Contains(t, s, s, k2.Cid().String(), "expected hash in directory listing")
+
+ // make request to directory listing at root
+ req, err = http.NewRequest(http.MethodGet, ts.URL, nil)
+ assert.Nil(t, err)
+ req.Host = "example.net"
+
+ res, err = doWithoutRedirect(req)
+ assert.Nil(t, err)
+
+ // expect correct backlinks at root
+ body, err = io.ReadAll(res.Body)
+ assert.Nil(t, err)
+
+ s = string(body)
+ t.Logf("body: %s\n", string(body))
+
+ assert.True(t, matchPathOrBreadcrumbs(s, "/"), "expected a path in directory listing")
+ assert.NotContains(t, s, " ", "expected no backlink in directory listing of the root CID")
+ assert.Contains(t, s, " ", "expected file in directory listing")
+ // https://github.com/ipfs/dir-index-html/issues/42
+ assert.Contains(t, s, " example.net /foo? #<' /bar "), "expected a path in directory listing")
+ assert.Contains(t, s, "", "expected backlink in directory listing")
+ assert.Contains(t, s, " ", "expected file in directory listing")
+ assert.Contains(t, s, k3.Cid().String(), "expected hash in directory listing")
+}
+
+func TestPretty404(t *testing.T) {
+ ts, api, root := newTestServerAndNode(t, nil)
+ t.Logf("test server url: %s", ts.URL)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ k, err := api.ResolvePath(ctx, ipath.Join(ipath.IpfsPath(root), t.Name()))
+ assert.Nil(t, err)
+
+ host := "example.net"
+ api.namesys["/ipns/"+host] = path.FromString(k.String())
+
+ for _, test := range []struct {
+ path string
+ accept string
+ status int
+ text string
+ }{
+ {"/ipfs-404.html", "text/html", http.StatusOK, "Custom 404"},
+ {"/nope", "text/html", http.StatusNotFound, "Custom 404"},
+ {"/nope", "text/*", http.StatusNotFound, "Custom 404"},
+ {"/nope", "*/*", http.StatusNotFound, "Custom 404"},
+ {"/nope", "application/json", http.StatusNotFound, fmt.Sprintf("failed to resolve /ipns/example.net/nope: no link named \"nope\" under %s\n", k.Cid().String())},
+ {"/deeper/nope", "text/html", http.StatusNotFound, "Deep custom 404"},
+ {"/deeper/", "text/html", http.StatusOK, ""},
+ {"/deeper", "text/html", http.StatusOK, ""},
+ {"/nope/nope", "text/html", http.StatusNotFound, "Custom 404"},
+ } {
+ testName := fmt.Sprintf("%s %s", test.path, test.accept)
+ t.Run(testName, func(t *testing.T) {
+ var c http.Client
+ req, err := http.NewRequest("GET", ts.URL+test.path, nil)
+ assert.Nil(t, err)
+ req.Header.Add("Accept", test.accept)
+ req.Host = host
+ resp, err := c.Do(req)
+ assert.Nil(t, err)
+ defer resp.Body.Close()
+ assert.Equal(t, test.status, resp.StatusCode)
+ body, err := io.ReadAll(resp.Body)
+ assert.Nil(t, err)
+ if test.text != "" {
+ assert.Equal(t, test.text, string(body))
+ }
+ })
+ }
+}
+
+func TestCacheControlImmutable(t *testing.T) {
+ ts, _, root := newTestServerAndNode(t, nil)
+ t.Logf("test server url: %s", ts.URL)
+
+ req, err := http.NewRequest(http.MethodGet, ts.URL+"/ipfs/"+root.String()+"/", nil)
+ assert.Nil(t, err)
+
+ res, err := doWithoutRedirect(req)
+ assert.Nil(t, err)
+
+ // check the immutable tag isn't set
+ hdrs, ok := res.Header["Cache-Control"]
+ if ok {
+ for _, hdr := range hdrs {
+ assert.NotContains(t, hdr, "immutable", "unexpected Cache-Control: immutable on directory listing")
+ }
+ }
+}
+
+func TestGoGetSupport(t *testing.T) {
+ ts, _, root := newTestServerAndNode(t, nil)
+ t.Logf("test server url: %s", ts.URL)
+
+ // mimic go-get
+ req, err := http.NewRequest(http.MethodGet, ts.URL+"/ipfs/"+root.String()+"?go-get=1", nil)
+ assert.Nil(t, err)
+
+ res, err := doWithoutRedirect(req)
+ assert.Nil(t, err)
+ assert.Equal(t, http.StatusOK, res.StatusCode)
+}
diff --git a/gateway/handler.go b/gateway/handler.go
new file mode 100644
index 0000000000..02b907f6f1
--- /dev/null
+++ b/gateway/handler.go
@@ -0,0 +1,880 @@
+package gateway
+
+import (
+ "context"
+ "fmt"
+ "html/template"
+ "io"
+ "mime"
+ "net/http"
+ "net/textproto"
+ "net/url"
+ gopath "path"
+ "regexp"
+ "runtime/debug"
+ "strings"
+ "time"
+
+ coreiface "github.com/ipfs/boxo/coreiface"
+ ipath "github.com/ipfs/boxo/coreiface/path"
+ cid "github.com/ipfs/go-cid"
+ logging "github.com/ipfs/go-log"
+ mc "github.com/multiformats/go-multicodec"
+ prometheus "github.com/prometheus/client_golang/prometheus"
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
+ "go.uber.org/zap"
+)
+
+var log = logging.Logger("core/server")
+
+const (
+ ipfsPathPrefix = "/ipfs/"
+ ipnsPathPrefix = "/ipns/"
+ immutableCacheControl = "public, max-age=29030400, immutable"
+)
+
+var (
+ onlyASCII = regexp.MustCompile("[[:^ascii:]]")
+ noModtime = time.Unix(0, 0) // disables Last-Modified header if passed as modtime
+)
+
+// HTML-based redirect for errors which can be recovered from, but we want
+// to provide hint to people that they should fix things on their end.
+var redirectTemplate = template.Must(template.New("redirect").Parse(`
+
+
+
+
+
+
+
+ {{.ErrorMsg}} (if a redirect does not happen in 10 seconds, use "{{.SuggestedPath}}" instead)
+
+`))
+
+type redirectTemplateData struct {
+ RedirectURL string
+ SuggestedPath string
+ ErrorMsg string
+}
+
+// handler is a HTTP handler that serves IPFS objects (accessible by default at /ipfs/)
+// (it serves requests like GET /ipfs/QmVRzPKPzNtSrEzBFm2UZfxmPAgnaLke4DMcerbsGGSaFe/link)
+type handler struct {
+ config Config
+ api API
+
+ // generic metrics
+ firstContentBlockGetMetric *prometheus.HistogramVec
+ unixfsGetMetric *prometheus.SummaryVec // deprecated, use firstContentBlockGetMetric
+
+ // response type metrics
+ getMetric *prometheus.HistogramVec
+ unixfsFileGetMetric *prometheus.HistogramVec
+ unixfsDirIndexGetMetric *prometheus.HistogramVec
+ unixfsGenDirListingGetMetric *prometheus.HistogramVec
+ carStreamGetMetric *prometheus.HistogramVec
+ rawBlockGetMetric *prometheus.HistogramVec
+ tarStreamGetMetric *prometheus.HistogramVec
+ jsoncborDocumentGetMetric *prometheus.HistogramVec
+ ipnsRecordGetMetric *prometheus.HistogramVec
+}
+
+// StatusResponseWriter enables us to override HTTP Status Code passed to
+// WriteHeader function inside of http.ServeContent. Decision is based on
+// presence of HTTP Headers such as Location.
+type statusResponseWriter struct {
+ http.ResponseWriter
+}
+
+func (sw *statusResponseWriter) WriteHeader(code int) {
+ // Check if we need to adjust Status Code to account for scheduled redirect
+ // This enables us to return payload along with HTTP 301
+ // for subdomain redirect in web browsers while also returning body for cli
+ // tools which do not follow redirects by default (curl, wget).
+ redirect := sw.ResponseWriter.Header().Get("Location")
+ if redirect != "" && code == http.StatusOK {
+ code = http.StatusMovedPermanently
+ log.Debugw("subdomain redirect", "location", redirect, "status", code)
+ }
+ sw.ResponseWriter.WriteHeader(code)
+}
+
+// ServeContent replies to the request using the content in the provided ReadSeeker
+// and returns the status code written and any error encountered during a write.
+// It wraps http.ServeContent which takes care of If-None-Match+Etag,
+// Content-Length and range requests.
+func ServeContent(w http.ResponseWriter, req *http.Request, name string, modtime time.Time, content io.ReadSeeker) (int, bool, error) {
+ ew := &errRecordingResponseWriter{ResponseWriter: w}
+ http.ServeContent(ew, req, name, modtime, content)
+
+ // When we calculate some metrics we want a flag that lets us to ignore
+ // errors and 304 Not Modified, and only care when requested data
+ // was sent in full.
+ dataSent := ew.code/100 == 2 && ew.err == nil
+
+ return ew.code, dataSent, ew.err
+}
+
+// errRecordingResponseWriter wraps a ResponseWriter to record the status code and any write error.
+type errRecordingResponseWriter struct {
+ http.ResponseWriter
+ code int
+ err error
+}
+
+func (w *errRecordingResponseWriter) WriteHeader(code int) {
+ if w.code == 0 {
+ w.code = code
+ }
+ w.ResponseWriter.WriteHeader(code)
+}
+
+func (w *errRecordingResponseWriter) Write(p []byte) (int, error) {
+ n, err := w.ResponseWriter.Write(p)
+ if err != nil && w.err == nil {
+ w.err = err
+ }
+ return n, err
+}
+
+// ReadFrom exposes errRecordingResponseWriter's underlying ResponseWriter to io.Copy
+// to allow optimized methods to be taken advantage of.
+func (w *errRecordingResponseWriter) ReadFrom(r io.Reader) (n int64, err error) {
+ n, err = io.Copy(w.ResponseWriter, r)
+ if err != nil && w.err == nil {
+ w.err = err
+ }
+ return n, err
+}
+
+func newSummaryMetric(name string, help string) *prometheus.SummaryVec {
+ summaryMetric := prometheus.NewSummaryVec(
+ prometheus.SummaryOpts{
+ Namespace: "ipfs",
+ Subsystem: "http",
+ Name: name,
+ Help: help,
+ },
+ []string{"gateway"},
+ )
+ if err := prometheus.Register(summaryMetric); err != nil {
+ if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
+ summaryMetric = are.ExistingCollector.(*prometheus.SummaryVec)
+ } else {
+ log.Errorf("failed to register ipfs_http_%s: %v", name, err)
+ }
+ }
+ return summaryMetric
+}
+
+func newHistogramMetric(name string, help string) *prometheus.HistogramVec {
+ // We can add buckets as a parameter in the future, but for now using static defaults
+ // suggested in https://github.com/ipfs/kubo/issues/8441
+ defaultBuckets := []float64{0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 30, 60}
+ histogramMetric := prometheus.NewHistogramVec(
+ prometheus.HistogramOpts{
+ Namespace: "ipfs",
+ Subsystem: "http",
+ Name: name,
+ Help: help,
+ Buckets: defaultBuckets,
+ },
+ []string{"gateway"},
+ )
+ if err := prometheus.Register(histogramMetric); err != nil {
+ if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
+ histogramMetric = are.ExistingCollector.(*prometheus.HistogramVec)
+ } else {
+ log.Errorf("failed to register ipfs_http_%s: %v", name, err)
+ }
+ }
+ return histogramMetric
+}
+
+// NewHandler returns an http.Handler that can act as a gateway to IPFS content
+// offlineApi is a version of the API that should not make network requests for missing data
+func NewHandler(c Config, api API) http.Handler {
+ return newHandler(c, api)
+}
+
+func newHandler(c Config, api API) *handler {
+ i := &handler{
+ config: c,
+ api: api,
+ // Improved Metrics
+ // ----------------------------
+ // Time till the first content block (bar in /ipfs/cid/foo/bar)
+ // (format-agnostic, across all response types)
+ firstContentBlockGetMetric: newHistogramMetric(
+ "gw_first_content_block_get_latency_seconds",
+ "The time till the first content block is received on GET from the gateway.",
+ ),
+
+ // Response-type specific metrics
+ // ----------------------------
+ // Generic: time it takes to execute a successful gateway request (all request types)
+ getMetric: newHistogramMetric(
+ "gw_get_duration_seconds",
+ "The time to GET a successful response to a request (all content types).",
+ ),
+ // UnixFS: time it takes to return a file
+ unixfsFileGetMetric: newHistogramMetric(
+ "gw_unixfs_file_get_duration_seconds",
+ "The time to serve an entire UnixFS file from the gateway.",
+ ),
+ // UnixFS: time it takes to find and serve an index.html file on behalf of a directory.
+ unixfsDirIndexGetMetric: newHistogramMetric(
+ "gw_unixfs_dir_indexhtml_get_duration_seconds",
+ "The time to serve an index.html file on behalf of a directory from the gateway. This is a subset of gw_unixfs_file_get_duration_seconds.",
+ ),
+ // UnixFS: time it takes to generate static HTML with directory listing
+ unixfsGenDirListingGetMetric: newHistogramMetric(
+ "gw_unixfs_gen_dir_listing_get_duration_seconds",
+ "The time to serve a generated UnixFS HTML directory listing from the gateway.",
+ ),
+ // CAR: time it takes to return requested CAR stream
+ carStreamGetMetric: newHistogramMetric(
+ "gw_car_stream_get_duration_seconds",
+ "The time to GET an entire CAR stream from the gateway.",
+ ),
+ // Block: time it takes to return requested Block
+ rawBlockGetMetric: newHistogramMetric(
+ "gw_raw_block_get_duration_seconds",
+ "The time to GET an entire raw Block from the gateway.",
+ ),
+ // TAR: time it takes to return requested TAR stream
+ tarStreamGetMetric: newHistogramMetric(
+ "gw_tar_stream_get_duration_seconds",
+ "The time to GET an entire TAR stream from the gateway.",
+ ),
+ // JSON/CBOR: time it takes to return requested DAG-JSON/-CBOR document
+ jsoncborDocumentGetMetric: newHistogramMetric(
+ "gw_jsoncbor_get_duration_seconds",
+ "The time to GET an entire DAG-JSON/CBOR block from the gateway.",
+ ),
+ // IPNS Record: time it takes to return IPNS record
+ ipnsRecordGetMetric: newHistogramMetric(
+ "gw_ipns_record_get_duration_seconds",
+ "The time to GET an entire IPNS Record from the gateway.",
+ ),
+
+ // Legacy Metrics
+ // ----------------------------
+ unixfsGetMetric: newSummaryMetric( // TODO: remove?
+ // (deprecated, use firstContentBlockGetMetric instead)
+ "unixfs_get_latency_seconds",
+ "DEPRECATED: does not do what you think, use gw_first_content_block_get_latency_seconds instead.",
+ ),
+ }
+ return i
+}
+
+func (i *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ defer panicHandler(w)
+
+ // the hour is a hard fallback, we don't expect it to happen, but just in case
+ ctx, cancel := context.WithTimeout(r.Context(), time.Hour)
+ defer cancel()
+ r = r.WithContext(ctx)
+
+ switch r.Method {
+ case http.MethodGet, http.MethodHead:
+ i.getOrHeadHandler(w, r)
+ return
+ case http.MethodOptions:
+ i.optionsHandler(w, r)
+ return
+ }
+
+ w.Header().Add("Allow", http.MethodGet)
+ w.Header().Add("Allow", http.MethodHead)
+ w.Header().Add("Allow", http.MethodOptions)
+
+ errmsg := "Method " + r.Method + " not allowed: read only access"
+ http.Error(w, errmsg, http.StatusMethodNotAllowed)
+}
+
+func (i *handler) optionsHandler(w http.ResponseWriter, r *http.Request) {
+ /*
+ OPTIONS is a noop request that is used by the browsers to check
+ if server accepts cross-site XMLHttpRequest (indicated by the presence of CORS headers)
+ https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests
+ */
+ i.addUserHeaders(w) // return all custom headers (including CORS ones, if set)
+}
+
+func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) {
+ begin := time.Now()
+
+ logger := log.With("from", r.RequestURI)
+ logger.Debug("http request received")
+
+ if err := handleUnsupportedHeaders(r); err != nil {
+ webRequestError(w, err)
+ return
+ }
+
+ if requestHandled := handleProtocolHandlerRedirect(w, r, logger); requestHandled {
+ return
+ }
+
+ if err := handleServiceWorkerRegistration(r); err != nil {
+ webRequestError(w, err)
+ return
+ }
+
+ contentPath := ipath.New(r.URL.Path)
+ ctx := context.WithValue(r.Context(), ContentPathKey, contentPath)
+ r = r.WithContext(ctx)
+
+ if requestHandled := i.handleOnlyIfCached(w, r, contentPath, logger); requestHandled {
+ return
+ }
+
+ if requestHandled := handleSuperfluousNamespace(w, r, contentPath); requestHandled {
+ return
+ }
+
+ // Detect when explicit Accept header or ?format parameter are present
+ responseFormat, formatParams, err := customResponseFormat(r)
+ if err != nil {
+ webError(w, fmt.Errorf("error while processing the Accept header: %w", err), http.StatusBadRequest)
+ return
+ }
+ trace.SpanFromContext(r.Context()).SetAttributes(attribute.String("ResponseFormat", responseFormat))
+
+ resolvedPath, contentPath, ok := i.handlePathResolution(w, r, responseFormat, contentPath, logger)
+ if !ok {
+ return
+ }
+ trace.SpanFromContext(r.Context()).SetAttributes(attribute.String("ResolvedPath", resolvedPath.String()))
+
+ // Detect when If-None-Match HTTP header allows returning HTTP 304 Not Modified
+ if inm := r.Header.Get("If-None-Match"); inm != "" {
+ pathCid := resolvedPath.Cid()
+ // need to check against both File and Dir Etag variants
+ // because this inexpensive check happens before we do any I/O
+ cidEtag := getEtag(r, pathCid)
+ dirEtag := getDirListingEtag(pathCid)
+ if etagMatch(inm, cidEtag, dirEtag) {
+ // Finish early if client already has a matching Etag
+ w.WriteHeader(http.StatusNotModified)
+ return
+ }
+ }
+
+ if err := i.handleGettingFirstBlock(r, begin, contentPath, resolvedPath); err != nil {
+ webRequestError(w, err)
+ return
+ }
+
+ if err := i.setCommonHeaders(w, r, contentPath); err != nil {
+ webRequestError(w, err)
+ return
+ }
+
+ var success bool
+
+ // Support custom response formats passed via ?format or Accept HTTP header
+ switch responseFormat {
+ case "", "application/json", "application/cbor":
+ switch mc.Code(resolvedPath.Cid().Prefix().Codec) {
+ case mc.Json, mc.DagJson, mc.Cbor, mc.DagCbor:
+ logger.Debugw("serving codec", "path", contentPath)
+ success = i.serveCodec(r.Context(), w, r, resolvedPath, contentPath, begin, responseFormat)
+ default:
+ logger.Debugw("serving unixfs", "path", contentPath)
+ success = i.serveUnixFS(r.Context(), w, r, resolvedPath, contentPath, begin, logger)
+ }
+ case "application/vnd.ipld.raw":
+ logger.Debugw("serving raw block", "path", contentPath)
+ success = i.serveRawBlock(r.Context(), w, r, resolvedPath, contentPath, begin)
+ case "application/vnd.ipld.car":
+ logger.Debugw("serving car stream", "path", contentPath)
+ carVersion := formatParams["version"]
+ success = i.serveCAR(r.Context(), w, r, resolvedPath, contentPath, carVersion, begin)
+ case "application/x-tar":
+ logger.Debugw("serving tar file", "path", contentPath)
+ success = i.serveTAR(r.Context(), w, r, resolvedPath, contentPath, begin, logger)
+ case "application/vnd.ipld.dag-json", "application/vnd.ipld.dag-cbor":
+ logger.Debugw("serving codec", "path", contentPath)
+ success = i.serveCodec(r.Context(), w, r, resolvedPath, contentPath, begin, responseFormat)
+ case "application/vnd.ipfs.ipns-record":
+ logger.Debugw("serving ipns record", "path", contentPath)
+ success = i.serveIpnsRecord(r.Context(), w, r, resolvedPath, contentPath, begin, logger)
+ default: // catch-all for unsuported application/vnd.*
+ err := fmt.Errorf("unsupported format %q", responseFormat)
+ webError(w, err, http.StatusBadRequest)
+ return
+ }
+
+ if success {
+ i.getMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
+ }
+}
+
+func (i *handler) addUserHeaders(w http.ResponseWriter) {
+ for k, v := range i.config.Headers {
+ w.Header()[k] = v
+ }
+}
+
+func panicHandler(w http.ResponseWriter) {
+ if r := recover(); r != nil {
+ log.Error("A panic occurred in the gateway handler!")
+ log.Error(r)
+ debug.PrintStack()
+ w.WriteHeader(http.StatusInternalServerError)
+ }
+}
+
+func addCacheControlHeaders(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, fileCid cid.Cid) (modtime time.Time) {
+ // Set Etag to based on CID (override whatever was set before)
+ w.Header().Set("Etag", getEtag(r, fileCid))
+
+ // Set Cache-Control and Last-Modified based on contentPath properties
+ if contentPath.Mutable() {
+ // mutable namespaces such as /ipns/ can't be cached forever
+
+ /* For now we set Last-Modified to Now() to leverage caching heuristics built into modern browsers:
+ * https://github.com/ipfs/kubo/pull/8074#pullrequestreview-645196768
+ * but we should not set it to fake values and use Cache-Control based on TTL instead */
+ modtime = time.Now()
+
+ // TODO: set Cache-Control based on TTL of IPNS/DNSLink: https://github.com/ipfs/kubo/issues/1818#issuecomment-1015849462
+ // TODO: set Last-Modified based on /ipns/ publishing timestamp?
+ } else {
+ // immutable! CACHE ALL THE THINGS, FOREVER! wolololol
+ w.Header().Set("Cache-Control", immutableCacheControl)
+
+ // Set modtime to 'zero time' to disable Last-Modified header (superseded by Cache-Control)
+ modtime = noModtime
+
+ // TODO: set Last-Modified? - TBD - /ipfs/ modification metadata is present in unixfs 1.5 https://github.com/ipfs/kubo/issues/6920?
+ }
+
+ return modtime
+}
+
+// Set Content-Disposition if filename URL query param is present, return preferred filename
+func addContentDispositionHeader(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) string {
+ /* This logic enables:
+ * - creation of HTML links that trigger "Save As.." dialog instead of being rendered by the browser
+ * - overriding the filename used when saving subresource assets on HTML page
+ * - providing a default filename for HTTP clients when downloading direct /ipfs/CID without any subpath
+ */
+
+ // URL param ?filename=cat.jpg triggers Content-Disposition: [..] filename
+ // which impacts default name used in "Save As.." dialog
+ name := getFilename(contentPath)
+ urlFilename := r.URL.Query().Get("filename")
+ if urlFilename != "" {
+ disposition := "inline"
+ // URL param ?download=true triggers Content-Disposition: [..] attachment
+ // which skips rendering and forces "Save As.." dialog in browsers
+ if r.URL.Query().Get("download") == "true" {
+ disposition = "attachment"
+ }
+ setContentDispositionHeader(w, urlFilename, disposition)
+ name = urlFilename
+ }
+ return name
+}
+
+// Set Content-Disposition to arbitrary filename and disposition
+func setContentDispositionHeader(w http.ResponseWriter, filename string, disposition string) {
+ utf8Name := url.PathEscape(filename)
+ asciiName := url.PathEscape(onlyASCII.ReplaceAllLiteralString(filename, "_"))
+ w.Header().Set("Content-Disposition", fmt.Sprintf("%s; filename=\"%s\"; filename*=UTF-8''%s", disposition, asciiName, utf8Name))
+}
+
+// Set X-Ipfs-Roots with logical CID array for efficient HTTP cache invalidation.
+func (i *handler) buildIpfsRootsHeader(contentPath string, r *http.Request) (string, error) {
+ /*
+ These are logical roots where each CID represent one path segment
+ and resolves to either a directory or the root block of a file.
+ The main purpose of this header is allow HTTP caches to do smarter decisions
+ around cache invalidation (eg. keep specific subdirectory/file if it did not change)
+
+ A good example is Wikipedia, which is HAMT-sharded, but we only care about
+ logical roots that represent each segment of the human-readable content
+ path:
+
+ Given contentPath = /ipns/en.wikipedia-on-ipfs.org/wiki/Block_of_Wikipedia_in_Turkey
+ rootCidList is a generated by doing `ipfs resolve -r` on each sub path:
+ /ipns/en.wikipedia-on-ipfs.org → bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze
+ /ipns/en.wikipedia-on-ipfs.org/wiki/ → bafybeihn2f7lhumh4grizksi2fl233cyszqadkn424ptjajfenykpsaiw4
+ /ipns/en.wikipedia-on-ipfs.org/wiki/Block_of_Wikipedia_in_Turkey → bafkreibn6euazfvoghepcm4efzqx5l3hieof2frhp254hio5y7n3hv5rma
+
+ The result is an ordered array of values:
+ X-Ipfs-Roots: bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze,bafybeihn2f7lhumh4grizksi2fl233cyszqadkn424ptjajfenykpsaiw4,bafkreibn6euazfvoghepcm4efzqx5l3hieof2frhp254hio5y7n3hv5rma
+
+ Note that while the top one will change every time any article is changed,
+ the last root (responsible for specific article) may not change at all.
+ */
+ var sp strings.Builder
+ var pathRoots []string
+ pathSegments := strings.Split(contentPath[6:], "/")
+ sp.WriteString(contentPath[:5]) // /ipfs or /ipns
+ for _, root := range pathSegments {
+ if root == "" {
+ continue
+ }
+ sp.WriteString("/")
+ sp.WriteString(root)
+ resolvedSubPath, err := i.api.ResolvePath(r.Context(), ipath.New(sp.String()))
+ if err != nil {
+ return "", err
+ }
+ pathRoots = append(pathRoots, resolvedSubPath.Cid().String())
+ }
+ rootCidList := strings.Join(pathRoots, ",") // convention from rfc2616#sec4.2
+ return rootCidList, nil
+}
+
+func getFilename(contentPath ipath.Path) string {
+ s := contentPath.String()
+ if (strings.HasPrefix(s, ipfsPathPrefix) || strings.HasPrefix(s, ipnsPathPrefix)) && strings.Count(gopath.Clean(s), "/") <= 2 {
+ // Don't want to treat ipfs.io in /ipns/ipfs.io as a filename.
+ return ""
+ }
+ return gopath.Base(s)
+}
+
+// etagMatch evaluates if we can respond with HTTP 304 Not Modified
+// It supports multiple weak and strong etags passed in If-None-Matc stringh
+// including the wildcard one.
+func etagMatch(ifNoneMatchHeader string, cidEtag string, dirEtag string) bool {
+ buf := ifNoneMatchHeader
+ for {
+ buf = textproto.TrimString(buf)
+ if len(buf) == 0 {
+ break
+ }
+ if buf[0] == ',' {
+ buf = buf[1:]
+ continue
+ }
+ // If-None-Match: * should match against any etag
+ if buf[0] == '*' {
+ return true
+ }
+ etag, remain := scanETag(buf)
+ if etag == "" {
+ break
+ }
+ // Check for match both strong and weak etags
+ if etagWeakMatch(etag, cidEtag) || etagWeakMatch(etag, dirEtag) {
+ return true
+ }
+ buf = remain
+ }
+ return false
+}
+
+// scanETag determines if a syntactically valid ETag is present at s. If so,
+// the ETag and remaining text after consuming ETag is returned. Otherwise,
+// it returns "", "".
+// (This is the same logic as one executed inside of http.ServeContent)
+func scanETag(s string) (etag string, remain string) {
+ s = textproto.TrimString(s)
+ start := 0
+ if strings.HasPrefix(s, "W/") {
+ start = 2
+ }
+ if len(s[start:]) < 2 || s[start] != '"' {
+ return "", ""
+ }
+ // ETag is either W/"text" or "text".
+ // See RFC 7232 2.3.
+ for i := start + 1; i < len(s); i++ {
+ c := s[i]
+ switch {
+ // Character values allowed in ETags.
+ case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80:
+ case c == '"':
+ return s[:i+1], s[i+1:]
+ default:
+ return "", ""
+ }
+ }
+ return "", ""
+}
+
+// etagWeakMatch reports whether a and b match using weak ETag comparison.
+func etagWeakMatch(a, b string) bool {
+ return strings.TrimPrefix(a, "W/") == strings.TrimPrefix(b, "W/")
+}
+
+// generate Etag value based on HTTP request and CID
+func getEtag(r *http.Request, cid cid.Cid) string {
+ prefix := `"`
+ suffix := `"`
+ responseFormat, _, err := customResponseFormat(r)
+ if err == nil && responseFormat != "" {
+ // application/vnd.ipld.foo → foo
+ // application/x-bar → x-bar
+ shortFormat := responseFormat[strings.LastIndexAny(responseFormat, "/.")+1:]
+ // Etag: "cid.shortFmt" (gives us nice compression together with Content-Disposition in block (raw) and car responses)
+ suffix = `.` + shortFormat + suffix
+ }
+ // TODO: include selector suffix when https://github.com/ipfs/kubo/issues/8769 lands
+ return prefix + cid.String() + suffix
+}
+
+// return explicit response format if specified in request as query parameter or via Accept HTTP header
+func customResponseFormat(r *http.Request) (mediaType string, params map[string]string, err error) {
+ if formatParam := r.URL.Query().Get("format"); formatParam != "" {
+ // translate query param to a content type
+ switch formatParam {
+ case "raw":
+ return "application/vnd.ipld.raw", nil, nil
+ case "car":
+ return "application/vnd.ipld.car", nil, nil
+ case "tar":
+ return "application/x-tar", nil, nil
+ case "json":
+ return "application/json", nil, nil
+ case "cbor":
+ return "application/cbor", nil, nil
+ case "dag-json":
+ return "application/vnd.ipld.dag-json", nil, nil
+ case "dag-cbor":
+ return "application/vnd.ipld.dag-cbor", nil, nil
+ case "ipns-record":
+ return "application/vnd.ipfs.ipns-record", nil, nil
+ }
+ }
+ // Browsers and other user agents will send Accept header with generic types like:
+ // Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
+ // We only care about explicit, vendor-specific content-types and respond to the first match (in order).
+ // TODO: make this RFC compliant and respect weights (eg. return CAR for Accept:application/vnd.ipld.dag-json;q=0.1,application/vnd.ipld.car;q=0.2)
+ for _, header := range r.Header.Values("Accept") {
+ for _, value := range strings.Split(header, ",") {
+ accept := strings.TrimSpace(value)
+ // respond to the very first matching content type
+ if strings.HasPrefix(accept, "application/vnd.ipld") ||
+ strings.HasPrefix(accept, "application/x-tar") ||
+ strings.HasPrefix(accept, "application/json") ||
+ strings.HasPrefix(accept, "application/cbor") ||
+ strings.HasPrefix(accept, "application/vnd.ipfs") {
+ mediatype, params, err := mime.ParseMediaType(accept)
+ if err != nil {
+ return "", nil, err
+ }
+ return mediatype, params, nil
+ }
+ }
+ }
+ // If none of special-cased content types is found, return empty string
+ // to indicate default, implicit UnixFS response should be prepared
+ return "", nil, nil
+}
+
+// returns unquoted path with all special characters revealed as \u codes
+func debugStr(path string) string {
+ q := fmt.Sprintf("%+q", path)
+ if len(q) >= 3 {
+ q = q[1 : len(q)-1]
+ }
+ return q
+}
+
+// Resolve the provided contentPath including any special handling related to
+// the requested responseFormat. Returned ok flag indicates if gateway handler
+// should continue processing the request.
+func (i *handler) handlePathResolution(w http.ResponseWriter, r *http.Request, responseFormat string, contentPath ipath.Path, logger *zap.SugaredLogger) (resolvedPath ipath.Resolved, newContentPath ipath.Path, ok bool) {
+ // Attempt to resolve the provided path.
+ resolvedPath, err := i.api.ResolvePath(r.Context(), contentPath)
+
+ switch err {
+ case nil:
+ return resolvedPath, contentPath, true
+ case coreiface.ErrOffline:
+ err = fmt.Errorf("failed to resolve %s: %w", debugStr(contentPath.String()), err)
+ webError(w, err, http.StatusServiceUnavailable)
+ return nil, nil, false
+ default:
+ // The path can't be resolved.
+ if isUnixfsResponseFormat(responseFormat) {
+ // If we have origin isolation (subdomain gw, DNSLink website),
+ // and response type is UnixFS (default for website hosting)
+ // check for presence of _redirects file and apply rules defined there.
+ // See: https://github.com/ipfs/specs/pull/290
+ if hasOriginIsolation(r) {
+ resolvedPath, newContentPath, ok, hadMatchingRule := i.serveRedirectsIfPresent(w, r, resolvedPath, contentPath, logger)
+ if hadMatchingRule {
+ logger.Debugw("applied a rule from _redirects file")
+ return resolvedPath, newContentPath, ok
+ }
+ }
+
+ // if Accept is text/html, see if ipfs-404.html is present
+ // This logic isn't documented and will likely be removed at some point.
+ // Any 404 logic in _redirects above will have already run by this time, so it's really an extra fall back
+ if i.serveLegacy404IfPresent(w, r, contentPath) {
+ logger.Debugw("served legacy 404")
+ return nil, nil, false
+ }
+ }
+
+ err = fmt.Errorf("failed to resolve %s: %w", debugStr(contentPath.String()), err)
+ webError(w, err, http.StatusInternalServerError)
+ return nil, nil, false
+ }
+}
+
+// Detect 'Cache-Control: only-if-cached' in request and return data if it is already in the local datastore.
+// https://github.com/ipfs/specs/blob/main/http-gateways/PATH_GATEWAY.md#cache-control-request-header
+func (i *handler) handleOnlyIfCached(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, logger *zap.SugaredLogger) (requestHandled bool) {
+ if r.Header.Get("Cache-Control") == "only-if-cached" {
+ if !i.api.IsCached(r.Context(), contentPath) {
+ if r.Method == http.MethodHead {
+ w.WriteHeader(http.StatusPreconditionFailed)
+ return true
+ }
+ errMsg := fmt.Sprintf("%q not in local datastore", contentPath.String())
+ http.Error(w, errMsg, http.StatusPreconditionFailed)
+ return true
+ }
+ if r.Method == http.MethodHead {
+ w.WriteHeader(http.StatusOK)
+ return true
+ }
+ }
+ return false
+}
+
+func handleUnsupportedHeaders(r *http.Request) (err *ErrorResponse) {
+ // X-Ipfs-Gateway-Prefix was removed (https://github.com/ipfs/kubo/issues/7702)
+ // TODO: remove this after go-ipfs 0.13 ships
+ if prfx := r.Header.Get("X-Ipfs-Gateway-Prefix"); prfx != "" {
+ err := fmt.Errorf("unsupported HTTP header: X-Ipfs-Gateway-Prefix support was removed: https://github.com/ipfs/kubo/issues/7702")
+ return NewErrorResponse(err, http.StatusBadRequest)
+ }
+ return nil
+}
+
+// ?uri query param support for requests produced by web browsers
+// via navigator.registerProtocolHandler Web API
+// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler
+// TLDR: redirect /ipfs/?uri=ipfs%3A%2F%2Fcid%3Fquery%3Dval to /ipfs/cid?query=val
+func handleProtocolHandlerRedirect(w http.ResponseWriter, r *http.Request, logger *zap.SugaredLogger) (requestHandled bool) {
+ if uriParam := r.URL.Query().Get("uri"); uriParam != "" {
+ u, err := url.Parse(uriParam)
+ if err != nil {
+ webError(w, fmt.Errorf("failed to parse uri query parameter: %w", err), http.StatusBadRequest)
+ return true
+ }
+ if u.Scheme != "ipfs" && u.Scheme != "ipns" {
+ webError(w, fmt.Errorf("uri query parameter scheme must be ipfs or ipns: %w", err), http.StatusBadRequest)
+ return true
+ }
+ path := u.Path
+ if u.RawQuery != "" { // preserve query if present
+ path = path + "?" + u.RawQuery
+ }
+
+ redirectURL := gopath.Join("/", u.Scheme, u.Host, path)
+ logger.Debugw("uri param, redirect", "to", redirectURL, "status", http.StatusMovedPermanently)
+ http.Redirect(w, r, redirectURL, http.StatusMovedPermanently)
+ return true
+ }
+
+ return false
+}
+
+// Disallow Service Worker registration on namespace roots
+// https://github.com/ipfs/kubo/issues/4025
+func handleServiceWorkerRegistration(r *http.Request) (err *ErrorResponse) {
+ if r.Header.Get("Service-Worker") == "script" {
+ matched, _ := regexp.MatchString(`^/ip[fn]s/[^/]+$`, r.URL.Path)
+ if matched {
+ err := fmt.Errorf("registration is not allowed for this scope")
+ return NewErrorResponse(fmt.Errorf("navigator.serviceWorker: %w", err), http.StatusBadRequest)
+ }
+ }
+
+ return nil
+}
+
+// Attempt to fix redundant /ipfs/ namespace as long as resulting
+// 'intended' path is valid. This is in case gremlins were tickled
+// wrong way and user ended up at /ipfs/ipfs/{cid} or /ipfs/ipns/{id}
+// like in bafybeien3m7mdn6imm425vc2s22erzyhbvk5n3ofzgikkhmdkh5cuqbpbq :^))
+func handleSuperfluousNamespace(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) (requestHandled bool) {
+ // If the path is valid, there's nothing to do
+ if pathErr := contentPath.IsValid(); pathErr == nil {
+ return false
+ }
+
+ // If there's no superflous namespace, there's nothing to do
+ if !(strings.HasPrefix(r.URL.Path, "/ipfs/ipfs/") || strings.HasPrefix(r.URL.Path, "/ipfs/ipns/")) {
+ return false
+ }
+
+ // Attempt to fix the superflous namespace
+ intendedPath := ipath.New(strings.TrimPrefix(r.URL.Path, "/ipfs"))
+ if err := intendedPath.IsValid(); err != nil {
+ webError(w, fmt.Errorf("invalid ipfs path: %w", err), http.StatusBadRequest)
+ return true
+ }
+ intendedURL := intendedPath.String()
+ if r.URL.RawQuery != "" {
+ // we render HTML, so ensure query entries are properly escaped
+ q, _ := url.ParseQuery(r.URL.RawQuery)
+ intendedURL = intendedURL + "?" + q.Encode()
+ }
+ // return HTTP 400 (Bad Request) with HTML error page that:
+ // - points at correct canonical path via header
+ // - displays human-readable error
+ // - redirects to intendedURL after a short delay
+
+ w.WriteHeader(http.StatusBadRequest)
+ if err := redirectTemplate.Execute(w, redirectTemplateData{
+ RedirectURL: intendedURL,
+ SuggestedPath: intendedPath.String(),
+ ErrorMsg: fmt.Sprintf("invalid path: %q should be %q", r.URL.Path, intendedPath.String()),
+ }); err != nil {
+ webError(w, fmt.Errorf("failed to redirect when fixing superfluous namespace: %w", err), http.StatusBadRequest)
+ }
+
+ return true
+}
+
+func (i *handler) handleGettingFirstBlock(r *http.Request, begin time.Time, contentPath ipath.Path, resolvedPath ipath.Resolved) *ErrorResponse {
+ // Update the global metric of the time it takes to read the final root block of the requested resource
+ // NOTE: for legacy reasons this happens before we go into content-type specific code paths
+ _, err := i.api.GetBlock(r.Context(), resolvedPath.Cid())
+ if err != nil {
+ err = fmt.Errorf("could not get block %s: %w", resolvedPath.Cid().String(), err)
+ return NewErrorResponse(err, http.StatusInternalServerError)
+ }
+ ns := contentPath.Namespace()
+ timeToGetFirstContentBlock := time.Since(begin).Seconds()
+ i.unixfsGetMetric.WithLabelValues(ns).Observe(timeToGetFirstContentBlock) // deprecated, use firstContentBlockGetMetric instead
+ i.firstContentBlockGetMetric.WithLabelValues(ns).Observe(timeToGetFirstContentBlock)
+ return nil
+}
+
+func (i *handler) setCommonHeaders(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) *ErrorResponse {
+ i.addUserHeaders(w) // ok, _now_ write user's headers.
+ w.Header().Set("X-Ipfs-Path", contentPath.String())
+
+ if rootCids, err := i.buildIpfsRootsHeader(contentPath.String(), r); err == nil {
+ w.Header().Set("X-Ipfs-Roots", rootCids)
+ } else { // this should never happen, as we resolved the contentPath already
+ err = fmt.Errorf("error while resolving X-Ipfs-Roots: %w", err)
+ return NewErrorResponse(err, http.StatusInternalServerError)
+ }
+
+ return nil
+}
+
+// spanTrace starts a new span using the standard IPFS tracing conventions.
+func spanTrace(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
+ return otel.Tracer("boxo").Start(ctx, fmt.Sprintf("%s.%s", " Gateway", spanName), opts...)
+}
diff --git a/gateway/handler_block.go b/gateway/handler_block.go
new file mode 100644
index 0000000000..773088c17d
--- /dev/null
+++ b/gateway/handler_block.go
@@ -0,0 +1,53 @@
+package gateway
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+
+ ipath "github.com/ipfs/boxo/coreiface/path"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
+)
+
+// serveRawBlock returns bytes behind a raw block
+func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time) bool {
+ ctx, span := spanTrace(ctx, "ServeRawBlock", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
+ defer span.End()
+
+ blockCid := resolvedPath.Cid()
+ block, err := i.api.GetBlock(ctx, blockCid)
+ if err != nil {
+ err = fmt.Errorf("error getting block %s: %w", blockCid.String(), err)
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+ content := bytes.NewReader(block.RawData())
+
+ // Set Content-Disposition
+ var name string
+ if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" {
+ name = urlFilename
+ } else {
+ name = blockCid.String() + ".bin"
+ }
+ setContentDispositionHeader(w, name, "attachment")
+
+ // Set remaining headers
+ modtime := addCacheControlHeaders(w, r, contentPath, blockCid)
+ w.Header().Set("Content-Type", "application/vnd.ipld.raw")
+ w.Header().Set("X-Content-Type-Options", "nosniff") // no funny business in the browsers :^)
+
+ // ServeContent will take care of
+ // If-None-Match+Etag, Content-Length and range requests
+ _, dataSent, _ := ServeContent(w, r, name, modtime, content)
+
+ if dataSent {
+ // Update metrics
+ i.rawBlockGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
+ }
+
+ return dataSent
+}
diff --git a/gateway/handler_car.go b/gateway/handler_car.go
new file mode 100644
index 0000000000..22611adaf0
--- /dev/null
+++ b/gateway/handler_car.go
@@ -0,0 +1,99 @@
+package gateway
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ ipath "github.com/ipfs/boxo/coreiface/path"
+ gocar "github.com/ipfs/boxo/ipld/car"
+ cid "github.com/ipfs/go-cid"
+ selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
+)
+
+// serveCAR returns a CAR stream for specific DAG+selector
+func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, carVersion string, begin time.Time) bool {
+ ctx, span := spanTrace(ctx, "ServeCAR", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
+ defer span.End()
+
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ switch carVersion {
+ case "": // noop, client does not care about version
+ case "1": // noop, we support this
+ default:
+ err := fmt.Errorf("unsupported CAR version: only version=1 is supported")
+ webError(w, err, http.StatusBadRequest)
+ return false
+ }
+ rootCid := resolvedPath.Cid()
+
+ // Set Content-Disposition
+ var name string
+ if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" {
+ name = urlFilename
+ } else {
+ name = rootCid.String() + ".car"
+ }
+ setContentDispositionHeader(w, name, "attachment")
+
+ // Set Cache-Control (same logic as for a regular files)
+ addCacheControlHeaders(w, r, contentPath, rootCid)
+
+ // Weak Etag W/ because we can't guarantee byte-for-byte identical
+ // responses, but still want to benefit from HTTP Caching. Two CAR
+ // responses for the same CID and selector will be logically equivalent,
+ // but when CAR is streamed, then in theory, blocks may arrive from
+ // datastore in non-deterministic order.
+ etag := `W/` + getEtag(r, rootCid)
+ w.Header().Set("Etag", etag)
+
+ // Finish early if Etag match
+ if r.Header.Get("If-None-Match") == etag {
+ w.WriteHeader(http.StatusNotModified)
+ return false
+ }
+
+ // Make it clear we don't support range-requests over a car stream
+ // Partial downloads and resumes should be handled using requests for
+ // sub-DAGs and IPLD selectors: https://github.com/ipfs/go-ipfs/issues/8769
+ w.Header().Set("Accept-Ranges", "none")
+
+ w.Header().Set("Content-Type", "application/vnd.ipld.car; version=1")
+ w.Header().Set("X-Content-Type-Options", "nosniff") // no funny business in the browsers :^)
+
+ // Same go-car settings as dag.export command
+ store := dagStore{api: i.api, ctx: ctx}
+
+ // TODO: support selectors passed as request param: https://github.com/ipfs/kubo/issues/8769
+ dag := gocar.Dag{Root: rootCid, Selector: selectorparse.CommonSelector_ExploreAllRecursively}
+ car := gocar.NewSelectiveCar(ctx, store, []gocar.Dag{dag}, gocar.TraverseLinksOnlyOnce())
+
+ if err := car.Write(w); err != nil {
+ // We return error as a trailer, however it is not something browsers can access
+ // (https://github.com/mdn/browser-compat-data/issues/14703)
+ // Due to this, we suggest client always verify that
+ // the received CAR stream response is matching requested DAG selector
+ w.Header().Set("X-Stream-Error", err.Error())
+ return false
+ }
+
+ // Update metrics
+ i.carStreamGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
+ return true
+}
+
+// FIXME(@Jorropo): https://github.com/ipld/go-car/issues/315
+type dagStore struct {
+ api API
+ ctx context.Context
+}
+
+func (ds dagStore) Get(_ context.Context, c cid.Cid) (blocks.Block, error) {
+ return ds.api.GetBlock(ds.ctx, c)
+}
diff --git a/gateway/handler_codec.go b/gateway/handler_codec.go
new file mode 100644
index 0000000000..35199679cc
--- /dev/null
+++ b/gateway/handler_codec.go
@@ -0,0 +1,271 @@
+package gateway
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ ipath "github.com/ipfs/boxo/coreiface/path"
+ "github.com/ipfs/boxo/gateway/assets"
+ cid "github.com/ipfs/go-cid"
+ "github.com/ipld/go-ipld-prime/multicodec"
+ "github.com/ipld/go-ipld-prime/node/basicnode"
+ mc "github.com/multiformats/go-multicodec"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
+)
+
+// codecToContentType maps the supported IPLD codecs to the HTTP Content
+// Type they should have.
+var codecToContentType = map[mc.Code]string{
+ mc.Json: "application/json",
+ mc.Cbor: "application/cbor",
+ mc.DagJson: "application/vnd.ipld.dag-json",
+ mc.DagCbor: "application/vnd.ipld.dag-cbor",
+}
+
+// contentTypeToRaw maps the HTTP Content Type to the respective codec that
+// allows raw response without any conversion.
+var contentTypeToRaw = map[string][]mc.Code{
+ "application/json": {mc.Json, mc.DagJson},
+ "application/cbor": {mc.Cbor, mc.DagCbor},
+}
+
+// contentTypeToCodec maps the HTTP Content Type to the respective codec. We
+// only add here the codecs that we want to convert-to-from.
+var contentTypeToCodec = map[string]mc.Code{
+ "application/vnd.ipld.dag-json": mc.DagJson,
+ "application/vnd.ipld.dag-cbor": mc.DagCbor,
+}
+
+// contentTypeToExtension maps the HTTP Content Type to the respective file
+// extension, used in Content-Disposition header when downloading the file.
+var contentTypeToExtension = map[string]string{
+ "application/json": ".json",
+ "application/vnd.ipld.dag-json": ".json",
+ "application/cbor": ".cbor",
+ "application/vnd.ipld.dag-cbor": ".cbor",
+}
+
+func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, requestedContentType string) bool {
+ ctx, span := spanTrace(ctx, "ServeCodec", trace.WithAttributes(attribute.String("path", resolvedPath.String()), attribute.String("requestedContentType", requestedContentType)))
+ defer span.End()
+
+ cidCodec := mc.Code(resolvedPath.Cid().Prefix().Codec)
+ responseContentType := requestedContentType
+
+ // If the resolved path still has some remainder, return error for now.
+ // TODO: handle this when we have IPLD Patch (https://ipld.io/specs/patch/) via HTTP PUT
+ // TODO: (depends on https://github.com/ipfs/kubo/issues/4801 and https://github.com/ipfs/kubo/issues/4782)
+ if resolvedPath.Remainder() != "" {
+ path := strings.TrimSuffix(resolvedPath.String(), resolvedPath.Remainder())
+ err := fmt.Errorf("%q of %q could not be returned: reading IPLD Kinds other than Links (CBOR Tag 42) is not implemented: try reading %q instead", resolvedPath.Remainder(), resolvedPath.String(), path)
+ webError(w, err, http.StatusNotImplemented)
+ return false
+ }
+
+ // If no explicit content type was requested, the response will have one based on the codec from the CID
+ if requestedContentType == "" {
+ cidContentType, ok := codecToContentType[cidCodec]
+ if !ok {
+ // Should not happen unless function is called with wrong parameters.
+ err := fmt.Errorf("content type not found for codec: %v", cidCodec)
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+ responseContentType = cidContentType
+ }
+
+ // Set HTTP headers (for caching etc)
+ modtime := addCacheControlHeaders(w, r, contentPath, resolvedPath.Cid())
+ name := setCodecContentDisposition(w, r, resolvedPath, responseContentType)
+ w.Header().Set("Content-Type", responseContentType)
+ w.Header().Set("X-Content-Type-Options", "nosniff")
+
+ // No content type is specified by the user (via Accept, or format=). However,
+ // we support this format. Let's handle it.
+ if requestedContentType == "" {
+ isDAG := cidCodec == mc.DagJson || cidCodec == mc.DagCbor
+ acceptsHTML := strings.Contains(r.Header.Get("Accept"), "text/html")
+ download := r.URL.Query().Get("download") == "true"
+
+ if isDAG && acceptsHTML && !download {
+ return i.serveCodecHTML(ctx, w, r, resolvedPath, contentPath)
+ } else {
+ // This covers CIDs with codec 'json' and 'cbor' as those do not have
+ // an explicit requested content type.
+ return i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime, begin)
+ }
+ }
+
+ // If DAG-JSON or DAG-CBOR was requested using corresponding plain content type
+ // return raw block as-is, without conversion
+ skipCodecs, ok := contentTypeToRaw[requestedContentType]
+ if ok {
+ for _, skipCodec := range skipCodecs {
+ if skipCodec == cidCodec {
+ return i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime, begin)
+ }
+ }
+ }
+
+ // Otherwise, the user has requested a specific content type (a DAG-* variant).
+ // Let's first get the codecs that can be used with this content type.
+ toCodec, ok := contentTypeToCodec[requestedContentType]
+ if !ok {
+ // This is never supposed to happen unless function is called with wrong parameters.
+ err := fmt.Errorf("unsupported content type: %q", requestedContentType)
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+
+ // This handles DAG-* conversions and validations.
+ return i.serveCodecConverted(ctx, w, r, resolvedPath, contentPath, toCodec, modtime, begin)
+}
+
+func (i *handler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path) bool {
+ // A HTML directory index will be presented, be sure to set the correct
+ // type instead of relying on autodetection (which may fail).
+ w.Header().Set("Content-Type", "text/html")
+
+ // Clear Content-Disposition -- we want HTML to be rendered inline
+ w.Header().Del("Content-Disposition")
+
+ // Generated index requires custom Etag (output may change between Kubo versions)
+ dagEtag := getDagIndexEtag(resolvedPath.Cid())
+ w.Header().Set("Etag", dagEtag)
+
+ // Remove Cache-Control for now to match UnixFS dir-index-html responses
+ // (we don't want browser to cache HTML forever)
+ // TODO: if we ever change behavior for UnixFS dir listings, same changes should be applied here
+ w.Header().Del("Cache-Control")
+
+ cidCodec := mc.Code(resolvedPath.Cid().Prefix().Codec)
+ if err := assets.DagTemplate.Execute(w, assets.DagTemplateData{
+ Path: contentPath.String(),
+ CID: resolvedPath.Cid().String(),
+ CodecName: cidCodec.String(),
+ CodecHex: fmt.Sprintf("0x%x", uint64(cidCodec)),
+ }); err != nil {
+ err = fmt.Errorf("failed to generate HTML listing for this DAG: try fetching raw block with ?format=raw: %w", err)
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+
+ return true
+}
+
+// serveCodecRaw returns the raw block without any conversion
+func (i *handler) serveCodecRaw(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, name string, modtime, begin time.Time) bool {
+ blockCid := resolvedPath.Cid()
+ block, err := i.api.GetBlock(ctx, blockCid)
+ if err != nil {
+ err = fmt.Errorf("error getting block %s: %w", blockCid.String(), err)
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+ content := bytes.NewReader(block.RawData())
+
+ // ServeContent will take care of
+ // If-None-Match+Etag, Content-Length and range requests
+ _, dataSent, _ := ServeContent(w, r, name, modtime, content)
+
+ if dataSent {
+ // Update metrics
+ i.jsoncborDocumentGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
+ }
+
+ return dataSent
+}
+
+// serveCodecConverted returns payload converted to codec specified in toCodec
+func (i *handler) serveCodecConverted(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, toCodec mc.Code, modtime, begin time.Time) bool {
+ blockCid := resolvedPath.Cid()
+ block, err := i.api.GetBlock(ctx, blockCid)
+ if err != nil {
+ err = fmt.Errorf("error getting block %s: %w", blockCid.String(), err)
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+
+ codec := blockCid.Prefix().Codec
+ decoder, err := multicodec.LookupDecoder(codec)
+ if err != nil {
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+
+ node := basicnode.Prototype.Any.NewBuilder()
+ err = decoder(node, bytes.NewReader(block.RawData()))
+ if err != nil {
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+
+ encoder, err := multicodec.LookupEncoder(uint64(toCodec))
+ if err != nil {
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+
+ // Ensure IPLD node conforms to the codec specification.
+ var buf bytes.Buffer
+ err = encoder(node.Build(), &buf)
+ if err != nil {
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+
+ // Sets correct Last-Modified header. This code is borrowed from the standard
+ // library (net/http/server.go) as we cannot use serveFile.
+ if !(modtime.IsZero() || modtime.Equal(unixEpochTime)) {
+ w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat))
+ }
+
+ _, err = w.Write(buf.Bytes())
+ if err == nil {
+ // Update metrics
+ i.jsoncborDocumentGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
+ return true
+ }
+
+ return false
+}
+
+func setCodecContentDisposition(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentType string) string {
+ var dispType, name string
+
+ ext, ok := contentTypeToExtension[contentType]
+ if !ok {
+ // Should never happen.
+ ext = ".bin"
+ }
+
+ if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" {
+ name = urlFilename
+ } else {
+ name = resolvedPath.Cid().String() + ext
+ }
+
+ // JSON should be inlined, but ?download=true should still override
+ if r.URL.Query().Get("download") == "true" {
+ dispType = "attachment"
+ } else {
+ switch ext {
+ case ".json": // codecs that serialize to JSON can be rendered by browsers
+ dispType = "inline"
+ default: // everything else is assumed binary / opaque bytes
+ dispType = "attachment"
+ }
+ }
+
+ setContentDispositionHeader(w, name, dispType)
+ return name
+}
+
+func getDagIndexEtag(dagCid cid.Cid) string {
+ return `"DagIndex-` + assets.AssetHash + `_CID-` + dagCid.String() + `"`
+}
diff --git a/gateway/handler_ipns_record.go b/gateway/handler_ipns_record.go
new file mode 100644
index 0000000000..a1487f0c8e
--- /dev/null
+++ b/gateway/handler_ipns_record.go
@@ -0,0 +1,90 @@
+package gateway
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/gogo/protobuf/proto"
+ ipath "github.com/ipfs/boxo/coreiface/path"
+ ipns_pb "github.com/ipfs/boxo/ipns/pb"
+ "github.com/ipfs/go-cid"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
+ "go.uber.org/zap"
+)
+
+func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) bool {
+ ctx, span := spanTrace(ctx, "ServeIPNSRecord", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
+ defer span.End()
+
+ if contentPath.Namespace() != "ipns" {
+ err := fmt.Errorf("%s is not an IPNS link", contentPath.String())
+ webError(w, err, http.StatusBadRequest)
+ return false
+ }
+
+ key := contentPath.String()
+ key = strings.TrimSuffix(key, "/")
+ key = strings.TrimPrefix(key, "/ipns/")
+ if strings.Count(key, "/") != 0 {
+ err := errors.New("cannot find ipns key for subpath")
+ webError(w, err, http.StatusBadRequest)
+ return false
+ }
+
+ c, err := cid.Decode(key)
+ if err != nil {
+ webError(w, err, http.StatusBadRequest)
+ return false
+ }
+
+ rawRecord, err := i.api.GetIPNSRecord(ctx, c)
+ if err != nil {
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+
+ var record ipns_pb.IpnsEntry
+ err = proto.Unmarshal(rawRecord, &record)
+ if err != nil {
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+
+ // Set cache control headers based on the TTL set in the IPNS record. If the
+ // TTL is not present, we use the Last-Modified tag. We are tracking IPNS
+ // caching on: https://github.com/ipfs/kubo/issues/1818.
+ // TODO: use addCacheControlHeaders once #1818 is fixed.
+ w.Header().Set("Etag", getEtag(r, resolvedPath.Cid()))
+ if record.Ttl != nil {
+ seconds := int(time.Duration(*record.Ttl).Seconds())
+ w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", seconds))
+ } else {
+ w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
+ }
+
+ // Set Content-Disposition
+ var name string
+ if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" {
+ name = urlFilename
+ } else {
+ name = key + ".ipns-record"
+ }
+ setContentDispositionHeader(w, name, "attachment")
+
+ w.Header().Set("Content-Type", "application/vnd.ipfs.ipns-record")
+ w.Header().Set("X-Content-Type-Options", "nosniff")
+
+ _, err = w.Write(rawRecord)
+ if err == nil {
+ // Update metrics
+ i.ipnsRecordGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
+ return true
+ }
+
+ return false
+}
diff --git a/gateway/handler_tar.go b/gateway/handler_tar.go
new file mode 100644
index 0000000000..7d835bb337
--- /dev/null
+++ b/gateway/handler_tar.go
@@ -0,0 +1,97 @@
+package gateway
+
+import (
+ "context"
+ "fmt"
+ "html"
+ "net/http"
+ "time"
+
+ ipath "github.com/ipfs/boxo/coreiface/path"
+ "github.com/ipfs/boxo/files"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
+ "go.uber.org/zap"
+)
+
+var unixEpochTime = time.Unix(0, 0)
+
+func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) bool {
+ ctx, span := spanTrace(ctx, "ServeTAR", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
+ defer span.End()
+
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ // Get Unixfs file
+ file, err := i.api.GetUnixFsNode(ctx, resolvedPath)
+ if err != nil {
+ err = fmt.Errorf("error getting UnixFS node for %s: %w", html.EscapeString(contentPath.String()), err)
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+ defer file.Close()
+
+ rootCid := resolvedPath.Cid()
+
+ // Set Cache-Control and read optional Last-Modified time
+ modtime := addCacheControlHeaders(w, r, contentPath, rootCid)
+
+ // Weak Etag W/ because we can't guarantee byte-for-byte identical
+ // responses, but still want to benefit from HTTP Caching. Two TAR
+ // responses for the same CID will be logically equivalent,
+ // but when TAR is streamed, then in theory, files and directories
+ // may arrive in different order (depends on TAR lib and filesystem/inodes).
+ etag := `W/` + getEtag(r, rootCid)
+ w.Header().Set("Etag", etag)
+
+ // Finish early if Etag match
+ if r.Header.Get("If-None-Match") == etag {
+ w.WriteHeader(http.StatusNotModified)
+ return false
+ }
+
+ // Set Content-Disposition
+ var name string
+ if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" {
+ name = urlFilename
+ } else {
+ name = rootCid.String() + ".tar"
+ }
+ setContentDispositionHeader(w, name, "attachment")
+
+ // Construct the TAR writer
+ tarw, err := files.NewTarWriter(w)
+ if err != nil {
+ webError(w, fmt.Errorf("could not build tar writer: %w", err), http.StatusInternalServerError)
+ return false
+ }
+ defer tarw.Close()
+
+ // Sets correct Last-Modified header. This code is borrowed from the standard
+ // library (net/http/server.go) as we cannot use serveFile without throwing the entire
+ // TAR into the memory first.
+ if !(modtime.IsZero() || modtime.Equal(unixEpochTime)) {
+ w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat))
+ }
+
+ w.Header().Set("Content-Type", "application/x-tar")
+ w.Header().Set("X-Content-Type-Options", "nosniff") // no funny business in the browsers :^)
+
+ // The TAR has a top-level directory (or file) named by the CID.
+ if err := tarw.WriteFile(file, rootCid.String()); err != nil {
+ w.Header().Set("X-Stream-Error", err.Error())
+ // Trailer headers do not work in web browsers
+ // (see https://github.com/mdn/browser-compat-data/issues/14703)
+ // and we have limited options around error handling in browser contexts.
+ // To improve UX/DX, we finish response stream with error message, allowing client to
+ // (1) detect error by having corrupted TAR
+ // (2) be able to reason what went wrong by instecting the tail of TAR stream
+ _, _ = w.Write([]byte(err.Error()))
+ return false
+ }
+
+ // Update metrics
+ i.tarStreamGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
+ return true
+}
diff --git a/gateway/handler_test.go b/gateway/handler_test.go
new file mode 100644
index 0000000000..57fcead819
--- /dev/null
+++ b/gateway/handler_test.go
@@ -0,0 +1,208 @@
+package gateway
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+
+ "testing"
+ "time"
+
+ "github.com/ipfs/boxo/blocks"
+ iface "github.com/ipfs/boxo/coreiface"
+ ipath "github.com/ipfs/boxo/coreiface/path"
+ "github.com/ipfs/boxo/files"
+ "github.com/ipfs/boxo/path/resolver"
+ cid "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestEtagMatch(t *testing.T) {
+ for _, test := range []struct {
+ header string // value in If-None-Match HTTP header
+ cidEtag string
+ dirEtag string
+ expected bool // expected result of etagMatch(header, cidEtag, dirEtag)
+ }{
+ {"", `"etag"`, "", false}, // no If-None-Match
+ {"", "", `"etag"`, false}, // no If-None-Match
+ {`"etag"`, `"etag"`, "", true}, // file etag match
+ {`W/"etag"`, `"etag"`, "", true}, // file etag match
+ {`"foo", W/"bar", W/"etag"`, `"etag"`, "", true}, // file etag match (array)
+ {`"foo",W/"bar",W/"etag"`, `"etag"`, "", true}, // file etag match (compact array)
+ {`"etag"`, "", `W/"etag"`, true}, // dir etag match
+ {`"etag"`, "", `W/"etag"`, true}, // dir etag match
+ {`W/"etag"`, "", `W/"etag"`, true}, // dir etag match
+ {`*`, `"etag"`, "", true}, // wildcard etag match
+ } {
+ result := etagMatch(test.header, test.cidEtag, test.dirEtag)
+ assert.Equalf(t, test.expected, result, "etagMatch(%q, %q, %q)", test.header, test.cidEtag, test.dirEtag)
+ }
+}
+
+type errorMockAPI struct {
+ err error
+}
+
+func (api *errorMockAPI) GetUnixFsNode(context.Context, ipath.Resolved) (files.Node, error) {
+ return nil, api.err
+}
+
+func (api *errorMockAPI) LsUnixFsDir(ctx context.Context, p ipath.Resolved) (<-chan iface.DirEntry, error) {
+ return nil, api.err
+}
+
+func (api *errorMockAPI) GetBlock(ctx context.Context, c cid.Cid) (blocks.Block, error) {
+ return nil, api.err
+}
+
+func (api *errorMockAPI) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, error) {
+ return nil, api.err
+}
+
+func (api *errorMockAPI) GetDNSLinkRecord(ctx context.Context, hostname string) (ipath.Path, error) {
+ return nil, api.err
+}
+
+func (api *errorMockAPI) IsCached(ctx context.Context, p ipath.Path) bool {
+ return false
+}
+
+func (api *errorMockAPI) ResolvePath(ctx context.Context, ip ipath.Path) (ipath.Resolved, error) {
+ return nil, api.err
+}
+
+func TestGatewayBadRequestInvalidPath(t *testing.T) {
+ api, _ := newMockAPI(t)
+ ts := newTestServer(t, api)
+ t.Logf("test server url: %s", ts.URL)
+
+ req, err := http.NewRequest(http.MethodGet, ts.URL+"/ipfs/QmInvalid/Path", nil)
+ assert.Nil(t, err)
+
+ res, err := ts.Client().Do(req)
+ assert.Nil(t, err)
+
+ assert.Equal(t, http.StatusBadRequest, res.StatusCode)
+}
+
+func TestErrorBubblingFromAPI(t *testing.T) {
+ t.Parallel()
+
+ for _, test := range []struct {
+ name string
+ err error
+ status int
+ }{
+ {"404 Not Found from IPLD", &ipld.ErrNotFound{}, http.StatusNotFound},
+ {"404 Not Found from path resolver", resolver.ErrNoLink{}, http.StatusNotFound},
+ {"502 Bad Gateway", ErrBadGateway, http.StatusBadGateway},
+ {"504 Gateway Timeout", ErrGatewayTimeout, http.StatusGatewayTimeout},
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ api := &errorMockAPI{err: fmt.Errorf("wrapped for testing purposes: %w", test.err)}
+ ts := newTestServer(t, api)
+ t.Logf("test server url: %s", ts.URL)
+
+ req, err := http.NewRequest(http.MethodGet, ts.URL+"/ipns/en.wikipedia-on-ipfs.org", nil)
+ assert.Nil(t, err)
+
+ res, err := ts.Client().Do(req)
+ assert.Nil(t, err)
+ assert.Equal(t, test.status, res.StatusCode)
+ })
+ }
+
+ for _, test := range []struct {
+ name string
+ err error
+ status int
+ headerName string
+ headerValue string
+ headerLength int // how many times was headerName set
+ }{
+ {"429 Too Many Requests without Retry-After header", ErrTooManyRequests, http.StatusTooManyRequests, "Retry-After", "", 0},
+ {"429 Too Many Requests without Retry-After header", NewErrorRetryAfter(ErrTooManyRequests, 0*time.Second), http.StatusTooManyRequests, "Retry-After", "", 0},
+ {"429 Too Many Requests with Retry-After header", NewErrorRetryAfter(ErrTooManyRequests, 3600*time.Second), http.StatusTooManyRequests, "Retry-After", "3600", 1},
+ } {
+ api := &errorMockAPI{err: fmt.Errorf("wrapped for testing purposes: %w", test.err)}
+ ts := newTestServer(t, api)
+ t.Logf("test server url: %s", ts.URL)
+
+ req, err := http.NewRequest(http.MethodGet, ts.URL+"/ipns/en.wikipedia-on-ipfs.org", nil)
+ assert.Nil(t, err)
+
+ res, err := ts.Client().Do(req)
+ assert.Nil(t, err)
+ assert.Equal(t, test.status, res.StatusCode)
+ assert.Equal(t, test.headerValue, res.Header.Get(test.headerName))
+ assert.Equal(t, test.headerLength, len(res.Header.Values(test.headerName)))
+ }
+}
+
+type panicMockAPI struct {
+ panicOnHostnameHandler bool
+}
+
+func (api *panicMockAPI) GetUnixFsNode(context.Context, ipath.Resolved) (files.Node, error) {
+ panic("i am panicking")
+}
+
+func (api *panicMockAPI) LsUnixFsDir(ctx context.Context, p ipath.Resolved) (<-chan iface.DirEntry, error) {
+ panic("i am panicking")
+}
+
+func (api *panicMockAPI) GetBlock(ctx context.Context, c cid.Cid) (blocks.Block, error) {
+ panic("i am panicking")
+}
+
+func (api *panicMockAPI) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, error) {
+ panic("i am panicking")
+}
+
+func (api *panicMockAPI) GetDNSLinkRecord(ctx context.Context, hostname string) (ipath.Path, error) {
+ // GetDNSLinkRecord is also called on the WithHostname handler. We have this option
+ // to disable panicking here so we can test if both the regular gateway handler
+ // and the hostname handler can handle panics.
+ if api.panicOnHostnameHandler {
+ panic("i am panicking")
+ }
+
+ return nil, errors.New("not implemented")
+}
+
+func (api *panicMockAPI) IsCached(ctx context.Context, p ipath.Path) bool {
+ panic("i am panicking")
+}
+
+func (api *panicMockAPI) ResolvePath(ctx context.Context, ip ipath.Path) (ipath.Resolved, error) {
+ panic("i am panicking")
+}
+
+func TestGatewayStatusCodeOnPanic(t *testing.T) {
+ api := &panicMockAPI{}
+ ts := newTestServer(t, api)
+ t.Logf("test server url: %s", ts.URL)
+
+ req, err := http.NewRequest(http.MethodGet, ts.URL+"/ipfs/bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e", nil)
+ assert.Nil(t, err)
+
+ res, err := ts.Client().Do(req)
+ assert.Nil(t, err)
+ assert.Equal(t, http.StatusInternalServerError, res.StatusCode)
+}
+
+func TestGatewayStatusCodeOnHostnamePanic(t *testing.T) {
+ api := &panicMockAPI{panicOnHostnameHandler: true}
+ ts := newTestServer(t, api)
+ t.Logf("test server url: %s", ts.URL)
+
+ req, err := http.NewRequest(http.MethodGet, ts.URL+"/ipfs/bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e", nil)
+ assert.Nil(t, err)
+
+ res, err := ts.Client().Do(req)
+ assert.Nil(t, err)
+ assert.Equal(t, http.StatusInternalServerError, res.StatusCode)
+}
diff --git a/gateway/handler_unixfs.go b/gateway/handler_unixfs.go
new file mode 100644
index 0000000000..f5a96b146a
--- /dev/null
+++ b/gateway/handler_unixfs.go
@@ -0,0 +1,44 @@
+package gateway
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+
+ ipath "github.com/ipfs/boxo/coreiface/path"
+ "github.com/ipfs/boxo/files"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
+ "go.uber.org/zap"
+)
+
+func (i *handler) serveUnixFS(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) bool {
+ ctx, span := spanTrace(ctx, "ServeUnixFS", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
+ defer span.End()
+
+ // Handling UnixFS
+ dr, err := i.api.GetUnixFsNode(ctx, resolvedPath)
+ if err != nil {
+ err = fmt.Errorf("error while getting UnixFS node: %w", err)
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+ defer dr.Close()
+
+ // Handling Unixfs file
+ if f, ok := dr.(files.File); ok {
+ logger.Debugw("serving unixfs file", "path", contentPath)
+ return i.serveFile(ctx, w, r, resolvedPath, contentPath, f, begin)
+ }
+
+ // Handling Unixfs directory
+ dir, ok := dr.(files.Directory)
+ if !ok {
+ webError(w, fmt.Errorf("unsupported UnixFS type"), http.StatusInternalServerError)
+ return false
+ }
+
+ logger.Debugw("serving unixfs directory", "path", contentPath)
+ return i.serveDirectory(ctx, w, r, resolvedPath, contentPath, dir, begin, logger)
+}
diff --git a/gateway/handler_unixfs__redirects.go b/gateway/handler_unixfs__redirects.go
new file mode 100644
index 0000000000..afd0c7c3c8
--- /dev/null
+++ b/gateway/handler_unixfs__redirects.go
@@ -0,0 +1,287 @@
+package gateway
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ gopath "path"
+ "strconv"
+ "strings"
+
+ ipath "github.com/ipfs/boxo/coreiface/path"
+ "github.com/ipfs/boxo/files"
+ redirects "github.com/ipfs/go-ipfs-redirects-file"
+ "go.uber.org/zap"
+)
+
+// Resolving a UnixFS path involves determining if the provided `path.Path` exists and returning the `path.Resolved`
+// corresponding to that path. For UnixFS, path resolution is more involved.
+//
+// When a path under requested CID does not exist, Gateway will check if a `_redirects` file exists
+// underneath the root CID of the path, and apply rules defined there.
+// See sepcification introduced in: https://github.com/ipfs/specs/pull/290
+//
+// Scenario 1:
+// If a path exists, we always return the `path.Resolved` corresponding to that path, regardless of the existence of a `_redirects` file.
+//
+// Scenario 2:
+// If a path does not exist, usually we should return a `nil` resolution path and an error indicating that the path
+// doesn't exist. However, a `_redirects` file may exist and contain a redirect rule that redirects that path to a different path.
+// We need to evaluate the rule and perform the redirect if present.
+//
+// Scenario 3:
+// Another possibility is that the path corresponds to a rewrite rule (i.e. a rule with a status of 200).
+// In this case, we don't perform a redirect, but do need to return a `path.Resolved` and `path.Path` corresponding to
+// the rewrite destination path.
+//
+// Note that for security reasons, redirect rules are only processed when the request has origin isolation.
+// See https://github.com/ipfs/specs/pull/290 for more information.
+func (i *handler) serveRedirectsIfPresent(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, logger *zap.SugaredLogger) (newResolvedPath ipath.Resolved, newContentPath ipath.Path, continueProcessing bool, hadMatchingRule bool) {
+ redirectsFile := i.getRedirectsFile(r, contentPath, logger)
+ if redirectsFile != nil {
+ redirectRules, err := i.getRedirectRules(r, redirectsFile)
+ if err != nil {
+ webError(w, err, http.StatusInternalServerError)
+ return nil, nil, false, true
+ }
+
+ redirected, newPath, err := i.handleRedirectsFileRules(w, r, contentPath, redirectRules)
+ if err != nil {
+ err = fmt.Errorf("trouble processing _redirects file at %q: %w", redirectsFile.String(), err)
+ webError(w, err, http.StatusInternalServerError)
+ return nil, nil, false, true
+ }
+
+ if redirected {
+ return nil, nil, false, true
+ }
+
+ // 200 is treated as a rewrite, so update the path and continue
+ if newPath != "" {
+ // Reassign contentPath and resolvedPath since the URL was rewritten
+ contentPath = ipath.New(newPath)
+ resolvedPath, err = i.api.ResolvePath(r.Context(), contentPath)
+ if err != nil {
+ webError(w, err, http.StatusInternalServerError)
+ return nil, nil, false, true
+ }
+
+ return resolvedPath, contentPath, true, true
+ }
+ }
+ // No matching rule, paths remain the same, continue regular processing
+ return resolvedPath, contentPath, true, false
+}
+
+func (i *handler) handleRedirectsFileRules(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, redirectRules []redirects.Rule) (redirected bool, newContentPath string, err error) {
+ // Attempt to match a rule to the URL path, and perform the corresponding redirect or rewrite
+ pathParts := strings.Split(contentPath.String(), "/")
+ if len(pathParts) > 3 {
+ // All paths should start with /ipfs/cid/, so get the path after that
+ urlPath := "/" + strings.Join(pathParts[3:], "/")
+ rootPath := strings.Join(pathParts[:3], "/")
+ // Trim off the trailing /
+ urlPath = strings.TrimSuffix(urlPath, "/")
+
+ for _, rule := range redirectRules {
+ // Error right away if the rule is invalid
+ if !rule.MatchAndExpandPlaceholders(urlPath) {
+ continue
+ }
+
+ // We have a match!
+
+ // Rewrite
+ if rule.Status == 200 {
+ // Prepend the rootPath
+ toPath := rootPath + rule.To
+ return false, toPath, nil
+ }
+
+ // Or 4xx
+ if rule.Status == 404 || rule.Status == 410 || rule.Status == 451 {
+ toPath := rootPath + rule.To
+ content4xxPath := ipath.New(toPath)
+ err := i.serve4xx(w, r, content4xxPath, rule.Status)
+ return true, toPath, err
+ }
+
+ // Or redirect
+ if rule.Status >= 301 && rule.Status <= 308 {
+ http.Redirect(w, r, rule.To, rule.Status)
+ return true, "", nil
+ }
+ }
+ }
+
+ // No redirects matched
+ return false, "", nil
+}
+
+func (i *handler) getRedirectRules(r *http.Request, redirectsFilePath ipath.Resolved) ([]redirects.Rule, error) {
+ // Convert the path into a file node
+ node, err := i.api.GetUnixFsNode(r.Context(), redirectsFilePath)
+ if err != nil {
+ return nil, fmt.Errorf("could not get _redirects: %w", err)
+ }
+ defer node.Close()
+
+ // Convert the node into a file
+ f, ok := node.(files.File)
+ if !ok {
+ return nil, fmt.Errorf("could not parse _redirects: %w", err)
+ }
+
+ // Parse redirect rules from file
+ redirectRules, err := redirects.Parse(f)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse _redirects: %w", err)
+ }
+
+ return redirectRules, nil
+}
+
+// Returns a resolved path to the _redirects file located in the root CID path of the requested path
+func (i *handler) getRedirectsFile(r *http.Request, contentPath ipath.Path, logger *zap.SugaredLogger) ipath.Resolved {
+ // contentPath is the full ipfs path to the requested resource,
+ // regardless of whether path or subdomain resolution is used.
+ rootPath := getRootPath(contentPath)
+
+ // Check for _redirects file.
+ // Any path resolution failures are ignored and we just assume there's no _redirects file.
+ // Note that ignoring these errors also ensures that the use of the empty CID (bafkqaaa) in tests doesn't fail.
+ path := ipath.Join(rootPath, "_redirects")
+ resolvedPath, err := i.api.ResolvePath(r.Context(), path)
+ if err != nil {
+ return nil
+ }
+ return resolvedPath
+}
+
+// Returns the root CID Path for the given path
+func getRootPath(path ipath.Path) ipath.Path {
+ parts := strings.Split(path.String(), "/")
+ return ipath.New(gopath.Join("/", path.Namespace(), parts[2]))
+}
+
+func (i *handler) serve4xx(w http.ResponseWriter, r *http.Request, content4xxPath ipath.Path, status int) error {
+ resolved4xxPath, err := i.api.ResolvePath(r.Context(), content4xxPath)
+ if err != nil {
+ return err
+ }
+
+ node, err := i.api.GetUnixFsNode(r.Context(), resolved4xxPath)
+ if err != nil {
+ return err
+ }
+ defer node.Close()
+
+ f, ok := node.(files.File)
+ if !ok {
+ return fmt.Errorf("could not convert node for %d page to file", status)
+ }
+
+ size, err := f.Size()
+ if err != nil {
+ return fmt.Errorf("could not get size of %d page", status)
+ }
+
+ log.Debugf("using _redirects: custom %d file at %q", status, content4xxPath)
+ w.Header().Set("Content-Type", "text/html")
+ w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
+ addCacheControlHeaders(w, r, content4xxPath, resolved4xxPath.Cid())
+ w.WriteHeader(status)
+ _, err = io.CopyN(w, f, size)
+ return err
+}
+
+func hasOriginIsolation(r *http.Request) bool {
+ _, gw := r.Context().Value(GatewayHostnameKey).(string)
+ _, dnslink := r.Context().Value(DNSLinkHostnameKey).(string)
+
+ if gw || dnslink {
+ return true
+ }
+
+ return false
+}
+
+func isUnixfsResponseFormat(responseFormat string) bool {
+ // The implicit response format is UnixFS
+ return responseFormat == ""
+}
+
+// Deprecated: legacy ipfs-404.html files are superseded by _redirects file
+// This is provided only for backward-compatibility, until websites migrate
+// to 404s managed via _redirects file (https://github.com/ipfs/specs/pull/290)
+func (i *handler) serveLegacy404IfPresent(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) bool {
+ resolved404Path, ctype, err := i.searchUpTreeFor404(r, contentPath)
+ if err != nil {
+ return false
+ }
+
+ dr, err := i.api.GetUnixFsNode(r.Context(), resolved404Path)
+ if err != nil {
+ return false
+ }
+ defer dr.Close()
+
+ f, ok := dr.(files.File)
+ if !ok {
+ return false
+ }
+
+ size, err := f.Size()
+ if err != nil {
+ return false
+ }
+
+ log.Debugw("using pretty 404 file", "path", contentPath)
+ w.Header().Set("Content-Type", ctype)
+ w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
+ w.WriteHeader(http.StatusNotFound)
+ _, err = io.CopyN(w, f, size)
+ return err == nil
+}
+
+func (i *handler) searchUpTreeFor404(r *http.Request, contentPath ipath.Path) (ipath.Resolved, string, error) {
+ filename404, ctype, err := preferred404Filename(r.Header.Values("Accept"))
+ if err != nil {
+ return nil, "", err
+ }
+
+ pathComponents := strings.Split(contentPath.String(), "/")
+
+ for idx := len(pathComponents); idx >= 3; idx-- {
+ pretty404 := gopath.Join(append(pathComponents[0:idx], filename404)...)
+ parsed404Path := ipath.New("/" + pretty404)
+ if parsed404Path.IsValid() != nil {
+ break
+ }
+ resolvedPath, err := i.api.ResolvePath(r.Context(), parsed404Path)
+ if err != nil {
+ continue
+ }
+ return resolvedPath, ctype, nil
+ }
+
+ return nil, "", fmt.Errorf("no pretty 404 in any parent folder")
+}
+
+func preferred404Filename(acceptHeaders []string) (string, string, error) {
+ // If we ever want to offer a 404 file for a different content type
+ // then this function will need to parse q weightings, but for now
+ // the presence of anything matching HTML is enough.
+ for _, acceptHeader := range acceptHeaders {
+ accepted := strings.Split(acceptHeader, ",")
+ for _, spec := range accepted {
+ contentType := strings.SplitN(spec, ";", 1)[0]
+ switch contentType {
+ case "*/*", "text/*", "text/html":
+ return "ipfs-404.html", "text/html", nil
+ }
+ }
+ }
+
+ return "", "", fmt.Errorf("there is no 404 file for the requested content types")
+}
diff --git a/gateway/handler_unixfs_dir.go b/gateway/handler_unixfs_dir.go
new file mode 100644
index 0000000000..40ea6ae0ba
--- /dev/null
+++ b/gateway/handler_unixfs_dir.go
@@ -0,0 +1,212 @@
+package gateway
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+ gopath "path"
+ "strings"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ ipath "github.com/ipfs/boxo/coreiface/path"
+ "github.com/ipfs/boxo/files"
+ "github.com/ipfs/boxo/gateway/assets"
+ path "github.com/ipfs/boxo/path"
+ "github.com/ipfs/boxo/path/resolver"
+ cid "github.com/ipfs/go-cid"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
+ "go.uber.org/zap"
+)
+
+// serveDirectory returns the best representation of UnixFS directory
+//
+// It will return index.html if present, or generate directory listing otherwise.
+func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, dir files.Directory, begin time.Time, logger *zap.SugaredLogger) bool {
+ ctx, span := spanTrace(ctx, "ServeDirectory", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
+ defer span.End()
+
+ // HostnameOption might have constructed an IPNS/IPFS path using the Host header.
+ // In this case, we need the original path for constructing redirects
+ // and links that match the requested URL.
+ // For example, http://example.net would become /ipns/example.net, and
+ // the redirects and links would end up as http://example.net/ipns/example.net
+ requestURI, err := url.ParseRequestURI(r.RequestURI)
+ if err != nil {
+ webError(w, fmt.Errorf("failed to parse request path: %w", err), http.StatusInternalServerError)
+ return false
+ }
+ originalURLPath := requestURI.Path
+
+ // Ensure directory paths end with '/'
+ if originalURLPath[len(originalURLPath)-1] != '/' {
+ // don't redirect to trailing slash if it's go get
+ // https://github.com/ipfs/kubo/pull/3963
+ goget := r.URL.Query().Get("go-get") == "1"
+ if !goget {
+ suffix := "/"
+ // preserve query parameters
+ if r.URL.RawQuery != "" {
+ suffix = suffix + "?" + r.URL.RawQuery
+ }
+ // /ipfs/cid/foo?bar must be redirected to /ipfs/cid/foo/?bar
+ redirectURL := originalURLPath + suffix
+ logger.Debugw("directory location moved permanently", "status", http.StatusMovedPermanently)
+ http.Redirect(w, r, redirectURL, http.StatusMovedPermanently)
+ return true
+ }
+ }
+
+ // Check if directory has index.html, if so, serveFile
+ idxPath := ipath.Join(contentPath, "index.html")
+ idxResolvedPath, err := i.api.ResolvePath(ctx, idxPath)
+ switch err.(type) {
+ case nil:
+ idx, err := i.api.GetUnixFsNode(ctx, idxResolvedPath)
+ if err != nil {
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+
+ f, ok := idx.(files.File)
+ if !ok {
+ webError(w, files.ErrNotReader, http.StatusInternalServerError)
+ return false
+ }
+
+ logger.Debugw("serving index.html file", "path", idxPath)
+ // write to request
+ success := i.serveFile(ctx, w, r, resolvedPath, idxPath, f, begin)
+ if success {
+ i.unixfsDirIndexGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
+ }
+ return success
+ case resolver.ErrNoLink:
+ logger.Debugw("no index.html; noop", "path", idxPath)
+ default:
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+
+ // See statusResponseWriter.WriteHeader
+ // and https://github.com/ipfs/kubo/issues/7164
+ // Note: this needs to occur before listingTemplate.Execute otherwise we get
+ // superfluous response.WriteHeader call from prometheus/client_golang
+ if w.Header().Get("Location") != "" {
+ logger.Debugw("location moved permanently", "status", http.StatusMovedPermanently)
+ w.WriteHeader(http.StatusMovedPermanently)
+ return true
+ }
+
+ // A HTML directory index will be presented, be sure to set the correct
+ // type instead of relying on autodetection (which may fail).
+ w.Header().Set("Content-Type", "text/html")
+
+ // Generated dir index requires custom Etag (output may change between go-ipfs versions)
+ dirEtag := getDirListingEtag(resolvedPath.Cid())
+ w.Header().Set("Etag", dirEtag)
+
+ if r.Method == http.MethodHead {
+ logger.Debug("return as request's HTTP method is HEAD")
+ return true
+ }
+
+ results, err := i.api.LsUnixFsDir(ctx, resolvedPath)
+ if err != nil {
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+
+ dirListing := make([]assets.DirectoryItem, 0, len(results))
+ for link := range results {
+ if link.Err != nil {
+ webError(w, link.Err, http.StatusInternalServerError)
+ return false
+ }
+
+ hash := link.Cid.String()
+ di := assets.DirectoryItem{
+ Size: humanize.Bytes(uint64(link.Size)),
+ Name: link.Name,
+ Path: gopath.Join(originalURLPath, link.Name),
+ Hash: hash,
+ ShortHash: assets.ShortHash(hash),
+ }
+ dirListing = append(dirListing, di)
+ }
+
+ // construct the correct back link
+ // https://github.com/ipfs/kubo/issues/1365
+ backLink := originalURLPath
+
+ // don't go further up than /ipfs/$hash/
+ pathSplit := path.SplitList(contentPath.String())
+ switch {
+ // skip backlink when listing a content root
+ case len(pathSplit) == 3: // url: /ipfs/$hash
+ backLink = ""
+
+ // skip backlink when listing a content root
+ case len(pathSplit) == 4 && pathSplit[3] == "": // url: /ipfs/$hash/
+ backLink = ""
+
+ // add the correct link depending on whether the path ends with a slash
+ default:
+ if strings.HasSuffix(backLink, "/") {
+ backLink += ".."
+ } else {
+ backLink += "/.."
+ }
+ }
+
+ size := "?"
+ if s, err := dir.Size(); err == nil {
+ // Size may not be defined/supported. Continue anyways.
+ size = humanize.Bytes(uint64(s))
+ }
+
+ hash := resolvedPath.Cid().String()
+
+ // Gateway root URL to be used when linking to other rootIDs.
+ // This will be blank unless subdomain or DNSLink resolution is being used
+ // for this request.
+ var gwURL string
+
+ // Get gateway hostname and build gateway URL.
+ if h, ok := r.Context().Value(GatewayHostnameKey).(string); ok {
+ gwURL = "//" + h
+ } else {
+ gwURL = ""
+ }
+
+ dnslink := assets.HasDNSLinkOrigin(gwURL, contentPath.String())
+
+ // See comment above where originalUrlPath is declared.
+ tplData := assets.DirectoryTemplateData{
+ GatewayURL: gwURL,
+ DNSLink: dnslink,
+ Listing: dirListing,
+ Size: size,
+ Path: contentPath.String(),
+ Breadcrumbs: assets.Breadcrumbs(contentPath.String(), dnslink),
+ BackLink: backLink,
+ Hash: hash,
+ }
+
+ logger.Debugw("request processed", "tplDataDNSLink", dnslink, "tplDataSize", size, "tplDataBackLink", backLink, "tplDataHash", hash)
+
+ if err := assets.DirectoryTemplate.Execute(w, tplData); err != nil {
+ webError(w, err, http.StatusInternalServerError)
+ return false
+ }
+
+ // Update metrics
+ i.unixfsGenDirListingGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
+ return true
+}
+
+func getDirListingEtag(dirCid cid.Cid) string {
+ return `"DirIndex-` + assets.AssetHash + `_CID-` + dirCid.String() + `"`
+}
diff --git a/gateway/handler_unixfs_file.go b/gateway/handler_unixfs_file.go
new file mode 100644
index 0000000000..980885cb2f
--- /dev/null
+++ b/gateway/handler_unixfs_file.go
@@ -0,0 +1,105 @@
+package gateway
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "mime"
+ "net/http"
+ gopath "path"
+ "strings"
+ "time"
+
+ "github.com/gabriel-vasile/mimetype"
+ ipath "github.com/ipfs/boxo/coreiface/path"
+ "github.com/ipfs/boxo/files"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
+)
+
+// serveFile returns data behind a file along with HTTP headers based on
+// the file itself, its CID and the contentPath used for accessing it.
+func (i *handler) serveFile(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, file files.File, begin time.Time) bool {
+ _, span := spanTrace(ctx, "ServeFile", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
+ defer span.End()
+
+ // Set Cache-Control and read optional Last-Modified time
+ modtime := addCacheControlHeaders(w, r, contentPath, resolvedPath.Cid())
+
+ // Set Content-Disposition
+ name := addContentDispositionHeader(w, r, contentPath)
+
+ // Prepare size value for Content-Length HTTP header (set inside of http.ServeContent)
+ size, err := file.Size()
+ if err != nil {
+ http.Error(w, "cannot serve files with unknown sizes", http.StatusBadGateway)
+ return false
+ }
+
+ if size == 0 {
+ // We override null files to 200 to avoid issues with fragment caching reverse proxies.
+ // Also whatever you are asking for, it's cheaper to just give you the complete file (nothing).
+ // TODO: remove this if clause once https://github.com/golang/go/issues/54794 is fixed in two latest releases of go
+ w.Header().Set("Content-Type", "text/plain")
+ w.WriteHeader(http.StatusOK)
+ return true
+ }
+
+ // Lazy seeker enables efficient range-requests and HTTP HEAD responses
+ content := &lazySeeker{
+ size: size,
+ reader: file,
+ }
+
+ // Calculate deterministic value for Content-Type HTTP header
+ // (we prefer to do it here, rather than using implicit sniffing in http.ServeContent)
+ var ctype string
+ if _, isSymlink := file.(*files.Symlink); isSymlink {
+ // We should be smarter about resolving symlinks but this is the
+ // "most correct" we can be without doing that.
+ ctype = "inode/symlink"
+ } else {
+ ctype = mime.TypeByExtension(gopath.Ext(name))
+ if ctype == "" {
+ // uses https://github.com/gabriel-vasile/mimetype library to determine the content type.
+ // Fixes https://github.com/ipfs/kubo/issues/7252
+ mimeType, err := mimetype.DetectReader(content)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("cannot detect content-type: %s", err.Error()), http.StatusInternalServerError)
+ return false
+ }
+
+ ctype = mimeType.String()
+ _, err = content.Seek(0, io.SeekStart)
+ if err != nil {
+ http.Error(w, "seeker can't seek", http.StatusInternalServerError)
+ return false
+ }
+ }
+ // Strip the encoding from the HTML Content-Type header and let the
+ // browser figure it out.
+ //
+ // Fixes https://github.com/ipfs/kubo/issues/2203
+ if strings.HasPrefix(ctype, "text/html;") {
+ ctype = "text/html"
+ }
+ }
+ // Setting explicit Content-Type to avoid mime-type sniffing on the client
+ // (unifies behavior across gateways and web browsers)
+ w.Header().Set("Content-Type", ctype)
+
+ // special fixup around redirects
+ w = &statusResponseWriter{w}
+
+ // ServeContent will take care of
+ // If-None-Match+Etag, Content-Length and range requests
+ _, dataSent, _ := ServeContent(w, r, name, modtime, content)
+
+ // Was response successful?
+ if dataSent {
+ // Update metrics
+ i.unixfsFileGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
+ }
+
+ return dataSent
+}
diff --git a/gateway/hostname.go b/gateway/hostname.go
new file mode 100644
index 0000000000..97e8bd41ee
--- /dev/null
+++ b/gateway/hostname.go
@@ -0,0 +1,594 @@
+package gateway
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strings"
+
+ cid "github.com/ipfs/go-cid"
+ "github.com/libp2p/go-libp2p/core/peer"
+ dns "github.com/miekg/dns"
+
+ mbase "github.com/multiformats/go-multibase"
+)
+
+// Specification is the specification of an IPFS Public Gateway.
+type Specification struct {
+ // Paths is explicit list of path prefixes that should be handled by
+ // this gateway. Example: `["/ipfs", "/ipns"]`
+ // Useful if you only want to support immutable `/ipfs`.
+ Paths []string
+
+ // UseSubdomains indicates whether or not this gateway uses subdomains
+ // for IPFS resources instead of paths. That is: http://CID.ipfs.GATEWAY/...
+ //
+ // If this flag is set, any /ipns/$id and/or /ipfs/$id paths in Paths
+ // will be permanently redirected to http://$id.[ipns|ipfs].$gateway/.
+ //
+ // We do not support using both paths and subdomains for a single domain
+ // for security reasons (Origin isolation).
+ UseSubdomains bool
+
+ // NoDNSLink configures this gateway to _not_ resolve DNSLink for the
+ // specific FQDN provided in `Host` HTTP header. Useful when you want to
+ // explicitly allow or refuse hosting a single hostname. To refuse all
+ // DNSLinks in `Host` processing, pass noDNSLink to `WithHostname` instead.
+ // This flag overrides the global one.
+ NoDNSLink bool
+
+ // InlineDNSLink configures this gateway to always inline DNSLink names
+ // (FQDN) into a single DNS label in order to interop with wildcard TLS certs
+ // and Origin per CID isolation provided by rules like https://publicsuffix.org
+ // This should be set to true if you use HTTPS.
+ InlineDNSLink bool
+}
+
+// WithHostname is a middleware that can wrap an http.Handler in order to parse the
+// Host header and translating it to the content path. This is useful for Subdomain
+// and DNSLink gateways.
+//
+// publicGateways configures the behavior of known public gateways. Each key is a
+// fully qualified domain name (FQDN).
+//
+// noDNSLink configures the gateway to _not_ perform DNS TXT record lookups in
+// response to requests with values in `Host` HTTP header. This flag can be overridden
+// per FQDN in publicGateways.
+func WithHostname(next http.Handler, api API, publicGateways map[string]*Specification, noDNSLink bool) http.HandlerFunc {
+ gateways := prepareHostnameGateways(publicGateways)
+
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ defer panicHandler(w)
+
+ // Unfortunately, many (well, ipfs.io) gateways use
+ // DNSLink so if we blindly rewrite with DNSLink, we'll
+ // break /ipfs links.
+ //
+ // We fix this by maintaining a list of known gateways
+ // and the paths that they serve "gateway" content on.
+ // That way, we can use DNSLink for everything else.
+
+ // Support X-Forwarded-Host if added by a reverse proxy
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host
+ host := r.Host
+ if xHost := r.Header.Get("X-Forwarded-Host"); xHost != "" {
+ host = xHost
+ }
+
+ // HTTP Host & Path check: is this one of our "known gateways"?
+ if gw, ok := gateways.isKnownHostname(host); ok {
+ // This is a known gateway but request is not using
+ // the subdomain feature.
+
+ // Does this gateway _handle_ this path?
+ if hasPrefix(r.URL.Path, gw.Paths...) {
+ // It does.
+
+ // Should this gateway use subdomains instead of paths?
+ if gw.UseSubdomains {
+ // Yes, redirect if applicable
+ // Example: dweb.link/ipfs/{cid} → {cid}.ipfs.dweb.link
+ useInlinedDNSLink := gw.InlineDNSLink
+ newURL, err := toSubdomainURL(host, r.URL.Path, r, useInlinedDNSLink, api)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ if newURL != "" {
+ // Set "Location" header with redirect destination.
+ // It is ignored by curl in default mode, but will
+ // be respected by user agents that follow
+ // redirects by default, namely web browsers
+ w.Header().Set("Location", newURL)
+
+ // Note: we continue regular gateway processing:
+ // HTTP Status Code http.StatusMovedPermanently
+ // will be set later, in statusResponseWriter
+ }
+ }
+
+ // Not a subdomain resource, continue with path processing
+ // Example: 127.0.0.1:8080/ipfs/{CID}, ipfs.io/ipfs/{CID} etc
+ next.ServeHTTP(w, r)
+ return
+ }
+ // Not a whitelisted path
+
+ // Try DNSLink, if it was not explicitly disabled for the hostname
+ if !gw.NoDNSLink && hasDNSLinkRecord(r.Context(), api, host) {
+ // rewrite path and handle as DNSLink
+ r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path
+ next.ServeHTTP(w, withHostnameContext(r, host))
+ return
+ }
+
+ // If not, resource does not exist on the hostname, return 404
+ http.NotFound(w, r)
+ return
+ }
+
+ // HTTP Host check: is this one of our subdomain-based "known gateways"?
+ // IPFS details extracted from the host: {rootID}.{ns}.{gwHostname}
+ // /ipfs/ example: {cid}.ipfs.localhost:8080, {cid}.ipfs.dweb.link
+ // /ipns/ example: {libp2p-key}.ipns.localhost:8080, {inlined-dnslink-fqdn}.ipns.dweb.link
+ if gw, gwHostname, ns, rootID, ok := gateways.knownSubdomainDetails(host); ok {
+ // Looks like we're using a known gateway in subdomain mode.
+
+ // Assemble original path prefix.
+ pathPrefix := "/" + ns + "/" + rootID
+
+ // Retrieve whether or not we should inline DNSLink.
+ useInlinedDNSLink := gw.InlineDNSLink
+
+ // Does this gateway _handle_ subdomains AND this path?
+ if !(gw.UseSubdomains && hasPrefix(pathPrefix, gw.Paths...)) {
+ // If not, resource does not exist, return 404
+ http.NotFound(w, r)
+ return
+ }
+
+ // Check if rootID is a valid CID
+ if rootCID, err := cid.Decode(rootID); err == nil {
+ // Do we need to redirect root CID to a canonical DNS representation?
+ dnsCID, err := toDNSLabel(rootID, rootCID)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ if !strings.HasPrefix(r.Host, dnsCID) {
+ dnsPrefix := "/" + ns + "/" + dnsCID
+ newURL, err := toSubdomainURL(gwHostname, dnsPrefix+r.URL.Path, r, useInlinedDNSLink, api)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ if newURL != "" {
+ // Redirect to deterministic CID to ensure CID
+ // always gets the same Origin on the web
+ http.Redirect(w, r, newURL, http.StatusMovedPermanently)
+ return
+ }
+ }
+
+ // Do we need to fix multicodec in PeerID represented as CIDv1?
+ if isPeerIDNamespace(ns) {
+ if rootCID.Type() != cid.Libp2pKey {
+ newURL, err := toSubdomainURL(gwHostname, pathPrefix+r.URL.Path, r, useInlinedDNSLink, api)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ if newURL != "" {
+ // Redirect to CID fixed inside of toSubdomainURL()
+ http.Redirect(w, r, newURL, http.StatusMovedPermanently)
+ return
+ }
+ }
+ }
+ } else { // rootID is not a CID..
+ // Check if rootID is a single DNS label with an inlined
+ // DNSLink FQDN a single DNS label. We support this so
+ // loading DNSLink names over TLS "just works" on public
+ // HTTP gateways.
+ //
+ // Rationale for doing this can be found under "Option C"
+ // at: https://github.com/ipfs/in-web-browsers/issues/169
+ //
+ // TLDR is:
+ // https://dweb.link/ipns/my.v-long.example.com
+ // can be loaded from a subdomain gateway with a wildcard
+ // TLS cert if represented as a single DNS label:
+ // https://my-v--long-example-com.ipns.dweb.link
+ if ns == "ipns" && !strings.Contains(rootID, ".") {
+ // if there is no TXT recordfor rootID
+ if !hasDNSLinkRecord(r.Context(), api, rootID) {
+ // my-v--long-example-com → my.v-long.example.com
+ dnslinkFQDN := toDNSLinkFQDN(rootID)
+ if hasDNSLinkRecord(r.Context(), api, dnslinkFQDN) {
+ // update path prefix to use real FQDN with DNSLink
+ pathPrefix = "/ipns/" + dnslinkFQDN
+ }
+ }
+ }
+ }
+
+ // Rewrite the path to not use subdomains
+ r.URL.Path = pathPrefix + r.URL.Path
+
+ // Serve path request
+ next.ServeHTTP(w, withHostnameContext(r, gwHostname))
+ return
+ }
+
+ // We don't have a known gateway. Fallback on DNSLink lookup
+
+ // Wildcard HTTP Host check:
+ // 1. is wildcard DNSLink enabled (Gateway.NoDNSLink=false)?
+ // 2. does Host header include a fully qualified domain name (FQDN)?
+ // 3. does DNSLink record exist in DNS?
+ if !noDNSLink && hasDNSLinkRecord(r.Context(), api, host) {
+ // rewrite path and handle as DNSLink
+ r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path
+ ctx := context.WithValue(r.Context(), DNSLinkHostnameKey, host)
+ next.ServeHTTP(w, withHostnameContext(r.WithContext(ctx), host))
+ return
+ }
+
+ // else, treat it as an old school gateway, I guess.
+ next.ServeHTTP(w, r)
+
+ })
+}
+
+// Extends request context to include hostname of a canonical gateway root
+// (subdomain root or dnslink fqdn)
+func withHostnameContext(r *http.Request, hostname string) *http.Request {
+ // This is required for links on directory listing pages to work correctly
+ // on subdomain and dnslink gateways. While DNSlink could read value from
+ // Host header, subdomain gateways have more comples rules (knownSubdomainDetails)
+ // More: https://github.com/ipfs/dir-index-html/issues/42
+ // nolint: staticcheck // non-backward compatible change
+ ctx := context.WithValue(r.Context(), GatewayHostnameKey, hostname)
+ return r.WithContext(ctx)
+}
+
+// isDomainNameAndNotPeerID returns bool if string looks like a valid DNS name AND is not a PeerID
+func isDomainNameAndNotPeerID(hostname string) bool {
+ if len(hostname) == 0 {
+ return false
+ }
+ if _, err := peer.Decode(hostname); err == nil {
+ return false
+ }
+ _, ok := dns.IsDomainName(hostname)
+ return ok
+}
+
+// hasDNSLinkRecord returns if a DNS TXT record exists for the provided host.
+func hasDNSLinkRecord(ctx context.Context, api API, host string) bool {
+ dnslinkName := stripPort(host)
+
+ if !isDomainNameAndNotPeerID(dnslinkName) {
+ return false
+ }
+
+ _, err := api.GetDNSLinkRecord(ctx, dnslinkName)
+ return err == nil
+}
+
+func isSubdomainNamespace(ns string) bool {
+ switch ns {
+ case "ipfs", "ipns", "p2p", "ipld":
+ // Note: 'p2p' and 'ipld' is only kept here for compatibility with Kubo.
+ return true
+ default:
+ return false
+ }
+}
+
+func isPeerIDNamespace(ns string) bool {
+ switch ns {
+ case "ipns", "p2p":
+ // Note: 'p2p' and 'ipld' is only kept here for compatibility with Kubo.
+ return true
+ default:
+ return false
+ }
+}
+
+// Label's max length in DNS (https://tools.ietf.org/html/rfc1034#page-7)
+const dnsLabelMaxLength int = 63
+
+// Converts a CID to DNS-safe representation that fits in 63 characters
+func toDNSLabel(rootID string, rootCID cid.Cid) (dnsCID string, err error) {
+ // Return as-is if things fit
+ if len(rootID) <= dnsLabelMaxLength {
+ return rootID, nil
+ }
+
+ // Convert to Base36 and see if that helped
+ rootID, err = cid.NewCidV1(rootCID.Type(), rootCID.Hash()).StringOfBase(mbase.Base36)
+ if err != nil {
+ return "", err
+ }
+ if len(rootID) <= dnsLabelMaxLength {
+ return rootID, nil
+ }
+
+ // Can't win with DNS at this point, return error
+ return "", fmt.Errorf("CID incompatible with DNS label length limit of 63: %s", rootID)
+}
+
+// Returns true if HTTP request involves TLS certificate.
+// See https://github.com/ipfs/in-web-browsers/issues/169 to understand how it
+// impacts DNSLink websites on public gateways.
+func isHTTPSRequest(r *http.Request) bool {
+ // X-Forwarded-Proto if added by a reverse proxy
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
+ xproto := r.Header.Get("X-Forwarded-Proto")
+ // Is request a native TLS (not used atm, but future-proofing)
+ // or a proxied HTTPS (eg. go-ipfs behind nginx at a public gw)?
+ return r.URL.Scheme == "https" || xproto == "https"
+}
+
+// Converts a FQDN to DNS-safe representation that fits in 63 characters:
+// my.v-long.example.com → my-v--long-example-com
+func toDNSLinkDNSLabel(fqdn string) (dnsLabel string, err error) {
+ dnsLabel = strings.ReplaceAll(fqdn, "-", "--")
+ dnsLabel = strings.ReplaceAll(dnsLabel, ".", "-")
+ if len(dnsLabel) > dnsLabelMaxLength {
+ return "", fmt.Errorf("DNSLink representation incompatible with DNS label length limit of 63: %s", dnsLabel)
+ }
+ return dnsLabel, nil
+}
+
+// Converts a DNS-safe representation of DNSLink FQDN to real FQDN:
+// my-v--long-example-com → my.v-long.example.com
+func toDNSLinkFQDN(dnsLabel string) (fqdn string) {
+ fqdn = strings.ReplaceAll(dnsLabel, "--", "@") // @ placeholder is unused in DNS labels
+ fqdn = strings.ReplaceAll(fqdn, "-", ".")
+ fqdn = strings.ReplaceAll(fqdn, "@", "-")
+ return fqdn
+}
+
+// Converts a hostname/path to a subdomain-based URL, if applicable.
+func toSubdomainURL(hostname, path string, r *http.Request, inlineDNSLink bool, api API) (redirURL string, err error) {
+ var scheme, ns, rootID, rest string
+
+ query := r.URL.RawQuery
+ parts := strings.SplitN(path, "/", 4)
+ isHTTPS := isHTTPSRequest(r)
+ safeRedirectURL := func(in string) (out string, err error) {
+ safeURI, err := url.ParseRequestURI(in)
+ if err != nil {
+ return "", err
+ }
+ return safeURI.String(), nil
+ }
+
+ if isHTTPS {
+ scheme = "https:"
+ } else {
+ scheme = "http:"
+ }
+
+ switch len(parts) {
+ case 4:
+ rest = parts[3]
+ fallthrough
+ case 3:
+ ns = parts[1]
+ rootID = parts[2]
+ default:
+ return "", nil
+ }
+
+ if !isSubdomainNamespace(ns) {
+ return "", nil
+ }
+
+ // add prefix if query is present
+ if query != "" {
+ query = "?" + query
+ }
+
+ // Normalize problematic PeerIDs (eg. ed25519+identity) to CID representation
+ if isPeerIDNamespace(ns) && !isDomainNameAndNotPeerID(rootID) {
+ peerID, err := peer.Decode(rootID)
+ // Note: PeerID CIDv1 with protobuf multicodec will fail, but we fix it
+ // in the next block
+ if err == nil {
+ rootID = peer.ToCid(peerID).String()
+ }
+ }
+
+ // If rootID is a CID, ensure it uses DNS-friendly text representation
+ if rootCID, err := cid.Decode(rootID); err == nil {
+ multicodec := rootCID.Type()
+ var base mbase.Encoding = mbase.Base32
+
+ // Normalizations specific to /ipns/{libp2p-key}
+ if isPeerIDNamespace(ns) {
+ // Using Base36 for /ipns/ for consistency
+ // Context: https://github.com/ipfs/kubo/pull/7441#discussion_r452372828
+ base = mbase.Base36
+
+ // PeerIDs represented as CIDv1 are expected to have libp2p-key
+ // multicodec (https://github.com/libp2p/specs/pull/209).
+ // We ease the transition by fixing multicodec on the fly:
+ // https://github.com/ipfs/kubo/issues/5287#issuecomment-492163929
+ if multicodec != cid.Libp2pKey {
+ multicodec = cid.Libp2pKey
+ }
+ }
+
+ // Ensure CID text representation used in subdomain is compatible
+ // with the way DNS and URIs are implemented in user agents.
+ //
+ // 1. Switch to CIDv1 and enable case-insensitive Base encoding
+ // to avoid issues when user agent force-lowercases the hostname
+ // before making the request
+ // (https://github.com/ipfs/in-web-browsers/issues/89)
+ rootCID = cid.NewCidV1(multicodec, rootCID.Hash())
+ rootID, err = rootCID.StringOfBase(base)
+ if err != nil {
+ return "", err
+ }
+ // 2. Make sure CID fits in a DNS label, adjust encoding if needed
+ // (https://github.com/ipfs/kubo/issues/7318)
+ rootID, err = toDNSLabel(rootID, rootCID)
+ if err != nil {
+ return "", err
+ }
+ } else { // rootID is not a CID
+
+ // Check if rootID is a FQDN with DNSLink and convert it to TLS-safe
+ // representation that fits in a single DNS label. We support this so
+ // loading DNSLink names over TLS "just works" on public HTTP gateways
+ // that pass 'https' in X-Forwarded-Proto to go-ipfs.
+ //
+ // Rationale can be found under "Option C"
+ // at: https://github.com/ipfs/in-web-browsers/issues/169
+ //
+ // TLDR is:
+ // /ipns/my.v-long.example.com
+ // can be loaded from a subdomain gateway with a wildcard TLS cert if
+ // represented as a single DNS label:
+ // https://my-v--long-example-com.ipns.dweb.link
+ if (inlineDNSLink || isHTTPS) && ns == "ipns" && strings.Contains(rootID, ".") {
+ if hasDNSLinkRecord(r.Context(), api, rootID) {
+ // my.v-long.example.com → my-v--long-example-com
+ dnsLabel, err := toDNSLinkDNSLabel(rootID)
+ if err != nil {
+ return "", err
+ }
+ // update path prefix to use real FQDN with DNSLink
+ rootID = dnsLabel
+ }
+ }
+ }
+
+ return safeRedirectURL(fmt.Sprintf(
+ "%s//%s.%s.%s/%s%s",
+ scheme,
+ rootID,
+ ns,
+ hostname,
+ rest,
+ query,
+ ))
+}
+
+func hasPrefix(path string, prefixes ...string) bool {
+ for _, prefix := range prefixes {
+ // Assume people are creative with trailing slashes in Gateway config
+ p := strings.TrimSuffix(prefix, "/")
+ // Support for both /version and /ipfs/$cid
+ if p == path || strings.HasPrefix(path, p+"/") {
+ return true
+ }
+ }
+ return false
+}
+
+func stripPort(hostname string) string {
+ host, _, err := net.SplitHostPort(hostname)
+ if err == nil {
+ return host
+ }
+ return hostname
+}
+
+type hostnameGateways struct {
+ exact map[string]*Specification
+ wildcard map[*regexp.Regexp]*Specification
+}
+
+// prepareHostnameGateways converts the user given gateways into an internal format
+// split between exact and wildcard-based gateway hostnames.
+func prepareHostnameGateways(gateways map[string]*Specification) *hostnameGateways {
+ h := &hostnameGateways{
+ exact: map[string]*Specification{},
+ wildcard: map[*regexp.Regexp]*Specification{},
+ }
+
+ for hostname, gw := range gateways {
+ if strings.Contains(hostname, "*") {
+ // from *.domain.tld, construct a regexp that match any direct subdomain
+ // of .domain.tld.
+ //
+ // Regexp will be in the form of ^[^.]+\.domain.tld(?::\d+)?$
+ escaped := strings.ReplaceAll(hostname, ".", `\.`)
+ regexed := strings.ReplaceAll(escaped, "*", "[^.]+")
+
+ re, err := regexp.Compile(fmt.Sprintf(`^%s(?::\d+)?$`, regexed))
+ if err != nil {
+ log.Warn("invalid wildcard gateway hostname \"%s\"", hostname)
+ }
+
+ h.wildcard[re] = gw
+ } else {
+ h.exact[hostname] = gw
+ }
+ }
+
+ return h
+}
+
+// isKnownHostname checks the given hostname gateways and returns a matching
+// specification with graceful fallback to version without port.
+func (gws *hostnameGateways) isKnownHostname(hostname string) (gw *Specification, ok bool) {
+ // Try hostname (host+optional port - value from Host header as-is)
+ if gw, ok := gws.exact[hostname]; ok {
+ return gw, ok
+ }
+ // Also test without port
+ if gw, ok = gws.exact[stripPort(hostname)]; ok {
+ return gw, ok
+ }
+
+ // Wildcard support. Test both with and without port.
+ for re, spec := range gws.wildcard {
+ if re.MatchString(hostname) {
+ return spec, true
+ }
+ }
+
+ return nil, false
+}
+
+// knownSubdomainDetails parses the Host header and looks for a known gateway matching
+// the subdomain host. If found, returns a Specification and the subdomain components
+// extracted from Host header: {rootID}.{ns}.{gwHostname}.
+// Note: hostname is host + optional port
+func (gws *hostnameGateways) knownSubdomainDetails(hostname string) (gw *Specification, gwHostname, ns, rootID string, ok bool) {
+ labels := strings.Split(hostname, ".")
+ // Look for FQDN of a known gateway hostname.
+ // Example: given "dist.ipfs.tech.ipns.dweb.link":
+ // 1. Lookup "link" TLD in knownGateways: negative
+ // 2. Lookup "dweb.link" in knownGateways: positive
+ //
+ // Stops when we have 2 or fewer labels left as we need at least a
+ // rootId and a namespace.
+ for i := len(labels) - 1; i >= 2; i-- {
+ fqdn := strings.Join(labels[i:], ".")
+ gw, ok := gws.isKnownHostname(fqdn)
+ if !ok {
+ continue
+ }
+
+ ns := labels[i-1]
+ if !isSubdomainNamespace(ns) {
+ continue
+ }
+
+ // Merge remaining labels (could be a FQDN with DNSLink)
+ rootID := strings.Join(labels[:i-1], ".")
+ return gw, fqdn, ns, rootID, true
+ }
+ // no match
+ return nil, "", "", "", false
+}
diff --git a/gateway/hostname_test.go b/gateway/hostname_test.go
new file mode 100644
index 0000000000..f8e7dff6c7
--- /dev/null
+++ b/gateway/hostname_test.go
@@ -0,0 +1,290 @@
+package gateway
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ path "github.com/ipfs/boxo/path"
+ cid "github.com/ipfs/go-cid"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestToSubdomainURL(t *testing.T) {
+ gwAPI, _ := newMockAPI(t)
+ testCID, err := cid.Decode("bafkqaglimvwgy3zakrsxg5cun5jxkyten5wwc2lokvjeycq")
+ assert.Nil(t, err)
+
+ gwAPI.namesys["/ipns/dnslink.long-name.example.com"] = path.FromString(testCID.String())
+ gwAPI.namesys["/ipns/dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com"] = path.FromString(testCID.String())
+ httpRequest := httptest.NewRequest("GET", "http://127.0.0.1:8080", nil)
+ httpsRequest := httptest.NewRequest("GET", "https://https-request-stub.example.com", nil)
+ httpsProxiedRequest := httptest.NewRequest("GET", "http://proxied-https-request-stub.example.com", nil)
+ httpsProxiedRequest.Header.Set("X-Forwarded-Proto", "https")
+
+ for _, test := range []struct {
+ // in:
+ request *http.Request
+ gwHostname string
+ inlineDNSLink bool
+ path string
+ // out:
+ url string
+ err error
+ }{
+
+ // DNSLink
+ {httpRequest, "localhost", false, "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", nil},
+ // Hostname with port
+ {httpRequest, "localhost:8080", false, "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", nil},
+ // CIDv0 → CIDv1base32
+ {httpRequest, "localhost", false, "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", nil},
+ // CIDv1 with long sha512
+ {httpRequest, "localhost", false, "/ipfs/bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")},
+ // PeerID as CIDv1 needs to have libp2p-key multicodec
+ {httpRequest, "localhost", false, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://k2k4r8n0flx3ra0y5dr8fmyvwbzy3eiztmtq6th694k5a3rznayp3e4o.ipns.localhost/", nil},
+ {httpRequest, "localhost", false, "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://k2k4r8l9ja7hkzynavdqup76ou46tnvuaqegbd04a4o1mpbsey0meucb.ipns.localhost/", nil},
+ // PeerID: ed25519+identity multihash → CIDv1Base36
+ {httpRequest, "localhost", false, "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://k51qzi5uqu5di608geewp3nqkg0bpujoasmka7ftkyxgcm3fh1aroup0gsdrna.ipns.localhost/", nil},
+ {httpRequest, "sub.localhost", false, "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.sub.localhost/", nil},
+ // HTTPS requires DNSLink name to fit in a single DNS label – see "Option C" from https://github.com/ipfs/in-web-browsers/issues/169
+ {httpRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "http://dnslink.long-name.example.com.ipns.dweb.link/", nil},
+ {httpsRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
+ {httpsProxiedRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
+ // HTTP requests can also be converted to fit into a single DNS label - https://github.com/ipfs/kubo/issues/9243
+ {httpRequest, "localhost", true, "/ipns/dnslink.long-name.example.com", "http://dnslink-long--name-example-com.ipns.localhost/", nil},
+ {httpRequest, "dweb.link", true, "/ipns/dnslink.long-name.example.com", "http://dnslink-long--name-example-com.ipns.dweb.link/", nil},
+ } {
+ testName := fmt.Sprintf("%s, %v, %s", test.gwHostname, test.inlineDNSLink, test.path)
+ t.Run(testName, func(t *testing.T) {
+ url, err := toSubdomainURL(test.gwHostname, test.path, test.request, test.inlineDNSLink, gwAPI)
+ assert.Equal(t, test.url, url)
+ assert.Equal(t, test.err, err)
+ })
+ }
+}
+
+func TestToDNSLinkDNSLabel(t *testing.T) {
+ for _, test := range []struct {
+ in string
+ out string
+ err error
+ }{
+ {"dnslink.long-name.example.com", "dnslink-long--name-example-com", nil},
+ {"dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com", "", errors.New("DNSLink representation incompatible with DNS label length limit of 63: dnslink-too--long-f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o-example-com")},
+ } {
+ t.Run(test.in, func(t *testing.T) {
+ out, err := toDNSLinkDNSLabel(test.in)
+ assert.Equal(t, test.out, out)
+ assert.Equal(t, test.err, err)
+ })
+ }
+}
+
+func TestToDNSLinkFQDN(t *testing.T) {
+ for _, test := range []struct {
+ in string
+ out string
+ }{
+ {"singlelabel", "singlelabel"},
+ {"docs-ipfs-tech", "docs.ipfs.tech"},
+ {"dnslink-long--name-example-com", "dnslink.long-name.example.com"},
+ } {
+ t.Run(test.in, func(t *testing.T) {
+ out := toDNSLinkFQDN(test.in)
+ assert.Equal(t, test.out, out)
+ })
+ }
+}
+
+func TestIsHTTPSRequest(t *testing.T) {
+ httpRequest := httptest.NewRequest("GET", "http://127.0.0.1:8080", nil)
+ httpsRequest := httptest.NewRequest("GET", "https://https-request-stub.example.com", nil)
+ httpsProxiedRequest := httptest.NewRequest("GET", "http://proxied-https-request-stub.example.com", nil)
+ httpsProxiedRequest.Header.Set("X-Forwarded-Proto", "https")
+ httpProxiedRequest := httptest.NewRequest("GET", "http://proxied-http-request-stub.example.com", nil)
+ httpProxiedRequest.Header.Set("X-Forwarded-Proto", "http")
+ oddballRequest := httptest.NewRequest("GET", "foo://127.0.0.1:8080", nil)
+ for _, test := range []struct {
+ in *http.Request
+ out bool
+ }{
+ {httpRequest, false},
+ {httpsRequest, true},
+ {httpsProxiedRequest, true},
+ {httpProxiedRequest, false},
+ {oddballRequest, false},
+ } {
+ testName := fmt.Sprintf("%+v", test.in)
+ t.Run(testName, func(t *testing.T) {
+ out := isHTTPSRequest(test.in)
+ assert.Equal(t, test.out, out)
+ })
+ }
+}
+
+func TestHasPrefix(t *testing.T) {
+ for _, test := range []struct {
+ prefixes []string
+ path string
+ out bool
+ }{
+ {[]string{"/ipfs"}, "/ipfs/cid", true},
+ {[]string{"/ipfs/"}, "/ipfs/cid", true},
+ {[]string{"/version/"}, "/version", true},
+ {[]string{"/version"}, "/version", true},
+ } {
+ testName := fmt.Sprintf("%+v, %s", test.prefixes, test.path)
+ t.Run(testName, func(t *testing.T) {
+ out := hasPrefix(test.path, test.prefixes...)
+ assert.Equal(t, test.out, out)
+ })
+ }
+}
+
+func TestIsDomainNameAndNotPeerID(t *testing.T) {
+ for _, test := range []struct {
+ hostname string
+ out bool
+ }{
+ {"", false},
+ {"example.com", true},
+ {"non-icann.something", true},
+ {"..", false},
+ {"12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", false}, // valid peerid
+ {"k51qzi5uqu5di608geewp3nqkg0bpujoasmka7ftkyxgcm3fh1aroup0gsdrna", false}, // valid peerid
+ } {
+ t.Run(test.hostname, func(t *testing.T) {
+ out := isDomainNameAndNotPeerID(test.hostname)
+ assert.Equal(t, test.out, out)
+ })
+ }
+}
+
+func TestPortStripping(t *testing.T) {
+ for _, test := range []struct {
+ in string
+ out string
+ }{
+ {"localhost:8080", "localhost"},
+ {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.localhost:8080", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.localhost"},
+ {"example.com:443", "example.com"},
+ {"example.com", "example.com"},
+ {"foo-dweb.ipfs.pvt.k12.ma.us:8080", "foo-dweb.ipfs.pvt.k12.ma.us"},
+ {"localhost", "localhost"},
+ {"[::1]:8080", "::1"},
+ } {
+ t.Run(test.in, func(t *testing.T) {
+ out := stripPort(test.in)
+ assert.Equal(t, test.out, out)
+ })
+ }
+}
+
+func TestToDNSLabel(t *testing.T) {
+ for _, test := range []struct {
+ in string
+ out string
+ err error
+ }{
+ // <= 63
+ {"QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", nil},
+ {"bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", nil},
+ // > 63
+ // PeerID: ed25519+identity multihash → CIDv1Base36
+ {"bafzaajaiaejca4syrpdu6gdx4wsdnokxkprgzxf4wrstuc34gxw5k5jrag2so5gk", "k51qzi5uqu5dj16qyiq0tajolkojyl9qdkr254920wxv7ghtuwcz593tp69z9m", nil},
+ // CIDv1 with long sha512 → error
+ {"bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")},
+ } {
+ t.Run(test.in, func(t *testing.T) {
+ inCID, _ := cid.Decode(test.in)
+ out, err := toDNSLabel(test.in, inCID)
+ assert.Equal(t, test.out, out)
+ assert.Equal(t, test.err, err)
+ })
+ }
+}
+
+func TestKnownSubdomainDetails(t *testing.T) {
+ gwLocalhost := &Specification{Paths: []string{"/ipfs", "/ipns", "/api"}, UseSubdomains: true}
+ gwDweb := &Specification{Paths: []string{"/ipfs", "/ipns", "/api"}, UseSubdomains: true}
+ gwLong := &Specification{Paths: []string{"/ipfs", "/ipns", "/api"}, UseSubdomains: true}
+ gwWildcard1 := &Specification{Paths: []string{"/ipfs", "/ipns", "/api"}, UseSubdomains: true}
+ gwWildcard2 := &Specification{Paths: []string{"/ipfs", "/ipns", "/api"}, UseSubdomains: true}
+
+ gateways := prepareHostnameGateways(map[string]*Specification{
+ "localhost": gwLocalhost,
+ "dweb.link": gwDweb,
+ "devgateway.dweb.link": gwDweb,
+ "dweb.ipfs.pvt.k12.ma.us": gwLong, // note the sneaky ".ipfs." ;-)
+ "*.wildcard1.tld": gwWildcard1,
+ "*.*.wildcard2.tld": gwWildcard2,
+ })
+
+ for _, test := range []struct {
+ // in:
+ hostHeader string
+ // out:
+ gw *Specification
+ hostname string
+ ns string
+ rootID string
+ ok bool
+ }{
+ // no subdomain
+ {"127.0.0.1:8080", nil, "", "", "", false},
+ {"[::1]:8080", nil, "", "", "", false},
+ {"hey.look.example.com", nil, "", "", "", false},
+ {"dweb.link", nil, "", "", "", false},
+ // malformed Host header
+ {".....dweb.link", nil, "", "", "", false},
+ {"link", nil, "", "", "", false},
+ {"8080:dweb.link", nil, "", "", "", false},
+ {" ", nil, "", "", "", false},
+ {"", nil, "", "", "", false},
+ // unknown gateway host
+ {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.unknown.example.com", nil, "", "", "", false},
+ // cid in subdomain, known gateway
+ {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.localhost:8080", gwLocalhost, "localhost:8080", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
+ {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.dweb.link", gwDweb, "dweb.link", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
+ {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.devgateway.dweb.link", gwDweb, "devgateway.dweb.link", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
+ // capture everything before .ipfs.
+ {"foo.bar.boo-buzz.ipfs.dweb.link", gwDweb, "dweb.link", "ipfs", "foo.bar.boo-buzz", true},
+ // ipns
+ {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.localhost:8080", gwLocalhost, "localhost:8080", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true},
+ {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.dweb.link", gwDweb, "dweb.link", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true},
+ // edge case check: public gateway under long TLD (see: https://publicsuffix.org)
+ {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.dweb.ipfs.pvt.k12.ma.us", gwLong, "dweb.ipfs.pvt.k12.ma.us", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
+ {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.dweb.ipfs.pvt.k12.ma.us", gwLong, "dweb.ipfs.pvt.k12.ma.us", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true},
+ // dnslink in subdomain
+ {"en.wikipedia-on-ipfs.org.ipns.localhost:8080", gwLocalhost, "localhost:8080", "ipns", "en.wikipedia-on-ipfs.org", true},
+ {"en.wikipedia-on-ipfs.org.ipns.localhost", gwLocalhost, "localhost", "ipns", "en.wikipedia-on-ipfs.org", true},
+ {"dist.ipfs.tech.ipns.localhost:8080", gwLocalhost, "localhost:8080", "ipns", "dist.ipfs.tech", true},
+ {"en.wikipedia-on-ipfs.org.ipns.dweb.link", gwDweb, "dweb.link", "ipns", "en.wikipedia-on-ipfs.org", true},
+ // edge case check: public gateway under long TLD (see: https://publicsuffix.org)
+ {"foo.dweb.ipfs.pvt.k12.ma.us", nil, "", "", "", false},
+ {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.dweb.ipfs.pvt.k12.ma.us", gwLong, "dweb.ipfs.pvt.k12.ma.us", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
+ {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.dweb.ipfs.pvt.k12.ma.us", gwLong, "dweb.ipfs.pvt.k12.ma.us", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true},
+ // other namespaces
+ {"api.localhost", nil, "", "", "", false},
+ {"peerid.p2p.localhost", gwLocalhost, "localhost", "p2p", "peerid", true},
+ // wildcards
+ {"wildcard1.tld", nil, "", "", "", false},
+ {".wildcard1.tld", nil, "", "", "", false},
+ {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.wildcard1.tld", nil, "", "", "", false},
+ {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.sub.wildcard1.tld", gwWildcard1, "sub.wildcard1.tld", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
+ {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.sub1.sub2.wildcard1.tld", nil, "", "", "", false},
+ {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.sub1.sub2.wildcard2.tld", gwWildcard2, "sub1.sub2.wildcard2.tld", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
+ } {
+ t.Run(test.hostHeader, func(t *testing.T) {
+ gw, hostname, ns, rootID, ok := gateways.knownSubdomainDetails(test.hostHeader)
+ assert.Equal(t, test.ok, ok)
+ assert.Equal(t, test.rootID, rootID)
+ assert.Equal(t, test.ns, ns)
+ assert.Equal(t, test.hostname, hostname)
+ assert.Equal(t, test.gw, gw)
+ })
+ }
+}
diff --git a/gateway/lazyseek.go b/gateway/lazyseek.go
new file mode 100644
index 0000000000..0f4920fad4
--- /dev/null
+++ b/gateway/lazyseek.go
@@ -0,0 +1,60 @@
+package gateway
+
+import (
+ "fmt"
+ "io"
+)
+
+// The HTTP server uses seek to determine the file size. Actually _seeking_ can
+// be slow so we wrap the seeker in a _lazy_ seeker.
+type lazySeeker struct {
+ reader io.ReadSeeker
+
+ size int64
+ offset int64
+ realOffset int64
+}
+
+func (s *lazySeeker) Seek(offset int64, whence int) (int64, error) {
+ switch whence {
+ case io.SeekEnd:
+ return s.Seek(s.size+offset, io.SeekStart)
+ case io.SeekCurrent:
+ return s.Seek(s.offset+offset, io.SeekStart)
+ case io.SeekStart:
+ if offset < 0 {
+ return s.offset, fmt.Errorf("invalid seek offset")
+ }
+ s.offset = offset
+ return s.offset, nil
+ default:
+ return s.offset, fmt.Errorf("invalid whence: %d", whence)
+ }
+}
+
+func (s *lazySeeker) Read(b []byte) (int, error) {
+ // If we're past the end, EOF.
+ if s.offset >= s.size {
+ return 0, io.EOF
+ }
+
+ // actually seek
+ for s.offset != s.realOffset {
+ off, err := s.reader.Seek(s.offset, io.SeekStart)
+ if err != nil {
+ return 0, err
+ }
+ s.realOffset = off
+ }
+ off, err := s.reader.Read(b)
+ s.realOffset += int64(off)
+ s.offset += int64(off)
+ return off, err
+}
+
+func (s *lazySeeker) Close() error {
+ if closer, ok := s.reader.(io.Closer); ok {
+ return closer.Close()
+ }
+ return nil
+}
diff --git a/gateway/lazyseek_test.go b/gateway/lazyseek_test.go
new file mode 100644
index 0000000000..b10b6a2754
--- /dev/null
+++ b/gateway/lazyseek_test.go
@@ -0,0 +1,98 @@
+package gateway
+
+import (
+ "fmt"
+ "io"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+type badSeeker struct {
+ io.ReadSeeker
+}
+
+var errBadSeek = fmt.Errorf("bad seeker")
+
+func (bs badSeeker) Seek(offset int64, whence int) (int64, error) {
+ off, err := bs.ReadSeeker.Seek(0, io.SeekCurrent)
+ if err != nil {
+ panic(err)
+ }
+ return off, errBadSeek
+}
+
+func TestLazySeekerError(t *testing.T) {
+ underlyingBuffer := strings.NewReader("fubar")
+ s := &lazySeeker{
+ reader: badSeeker{underlyingBuffer},
+ size: underlyingBuffer.Size(),
+ }
+ off, err := s.Seek(0, io.SeekEnd)
+ assert.Nil(t, err)
+ assert.Equal(t, s.size, off, "expected to seek to the end")
+
+ // shouldn't have actually seeked.
+ b, err := io.ReadAll(s)
+ assert.Nil(t, err)
+ assert.Equal(t, 0, len(b), "expected to read nothing")
+
+ // shouldn't need to actually seek.
+ off, err = s.Seek(0, io.SeekStart)
+ assert.Nil(t, err)
+ assert.Equal(t, int64(0), off, "expected to seek to the start")
+
+ b, err = io.ReadAll(s)
+ assert.Nil(t, err)
+ assert.Equal(t, "fubar", string(b), "expected to read string")
+
+ // should fail the second time.
+ off, err = s.Seek(0, io.SeekStart)
+ assert.Nil(t, err)
+ assert.Equal(t, int64(0), off, "expected to seek to the start")
+
+ // right here...
+ b, err = io.ReadAll(s)
+ assert.NotNil(t, err)
+ assert.Equal(t, errBadSeek, err)
+ assert.Equal(t, 0, len(b), "expected to read nothing")
+}
+
+func TestLazySeeker(t *testing.T) {
+ underlyingBuffer := strings.NewReader("fubar")
+ s := &lazySeeker{
+ reader: underlyingBuffer,
+ size: underlyingBuffer.Size(),
+ }
+ expectByte := func(b byte) {
+ t.Helper()
+ var buf [1]byte
+ n, err := io.ReadFull(s, buf[:])
+ assert.Nil(t, err)
+ assert.Equal(t, 1, n, "expected to read one byte, read %d", n)
+ assert.Equal(t, b, buf[0])
+ }
+ expectSeek := func(whence int, off, expOff int64, expErr string) {
+ t.Helper()
+ n, err := s.Seek(off, whence)
+ if expErr == "" {
+ assert.Nil(t, err)
+ } else {
+ assert.EqualError(t, err, expErr)
+ }
+ assert.Equal(t, expOff, n)
+ }
+
+ expectSeek(io.SeekEnd, 0, s.size, "")
+ b, err := io.ReadAll(s)
+ assert.Nil(t, err)
+ assert.Equal(t, 0, len(b), "expected to read nothing")
+ expectSeek(io.SeekEnd, -1, s.size-1, "")
+ expectByte('r')
+ expectSeek(io.SeekStart, 0, 0, "")
+ expectByte('f')
+ expectSeek(io.SeekCurrent, 1, 2, "")
+ expectByte('b')
+ expectSeek(io.SeekCurrent, -100, 3, "invalid seek offset")
+}
diff --git a/gateway/testdata/fixtures.car b/gateway/testdata/fixtures.car
new file mode 100644
index 0000000000..e01ca5c31c
Binary files /dev/null and b/gateway/testdata/fixtures.car differ
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000000..e3bf4c64e8
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,184 @@
+module github.com/ipfs/boxo
+
+go 1.19
+
+require (
+ github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a
+ github.com/benbjohnson/clock v1.3.0
+ github.com/cenkalti/backoff v2.2.1+incompatible
+ github.com/cespare/xxhash v1.1.0
+ github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3
+ github.com/cskr/pubsub v1.0.2
+ github.com/dustin/go-humanize v1.0.0
+ github.com/gabriel-vasile/mimetype v1.4.1
+ github.com/gogo/protobuf v1.3.2
+ github.com/google/uuid v1.3.0
+ github.com/gorilla/mux v1.8.0
+ github.com/hashicorp/golang-lru v0.5.4
+ github.com/ipfs/bbloom v0.0.4
+ github.com/ipfs/go-bitfield v1.1.0
+ github.com/ipfs/go-cid v0.4.0
+ github.com/ipfs/go-cidutil v0.1.0
+ github.com/ipfs/go-datastore v0.6.0
+ github.com/ipfs/go-detect-race v0.0.1
+ github.com/ipfs/go-ds-badger v0.3.0
+ github.com/ipfs/go-ds-leveldb v0.5.0
+ github.com/ipfs/go-ipfs-blockstore v1.3.0
+ github.com/ipfs/go-ipfs-blocksutil v0.0.1
+ github.com/ipfs/go-ipfs-delay v0.0.1
+ github.com/ipfs/go-ipfs-redirects-file v0.1.1
+ github.com/ipfs/go-ipld-cbor v0.0.6
+ github.com/ipfs/go-ipld-format v0.4.0
+ github.com/ipfs/go-ipld-legacy v0.1.1
+ github.com/ipfs/go-libipfs v0.6.1
+ github.com/ipfs/go-log v1.0.5
+ github.com/ipfs/go-log/v2 v2.5.1
+ github.com/ipfs/go-merkledag v0.10.0
+ github.com/ipfs/go-metrics-interface v0.0.1
+ github.com/ipfs/go-peertaskqueue v0.8.1
+ github.com/ipfs/go-unixfsnode v1.6.0
+ github.com/ipld/go-car v0.6.1-0.20230320234631-0db9700abe2e
+ github.com/ipld/go-car/v2 v2.8.3-0.20230320234631-0db9700abe2e
+ github.com/ipld/go-codec-dagpb v1.6.0
+ github.com/ipld/go-ipld-prime v0.20.0
+ github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd
+ github.com/jbenet/goprocess v0.1.4
+ github.com/libp2p/go-buffer-pool v0.1.0
+ github.com/libp2p/go-libp2p v0.26.3
+ github.com/libp2p/go-libp2p-kad-dht v0.21.1
+ github.com/libp2p/go-libp2p-record v0.2.0
+ github.com/libp2p/go-libp2p-testing v0.12.0
+ github.com/libp2p/go-msgio v0.3.0
+ github.com/miekg/dns v1.1.50
+ github.com/mr-tron/base58 v1.2.0
+ github.com/multiformats/go-base32 v0.1.0
+ github.com/multiformats/go-multiaddr v0.8.0
+ github.com/multiformats/go-multiaddr-dns v0.3.1
+ github.com/multiformats/go-multibase v0.1.1
+ github.com/multiformats/go-multicodec v0.8.1
+ github.com/multiformats/go-multihash v0.2.1
+ github.com/multiformats/go-multistream v0.4.1
+ github.com/multiformats/go-varint v0.0.7
+ github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9
+ github.com/pkg/errors v0.9.1
+ github.com/polydawn/refmt v0.89.0
+ github.com/prometheus/client_golang v1.14.0
+ github.com/rogpeppe/go-internal v1.9.0
+ github.com/samber/lo v1.36.0
+ github.com/spaolacci/murmur3 v1.1.0
+ github.com/stretchr/testify v1.8.2
+ github.com/urfave/cli/v2 v2.25.0
+ github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc
+ github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11
+ github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f
+ go.opencensus.io v0.24.0
+ go.opentelemetry.io/otel v1.13.0
+ go.opentelemetry.io/otel/trace v1.13.0
+ go.uber.org/atomic v1.10.0
+ go.uber.org/multierr v1.9.0
+ go.uber.org/zap v1.24.0
+ golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
+ golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
+ golang.org/x/sync v0.1.0
+ golang.org/x/sys v0.5.0
+)
+
+require (
+ github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/cespare/xxhash/v2 v2.2.0 // indirect
+ github.com/containerd/cgroups v1.0.4 // indirect
+ github.com/coreos/go-systemd/v22 v22.5.0 // indirect
+ github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
+ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
+ github.com/dgraph-io/badger v1.6.2 // indirect
+ github.com/dgraph-io/ristretto v0.0.2 // indirect
+ github.com/docker/go-units v0.5.0 // indirect
+ github.com/elastic/gosigar v0.14.2 // indirect
+ github.com/flynn/noise v1.0.0 // indirect
+ github.com/francoispqt/gojay v1.2.13 // indirect
+ github.com/fsnotify/fsnotify v1.6.0 // indirect
+ github.com/go-logr/logr v1.2.3 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
+ github.com/godbus/dbus/v5 v5.1.0 // indirect
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+ github.com/golang/mock v1.6.0 // indirect
+ github.com/golang/protobuf v1.5.2 // indirect
+ github.com/golang/snappy v0.0.4 // indirect
+ github.com/google/gopacket v1.1.19 // indirect
+ github.com/google/pprof v0.0.0-20221203041831-ce31453925ec // indirect
+ github.com/hashicorp/errwrap v1.1.0 // indirect
+ github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/huin/goupnp v1.0.3 // indirect
+ github.com/ipfs/go-block-format v0.1.2-0.20230320222416-ba43dc7de213 // indirect
+ github.com/ipfs/go-blockservice v0.5.0 // indirect
+ github.com/ipfs/go-ipfs-chunker v0.0.5 // indirect
+ github.com/ipfs/go-ipfs-ds-help v1.1.0 // indirect
+ github.com/ipfs/go-ipfs-exchange-interface v0.2.0 // indirect
+ github.com/ipfs/go-ipfs-exchange-offline v0.3.0 // indirect
+ github.com/ipfs/go-ipfs-pq v0.0.3 // indirect
+ github.com/ipfs/go-ipfs-util v0.0.2 // indirect
+ github.com/ipfs/go-ipns v0.3.0 // indirect
+ github.com/ipfs/go-unixfs v0.4.5-0.20230321002036-311d68ceee08 // indirect
+ github.com/ipfs/go-verifcid v0.0.2 // indirect
+ github.com/jackpal/go-nat-pmp v1.0.2 // indirect
+ github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
+ github.com/klauspost/compress v1.15.12 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.3 // indirect
+ github.com/koron/go-ssdp v0.0.3 // indirect
+ github.com/libp2p/go-cidranger v1.1.0 // indirect
+ github.com/libp2p/go-flow-metrics v0.1.0 // indirect
+ github.com/libp2p/go-libp2p-asn-util v0.2.0 // indirect
+ github.com/libp2p/go-libp2p-kbucket v0.5.0 // indirect
+ github.com/libp2p/go-nat v0.1.0 // indirect
+ github.com/libp2p/go-netroute v0.2.1 // indirect
+ github.com/libp2p/go-reuseport v0.2.0 // indirect
+ github.com/libp2p/go-yamux/v4 v4.0.0 // indirect
+ github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
+ github.com/mattn/go-isatty v0.0.17 // indirect
+ github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
+ github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
+ github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
+ github.com/minio/sha256-simd v1.0.0 // indirect
+ github.com/multiformats/go-base36 v0.2.0 // indirect
+ github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
+ github.com/nxadm/tail v1.4.8 // indirect
+ github.com/onsi/ginkgo/v2 v2.5.1 // indirect
+ github.com/opencontainers/runtime-spec v1.0.2 // indirect
+ github.com/opentracing/opentracing-go v1.2.0 // indirect
+ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
+ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/prometheus/client_model v0.3.0 // indirect
+ github.com/prometheus/common v0.37.0 // indirect
+ github.com/prometheus/procfs v0.8.0 // indirect
+ github.com/quic-go/qpack v0.4.0 // indirect
+ github.com/quic-go/qtls-go1-19 v0.2.1 // indirect
+ github.com/quic-go/qtls-go1-20 v0.1.1 // indirect
+ github.com/quic-go/quic-go v0.33.0 // indirect
+ github.com/quic-go/webtransport-go v0.5.2 // indirect
+ github.com/raulk/go-watchdog v1.3.0 // indirect
+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
+ github.com/stretchr/objx v0.5.0 // indirect
+ github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
+ github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb // indirect
+ github.com/whyrusleeping/cbor-gen v0.0.0-20230126041949-52956bd4c9aa // indirect
+ github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
+ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
+ go.uber.org/dig v1.15.0 // indirect
+ go.uber.org/fx v1.18.2 // indirect
+ golang.org/x/crypto v0.6.0 // indirect
+ golang.org/x/mod v0.7.0 // indirect
+ golang.org/x/net v0.7.0 // indirect
+ golang.org/x/text v0.7.0 // indirect
+ golang.org/x/tools v0.3.0 // indirect
+ golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
+ google.golang.org/appengine v1.6.6 // indirect
+ google.golang.org/protobuf v1.28.1 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ lukechampine.com/blake3 v1.1.7 // indirect
+ nhooyr.io/websocket v1.8.7 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000000..5de81e9a41
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,1174 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
+dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
+dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
+git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
+github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
+github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
+github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a h1:E/8AP5dFtMhl5KPJz66Kt9G0n+7Sn41Fy1wv9/jHOrc=
+github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
+github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
+github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
+github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
+github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
+github.com/containerd/cgroups v1.0.4 h1:jN/mbWBEaz+T1pi5OFtnkQ+8qnmEbAr1Oo1FRm5B0dA=
+github.com/containerd/cgroups v1.0.4/go.mod h1:nLNQtsF7Sl2HxNebu77i1R0oDlhiTG+kO4JTrUzo6IA=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
+github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 h1:HVTnpeuvF6Owjd5mniCL8DEXo7uYXdQEmOP4FJbV5tg=
+github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE=
+github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0=
+github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU=
+github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U=
+github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
+github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
+github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=
+github.com/dgraph-io/ristretto v0.0.2 h1:a5WaUrDa0qm0YrAAS1tUykT5El3kt62KNZZeMxQn3po=
+github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
+github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
+github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
+github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
+github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4=
+github.com/elastic/gosigar v0.14.2/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ=
+github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
+github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
+github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
+github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
+github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
+github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
+github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
+github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
+github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
+github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
+github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
+github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
+github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
+github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
+github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
+github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
+github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
+github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
+github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
+github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20221203041831-ce31453925ec h1:fR20TYVVwhK4O7r7y+McjRYyaTH6/vjwJOajE+XhlzM=
+github.com/google/pprof v0.0.0-20221203041831-ce31453925ec/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
+github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
+github.com/gxed/hashland/keccakpg v0.0.1/go.mod h1:kRzw3HkwxFU1mpmPP8v1WyQzwdGfmKFJ6tItnhQ67kU=
+github.com/gxed/hashland/murmur3 v0.0.1/go.mod h1:KjXop02n4/ckmZSnY2+HKcLud/tcmvhST0bie/0lS48=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc=
+github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ=
+github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y=
+github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
+github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
+github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
+github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
+github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ=
+github.com/ipfs/go-block-format v0.0.2/go.mod h1:AWR46JfpcObNfg3ok2JHDUfdiHRgWhJgCQF+KIgOPJY=
+github.com/ipfs/go-block-format v0.0.3/go.mod h1:4LmD4ZUw0mhO+JSKdpWwrzATiEfM7WWgQ8H5l6P8MVk=
+github.com/ipfs/go-block-format v0.1.2-0.20230320222416-ba43dc7de213 h1:GxBB4xUUZj+DwG+njsJYURAa7RS8Gu0FdwYNDLRZhqk=
+github.com/ipfs/go-block-format v0.1.2-0.20230320222416-ba43dc7de213/go.mod h1:4G99sJwXnroF0DtCHrujotIAEedtJn2olyQyBIzoWS8=
+github.com/ipfs/go-blockservice v0.5.0 h1:B2mwhhhVQl2ntW2EIpaWPwSCxSuqr5fFA93Ms4bYLEY=
+github.com/ipfs/go-blockservice v0.5.0/go.mod h1:W6brZ5k20AehbmERplmERn8o2Ni3ZZubvAxaIUeaT6w=
+github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
+github.com/ipfs/go-cid v0.0.2/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
+github.com/ipfs/go-cid v0.0.3/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
+github.com/ipfs/go-cid v0.0.4/go.mod h1:4LLaPOQwmk5z9LBgQnpkivrx8BJjUyGwTXCd5Xfj6+M=
+github.com/ipfs/go-cid v0.0.5/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67FexhXog=
+github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I=
+github.com/ipfs/go-cid v0.0.7/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I=
+github.com/ipfs/go-cid v0.4.0 h1:a4pdZq0sx6ZSxbCizebnKiMCx/xI/aBBFlB73IgH4rA=
+github.com/ipfs/go-cid v0.4.0/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
+github.com/ipfs/go-cidutil v0.1.0 h1:RW5hO7Vcf16dplUU60Hs0AKDkQAVPVplr7lk97CFL+Q=
+github.com/ipfs/go-cidutil v0.1.0/go.mod h1:e7OEVBMIv9JaOxt9zaGEmAoSlXW9jdFZ5lP/0PwcfpA=
+github.com/ipfs/go-datastore v0.5.0/go.mod h1:9zhEApYMTl17C8YDp7JmU7sQZi2/wqiYh73hakZ90Bk=
+github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
+github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8=
+github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk=
+github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps=
+github.com/ipfs/go-ds-badger v0.3.0 h1:xREL3V0EH9S219kFFueOYJJTcjgNSZ2HY1iSvN7U1Ro=
+github.com/ipfs/go-ds-badger v0.3.0/go.mod h1:1ke6mXNqeV8K3y5Ak2bAA0osoTfmxUdupVCGm4QUIek=
+github.com/ipfs/go-ds-leveldb v0.5.0 h1:s++MEBbD3ZKc9/8/njrn4flZLnCuY9I79v94gBUNumo=
+github.com/ipfs/go-ds-leveldb v0.5.0/go.mod h1:d3XG9RUDzQ6V4SHi8+Xgj9j1XuEk1z82lquxrVbml/Q=
+github.com/ipfs/go-ipfs-blockstore v1.3.0 h1:m2EXaWgwTzAfsmt5UdJ7Is6l4gJcaM/A12XwJyvYvMM=
+github.com/ipfs/go-ipfs-blockstore v1.3.0/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE=
+github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IWFJMcGIPQ=
+github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk=
+github.com/ipfs/go-ipfs-chunker v0.0.5 h1:ojCf7HV/m+uS2vhUGWcogIIxiO5ubl5O57Q7NapWLY8=
+github.com/ipfs/go-ipfs-chunker v0.0.5/go.mod h1:jhgdF8vxRHycr00k13FM8Y0E+6BoalYeobXmUyTreP8=
+github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
+github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ=
+github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
+github.com/ipfs/go-ipfs-ds-help v1.1.0 h1:yLE2w9RAsl31LtfMt91tRZcrx+e61O5mDxFRR994w4Q=
+github.com/ipfs/go-ipfs-ds-help v1.1.0/go.mod h1:YR5+6EaebOhfcqVCyqemItCLthrpVNot+rsOU/5IatU=
+github.com/ipfs/go-ipfs-exchange-interface v0.2.0 h1:8lMSJmKogZYNo2jjhUs0izT+dck05pqUw4mWNW9Pw6Y=
+github.com/ipfs/go-ipfs-exchange-interface v0.2.0/go.mod h1:z6+RhJuDQbqKguVyslSOuVDhqF9JtTrO3eptSAiW2/Y=
+github.com/ipfs/go-ipfs-exchange-offline v0.3.0 h1:c/Dg8GDPzixGd0MC8Jh6mjOwU57uYokgWRFidfvEkuA=
+github.com/ipfs/go-ipfs-exchange-offline v0.3.0/go.mod h1:MOdJ9DChbb5u37M1IcbrRB02e++Z7521fMxqCNRrz9s=
+github.com/ipfs/go-ipfs-pq v0.0.3 h1:YpoHVJB+jzK15mr/xsWC574tyDLkezVrDNeaalQBsTE=
+github.com/ipfs/go-ipfs-pq v0.0.3/go.mod h1:btNw5hsHBpRcSSgZtiNm/SLj5gYIZ18AKtv3kERkRb4=
+github.com/ipfs/go-ipfs-redirects-file v0.1.1 h1:Io++k0Vf/wK+tfnhEh63Yte1oQK5VGT2hIEYpD0Rzx8=
+github.com/ipfs/go-ipfs-redirects-file v0.1.1/go.mod h1:tAwRjCV0RjLTjH8DR/AU7VYvfQECg+lpUy2Mdzv7gyk=
+github.com/ipfs/go-ipfs-routing v0.3.0 h1:9W/W3N+g+y4ZDeffSgqhgo7BsBSJwPMcyssET9OWevc=
+github.com/ipfs/go-ipfs-util v0.0.1/go.mod h1:spsl5z8KUnrve+73pOhSVZND1SIxPW5RyBCNzQxlJBc=
+github.com/ipfs/go-ipfs-util v0.0.2 h1:59Sswnk1MFaiq+VcaknX7aYEyGyGDAA73ilhEK2POp8=
+github.com/ipfs/go-ipfs-util v0.0.2/go.mod h1:CbPtkWJzjLdEcezDns2XYaehFVNXG9zrdrtMecczcsQ=
+github.com/ipfs/go-ipld-cbor v0.0.6 h1:pYuWHyvSpIsOOLw4Jy7NbBkCyzLDcl64Bf/LZW7eBQ0=
+github.com/ipfs/go-ipld-cbor v0.0.6/go.mod h1:ssdxxaLJPXH7OjF5V4NSjBbcfh+evoR4ukuru0oPXMA=
+github.com/ipfs/go-ipld-format v0.0.1/go.mod h1:kyJtbkDALmFHv3QR6et67i35QzO3S0dCDnkOJhcZkms=
+github.com/ipfs/go-ipld-format v0.2.0/go.mod h1:3l3C1uKoadTPbeNfrDi+xMInYKlx2Cvg1BuydPSdzQs=
+github.com/ipfs/go-ipld-format v0.4.0 h1:yqJSaJftjmjc9jEOFYlpkwOLVKv68OD27jFLlSghBlQ=
+github.com/ipfs/go-ipld-format v0.4.0/go.mod h1:co/SdBE8h99968X0hViiw1MNlh6fvxxnHpvVLnH7jSM=
+github.com/ipfs/go-ipld-legacy v0.1.1 h1:BvD8PEuqwBHLTKqlGFTHSwrwFOMkVESEvwIYwR2cdcc=
+github.com/ipfs/go-ipld-legacy v0.1.1/go.mod h1:8AyKFCjgRPsQFf15ZQgDB8Din4DML/fOmKZkkFkrIEg=
+github.com/ipfs/go-ipns v0.3.0 h1:ai791nTgVo+zTuq2bLvEGmWP1M0A6kGTXUsgv/Yq67A=
+github.com/ipfs/go-ipns v0.3.0/go.mod h1:3cLT2rbvgPZGkHJoPO1YMJeh6LtkxopCkKFcio/wE24=
+github.com/ipfs/go-libipfs v0.6.1 h1:OSO9cm1H3r4OXfP0MP1Q5UhTnhd2fByGl6CVYyz/Rhk=
+github.com/ipfs/go-libipfs v0.6.1/go.mod h1:FmhKgxMOQA572TK5DA3MZ5GL44ZqsMHIrkgK4gLn4A8=
+github.com/ipfs/go-log v0.0.1/go.mod h1:kL1d2/hzSpI0thNYjiKfjanbVNU+IIGA/WnNESY9leM=
+github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8=
+github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo=
+github.com/ipfs/go-log/v2 v2.0.5/go.mod h1:eZs4Xt4ZUJQFM3DlanGhy7TkwwawCZcSByscwkWG+dw=
+github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g=
+github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY=
+github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI=
+github.com/ipfs/go-merkledag v0.10.0 h1:IUQhj/kzTZfam4e+LnaEpoiZ9vZF6ldimVlby+6OXL4=
+github.com/ipfs/go-merkledag v0.10.0/go.mod h1:zkVav8KiYlmbzUzNM6kENzkdP5+qR7+2mCwxkQ6GIj8=
+github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg=
+github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY=
+github.com/ipfs/go-peertaskqueue v0.8.1 h1:YhxAs1+wxb5jk7RvS0LHdyiILpNmRIRnZVztekOF0pg=
+github.com/ipfs/go-peertaskqueue v0.8.1/go.mod h1:Oxxd3eaK279FxeydSPPVGHzbwVeHjatZ2GA8XD+KbPU=
+github.com/ipfs/go-unixfs v0.4.5-0.20230321002036-311d68ceee08 h1:9LBAo8SC8lxcXT3fY+qpIqpzwCahB7v26b3Bhb9KEMA=
+github.com/ipfs/go-unixfs v0.4.5-0.20230321002036-311d68ceee08/go.mod h1:WoNBxHWDOE2KowODZfEX2+NXR5DJGE7lV1h8870DpoY=
+github.com/ipfs/go-unixfsnode v1.6.0 h1:JOSA02yaLylRNi2rlB4ldPr5VcZhcnaIVj5zNLcOjDo=
+github.com/ipfs/go-unixfsnode v1.6.0/go.mod h1:PVfoyZkX1B34qzT3vJO4nsLUpRCyhnMuHBznRcXirlk=
+github.com/ipfs/go-verifcid v0.0.2 h1:XPnUv0XmdH+ZIhLGKg6U2vaPaRDXb9urMyNVCE7uvTs=
+github.com/ipfs/go-verifcid v0.0.2/go.mod h1:40cD9x1y4OWnFXbLNJYRe7MpNvWlMn3LZAG5Wb4xnPU=
+github.com/ipld/go-car v0.6.1-0.20230320234631-0db9700abe2e h1:cwXJD7nqDVGts41pnDjRdnTjE+jqRqMPnnl+RGSoa8U=
+github.com/ipld/go-car v0.6.1-0.20230320234631-0db9700abe2e/go.mod h1:Ug2htCfKi+ftd54ocCDcvc+Yj3O2xv7hfw+tauTr3Q8=
+github.com/ipld/go-car/v2 v2.8.3-0.20230320234631-0db9700abe2e h1:CpmVTo3YgNIMXbXYtxOVQ9miC4B06fHpvjb5cED6e0k=
+github.com/ipld/go-car/v2 v2.8.3-0.20230320234631-0db9700abe2e/go.mod h1:3Vx8oMPD4JMZ/vPNPmzRLLfVlLNb1w8oxX/RWa+eeNA=
+github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc=
+github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s=
+github.com/ipld/go-ipld-prime v0.9.1-0.20210324083106-dc342a9917db/go.mod h1:KvBLMr4PX1gWptgkzRjVZCrLmSGcZCb/jioOQwCqZN8=
+github.com/ipld/go-ipld-prime v0.20.0 h1:Ud3VwE9ClxpO2LkCYP7vWPc0Fo+dYdYzgxUJZ3uRG4g=
+github.com/ipld/go-ipld-prime v0.20.0/go.mod h1:PzqZ/ZR981eKbgdr3y2DJYeD/8bgMawdGVlJDE8kK+M=
+github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd h1:gMlw/MhNr2Wtp5RwGdsW23cs+yCuj9k2ON7i9MiJlRo=
+github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd/go.mod h1:wZ8hH8UxeryOs4kJEJaiui/s00hDSbE37OKsL47g+Sw=
+github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
+github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
+github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA=
+github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=
+github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk=
+github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o=
+github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
+github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
+github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
+github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
+github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk=
+github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8=
+github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q5lDvA=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM=
+github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
+github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
+github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c=
+github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic=
+github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM=
+github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro=
+github.com/libp2p/go-libp2p v0.26.3 h1:6g/psubqwdaBqNNoidbRKSTBEYgaOuKBhHl8Q5tO+PM=
+github.com/libp2p/go-libp2p v0.26.3/go.mod h1:x75BN32YbwuY0Awm2Uix4d4KOz+/4piInkp4Wr3yOo8=
+github.com/libp2p/go-libp2p-asn-util v0.2.0 h1:rg3+Os8jbnO5DxkC7K/Utdi+DkY3q/d1/1q+8WeNAsw=
+github.com/libp2p/go-libp2p-asn-util v0.2.0/go.mod h1:WoaWxbHKBymSN41hWSq/lGKJEca7TNm58+gGJi2WsLI=
+github.com/libp2p/go-libp2p-kad-dht v0.21.1 h1:xpfp8/t9+X2ip1l8Umap1/UGNnJ3RHJgKGAEsnRAlTo=
+github.com/libp2p/go-libp2p-kad-dht v0.21.1/go.mod h1:Oy8wvbdjpB70eS5AaFaI68tOtrdo3KylTvXDjikxqFo=
+github.com/libp2p/go-libp2p-kbucket v0.5.0 h1:g/7tVm8ACHDxH29BGrpsQlnNeu+6OF1A9bno/4/U1oA=
+github.com/libp2p/go-libp2p-kbucket v0.5.0/go.mod h1:zGzGCpQd78b5BNTDGHNDLaTt9aDK/A02xeZp9QeFC4U=
+github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0=
+github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk=
+github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA=
+github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg=
+github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0=
+github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM=
+github.com/libp2p/go-nat v0.1.0 h1:MfVsH6DLcpa04Xr+p8hmVRG4juse0s3J8HyNWYHffXg=
+github.com/libp2p/go-nat v0.1.0/go.mod h1:X7teVkwRHNInVNWQiO/tAiAVRwSr5zoRz4YSTC3uRBM=
+github.com/libp2p/go-netroute v0.1.2/go.mod h1:jZLDV+1PE8y5XxBySEBgbuVAXbhtuHSdmLPL2n9MKbk=
+github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU=
+github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ=
+github.com/libp2p/go-reuseport v0.2.0 h1:18PRvIMlpY6ZK85nIAicSBuXXvrYoSw3dsBAR7zc560=
+github.com/libp2p/go-reuseport v0.2.0/go.mod h1:bvVho6eLMm6Bz5hmU0LYN3ixd3nPPvtIlaURZZgOY4k=
+github.com/libp2p/go-sockaddr v0.0.2/go.mod h1:syPvOmNs24S3dFVGJA1/mrqdeijPxLV2Le3BRLKd68k=
+github.com/libp2p/go-yamux/v4 v4.0.0 h1:+Y80dV2Yx/kv7Y7JKu0LECyVdMXm1VUoko+VQ9rBfZQ=
+github.com/libp2p/go-yamux/v4 v4.0.0/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4=
+github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk=
+github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU=
+github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
+github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
+github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
+github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
+github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
+github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
+github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
+github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8=
+github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms=
+github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc=
+github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU=
+github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc=
+github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s=
+github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=
+github.com/minio/sha256-simd v0.0.0-20190131020904-2d45a736cd16/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U=
+github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
+github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
+github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
+github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
+github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
+github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
+github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
+github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA=
+github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
+github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
+github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM=
+github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
+github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
+github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo=
+github.com/multiformats/go-multiaddr v0.2.0/go.mod h1:0nO36NvPpyV4QzvTLi/lafl2y95ncPj0vFwVF6k6wJ4=
+github.com/multiformats/go-multiaddr v0.8.0 h1:aqjksEcqK+iD/Foe1RRFsGZh8+XFiGo7FgUCZlpv3LU=
+github.com/multiformats/go-multiaddr v0.8.0/go.mod h1:Fs50eBDWvZu+l3/9S6xAE7ZYj6yhxlvaVZjakWN7xRs=
+github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A=
+github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk=
+github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E=
+github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo=
+github.com/multiformats/go-multibase v0.0.1/go.mod h1:bja2MqRZ3ggyXtZSEDKpl0uO/gviWFaSteVbWT51qgs=
+github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc=
+github.com/multiformats/go-multibase v0.1.1 h1:3ASCDsuLX8+j4kx58qnJ4YFq/JWTJpCyDW27ztsVTOI=
+github.com/multiformats/go-multibase v0.1.1/go.mod h1:ZEjHE+IsUrgp5mhlEAYjMtZwK1k4haNkcaPg9aoe1a8=
+github.com/multiformats/go-multicodec v0.8.1 h1:ycepHwavHafh3grIbR1jIXnKCsFm0fqsfEOsJ8NtKE8=
+github.com/multiformats/go-multicodec v0.8.1/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
+github.com/multiformats/go-multihash v0.0.1/go.mod h1:w/5tugSrLEbWqlcgJabL3oHFKTwfvkofsjW2Qa1ct4U=
+github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
+github.com/multiformats/go-multihash v0.0.10/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
+github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc=
+github.com/multiformats/go-multihash v0.0.14/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc=
+github.com/multiformats/go-multihash v0.0.15/go.mod h1:D6aZrWNLFTV/ynMpKsNtB40mJzmCl4jb1alC0OvHiHg=
+github.com/multiformats/go-multihash v0.2.1 h1:aem8ZT0VA2nCHHk7bPJ1BjUbHNciqZC/d16Vve9l108=
+github.com/multiformats/go-multihash v0.2.1/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc=
+github.com/multiformats/go-multistream v0.4.1 h1:rFy0Iiyn3YT0asivDUIR05leAdwZq3de4741sbiSdfo=
+github.com/multiformats/go-multistream v0.4.1/go.mod h1:Mz5eykRVAjJWckE2U78c6xqdtyNUEhKSM0Lwar2p77Q=
+github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
+github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
+github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
+github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
+github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
+github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/ginkgo/v2 v2.5.1 h1:auzK7OI497k6x4OvWq+TKAcpcSAlod0doAH72oIN0Jw=
+github.com/onsi/ginkgo/v2 v2.5.1/go.mod h1:63DOGlLAH8+REH8jUGdL3YpCpu7JODesutUjdENfUAc=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.24.0 h1:+0glovB9Jd6z3VR+ScSwQqXVTIfJcGA9UBM8yzQxhqg=
+github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0=
+github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
+github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
+github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
+github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
+github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 h1:1/WtZae0yGtPq+TI6+Tv1WTxkukpXeMlviSxvL7SRgk=
+github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9/go.mod h1:x3N5drFsm2uilKKuuYo6LdyD8vZAW55sH/9w+pbo1sw=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/polydawn/refmt v0.0.0-20190221155625-df39d6c2d992/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o=
+github.com/polydawn/refmt v0.0.0-20190807091052-3d65705ee9f1/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o=
+github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4=
+github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
+github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
+github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
+github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
+github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
+github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
+github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
+github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
+github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
+github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
+github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
+github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
+github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
+github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
+github.com/quic-go/qtls-go1-19 v0.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A=
+github.com/quic-go/qtls-go1-19 v0.2.1/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
+github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3wSwQk=
+github.com/quic-go/qtls-go1-20 v0.1.1/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
+github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
+github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA=
+github.com/quic-go/webtransport-go v0.5.2 h1:GA6Bl6oZY+g/flt00Pnu0XtivSD8vukOu3lYhJjnGEk=
+github.com/quic-go/webtransport-go v0.5.2/go.mod h1:OhmmgJIzTTqXK5xvtuX0oBpLV2GkLWNDA+UeTGJXErU=
+github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
+github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/samber/lo v1.36.0 h1:4LaOxH1mHnbDGhTVE0i1z8v/lWaQW8AIfOD3HU4mSaw=
+github.com/samber/lo v1.36.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
+github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
+github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
+github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
+github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
+github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
+github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
+github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
+github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
+github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
+github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
+github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
+github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
+github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
+github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
+github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
+github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
+github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
+github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
+github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
+github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
+github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
+github.com/smartystreets/goconvey v0.0.0-20190222223459-a17d461953aa/go.mod h1:2RVY1rIf+2J2o/IM9+vPq9RzmHDSseB7FoXiSNIUsoU=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
+github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
+github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
+github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
+github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
+github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
+github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
+github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
+github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
+github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
+github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb h1:Ywfo8sUltxogBpFuMOFRrrSifO788kAFxmvVw31PtQQ=
+github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb/go.mod h1:ikPs9bRWicNw3S7XpJ8sK/smGwU9WcSVU3dy9qahYBM=
+github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/urfave/cli/v2 v2.25.0 h1:ykdZKuQey2zq0yin/l7JOm9Mh+pg72ngYMeB0ABn6q8=
+github.com/urfave/cli/v2 v2.25.0/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
+github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
+github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
+github.com/warpfork/go-testmark v0.11.0 h1:J6LnV8KpceDvo7spaNU4+DauH2n1x+6RaO2rJrmpQ9U=
+github.com/warpfork/go-wish v0.0.0-20180510122957-5ad1f5abf436/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
+github.com/warpfork/go-wish v0.0.0-20200122115046-b9ea61034e4a/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
+github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
+github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
+github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc h1:BCPnHtcboadS0DvysUuJXZ4lWVv5Bh5i7+tbIyi+ck4=
+github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc/go.mod h1:r45hJU7yEoA81k6MWNhpMj/kms0n14dkzkxYHoB96UM=
+github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 h1:5HZfQkwe0mIfyDmc1Em5GqlNRzcdtlv4HTNmdpt7XH0=
+github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11/go.mod h1:Wlo/SzPmxVp6vXpGt/zaXhHH0fn4IxgqZc82aKg6bpQ=
+github.com/whyrusleeping/cbor-gen v0.0.0-20200123233031-1cdf64d27158/go.mod h1:Xj/M2wWU+QdTdRbu/L/1dIZY8/Wb2K9pAhtroQuxJJI=
+github.com/whyrusleeping/cbor-gen v0.0.0-20230126041949-52956bd4c9aa h1:EyA027ZAkuaCLoxVX4r1TZMPy1d31fM6hbfQ4OU4I5o=
+github.com/whyrusleeping/cbor-gen v0.0.0-20230126041949-52956bd4c9aa/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ=
+github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f h1:jQa4QT2UP9WYv2nzyawpKMOCl+Z/jW7djv2/J50lj9E=
+github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f/go.mod h1:p9UJB6dDgdPgMJZs7UjUOdulKyRr9fqkS+6JKAInPy8=
+github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdznlJHPMoKr0XTrX+IlJs1LH3lyx2nfr1dOlZ79k=
+github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc=
+github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc/go.mod h1:bopw91TMyo8J3tvftk8xmU2kPmlrt4nScJQZU2hE5EM=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
+github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/otel v1.13.0 h1:1ZAKnNQKwBBxFtww/GwxNUyTf0AxkZzrukO8MeXqe4Y=
+go.opentelemetry.io/otel v1.13.0/go.mod h1:FH3RtdZCzRkJYFTCsAKDy9l/XYjMdNv6QrkFFB8DvVg=
+go.opentelemetry.io/otel/trace v1.13.0 h1:CBgRZ6ntv+Amuj1jDsMhZtlAPT6gbyIRdaIzFhfBSdY=
+go.opentelemetry.io/otel/trace v1.13.0/go.mod h1:muCvmmO9KKpvuXSf3KKAXXB2ygNYHQ+ZfI5X08d3tds=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
+go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/dig v1.15.0 h1:vq3YWr8zRj1eFGC7Gvf907hE0eRjPTZ1d3xHadD6liE=
+go.uber.org/dig v1.15.0/go.mod h1:pKHs0wMynzL6brANhB2hLMro+zalv1osARTviTcqHLM=
+go.uber.org/fx v1.18.2 h1:bUNI6oShr+OVFQeU8cDNbnN7VFsu+SsjHzUF51V/GAU=
+go.uber.org/fx v1.18.2/go.mod h1:g0V1KMQ66zIRk8bLu3Ea5Jt2w/cHlOIp4wdRsgh0JaY=
+go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
+go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
+go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
+go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
+go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
+go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
+go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
+go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
+go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
+golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w=
+golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
+golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190227160552-c95aed5357e7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg=
+golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
+golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
+google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
+google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
+honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
+lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
+nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
+nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
+sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
diff --git a/internal/test/flaky.go b/internal/test/flaky.go
new file mode 100644
index 0000000000..6319e5247c
--- /dev/null
+++ b/internal/test/flaky.go
@@ -0,0 +1,16 @@
+package test
+
+import (
+ "os"
+ "testing"
+)
+
+// Flaky will skip the test if the RUN_FLAKY_TESTS environment variable is empty.
+func Flaky(t *testing.T) {
+ // We can't use flags because it fails for tests that does not import this package
+ if os.Getenv("RUN_FLAKY_TESTS") != "" {
+ return
+ }
+
+ t.Skip("flaky")
+}
diff --git a/ipld/car/.gitattributes b/ipld/car/.gitattributes
new file mode 100644
index 0000000000..6f95229927
--- /dev/null
+++ b/ipld/car/.gitattributes
@@ -0,0 +1,2 @@
+# To prevent CRLF breakages on Windows for fragile files, like testdata.
+* -text
diff --git a/ipld/car/.gitignore b/ipld/car/.gitignore
new file mode 100644
index 0000000000..b3f7c18ae7
--- /dev/null
+++ b/ipld/car/.gitignore
@@ -0,0 +1,4 @@
+car/car
+main
+coverage.txt
+dist/
diff --git a/ipld/car/README.md b/ipld/car/README.md
new file mode 100644
index 0000000000..b91dcf148d
--- /dev/null
+++ b/ipld/car/README.md
@@ -0,0 +1,68 @@
+go-car (go!)
+==================
+
+[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](https://protocol.ai)
+[![Go Reference](https://pkg.go.dev/badge/github.com/boxo/ipld/car.svg)](https://pkg.go.dev/github.com/boxo/ipld/car)
+
+> Work with car (Content addressed ARchive) files!
+
+This is a Golang implementation of the [CAR specifications](https://ipld.io/specs/transport/car/), both [CARv1](https://ipld.io/specs/transport/car/carv1/) and [CARv2](https://ipld.io/specs/transport/car/carv2/).
+
+As a format, there are two major module versions:
+
+* [`go-car/v2`](v2/) is geared towards reading and writing CARv2 files, and also
+ supports consuming CARv1 files and using CAR files as an IPFS blockstore.
+* `go-car`, in the root directory, only supports reading and writing CARv1 files.
+
+Most users should use v2, especially for new software, since the v2 API transparently supports both CAR formats.
+
+# **Unstable**
+
+This package will be refactored and thinned out soon. You can find an
+implementation that supports indexes and go-ipld-prime there at [`ipld/go-car`](github.com/ipld/go-car).
+
+## Usage / Installation
+
+This repository provides a `car` binary that can be used for creating, extracting, and working with car files.
+
+To install the latest version of `car`, run:
+```shell script
+go install github.com/boxo/ipld/car/cmd/car@latest
+```
+
+More information about this binary is available in [`cmd/car`](cmd/car/)
+
+
+## Features
+
+[CARv2](v2) features:
+* [Generate index](https://pkg.go.dev/github.com/boxo/ipld/car/v2#GenerateIndex) from an existing CARv1 file
+* [Wrap](https://pkg.go.dev/github.com/boxo/ipld/car/v2#WrapV1) CARv1 files into a CARv2 with automatic index generation.
+* Random-access to blocks in a CAR file given their CID via [Read-Only blockstore](https://pkg.go.dev/github.com/boxo/ipld/car/v2/blockstore#NewReadOnly) API, with transparent support for both CARv1 and CARv2
+* Write CARv2 files via [Read-Write blockstore](https://pkg.go.dev/github.com/boxo/ipld/car/v2/blockstore#OpenReadWrite) API, with support for appending blocks to an existing CARv2 file, and resumption from a partially written CARv2 files.
+* Individual access to [inner CARv1 data payload]((https://pkg.go.dev/github.com/boxo/ipld/car/v2#Reader.DataReader)) and [index]((https://pkg.go.dev/github.com/boxo/ipld/car/v2#Reader.IndexReader)) of a CARv2 file via the `Reader` API.
+
+
+## API Documentation
+
+See docs on [pkg.go.dev](https://pkg.go.dev/github.com/boxo/ipld/car).
+
+## Examples
+
+Here is a shortlist of other examples from the documentation
+
+* [Wrap an existing CARv1 file into an indexed CARv2 file](https://pkg.go.dev/github.com/boxo/ipld/car/v2#example-WrapV1File)
+* [Open read-only blockstore from a CAR file](https://pkg.go.dev/github.com/boxo/ipld/car/v2/blockstore#example-OpenReadOnly)
+* [Open read-write blockstore from a CAR file](https://pkg.go.dev/github.com/boxo/ipld/car/v2/blockstore#example-OpenReadWrite)
+* [Read the index from an existing CARv2 file](https://pkg.go.dev/github.com/boxo/ipld/car/v2/index#example-ReadFrom)
+* [Extract the index from a CARv2 file and store it as a separate file](https://pkg.go.dev/github.com/boxo/ipld/car/v2/index#example-WriteTo)
+
+## Contribute
+
+PRs are welcome!
+
+When editing the Readme, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification.
+
+## License
+
+Apache-2.0/MIT © Protocol Labs
diff --git a/ipld/car/car.go b/ipld/car/car.go
new file mode 100644
index 0000000000..c4cfb2477d
--- /dev/null
+++ b/ipld/car/car.go
@@ -0,0 +1,223 @@
+package car
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "io"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ "github.com/ipfs/boxo/ipld/merkledag"
+ cid "github.com/ipfs/go-cid"
+ cbor "github.com/ipfs/go-ipld-cbor"
+ format "github.com/ipfs/go-ipld-format"
+
+ util "github.com/ipfs/boxo/ipld/car/util"
+)
+
+func init() {
+ cbor.RegisterCborType(CarHeader{})
+}
+
+type Store interface {
+ Put(context.Context, blocks.Block) error
+}
+
+type ReadStore interface {
+ Get(context.Context, cid.Cid) (blocks.Block, error)
+}
+
+type CarHeader struct {
+ Roots []cid.Cid
+ Version uint64
+}
+
+type carWriter struct {
+ ds format.NodeGetter
+ w io.Writer
+ walk WalkFunc
+}
+
+type WalkFunc func(format.Node) ([]*format.Link, error)
+
+func WriteCar(ctx context.Context, ds format.NodeGetter, roots []cid.Cid, w io.Writer, options ...merkledag.WalkOption) error {
+ return WriteCarWithWalker(ctx, ds, roots, w, DefaultWalkFunc, options...)
+}
+
+func WriteCarWithWalker(ctx context.Context, ds format.NodeGetter, roots []cid.Cid, w io.Writer, walk WalkFunc, options ...merkledag.WalkOption) error {
+
+ h := &CarHeader{
+ Roots: roots,
+ Version: 1,
+ }
+
+ if err := WriteHeader(h, w); err != nil {
+ return fmt.Errorf("failed to write car header: %s", err)
+ }
+
+ cw := &carWriter{ds: ds, w: w, walk: walk}
+ seen := cid.NewSet()
+ for _, r := range roots {
+ if err := merkledag.Walk(ctx, cw.enumGetLinks, r, seen.Visit, options...); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func DefaultWalkFunc(nd format.Node) ([]*format.Link, error) {
+ return nd.Links(), nil
+}
+
+func ReadHeader(br *bufio.Reader) (*CarHeader, error) {
+ hb, err := util.LdRead(br)
+ if err != nil {
+ return nil, err
+ }
+
+ var ch CarHeader
+ if err := cbor.DecodeInto(hb, &ch); err != nil {
+ return nil, fmt.Errorf("invalid header: %v", err)
+ }
+
+ return &ch, nil
+}
+
+func WriteHeader(h *CarHeader, w io.Writer) error {
+ hb, err := cbor.DumpObject(h)
+ if err != nil {
+ return err
+ }
+
+ return util.LdWrite(w, hb)
+}
+
+func HeaderSize(h *CarHeader) (uint64, error) {
+ hb, err := cbor.DumpObject(h)
+ if err != nil {
+ return 0, err
+ }
+
+ return util.LdSize(hb), nil
+}
+
+func (cw *carWriter) enumGetLinks(ctx context.Context, c cid.Cid) ([]*format.Link, error) {
+ nd, err := cw.ds.Get(ctx, c)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := cw.writeNode(ctx, nd); err != nil {
+ return nil, err
+ }
+
+ return cw.walk(nd)
+}
+
+func (cw *carWriter) writeNode(ctx context.Context, nd format.Node) error {
+ return util.LdWrite(cw.w, nd.Cid().Bytes(), nd.RawData())
+}
+
+type CarReader struct {
+ br *bufio.Reader
+ Header *CarHeader
+}
+
+func NewCarReader(r io.Reader) (*CarReader, error) {
+ br := bufio.NewReader(r)
+ ch, err := ReadHeader(br)
+ if err != nil {
+ return nil, err
+ }
+
+ if ch.Version != 1 {
+ return nil, fmt.Errorf("invalid car version: %d", ch.Version)
+ }
+
+ if len(ch.Roots) == 0 {
+ return nil, fmt.Errorf("empty car, no roots")
+ }
+
+ return &CarReader{
+ br: br,
+ Header: ch,
+ }, nil
+}
+
+func (cr *CarReader) Next() (blocks.Block, error) {
+ c, data, err := util.ReadNode(cr.br)
+ if err != nil {
+ return nil, err
+ }
+
+ hashed, err := c.Prefix().Sum(data)
+ if err != nil {
+ return nil, err
+ }
+
+ if !hashed.Equals(c) {
+ return nil, fmt.Errorf("mismatch in content integrity, name: %s, data: %s", c, hashed)
+ }
+
+ return blocks.NewBlockWithCid(data, c)
+}
+
+type batchStore interface {
+ PutMany(context.Context, []blocks.Block) error
+}
+
+func LoadCar(ctx context.Context, s Store, r io.Reader) (*CarHeader, error) {
+ cr, err := NewCarReader(r)
+ if err != nil {
+ return nil, err
+ }
+
+ if bs, ok := s.(batchStore); ok {
+ return loadCarFast(ctx, bs, cr)
+ }
+
+ return loadCarSlow(ctx, s, cr)
+}
+
+func loadCarFast(ctx context.Context, s batchStore, cr *CarReader) (*CarHeader, error) {
+ var buf []blocks.Block
+ for {
+ blk, err := cr.Next()
+ if err != nil {
+ if err == io.EOF {
+ if len(buf) > 0 {
+ if err := s.PutMany(ctx, buf); err != nil {
+ return nil, err
+ }
+ }
+ return cr.Header, nil
+ }
+ return nil, err
+ }
+
+ buf = append(buf, blk)
+
+ if len(buf) > 1000 {
+ if err := s.PutMany(ctx, buf); err != nil {
+ return nil, err
+ }
+ buf = buf[:0]
+ }
+ }
+}
+
+func loadCarSlow(ctx context.Context, s Store, cr *CarReader) (*CarHeader, error) {
+ for {
+ blk, err := cr.Next()
+ if err != nil {
+ if err == io.EOF {
+ return cr.Header, nil
+ }
+ return nil, err
+ }
+
+ if err := s.Put(ctx, blk); err != nil {
+ return nil, err
+ }
+ }
+}
diff --git a/ipld/car/car_test.go b/ipld/car/car_test.go
new file mode 100644
index 0000000000..3b1a8faa27
--- /dev/null
+++ b/ipld/car/car_test.go
@@ -0,0 +1,229 @@
+package car_test
+
+import (
+ "bytes"
+ "context"
+ "encoding/hex"
+ "io"
+ "strings"
+ "testing"
+
+ car "github.com/ipfs/boxo/ipld/car"
+ "github.com/ipfs/boxo/ipld/merkledag"
+ dstest "github.com/ipfs/boxo/ipld/merkledag/test"
+ "github.com/ipfs/go-cid"
+ format "github.com/ipfs/go-ipld-format"
+)
+
+func assertAddNodes(t *testing.T, ds format.DAGService, nds ...format.Node) {
+ for _, nd := range nds {
+ if err := ds.Add(context.Background(), nd); err != nil {
+ t.Fatal(err)
+ }
+ }
+}
+
+func TestRoundtrip(t *testing.T) {
+ ctx := context.Background()
+ dserv := dstest.Mock()
+ a := merkledag.NewRawNode([]byte("aaaa"))
+ b := merkledag.NewRawNode([]byte("bbbb"))
+ c := merkledag.NewRawNode([]byte("cccc"))
+
+ nd1 := &merkledag.ProtoNode{}
+ nd1.AddNodeLink("cat", a)
+
+ nd2 := &merkledag.ProtoNode{}
+ nd2.AddNodeLink("first", nd1)
+ nd2.AddNodeLink("dog", b)
+
+ nd3 := &merkledag.ProtoNode{}
+ nd3.AddNodeLink("second", nd2)
+ nd3.AddNodeLink("bear", c)
+
+ assertAddNodes(t, dserv, a, b, c, nd1, nd2, nd3)
+
+ buf := new(bytes.Buffer)
+ if err := car.WriteCar(context.Background(), dserv, []cid.Cid{nd3.Cid()}, buf); err != nil {
+ t.Fatal(err)
+ }
+
+ bserv := dstest.Bserv()
+ ch, err := car.LoadCar(ctx, bserv.Blockstore(), buf)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(ch.Roots) != 1 {
+ t.Fatal("should have one root")
+ }
+
+ if !ch.Roots[0].Equals(nd3.Cid()) {
+ t.Fatal("got wrong cid")
+ }
+
+ bs := bserv.Blockstore()
+ for _, nd := range []format.Node{a, b, c, nd1, nd2, nd3} {
+ has, err := bs.Has(ctx, nd.Cid())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !has {
+ t.Fatal("should have cid in blockstore")
+ }
+ }
+}
+
+// fixture is a clean single-block, single-root CAR
+const fixtureStr = "3aa265726f6f747381d82a58250001711220151fe9e73c6267a7060c6f6c4cca943c236f4b196723489608edb42a8b8fa80b6776657273696f6e012c01711220151fe9e73c6267a7060c6f6c4cca943c236f4b196723489608edb42a8b8fa80ba165646f646779f5"
+
+func TestEOFHandling(t *testing.T) {
+ fixture, err := hex.DecodeString(fixtureStr)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ load := func(t *testing.T, byts []byte) *car.CarReader {
+ cr, err := car.NewCarReader(bytes.NewReader(byts))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ blk, err := cr.Next()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if blk.Cid().String() != "bafyreiavd7u6opdcm6tqmddpnrgmvfb4enxuwglhenejmchnwqvixd5ibm" {
+ t.Fatal("unexpected CID")
+ }
+
+ return cr
+ }
+
+ t.Run("CleanEOF", func(t *testing.T) {
+ cr := load(t, fixture)
+
+ blk, err := cr.Next()
+ if err != io.EOF {
+ t.Fatal("Didn't get expected EOF")
+ }
+ if blk != nil {
+ t.Fatal("EOF returned expected block")
+ }
+ })
+
+ t.Run("BadVarint", func(t *testing.T) {
+ fixtureBadVarint := append(fixture, 160)
+ cr := load(t, fixtureBadVarint)
+
+ blk, err := cr.Next()
+ if err != io.ErrUnexpectedEOF {
+ t.Fatal("Didn't get unexpected EOF")
+ }
+ if blk != nil {
+ t.Fatal("EOF returned unexpected block")
+ }
+ })
+
+ t.Run("TruncatedBlock", func(t *testing.T) {
+ fixtureTruncatedBlock := append(fixture, 100, 0, 0)
+ cr := load(t, fixtureTruncatedBlock)
+
+ blk, err := cr.Next()
+ if err != io.ErrUnexpectedEOF {
+ t.Fatal("Didn't get unexpected EOF")
+ }
+ if blk != nil {
+ t.Fatal("EOF returned unexpected block")
+ }
+ })
+}
+
+func TestBadHeaders(t *testing.T) {
+ testCases := []struct {
+ name string
+ hex string
+ errStr string // either the whole error string
+ errPfx string // or just the prefix
+ }{
+ {
+ "{version:2}",
+ "0aa16776657273696f6e02",
+ "invalid car version: 2",
+ "",
+ },
+ {
+ // an unfortunate error because we don't use a pointer
+ "{roots:[baeaaaa3bmjrq]}",
+ "13a165726f6f747381d82a480001000003616263",
+ "invalid car version: 0",
+ "",
+ }, {
+ "{version:\"1\",roots:[baeaaaa3bmjrq]}",
+ "1da265726f6f747381d82a4800010000036162636776657273696f6e6131",
+ "", "invalid header: ",
+ }, {
+ "{version:1}",
+ "0aa16776657273696f6e01",
+ "empty car, no roots",
+ "",
+ }, {
+ "{version:1,roots:{cid:baeaaaa3bmjrq}}",
+ "20a265726f6f7473a163636964d82a4800010000036162636776657273696f6e01",
+ "",
+ "invalid header: ",
+ }, {
+ "{version:1,roots:[baeaaaa3bmjrq],blip:true}",
+ "22a364626c6970f565726f6f747381d82a4800010000036162636776657273696f6e01",
+ "",
+ "invalid header: ",
+ }, {
+ "[1,[]]",
+ "03820180",
+ "",
+ "invalid header: ",
+ }, {
+ // this is an unfortunate error, it'd be nice to catch it better but it's
+ // very unlikely we'd ever see this in practice
+ "null",
+ "01f6",
+ "",
+ "invalid car version: 0",
+ },
+ }
+
+ makeCar := func(t *testing.T, byts string) error {
+ fixture, err := hex.DecodeString(byts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = car.NewCarReader(bytes.NewReader(fixture))
+ return err
+ }
+
+ t.Run("Sanity check {version:1,roots:[baeaaaa3bmjrq]}", func(t *testing.T) {
+ err := makeCar(t, "1ca265726f6f747381d82a4800010000036162636776657273696f6e01")
+ if err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ err := makeCar(t, tc.hex)
+ if err == nil {
+ t.Fatal("expected error from bad header, didn't get one")
+ }
+ if tc.errStr != "" {
+ if err.Error() != tc.errStr {
+ t.Fatalf("bad error: %v", err)
+ }
+ } else {
+ if !strings.HasPrefix(err.Error(), tc.errPfx) {
+ t.Fatalf("bad error: %v", err)
+ }
+ }
+ })
+ }
+}
diff --git a/ipld/car/fuzz_test.go b/ipld/car/fuzz_test.go
new file mode 100644
index 0000000000..4f4a3f55cf
--- /dev/null
+++ b/ipld/car/fuzz_test.go
@@ -0,0 +1,34 @@
+//go:build go1.18
+
+package car_test
+
+import (
+ "bytes"
+ "encoding/hex"
+ "io"
+ "testing"
+
+ car "github.com/ipfs/boxo/ipld/car"
+)
+
+func FuzzCarReader(f *testing.F) {
+ fixture, err := hex.DecodeString(fixtureStr)
+ if err != nil {
+ f.Fatal(err)
+ }
+ f.Add(fixture)
+
+ f.Fuzz(func(t *testing.T, data []byte) {
+ r, err := car.NewCarReader(bytes.NewReader(data))
+ if err != nil {
+ return
+ }
+
+ for {
+ _, err = r.Next()
+ if err == io.EOF {
+ return
+ }
+ }
+ })
+}
diff --git a/ipld/car/options.go b/ipld/car/options.go
new file mode 100644
index 0000000000..e317f9cc9e
--- /dev/null
+++ b/ipld/car/options.go
@@ -0,0 +1,51 @@
+package car
+
+import "math"
+
+// options holds the configured options after applying a number of
+// Option funcs.
+type options struct {
+ TraverseLinksOnlyOnce bool
+ MaxTraversalLinks uint64
+}
+
+// Option describes an option which affects behavior when
+// interacting with the interface.
+type Option func(*options)
+
+// TraverseLinksOnlyOnce prevents the traversal engine from repeatedly visiting
+// the same links more than once.
+//
+// This can be an efficient strategy for an exhaustive selector where it's known
+// that repeat visits won't impact the completeness of execution. However it
+// should be used with caution with most other selectors as repeat visits of
+// links for different reasons during selector execution can be valid and
+// necessary to perform full traversal.
+func TraverseLinksOnlyOnce() Option {
+ return func(sco *options) {
+ sco.TraverseLinksOnlyOnce = true
+ }
+}
+
+// MaxTraversalLinks changes the allowed number of links a selector traversal
+// can execute before failing.
+//
+// Note that setting this option may cause an error to be returned from selector
+// execution when building a SelectiveCar.
+func MaxTraversalLinks(MaxTraversalLinks uint64) Option {
+ return func(sco *options) {
+ sco.MaxTraversalLinks = MaxTraversalLinks
+ }
+}
+
+// applyOptions applies given opts and returns the resulting options.
+func applyOptions(opt ...Option) options {
+ opts := options{
+ TraverseLinksOnlyOnce: false, // default: recurse until exhausted
+ MaxTraversalLinks: math.MaxInt64, // default: traverse all
+ }
+ for _, o := range opt {
+ o(&opts)
+ }
+ return opts
+}
diff --git a/ipld/car/options_test.go b/ipld/car/options_test.go
new file mode 100644
index 0000000000..250c672037
--- /dev/null
+++ b/ipld/car/options_test.go
@@ -0,0 +1,27 @@
+package car
+
+import (
+ "math"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestApplyOptions_SetsExpectedDefaults(t *testing.T) {
+ require.Equal(t, options{
+ MaxTraversalLinks: math.MaxInt64,
+ TraverseLinksOnlyOnce: false,
+ }, applyOptions())
+}
+
+func TestApplyOptions_AppliesOptions(t *testing.T) {
+ require.Equal(t,
+ options{
+ MaxTraversalLinks: 123,
+ TraverseLinksOnlyOnce: true,
+ },
+ applyOptions(
+ MaxTraversalLinks(123),
+ TraverseLinksOnlyOnce(),
+ ))
+}
diff --git a/ipld/car/selectivecar.go b/ipld/car/selectivecar.go
new file mode 100644
index 0000000000..d4dbf11fed
--- /dev/null
+++ b/ipld/car/selectivecar.go
@@ -0,0 +1,289 @@
+package car
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "math"
+
+ util "github.com/ipfs/boxo/ipld/car/util"
+ cid "github.com/ipfs/go-cid"
+ "github.com/ipld/go-ipld-prime"
+ cidlink "github.com/ipld/go-ipld-prime/linking/cid"
+ basicnode "github.com/ipld/go-ipld-prime/node/basic"
+ "github.com/ipld/go-ipld-prime/traversal"
+ "github.com/ipld/go-ipld-prime/traversal/selector"
+
+ // The dag-pb and raw codecs are necessary for unixfs.
+ dagpb "github.com/ipld/go-codec-dagpb"
+ _ "github.com/ipld/go-ipld-prime/codec/raw"
+)
+
+// Dag is a root/selector combo to put into a car
+type Dag struct {
+ Root cid.Cid
+ Selector ipld.Node
+}
+
+// Block is all information and metadata about a block that is part of a car file
+type Block struct {
+ BlockCID cid.Cid
+ Data []byte
+ Offset uint64
+ Size uint64
+}
+
+// SelectiveCar is a car file based on root + selector combos instead of just
+// a single root and complete dag walk
+type SelectiveCar struct {
+ ctx context.Context
+ dags []Dag
+ store ReadStore
+ opts options
+}
+
+// OnCarHeaderFunc is called during traversal when the header is created
+type OnCarHeaderFunc func(CarHeader) error
+
+// OnNewCarBlockFunc is called during traveral when a new unique block is encountered
+type OnNewCarBlockFunc func(Block) error
+
+// SelectiveCarPrepared is a SelectiveCar that has already been traversed, such that it
+// can be written quicker with Dump. It also contains metadata already collection about
+// the Car file like size and number of blocks that go into it
+type SelectiveCarPrepared struct {
+ SelectiveCar
+ size uint64
+ header CarHeader
+ cids []cid.Cid
+ userOnNewCarBlocks []OnNewCarBlockFunc
+}
+
+// NewSelectiveCar creates a new SelectiveCar for the given car file based
+// a block store and set of root+selector pairs
+func NewSelectiveCar(ctx context.Context, store ReadStore, dags []Dag, opts ...Option) SelectiveCar {
+ return SelectiveCar{
+ ctx: ctx,
+ store: store,
+ dags: dags,
+ opts: applyOptions(opts...),
+ }
+}
+
+func (sc SelectiveCar) traverse(onCarHeader OnCarHeaderFunc, onNewCarBlock OnNewCarBlockFunc) (uint64, error) {
+ traverser := &selectiveCarTraverser{onCarHeader, onNewCarBlock, 0, cid.NewSet(), sc, cidlink.DefaultLinkSystem()}
+ traverser.lsys.StorageReadOpener = traverser.loader
+ return traverser.traverse()
+}
+
+// Prepare traverse a car file and collects data on what is about to be written, but
+// does not actually write the file
+func (sc SelectiveCar) Prepare(userOnNewCarBlocks ...OnNewCarBlockFunc) (SelectiveCarPrepared, error) {
+ var header CarHeader
+ var cids []cid.Cid
+
+ onCarHeader := func(h CarHeader) error {
+ header = h
+ return nil
+ }
+ onNewCarBlock := func(block Block) error {
+ cids = append(cids, block.BlockCID)
+ return nil
+ }
+ size, err := sc.traverse(onCarHeader, onNewCarBlock)
+ if err != nil {
+ return SelectiveCarPrepared{}, err
+ }
+ return SelectiveCarPrepared{sc, size, header, cids, userOnNewCarBlocks}, nil
+}
+
+func (sc SelectiveCar) Write(w io.Writer, userOnNewCarBlocks ...OnNewCarBlockFunc) error {
+ onCarHeader := func(h CarHeader) error {
+ if err := WriteHeader(&h, w); err != nil {
+ return fmt.Errorf("failed to write car header: %s", err)
+ }
+ return nil
+ }
+ onNewCarBlock := func(block Block) error {
+ err := util.LdWrite(w, block.BlockCID.Bytes(), block.Data)
+ if err != nil {
+ return err
+ }
+ for _, userOnNewCarBlock := range userOnNewCarBlocks {
+ err := userOnNewCarBlock(block)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+ _, err := sc.traverse(onCarHeader, onNewCarBlock)
+ return err
+}
+
+// Size returns the total size in bytes of the car file that will be written
+func (sc SelectiveCarPrepared) Size() uint64 {
+ return sc.size
+}
+
+// Header returns the header for the car file that will be written
+func (sc SelectiveCarPrepared) Header() CarHeader {
+ return sc.header
+}
+
+// Cids returns the list of unique block cids that will be written to the car file
+func (sc SelectiveCarPrepared) Cids() []cid.Cid {
+ return sc.cids
+}
+
+// Dump writes the car file as quickly as possible based on information already
+// collected
+func (sc SelectiveCarPrepared) Dump(ctx context.Context, w io.Writer) error {
+ offset, err := HeaderSize(&sc.header)
+ if err != nil {
+ return fmt.Errorf("failed to size car header: %s", err)
+ }
+ if err := WriteHeader(&sc.header, w); err != nil {
+ return fmt.Errorf("failed to write car header: %s", err)
+ }
+ for _, c := range sc.cids {
+ blk, err := sc.store.Get(ctx, c)
+ if err != nil {
+ return err
+ }
+ raw := blk.RawData()
+ size := util.LdSize(c.Bytes(), raw)
+ err = util.LdWrite(w, c.Bytes(), raw)
+ if err != nil {
+ return err
+ }
+ for _, userOnNewCarBlock := range sc.userOnNewCarBlocks {
+ err := userOnNewCarBlock(Block{
+ BlockCID: c,
+ Data: raw,
+ Offset: offset,
+ Size: size,
+ })
+ if err != nil {
+ return err
+ }
+ }
+ offset += size
+ }
+ return nil
+}
+
+type selectiveCarTraverser struct {
+ onCarHeader OnCarHeaderFunc
+ onNewCarBlock OnNewCarBlockFunc
+ offset uint64
+ cidSet *cid.Set
+ sc SelectiveCar
+ lsys ipld.LinkSystem
+}
+
+func (sct *selectiveCarTraverser) traverse() (uint64, error) {
+ err := sct.traverseHeader()
+ if err != nil {
+ return 0, err
+ }
+ err = sct.traverseBlocks()
+ if err != nil {
+ return 0, err
+ }
+ return sct.offset, nil
+}
+
+func (sct *selectiveCarTraverser) traverseHeader() error {
+ roots := make([]cid.Cid, 0, len(sct.sc.dags))
+ for _, carDag := range sct.sc.dags {
+ roots = append(roots, carDag.Root)
+ }
+
+ header := CarHeader{
+ Roots: roots,
+ Version: 1,
+ }
+
+ size, err := HeaderSize(&header)
+ if err != nil {
+ return err
+ }
+
+ sct.offset += size
+
+ return sct.onCarHeader(header)
+}
+
+func (sct *selectiveCarTraverser) loader(ctx ipld.LinkContext, lnk ipld.Link) (io.Reader, error) {
+ cl, ok := lnk.(cidlink.Link)
+ if !ok {
+ return nil, errors.New("incorrect link type")
+ }
+ c := cl.Cid
+ blk, err := sct.sc.store.Get(ctx.Ctx, c)
+ if err != nil {
+ return nil, err
+ }
+ raw := blk.RawData()
+ if !sct.cidSet.Has(c) {
+ sct.cidSet.Add(c)
+ size := util.LdSize(c.Bytes(), raw)
+ err := sct.onNewCarBlock(Block{
+ BlockCID: c,
+ Data: raw,
+ Offset: sct.offset,
+ Size: size,
+ })
+ if err != nil {
+ return nil, err
+ }
+ sct.offset += size
+ }
+ return bytes.NewReader(raw), nil
+}
+
+func (sct *selectiveCarTraverser) traverseBlocks() error {
+ nsc := func(lnk ipld.Link, lctx ipld.LinkContext) (ipld.NodePrototype, error) {
+ // We can decode all nodes into basicnode's Any, except for
+ // dagpb nodes, which must explicitly use the PBNode prototype.
+ if lnk, ok := lnk.(cidlink.Link); ok && lnk.Cid.Prefix().Codec == 0x70 {
+ return dagpb.Type.PBNode, nil
+ }
+ return basicnode.Prototype.Any, nil
+ }
+
+ for _, carDag := range sct.sc.dags {
+ parsed, err := selector.ParseSelector(carDag.Selector)
+ if err != nil {
+ return err
+ }
+ lnk := cidlink.Link{Cid: carDag.Root}
+ ns, _ := nsc(lnk, ipld.LinkContext{}) // nsc won't error
+ nd, err := sct.lsys.Load(ipld.LinkContext{Ctx: sct.sc.ctx}, lnk, ns)
+ if err != nil {
+ return err
+ }
+ prog := traversal.Progress{
+ Cfg: &traversal.Config{
+ Ctx: sct.sc.ctx,
+ LinkSystem: sct.lsys,
+ LinkTargetNodePrototypeChooser: nsc,
+ LinkVisitOnlyOnce: sct.sc.opts.TraverseLinksOnlyOnce,
+ },
+ }
+ if sct.sc.opts.MaxTraversalLinks < math.MaxInt64 {
+ prog.Budget = &traversal.Budget{
+ NodeBudget: math.MaxInt64,
+ LinkBudget: int64(sct.sc.opts.MaxTraversalLinks),
+ }
+ }
+ err = prog.WalkAdv(nd, parsed, func(traversal.Progress, ipld.Node, traversal.VisitReason) error { return nil })
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/ipld/car/selectivecar_test.go b/ipld/car/selectivecar_test.go
new file mode 100644
index 0000000000..a49b99cdec
--- /dev/null
+++ b/ipld/car/selectivecar_test.go
@@ -0,0 +1,228 @@
+package car_test
+
+import (
+ "bytes"
+ "context"
+ "testing"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ car "github.com/ipfs/boxo/ipld/car"
+ "github.com/ipfs/boxo/ipld/merkledag"
+ dstest "github.com/ipfs/boxo/ipld/merkledag/test"
+ cid "github.com/ipfs/go-cid"
+ format "github.com/ipfs/go-ipld-format"
+ basicnode "github.com/ipld/go-ipld-prime/node/basic"
+ "github.com/ipld/go-ipld-prime/traversal/selector"
+ "github.com/ipld/go-ipld-prime/traversal/selector/builder"
+ selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRoundtripSelective(t *testing.T) {
+ ctx := context.Background()
+ sourceBserv := dstest.Bserv()
+ sourceBs := sourceBserv.Blockstore()
+ dserv := merkledag.NewDAGService(sourceBserv)
+ a := merkledag.NewRawNode([]byte("aaaa"))
+ b := merkledag.NewRawNode([]byte("bbbb"))
+ c := merkledag.NewRawNode([]byte("cccc"))
+
+ nd1 := &merkledag.ProtoNode{}
+ nd1.AddNodeLink("cat", a)
+
+ nd2 := &merkledag.ProtoNode{}
+ nd2.AddNodeLink("first", nd1)
+ nd2.AddNodeLink("dog", b)
+ nd2.AddNodeLink("repeat", nd1)
+
+ nd3 := &merkledag.ProtoNode{}
+ nd3.AddNodeLink("second", nd2)
+ nd3.AddNodeLink("bear", c)
+
+ assertAddNodes(t, dserv, a, b, c, nd1, nd2, nd3)
+
+ ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any)
+
+ // the graph assembled above looks as follows, in order:
+ // nd3 -> [c, nd2 -> [nd1 -> a, b, nd1 -> a]]
+ // this selector starts at n3, and traverses a link at index 1 (nd2, the second link, zero indexed)
+ // it then recursively traverses all of its children
+ // the only node skipped is 'c' -- link at index 0 immediately below nd3
+ // the purpose is simply to show we are not writing the entire merkledag underneath
+ // nd3
+ selector := ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
+ efsb.Insert("Links",
+ ssb.ExploreIndex(1, ssb.ExploreRecursive(selector.RecursionLimitNone(), ssb.ExploreAll(ssb.ExploreRecursiveEdge()))))
+ }).Node()
+
+ sc := car.NewSelectiveCar(context.Background(), sourceBs, []car.Dag{{Root: nd3.Cid(), Selector: selector}})
+
+ // write car in one step
+ buf := new(bytes.Buffer)
+ blockCount := 0
+ var oneStepBlocks []car.Block
+ err := sc.Write(buf, func(block car.Block) error {
+ oneStepBlocks = append(oneStepBlocks, block)
+ blockCount++
+ return nil
+ })
+ require.Equal(t, blockCount, 5)
+ require.NoError(t, err)
+
+ // create a new builder for two-step write
+ sc2 := car.NewSelectiveCar(context.Background(), sourceBs, []car.Dag{{Root: nd3.Cid(), Selector: selector}})
+
+ // write car in two steps
+ var twoStepBlocks []car.Block
+ scp, err := sc2.Prepare(func(block car.Block) error {
+ twoStepBlocks = append(twoStepBlocks, block)
+ return nil
+ })
+ require.NoError(t, err)
+ buf2 := new(bytes.Buffer)
+ err = scp.Dump(ctx, buf2)
+ require.NoError(t, err)
+
+ // verify preparation step correctly assesed length and blocks
+ require.Equal(t, scp.Size(), uint64(buf.Len()))
+ require.Equal(t, len(scp.Cids()), blockCount)
+
+ // verify equal data written by both methods
+ require.Equal(t, buf.Bytes(), buf2.Bytes())
+
+ // verify equal blocks were passed to user block hook funcs
+ require.Equal(t, oneStepBlocks, twoStepBlocks)
+
+ // readout car and verify contents
+ bserv := dstest.Bserv()
+ ch, err := car.LoadCar(ctx, bserv.Blockstore(), buf)
+ require.NoError(t, err)
+ require.Equal(t, len(ch.Roots), 1)
+
+ require.True(t, ch.Roots[0].Equals(nd3.Cid()))
+
+ bs := bserv.Blockstore()
+ for _, nd := range []format.Node{a, b, nd1, nd2, nd3} {
+ has, err := bs.Has(ctx, nd.Cid())
+ require.NoError(t, err)
+ require.True(t, has)
+ }
+
+ for _, nd := range []format.Node{c} {
+ has, err := bs.Has(ctx, nd.Cid())
+ require.NoError(t, err)
+ require.False(t, has)
+ }
+}
+
+func TestNoLinkRepeatSelective(t *testing.T) {
+ sourceBserv := dstest.Bserv()
+ sourceBs := countingReadStore{bs: sourceBserv.Blockstore()}
+ dserv := merkledag.NewDAGService(sourceBserv)
+ a := merkledag.NewRawNode([]byte("aaaa"))
+ b := merkledag.NewRawNode([]byte("bbbb"))
+ c := merkledag.NewRawNode([]byte("cccc"))
+
+ nd1 := &merkledag.ProtoNode{}
+ nd1.AddNodeLink("cat", a)
+
+ nd2 := &merkledag.ProtoNode{}
+ nd2.AddNodeLink("first", nd1)
+ nd2.AddNodeLink("dog", b)
+ nd2.AddNodeLink("repeat", nd1)
+
+ nd3 := &merkledag.ProtoNode{}
+ nd3.AddNodeLink("second", nd2)
+ nd3.AddNodeLink("bear", c)
+ nd3.AddNodeLink("bearagain1", c)
+ nd3.AddNodeLink("bearagain2", c)
+ nd3.AddNodeLink("bearagain3", c)
+
+ assertAddNodes(t, dserv, a, b, c, nd1, nd2, nd3)
+
+ t.Run("TraverseLinksOnlyOnce off", func(t *testing.T) {
+ sourceBs.count = 0
+ sc := car.NewSelectiveCar(context.Background(),
+ &sourceBs,
+ []car.Dag{{Root: nd3.Cid(), Selector: selectorparse.CommonSelector_ExploreAllRecursively}},
+ )
+
+ buf := new(bytes.Buffer)
+ blockCount := 0
+ err := sc.Write(buf, func(block car.Block) error {
+ blockCount++
+ return nil
+ })
+ require.Equal(t, blockCount, 6)
+ require.Equal(t, sourceBs.count, 11) // with TraverseLinksOnlyOnce off, we expect repeat block visits because our DAG has repeat links
+ require.NoError(t, err)
+ })
+
+ t.Run("TraverseLinksOnlyOnce on", func(t *testing.T) {
+ sourceBs.count = 0
+
+ sc := car.NewSelectiveCar(context.Background(),
+ &sourceBs,
+ []car.Dag{{Root: nd3.Cid(), Selector: selectorparse.CommonSelector_ExploreAllRecursively}},
+ car.TraverseLinksOnlyOnce(),
+ )
+
+ buf := new(bytes.Buffer)
+ blockCount := 0
+ err := sc.Write(buf, func(block car.Block) error {
+ blockCount++
+ return nil
+ })
+ require.Equal(t, blockCount, 6)
+ require.Equal(t, sourceBs.count, 6) // only 6 blocks to load, no duplicate loading expected
+ require.NoError(t, err)
+ })
+}
+
+func TestLinkLimitSelective(t *testing.T) {
+ sourceBserv := dstest.Bserv()
+ sourceBs := sourceBserv.Blockstore()
+ dserv := merkledag.NewDAGService(sourceBserv)
+ a := merkledag.NewRawNode([]byte("aaaa"))
+ b := merkledag.NewRawNode([]byte("bbbb"))
+ c := merkledag.NewRawNode([]byte("cccc"))
+
+ nd1 := &merkledag.ProtoNode{}
+ nd1.AddNodeLink("cat", a)
+
+ nd2 := &merkledag.ProtoNode{}
+ nd2.AddNodeLink("first", nd1)
+ nd2.AddNodeLink("dog", b)
+ nd2.AddNodeLink("repeat", nd1)
+
+ nd3 := &merkledag.ProtoNode{}
+ nd3.AddNodeLink("second", nd2)
+ nd3.AddNodeLink("bear", c)
+
+ assertAddNodes(t, dserv, a, b, c, nd1, nd2, nd3)
+
+ sc := car.NewSelectiveCar(context.Background(),
+ sourceBs,
+ []car.Dag{{Root: nd3.Cid(), Selector: selectorparse.CommonSelector_ExploreAllRecursively}},
+ car.MaxTraversalLinks(2))
+
+ buf := new(bytes.Buffer)
+ blockCount := 0
+ err := sc.Write(buf, func(block car.Block) error {
+ blockCount++
+ return nil
+ })
+ require.Equal(t, blockCount, 3) // root + 2
+ require.Error(t, err)
+ require.Regexp(t, "^traversal budget exceeded: budget for links reached zero while on path .*", err)
+}
+
+type countingReadStore struct {
+ bs car.ReadStore
+ count int
+}
+
+func (rs *countingReadStore) Get(ctx context.Context, c cid.Cid) (blocks.Block, error) {
+ rs.count++
+ return rs.bs.Get(ctx, c)
+}
diff --git a/ipld/car/testdata/fuzz/FuzzCarReader/21a90a70853c333c6b9ddc133bae39a14164874ed8abdee1f6a6795311a0e546 b/ipld/car/testdata/fuzz/FuzzCarReader/21a90a70853c333c6b9ddc133bae39a14164874ed8abdee1f6a6795311a0e546
new file mode 100644
index 0000000000..a7ab1d519c
--- /dev/null
+++ b/ipld/car/testdata/fuzz/FuzzCarReader/21a90a70853c333c6b9ddc133bae39a14164874ed8abdee1f6a6795311a0e546
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("\xe0\xe0\xe0\xe0\xa7\x06\folLʔ<#oK\x19g#H\x96\b\xed\xb4*\x8b\x8f\xa8\vgversion\x19")
diff --git a/ipld/car/testdata/fuzz/FuzzCarReader/5857e57e4072c6b0d8684030cc13b5570849c89b3dbf5bc0152abc66c9642f3e b/ipld/car/testdata/fuzz/FuzzCarReader/5857e57e4072c6b0d8684030cc13b5570849c89b3dbf5bc0152abc66c9642f3e
new file mode 100644
index 0000000000..3e680cf60e
--- /dev/null
+++ b/ipld/car/testdata/fuzz/FuzzCarReader/5857e57e4072c6b0d8684030cc13b5570849c89b3dbf5bc0152abc66c9642f3e
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte(":\xa2eroots\x81\xd80X%\x00\x0100 00000000000000000000000000000000gversion\x01\x010")
diff --git a/ipld/car/util/util.go b/ipld/car/util/util.go
new file mode 100644
index 0000000000..af2f38e420
--- /dev/null
+++ b/ipld/car/util/util.go
@@ -0,0 +1,136 @@
+package util
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+
+ cid "github.com/ipfs/go-cid"
+ mh "github.com/multiformats/go-multihash"
+)
+
+// MaxAllowedSectionSize dictates the maximum number of bytes that a CARv1 header
+// or section is allowed to occupy without causing a decode to error.
+// This cannot be supplied as an option, only adjusted as a global. You should
+// use v2#NewReader instead since it allows for options to be passed in.
+var MaxAllowedSectionSize uint = 32 << 20 // 32MiB
+
+var cidv0Pref = []byte{0x12, 0x20}
+
+type BytesReader interface {
+ io.Reader
+ io.ByteReader
+}
+
+// Deprecated: ReadCid shouldn't be used directly, use CidFromReader from go-cid
+func ReadCid(buf []byte) (cid.Cid, int, error) {
+ if len(buf) >= 2 && bytes.Equal(buf[:2], cidv0Pref) {
+ i := 34
+ if len(buf) < i {
+ i = len(buf)
+ }
+ c, err := cid.Cast(buf[:i])
+ return c, i, err
+ }
+
+ br := bytes.NewReader(buf)
+
+ // assume cidv1
+ vers, err := binary.ReadUvarint(br)
+ if err != nil {
+ return cid.Cid{}, 0, err
+ }
+
+ // TODO: the go-cid package allows version 0 here as well
+ if vers != 1 {
+ return cid.Cid{}, 0, fmt.Errorf("invalid cid version number")
+ }
+
+ codec, err := binary.ReadUvarint(br)
+ if err != nil {
+ return cid.Cid{}, 0, err
+ }
+
+ mhr := mh.NewReader(br)
+ h, err := mhr.ReadMultihash()
+ if err != nil {
+ return cid.Cid{}, 0, err
+ }
+
+ return cid.NewCidV1(codec, h), len(buf) - br.Len(), nil
+}
+
+func ReadNode(br *bufio.Reader) (cid.Cid, []byte, error) {
+ data, err := LdRead(br)
+ if err != nil {
+ return cid.Cid{}, nil, err
+ }
+
+ n, c, err := cid.CidFromReader(bytes.NewReader(data))
+ if err != nil {
+ return cid.Cid{}, nil, err
+ }
+
+ return c, data[n:], nil
+}
+
+func LdWrite(w io.Writer, d ...[]byte) error {
+ var sum uint64
+ for _, s := range d {
+ sum += uint64(len(s))
+ }
+
+ buf := make([]byte, 8)
+ n := binary.PutUvarint(buf, sum)
+ _, err := w.Write(buf[:n])
+ if err != nil {
+ return err
+ }
+
+ for _, s := range d {
+ _, err = w.Write(s)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func LdSize(d ...[]byte) uint64 {
+ var sum uint64
+ for _, s := range d {
+ sum += uint64(len(s))
+ }
+ buf := make([]byte, 8)
+ n := binary.PutUvarint(buf, sum)
+ return sum + uint64(n)
+}
+
+func LdRead(r *bufio.Reader) ([]byte, error) {
+ if _, err := r.Peek(1); err != nil { // no more blocks, likely clean io.EOF
+ return nil, err
+ }
+
+ l, err := binary.ReadUvarint(r)
+ if err != nil {
+ if err == io.EOF {
+ return nil, io.ErrUnexpectedEOF // don't silently pretend this is a clean EOF
+ }
+ return nil, err
+ }
+
+ if l > uint64(MaxAllowedSectionSize) { // Don't OOM
+ return nil, errors.New("malformed car; header is bigger than util.MaxAllowedSectionSize")
+ }
+
+ buf := make([]byte, l)
+ if _, err := io.ReadFull(r, buf); err != nil {
+ return nil, err
+ }
+
+ return buf, nil
+}
diff --git a/ipld/car/util/util_test.go b/ipld/car/util/util_test.go
new file mode 100644
index 0000000000..4708f3f316
--- /dev/null
+++ b/ipld/car/util/util_test.go
@@ -0,0 +1,27 @@
+package util_test
+
+import (
+ "bytes"
+ crand "crypto/rand"
+ "math/rand"
+ "testing"
+
+ "github.com/ipfs/boxo/ipld/car/util"
+ "github.com/stretchr/testify/require"
+)
+
+func TestLdSize(t *testing.T) {
+ for i := 0; i < 5; i++ {
+ var buf bytes.Buffer
+ data := make([][]byte, 5)
+ for j := 0; j < 5; j++ {
+ data[j] = make([]byte, rand.Intn(30))
+ _, err := crand.Read(data[j])
+ require.NoError(t, err)
+ }
+ size := util.LdSize(data...)
+ err := util.LdWrite(&buf, data...)
+ require.NoError(t, err)
+ require.Equal(t, uint64(len(buf.Bytes())), size)
+ }
+}
diff --git a/ipld/car/v2/LICENSE.md b/ipld/car/v2/LICENSE.md
new file mode 100644
index 0000000000..2fa16a1537
--- /dev/null
+++ b/ipld/car/v2/LICENSE.md
@@ -0,0 +1,229 @@
+The contents of this repository are Copyright (c) corresponding authors and
+contributors, licensed under the `Permissive License Stack` meaning either of:
+
+- Apache-2.0 Software License: https://www.apache.org/licenses/LICENSE-2.0
+ ([...4tr2kfsq](https://dweb.link/ipfs/bafkreiankqxazcae4onkp436wag2lj3ccso4nawxqkkfckd6cg4tr2kfsq))
+
+- MIT Software License: https://opensource.org/licenses/MIT
+ ([...vljevcba](https://dweb.link/ipfs/bafkreiepofszg4gfe2gzuhojmksgemsub2h4uy2gewdnr35kswvljevcba))
+
+You may not use the contents of this repository except in compliance
+with one of the listed Licenses. For an extended clarification of the
+intent behind the choice of Licensing please refer to
+https://protocol.ai/blog/announcing-the-permissive-license-stack/
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the terms listed in this notice is distributed on
+an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+either express or implied. See each License for the specific language
+governing permissions and limitations under that License.
+
+
+`SPDX-License-Identifier: Apache-2.0 OR MIT`
+
+Verbatim copies of both licenses are included below:
+
+Apache-2.0 Software License
+
+```
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+```
+
+
+MIT Software License
+
+```
+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.
+```
+
diff --git a/ipld/car/v2/bench_test.go b/ipld/car/v2/bench_test.go
new file mode 100644
index 0000000000..89a579df58
--- /dev/null
+++ b/ipld/car/v2/bench_test.go
@@ -0,0 +1,203 @@
+package car_test
+
+import (
+ "context"
+ "io"
+ "math/rand"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/ipfs/boxo/ipld/car/v2/blockstore"
+ "github.com/ipfs/boxo/ipld/merkledag"
+ "github.com/ipfs/go-cid"
+
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+)
+
+// BenchmarkReadBlocks instantiates a BlockReader, and iterates over all blocks.
+// It essentially looks at the contents of any CARv1 or CARv2 file.
+// Note that this also uses internal carv1.ReadHeader underneath.
+func BenchmarkReadBlocks(b *testing.B) {
+ path := "testdata/sample-wrapped-v2.car"
+
+ info, err := os.Stat(path)
+ if err != nil {
+ b.Fatal(err)
+ }
+ b.SetBytes(info.Size())
+ b.ReportAllocs()
+
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ r, err := os.Open("testdata/sample-wrapped-v2.car")
+ if err != nil {
+ b.Fatal(err)
+ }
+ br, err := carv2.NewBlockReader(r)
+ if err != nil {
+ b.Fatal(err)
+ }
+ for {
+ _, err := br.Next()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+
+ if err := r.Close(); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}
+
+// BenchmarkExtractV1File extracts inner CARv1 payload from a sample CARv2 file using ExtractV1File.
+func BenchmarkExtractV1File(b *testing.B) {
+ path := filepath.Join(b.TempDir(), "bench-large-v2.car")
+ generateRandomCarV2File(b, path, 10<<20) // 10 MiB
+ defer os.Remove(path)
+
+ info, err := os.Stat(path)
+ if err != nil {
+ b.Fatal(err)
+ }
+ b.SetBytes(info.Size())
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ b.RunParallel(func(pb *testing.PB) {
+ dstPath := filepath.Join(b.TempDir(), "destination.car")
+ for pb.Next() {
+ err = carv2.ExtractV1File(path, dstPath)
+ if err != nil {
+ b.Fatal(err)
+ }
+ _ = os.Remove(dstPath)
+ }
+ })
+}
+
+// BenchmarkExtractV1UsingReader extracts inner CARv1 payload from a sample CARv2 file using Reader
+// API. This benchmark is implemented to be used as a comparison in conjunction with
+// BenchmarkExtractV1File.
+func BenchmarkExtractV1UsingReader(b *testing.B) {
+ path := filepath.Join(b.TempDir(), "bench-large-v2.car")
+ generateRandomCarV2File(b, path, 10<<20) // 10 MiB
+ defer os.Remove(path)
+
+ info, err := os.Stat(path)
+ if err != nil {
+ b.Fatal(err)
+ }
+ b.SetBytes(info.Size())
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ b.RunParallel(func(pb *testing.PB) {
+ dstPath := filepath.Join(b.TempDir(), "destination.car")
+ for pb.Next() {
+ dst, err := os.Create(dstPath)
+ if err != nil {
+ b.Fatal(err)
+ }
+ reader, err := carv2.OpenReader(path)
+ if err != nil {
+ b.Fatal(err)
+ }
+ dr, err := reader.DataReader()
+ if err != nil {
+ b.Fatal(err)
+ }
+ _, err = io.Copy(dst, dr)
+ if err != nil {
+ b.Fatal(err)
+ }
+ if err := dst.Close(); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}
+
+// BenchmarkReader_InspectWithBlockValidation benchmarks Reader.Inspect with block hash validation
+// for a randomly generated CARv2 file of size 10 MiB.
+func BenchmarkReader_InspectWithBlockValidation(b *testing.B) {
+ path := filepath.Join(b.TempDir(), "bench-large-v2.car")
+ generateRandomCarV2File(b, path, 10<<20) // 10 MiB
+ defer os.Remove(path)
+
+ info, err := os.Stat(path)
+ if err != nil {
+ b.Fatal(err)
+ }
+ b.SetBytes(info.Size())
+ b.ReportAllocs()
+ b.ResetTimer()
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ benchmarkInspect(b, path, true)
+ }
+ })
+}
+
+// BenchmarkReader_InspectWithoutBlockValidation benchmarks Reader.Inspect without block hash
+// validation for a randomly generated CARv2 file of size 10 MiB.
+func BenchmarkReader_InspectWithoutBlockValidation(b *testing.B) {
+ path := filepath.Join(b.TempDir(), "bench-large-v2.car")
+ generateRandomCarV2File(b, path, 10<<20) // 10 MiB
+ defer os.Remove(path)
+
+ info, err := os.Stat(path)
+ if err != nil {
+ b.Fatal(err)
+ }
+ b.SetBytes(info.Size())
+ b.ReportAllocs()
+ b.ResetTimer()
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ benchmarkInspect(b, path, false)
+ }
+ })
+}
+
+func benchmarkInspect(b *testing.B, path string, validateBlockHash bool) {
+ reader, err := carv2.OpenReader(path)
+ if err != nil {
+ b.Fatal(err)
+ }
+ if _, err := reader.Inspect(validateBlockHash); err != nil {
+ b.Fatal(err)
+ }
+}
+func generateRandomCarV2File(b *testing.B, path string, minTotalBlockSize int) {
+ // Use fixed RNG for determinism across benchmarks.
+ rng := rand.New(rand.NewSource(1413))
+ bs, err := blockstore.OpenReadWrite(path, []cid.Cid{})
+ defer func() {
+ if err := bs.Finalize(); err != nil {
+ b.Fatal(err)
+ }
+ }()
+ if err != nil {
+ b.Fatal(err)
+ }
+ buf := make([]byte, 32<<10) // 32 KiB
+ var totalBlockSize int
+ for totalBlockSize < minTotalBlockSize {
+ size, err := rng.Read(buf)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ blk := merkledag.NewRawNode(buf)
+ if err := bs.Put(context.TODO(), blk); err != nil {
+ b.Fatal(err)
+ }
+ totalBlockSize += size
+ }
+}
diff --git a/ipld/car/v2/block_reader.go b/ipld/car/v2/block_reader.go
new file mode 100644
index 0000000000..367df743ff
--- /dev/null
+++ b/ipld/car/v2/block_reader.go
@@ -0,0 +1,221 @@
+package car
+
+import (
+ "fmt"
+ "io"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1/util"
+ internalio "github.com/ipfs/boxo/ipld/car/v2/internal/io"
+ "github.com/ipfs/go-cid"
+ "github.com/multiformats/go-varint"
+)
+
+// BlockReader facilitates iteration over CAR blocks for both CARv1 and CARv2.
+// See NewBlockReader
+type BlockReader struct {
+ // The detected version of the CAR payload.
+ Version uint64
+ // The roots of the CAR payload. May be empty.
+ Roots []cid.Cid
+
+ // Used internally only, by BlockReader.Next during iteration over blocks.
+ r io.Reader
+ offset uint64
+ readerSize int64
+ opts Options
+}
+
+// NewBlockReader instantiates a new BlockReader facilitating iteration over blocks in CARv1 or
+// CARv2 payload. Upon instantiation, the version is automatically detected and exposed via
+// BlockReader.Version. The root CIDs of the CAR payload are exposed via BlockReader.Roots
+//
+// See BlockReader.Next
+func NewBlockReader(r io.Reader, opts ...Option) (*BlockReader, error) {
+ options := ApplyOptions(opts...)
+
+ // Read CARv1 header or CARv2 pragma.
+ // Both are a valid CARv1 header, therefore are read as such.
+ pragmaOrV1Header, err := carv1.ReadHeader(r, options.MaxAllowedHeaderSize)
+ if err != nil {
+ return nil, err
+ }
+
+ // Populate the block reader version and options.
+ br := &BlockReader{
+ Version: pragmaOrV1Header.Version,
+ opts: options,
+ }
+
+ // Expect either version 1 or 2.
+ switch br.Version {
+ case 1:
+ // If version is 1, r represents a CARv1.
+ // Simply populate br.Roots and br.r without modifying r.
+ br.Roots = pragmaOrV1Header.Roots
+ br.r = r
+ br.readerSize = -1
+ br.offset, _ = carv1.HeaderSize(pragmaOrV1Header)
+ case 2:
+ // If the version is 2:
+ // 1. Read CARv2 specific header to locate the inner CARv1 data payload offset and size.
+ // 2. Skip to the beginning of the inner CARv1 data payload.
+ // 3. Re-initialize r as a LimitReader, limited to the size of the inner CARv1 payload.
+ // 4. Read the header of inner CARv1 data payload via r to populate br.Roots.
+
+ // Read CARv2-specific header.
+ v2h := Header{}
+ if _, err := v2h.ReadFrom(r); err != nil {
+ return nil, err
+ }
+
+ // Skip to the beginning of inner CARv1 data payload.
+ // Note, at this point the pragma and CARv1 header have been read.
+ // An io.ReadSeeker is opportunistically constructed from the given io.Reader r.
+ // The constructor does not take an initial offset, so we use Seek in io.SeekCurrent to
+ // fast forward to the beginning of data payload by subtracting pragma and header size from
+ // dataOffset.
+ rs := internalio.ToByteReadSeeker(r)
+ if _, err := rs.Seek(int64(v2h.DataOffset)-PragmaSize-HeaderSize, io.SeekCurrent); err != nil {
+ return nil, err
+ }
+ br.offset = uint64(v2h.DataOffset)
+ br.readerSize = int64(v2h.DataOffset + v2h.DataSize)
+
+ // Set br.r to a LimitReader reading from r limited to dataSize.
+ br.r = io.LimitReader(r, int64(v2h.DataSize))
+
+ // Populate br.Roots by reading the inner CARv1 data payload header.
+ header, err := carv1.ReadHeader(br.r, options.MaxAllowedHeaderSize)
+ if err != nil {
+ return nil, err
+ }
+ // Assert that the data payload header is exactly 1, i.e. the header represents a CARv1.
+ if header.Version != 1 {
+ return nil, fmt.Errorf("invalid data payload header version; expected 1, got %v", header.Version)
+ }
+ br.Roots = header.Roots
+ default:
+ // Otherwise, error out with invalid version since only versions 1 or 2 are expected.
+ return nil, fmt.Errorf("invalid car version: %d", br.Version)
+ }
+ return br, nil
+}
+
+// Next iterates over blocks in the underlying CAR payload with an io.EOF error indicating the end
+// is reached. Note, this function is forward-only; once the end has been reached it will always
+// return io.EOF.
+//
+// When the payload represents a CARv1 the BlockReader.Next simply iterates over blocks until it
+// reaches the end of the underlying io.Reader stream.
+//
+// As for CARv2 payload, the underlying io.Reader is read only up to the end of the last block.
+// Note, in a case where ZeroLengthSectionAsEOF Option is enabled, io.EOF is returned
+// immediately upon encountering a zero-length section without reading any further bytes from the
+// underlying io.Reader.
+func (br *BlockReader) Next() (blocks.Block, error) {
+ c, data, err := util.ReadNode(br.r, br.opts.ZeroLengthSectionAsEOF, br.opts.MaxAllowedSectionSize)
+ if err != nil {
+ return nil, err
+ }
+
+ if !br.opts.TrustedCAR {
+ hashed, err := c.Prefix().Sum(data)
+ if err != nil {
+ return nil, err
+ }
+
+ if !hashed.Equals(c) {
+ return nil, fmt.Errorf("mismatch in content integrity, expected: %s, got: %s", c, hashed)
+ }
+ }
+
+ ss := uint64(c.ByteLen()) + uint64(len(data))
+ br.offset += uint64(varint.UvarintSize(ss)) + ss
+ return blocks.NewBlockWithCid(data, c)
+}
+
+type BlockMetadata struct {
+ cid.Cid
+ Offset uint64
+ Size uint64
+}
+
+// SkipNext jumps over the next block, returning metadata about what it is (the CID, offset, and size).
+// Like Next it will return an io.EOF once it has reached the end.
+//
+// If the underlying reader used by the BlockReader is actually a ReadSeeker, this method will attempt to
+// seek over the underlying data rather than reading it into memory.
+func (br *BlockReader) SkipNext() (*BlockMetadata, error) {
+ sctSize, err := util.LdReadSize(br.r, br.opts.ZeroLengthSectionAsEOF, br.opts.MaxAllowedSectionSize)
+ if err != nil {
+ return nil, err
+ }
+
+ if sctSize == 0 {
+ _, _, err := cid.CidFromBytes([]byte{})
+ return nil, err
+ }
+
+ cidSize, c, err := cid.CidFromReader(io.LimitReader(br.r, int64(sctSize)))
+ if err != nil {
+ return nil, err
+ }
+
+ blkSize := sctSize - uint64(cidSize)
+ if brs, ok := br.r.(io.ReadSeeker); ok {
+ // carv1 and we don't know the size, so work it out and cache it
+ if br.readerSize == -1 {
+ cur, err := brs.Seek(0, io.SeekCurrent)
+ if err != nil {
+ return nil, err
+ }
+ end, err := brs.Seek(0, io.SeekEnd)
+ if err != nil {
+ return nil, err
+ }
+ br.readerSize = end
+ if _, err = brs.Seek(cur, io.SeekStart); err != nil {
+ return nil, err
+ }
+ }
+ // seek.
+ finalOffset, err := brs.Seek(int64(blkSize), io.SeekCurrent)
+ if err != nil {
+ return nil, err
+ }
+ if finalOffset != int64(br.offset)+int64(sctSize)+int64(varint.UvarintSize(sctSize)) {
+ return nil, fmt.Errorf("unexpected length")
+ }
+ if finalOffset > br.readerSize {
+ return nil, io.ErrUnexpectedEOF
+ }
+ br.offset = uint64(finalOffset)
+ return &BlockMetadata{
+ c,
+ uint64(finalOffset) - sctSize - uint64(varint.UvarintSize(sctSize)),
+ blkSize,
+ }, nil
+ }
+
+ // read to end.
+ readCnt, err := io.CopyN(io.Discard, br.r, int64(blkSize))
+ if err != nil {
+ if err == io.EOF {
+ return nil, io.ErrUnexpectedEOF
+ }
+ return nil, err
+ }
+ if readCnt != int64(blkSize) {
+ return nil, fmt.Errorf("unexpected length")
+ }
+ origOffset := br.offset
+ br.offset += uint64(varint.UvarintSize(sctSize)) + sctSize
+
+ return &BlockMetadata{
+ c,
+ origOffset,
+ blkSize,
+ }, nil
+}
diff --git a/ipld/car/v2/block_reader_test.go b/ipld/car/v2/block_reader_test.go
new file mode 100644
index 0000000000..8a65d0c72a
--- /dev/null
+++ b/ipld/car/v2/block_reader_test.go
@@ -0,0 +1,237 @@
+package car_test
+
+import (
+ "bytes"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "os"
+ "testing"
+
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+ "github.com/ipfs/go-cid"
+ mh "github.com/multiformats/go-multihash"
+ "github.com/multiformats/go-varint"
+ "github.com/stretchr/testify/require"
+)
+
+func TestBlockReaderFailsOnUnknownVersion(t *testing.T) {
+ r := requireReaderFromPath(t, "testdata/sample-rootless-v42.car")
+ _, err := carv2.NewBlockReader(r)
+ require.EqualError(t, err, "invalid car version: 42")
+}
+
+func TestBlockReaderFailsOnCorruptPragma(t *testing.T) {
+ r := requireReaderFromPath(t, "testdata/sample-corrupt-pragma.car")
+ _, err := carv2.NewBlockReader(r)
+ require.EqualError(t, err, "unexpected EOF")
+}
+
+func TestBlockReader_WithCarV1Consistency(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ zerLenAsEOF bool
+ wantVersion uint64
+ }{
+ {
+ name: "CarV1WithoutZeroLengthSection",
+ path: "testdata/sample-v1.car",
+ wantVersion: 1,
+ },
+ {
+ name: "CarV1WithZeroLenSection",
+ path: "testdata/sample-v1-with-zero-len-section.car",
+ zerLenAsEOF: true,
+ wantVersion: 1,
+ },
+ {
+ name: "AnotherCarV1WithZeroLenSection",
+ path: "testdata/sample-v1-with-zero-len-section2.car",
+ zerLenAsEOF: true,
+ wantVersion: 1,
+ },
+ {
+ name: "CarV1WithZeroLenSectionWithoutOption",
+ path: "testdata/sample-v1-with-zero-len-section.car",
+ wantVersion: 1,
+ },
+ {
+ name: "AnotherCarV1WithZeroLenSectionWithoutOption",
+ path: "testdata/sample-v1-with-zero-len-section2.car",
+ wantVersion: 1,
+ },
+ {
+ name: "CorruptCarV1",
+ path: "testdata/sample-v1-tailing-corrupt-section.car",
+ wantVersion: 1,
+ },
+ {
+ name: "CarV2WrappingV1",
+ path: "testdata/sample-wrapped-v2.car",
+ wantVersion: 2,
+ },
+ {
+ name: "CarV2ProducedByBlockstore",
+ path: "testdata/sample-rw-bs-v2.car",
+ wantVersion: 2,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := requireReaderFromPath(t, tt.path)
+ subject, err := carv2.NewBlockReader(r, carv2.ZeroLengthSectionAsEOF(tt.zerLenAsEOF))
+ require.NoError(t, err)
+
+ require.Equal(t, tt.wantVersion, subject.Version)
+
+ var wantReader *carv1.CarReader
+ switch tt.wantVersion {
+ case 1:
+ wantReader = requireNewCarV1ReaderFromV1File(t, tt.path, tt.zerLenAsEOF)
+ case 2:
+ wantReader = requireNewCarV1ReaderFromV2File(t, tt.path, tt.zerLenAsEOF)
+ default:
+ require.Failf(t, "invalid test-case", "unknown wantVersion %v", tt.wantVersion)
+ }
+ require.Equal(t, wantReader.Header.Roots, subject.Roots)
+
+ for {
+ gotBlock, gotErr := subject.Next()
+ wantBlock, wantErr := wantReader.Next()
+ require.Equal(t, wantBlock, gotBlock)
+ require.Equal(t, wantErr, gotErr)
+ if gotErr == io.EOF {
+ break
+ }
+ }
+ })
+ t.Run(tt.name+"-skipping-reads", func(t *testing.T) {
+ r := requireReaderFromPath(t, tt.path)
+ subject, err := carv2.NewBlockReader(r, carv2.ZeroLengthSectionAsEOF(tt.zerLenAsEOF))
+ require.NoError(t, err)
+
+ require.Equal(t, tt.wantVersion, subject.Version)
+
+ var wantReader *carv1.CarReader
+ switch tt.wantVersion {
+ case 1:
+ wantReader = requireNewCarV1ReaderFromV1File(t, tt.path, tt.zerLenAsEOF)
+ case 2:
+ wantReader = requireNewCarV1ReaderFromV2File(t, tt.path, tt.zerLenAsEOF)
+ default:
+ require.Failf(t, "invalid test-case", "unknown wantVersion %v", tt.wantVersion)
+ }
+ require.Equal(t, wantReader.Header.Roots, subject.Roots)
+
+ for {
+ gotBlock, gotErr := subject.SkipNext()
+ wantBlock, wantErr := wantReader.Next()
+ if wantErr != nil && gotErr == nil {
+ fmt.Printf("want was %+v\n", wantReader)
+ fmt.Printf("want was err, got was %+v / %d\n", gotBlock, gotBlock.Size)
+ }
+ require.Equal(t, wantErr, gotErr)
+ if gotErr == io.EOF {
+ break
+ }
+ if gotErr == nil {
+ require.Equal(t, wantBlock.Cid(), gotBlock.Cid)
+ require.Equal(t, uint64(len(wantBlock.RawData())), gotBlock.Size)
+ }
+ }
+ })
+ }
+}
+
+func TestMaxSectionLength(t *testing.T) {
+ // headerHex is the zero-roots CARv1 header
+ const headerHex = "11a265726f6f7473806776657273696f6e01"
+ headerBytes, _ := hex.DecodeString(headerHex)
+ // 8 MiB block of zeros
+ block := make([]byte, 8<<20)
+ // CID for that block
+ pfx := cid.NewPrefixV1(cid.Raw, mh.SHA2_256)
+ cid, err := pfx.Sum(block)
+ require.NoError(t, err)
+
+ // construct CAR
+ var buf bytes.Buffer
+ buf.Write(headerBytes)
+ buf.Write(varint.ToUvarint(uint64(len(cid.Bytes()) + len(block))))
+ buf.Write(cid.Bytes())
+ buf.Write(block)
+
+ // try to read it
+ car, err := carv2.NewBlockReader(bytes.NewReader(buf.Bytes()))
+ require.NoError(t, err)
+ // error should occur on first section read
+ _, err = car.Next()
+ require.EqualError(t, err, "invalid section data, length of read beyond allowable maximum")
+
+ // successful read by expanding the max section size
+ car, err = carv2.NewBlockReader(bytes.NewReader(buf.Bytes()), carv2.MaxAllowedSectionSize((8<<20)+40))
+ require.NoError(t, err)
+ // can now read block and get our 8 MiB zeroed byte array
+ readBlock, err := car.Next()
+ require.NoError(t, err)
+ require.True(t, bytes.Equal(block, readBlock.RawData()))
+}
+
+func TestTrustedCAR(t *testing.T) {
+ // headerHex is the zero-roots CARv1 header
+ const headerHex = "11a265726f6f7473806776657273696f6e01"
+ headerBytes, _ := hex.DecodeString(headerHex)
+ // block of zeros
+ block := make([]byte, 5)
+ // CID for that block
+ pfx := cid.NewPrefixV1(cid.Raw, mh.SHA2_256)
+ cid, err := pfx.Sum(block)
+ require.NoError(t, err)
+
+ // Modify the block so it won't match CID anymore
+ block[2] = 0xFF
+ // construct CAR
+ var buf bytes.Buffer
+ buf.Write(headerBytes)
+ buf.Write(varint.ToUvarint(uint64(len(cid.Bytes()) + len(block))))
+ buf.Write(cid.Bytes())
+ buf.Write(block)
+
+ // try to read it as trusted
+ car, err := carv2.NewBlockReader(bytes.NewReader(buf.Bytes()), carv2.WithTrustedCAR(true))
+ require.NoError(t, err)
+ _, err = car.Next()
+ require.NoError(t, err)
+
+ // Try to read it as untrusted - should fail
+ car, err = carv2.NewBlockReader(bytes.NewReader(buf.Bytes()), carv2.WithTrustedCAR(false))
+ require.NoError(t, err)
+ // error should occur on first section read
+ _, err = car.Next()
+ require.EqualError(t, err, "mismatch in content integrity, expected: bafkreieikviivlpbn3cxhuq6njef37ikoysaqxa2cs26zxleqxpay2bzuq, got: bafkreidgklrppelx4fxcsna7cxvo3g7ayedfojkqeuus6kz6e4hy7gukmy")
+}
+
+func TestMaxHeaderLength(t *testing.T) {
+ // headerHex is the is a 5 root CARv1 header
+ const headerHex = "de01a265726f6f747385d82a58250001711220785197229dc8bb1152945da58e2348f7e279eeded06cc2ca736d0e879858b501d82a58250001711220785197229dc8bb1152945da58e2348f7e279eeded06cc2ca736d0e879858b501d82a58250001711220785197229dc8bb1152945da58e2348f7e279eeded06cc2ca736d0e879858b501d82a58250001711220785197229dc8bb1152945da58e2348f7e279eeded06cc2ca736d0e879858b501d82a58250001711220785197229dc8bb1152945da58e2348f7e279eeded06cc2ca736d0e879858b5016776657273696f6e01"
+ headerBytes, _ := hex.DecodeString(headerHex)
+ c, _ := cid.Decode("bafyreidykglsfhoixmivffc5uwhcgshx4j465xwqntbmu43nb2dzqwfvae")
+
+ // successful read
+ car, err := carv2.NewBlockReader(bytes.NewReader(headerBytes))
+ require.NoError(t, err)
+ require.ElementsMatch(t, []cid.Cid{c, c, c, c, c}, car.Roots)
+
+ // unsuccessful read, low allowable max header length (length - 3 because there are 2 bytes in the length varint prefix)
+ _, err = carv2.NewBlockReader(bytes.NewReader(headerBytes), carv2.MaxAllowedHeaderSize(uint64(len(headerBytes)-3)))
+ require.EqualError(t, err, "invalid header data, length of read beyond allowable maximum")
+}
+
+func requireReaderFromPath(t *testing.T, path string) io.Reader {
+ f, err := os.Open(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, f.Close()) })
+ return f
+}
diff --git a/ipld/car/v2/blockstore/bench_test.go b/ipld/car/v2/blockstore/bench_test.go
new file mode 100644
index 0000000000..8c83b4f19e
--- /dev/null
+++ b/ipld/car/v2/blockstore/bench_test.go
@@ -0,0 +1,80 @@
+package blockstore_test
+
+import (
+ "context"
+ "io"
+ mathrand "math/rand"
+ "os"
+ "testing"
+
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/blockstore"
+ "github.com/ipfs/go-cid"
+)
+
+// BenchmarkOpenReadOnlyV1 opens a read-only blockstore,
+// and retrieves all blocks in a shuffled order.
+// Note that this benchmark includes generating an index,
+// since the input file is a CARv1.
+func BenchmarkOpenReadOnlyV1(b *testing.B) {
+ path := "../testdata/sample-v1.car"
+ f, err := os.Open("../testdata/sample-v1.car")
+ if err != nil {
+ b.Fatal(err)
+ }
+ defer func() {
+ if err := f.Close(); err != nil {
+ b.Fatal(err)
+ }
+ }()
+ info, err := os.Stat(path)
+ if err != nil {
+ b.Fatal(err)
+ }
+ b.SetBytes(info.Size())
+ b.ReportAllocs()
+
+ var shuffledCIDs []cid.Cid
+ br, err := carv2.NewBlockReader(f)
+ if err != nil {
+ b.Fatal(err)
+ }
+ for {
+ block, err := br.Next()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ b.Fatal(err)
+ }
+ shuffledCIDs = append(shuffledCIDs, block.Cid())
+ }
+
+ // The shuffling needs to be deterministic,
+ // for the sake of stable benchmark results.
+ // Any source number works as long as it's fixed.
+ rnd := mathrand.New(mathrand.NewSource(123456))
+ rnd.Shuffle(len(shuffledCIDs), func(i, j int) {
+ shuffledCIDs[i], shuffledCIDs[j] = shuffledCIDs[j], shuffledCIDs[i]
+ })
+
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ bs, err := blockstore.OpenReadOnly(path)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ for _, c := range shuffledCIDs {
+ _, err := bs.Get(context.TODO(), c)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+
+ if err := bs.Close(); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}
diff --git a/ipld/car/v2/blockstore/doc.go b/ipld/car/v2/blockstore/doc.go
new file mode 100644
index 0000000000..4aa4cfdfc9
--- /dev/null
+++ b/ipld/car/v2/blockstore/doc.go
@@ -0,0 +1,28 @@
+// Package blockstore implements the IPFS blockstore interface backed by a CAR file.
+// This package provides two flavours of blockstore: ReadOnly and ReadWrite.
+//
+// The ReadOnly blockstore provides a read-only random access from a given data payload either in
+// unindexed CARv1 format or indexed/unindexed v2 format:
+// - ReadOnly.NewReadOnly can be used to instantiate a new read-only blockstore for a given CARv1
+// or CARv2 data payload with an optional index override.
+// - ReadOnly.OpenReadOnly can be used to instantiate a new read-only blockstore for a given CARv1
+// or CARv2 file with automatic index generation if the index is not present.
+//
+// The ReadWrite blockstore allows writing and reading of the blocks concurrently. The user of this
+// blockstore is responsible for calling ReadWrite.Finalize when finished writing blocks.
+// Upon finalization, the instance can no longer be used for reading or writing blocks and will
+// error if used. To continue reading the blocks users are encouraged to use ReadOnly blockstore
+// instantiated from the same file path using OpenReadOnly.
+// A user may resume reading/writing from files produced by an instance of ReadWrite blockstore. The
+// resumption is attempted automatically, if the path passed to OpenReadWrite exists.
+//
+// Note that the blockstore implementations in this package behave similarly to IPFS IdStore wrapper
+// when given CIDs with multihash.IDENTITY code.
+// More specifically, for CIDs with multhash.IDENTITY code:
+// * blockstore.Has will always return true.
+// * blockstore.Get will always succeed, returning the multihash digest of the given CID.
+// * blockstore.GetSize will always succeed, returning the multihash digest length of the given CID.
+// * blockstore.Put and blockstore.PutMany will always succeed without performing any operation unless car.StoreIdentityCIDs is enabled.
+//
+// See: https://pkg.go.dev/github.com/ipfs/go-ipfs-blockstore#NewIdStore
+package blockstore
diff --git a/ipld/car/v2/blockstore/example_test.go b/ipld/car/v2/blockstore/example_test.go
new file mode 100644
index 0000000000..46f1781265
--- /dev/null
+++ b/ipld/car/v2/blockstore/example_test.go
@@ -0,0 +1,159 @@
+package blockstore_test
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/blockstore"
+ "github.com/ipfs/boxo/ipld/merkledag"
+ "github.com/ipfs/go-cid"
+)
+
+const cidPrintCount = 5
+
+// ExampleOpenReadOnly opens a read-only blockstore from a CARv1 file, and prints its root CIDs
+// along with CID mapping to raw data size of blocks for first five sections in the CAR file.
+func ExampleOpenReadOnly() {
+ // Open a new ReadOnly blockstore from a CARv1 file.
+ // Note, `OpenReadOnly` accepts bot CARv1 and CARv2 formats and transparently generate index
+ // in the background if necessary.
+ // This instance sets ZeroLengthSectionAsEOF option to treat zero sized sections in file as EOF.
+ robs, err := blockstore.OpenReadOnly("../testdata/sample-v1.car",
+ blockstore.UseWholeCIDs(true),
+ carv2.ZeroLengthSectionAsEOF(true),
+ )
+ if err != nil {
+ panic(err)
+ }
+ defer robs.Close()
+
+ // Print root CIDs.
+ roots, err := robs.Roots()
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("Contains %v root CID(s):\n", len(roots))
+ for _, r := range roots {
+ fmt.Printf("\t%v\n", r)
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ // Print the raw data size for the first 5 CIDs in the CAR file.
+ keysChan, err := robs.AllKeysChan(ctx)
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("List of first %v CIDs and their raw data size:\n", cidPrintCount)
+ i := 1
+ for k := range keysChan {
+ if i > cidPrintCount {
+ cancel()
+ break
+ }
+ size, err := robs.GetSize(context.TODO(), k)
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("\t%v -> %v bytes\n", k, size)
+ i++
+ }
+
+ // Output:
+ // Contains 1 root CID(s):
+ // bafy2bzaced4ueelaegfs5fqu4tzsh6ywbbpfk3cxppupmxfdhbpbhzawfw5oy
+ // List of first 5 CIDs and their raw data size:
+ // bafy2bzaced4ueelaegfs5fqu4tzsh6ywbbpfk3cxppupmxfdhbpbhzawfw5oy -> 821 bytes
+ // bafy2bzaceaycv7jhaegckatnncu5yugzkrnzeqsppzegufr35lroxxnsnpspu -> 1053 bytes
+ // bafy2bzaceb62wdepofqu34afqhbcn4a7jziwblt2ih5hhqqm6zitd3qpzhdp4 -> 1094 bytes
+ // bafy2bzaceb3utcspm5jqcdqpih3ztbaztv7yunzkiyfq7up7xmokpxemwgu5u -> 1051 bytes
+ // bafy2bzacedjwekyjresrwjqj4n2r5bnuuu3klncgjo2r3slsp6wgqb37sz4ck -> 821 bytes
+}
+
+// ExampleOpenReadWrite creates a read-write blockstore and puts
+func ExampleOpenReadWrite() {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ thisBlock := merkledag.NewRawNode([]byte("fish")).Block
+ thatBlock := merkledag.NewRawNode([]byte("lobster")).Block
+ andTheOtherBlock := merkledag.NewRawNode([]byte("barreleye")).Block
+
+ tdir, err := os.MkdirTemp(os.TempDir(), "example-*")
+ if err != nil {
+ panic(err)
+ }
+ dst := filepath.Join(tdir, "sample-rw-bs-v2.car")
+ roots := []cid.Cid{thisBlock.Cid(), thatBlock.Cid(), andTheOtherBlock.Cid()}
+
+ rwbs, err := blockstore.OpenReadWrite(dst, roots, carv2.UseDataPadding(1413), carv2.UseIndexPadding(42))
+ if err != nil {
+ panic(err)
+ }
+
+ // Put all blocks onto the blockstore.
+ blocks := []blocks.Block{thisBlock, thatBlock}
+ if err := rwbs.PutMany(ctx, blocks); err != nil {
+ panic(err)
+ }
+ fmt.Printf("Successfully wrote %v blocks into the blockstore.\n", len(blocks))
+
+ // Any blocks put can be read back using the same blockstore instance.
+ block, err := rwbs.Get(ctx, thatBlock.Cid())
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("Read back block just put with raw value of `%v`.\n", string(block.RawData()))
+
+ // Finalize the blockstore to flush out the index and make a complete CARv2.
+ if err := rwbs.Finalize(); err != nil {
+ panic(err)
+ }
+
+ // Resume from the same file to add more blocks.
+ // Note the UseDataPadding and roots must match the values passed to the blockstore instance
+ // that created the original file. Otherwise, we cannot resume from the same file.
+ resumedRwbos, err := blockstore.OpenReadWrite(dst, roots, carv2.UseDataPadding(1413))
+ if err != nil {
+ panic(err)
+ }
+
+ // Put another block, appending it to the set of blocks that are written previously.
+ if err := resumedRwbos.Put(ctx, andTheOtherBlock); err != nil {
+ panic(err)
+ }
+
+ // Read back the the block put before resumption.
+ // Blocks previously put are present.
+ block, err = resumedRwbos.Get(ctx, thatBlock.Cid())
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("Resumed blockstore contains blocks put previously with raw value of `%v`.\n", string(block.RawData()))
+
+ // Put an additional block to the CAR.
+ // Blocks put after resumption are also present.
+ block, err = resumedRwbos.Get(ctx, andTheOtherBlock.Cid())
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("It also contains the block put after resumption with raw value of `%v`.\n", string(block.RawData()))
+
+ // Finalize the blockstore to flush out the index and make a complete CARv2.
+ // Note, Finalize must be called on an open ReadWrite blockstore to flush out a complete CARv2.
+ if err := resumedRwbos.Finalize(); err != nil {
+ panic(err)
+ }
+
+ // Output:
+ // Successfully wrote 2 blocks into the blockstore.
+ // Read back block just put with raw value of `lobster`.
+ // Resumed blockstore contains blocks put previously with raw value of `lobster`.
+ // It also contains the block put after resumption with raw value of `barreleye`.
+}
diff --git a/ipld/car/v2/blockstore/readonly.go b/ipld/car/v2/blockstore/readonly.go
new file mode 100644
index 0000000000..7c6d7f2ea2
--- /dev/null
+++ b/ipld/car/v2/blockstore/readonly.go
@@ -0,0 +1,474 @@
+package blockstore
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "sync"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ blockstore "github.com/ipfs/boxo/blockstore"
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+ internalio "github.com/ipfs/boxo/ipld/car/v2/internal/io"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/store"
+ "github.com/ipfs/go-cid"
+ format "github.com/ipfs/go-ipld-format"
+ "github.com/multiformats/go-varint"
+ "golang.org/x/exp/mmap"
+)
+
+var _ blockstore.Blockstore = (*ReadOnly)(nil)
+
+var (
+ errZeroLengthSection = fmt.Errorf("zero-length carv2 section not allowed by default; see WithZeroLengthSectionAsEOF option")
+ errReadOnly = fmt.Errorf("called write method on a read-only carv2 blockstore")
+ errClosed = fmt.Errorf("cannot use a carv2 blockstore after closing")
+)
+
+// ReadOnly provides a read-only CAR Block Store.
+type ReadOnly struct {
+ // mu allows ReadWrite to be safe for concurrent use.
+ // It's in ReadOnly so that read operations also grab read locks,
+ // given that ReadWrite embeds ReadOnly for methods like Get and Has.
+ //
+ // The main fields guarded by the mutex are the index and the underlying writers.
+ // For simplicity, the entirety of the blockstore methods grab the mutex.
+ mu sync.RWMutex
+
+ // When true, the blockstore has been closed via Close, Discard, or
+ // Finalize, and must not be used. Any further blockstore method calls
+ // will return errClosed to avoid panics or broken behavior.
+ closed bool
+
+ // The backing containing the data payload in CARv1 format.
+ backing io.ReaderAt
+
+ // The CARv1 content index.
+ idx index.Index
+
+ // If we called carv2.NewReaderMmap, remember to close it too.
+ carv2Closer io.Closer
+
+ opts carv2.Options
+}
+
+type contextKey string
+
+const asyncErrHandlerKey contextKey = "asyncErrorHandlerKey"
+
+var UseWholeCIDs = carv2.UseWholeCIDs
+
+// NewReadOnly creates a new ReadOnly blockstore from the backing with a optional index as idx.
+// This function accepts both CARv1 and CARv2 backing.
+// The blockstore is instantiated with the given index if it is not nil.
+//
+// Otherwise:
+// * For a CARv1 backing an index is generated.
+// * For a CARv2 backing an index is only generated if Header.HasIndex returns false.
+//
+// There is no need to call ReadOnly.Close on instances returned by this function.
+func NewReadOnly(backing io.ReaderAt, idx index.Index, opts ...carv2.Option) (*ReadOnly, error) {
+ b := &ReadOnly{
+ opts: carv2.ApplyOptions(opts...),
+ }
+
+ version, err := readVersion(backing, opts...)
+ if err != nil {
+ return nil, err
+ }
+ switch version {
+ case 1:
+ if idx == nil {
+ if idx, err = generateIndex(backing, opts...); err != nil {
+ return nil, err
+ }
+ }
+ b.backing = backing
+ b.idx = idx
+ return b, nil
+ case 2:
+ v2r, err := carv2.NewReader(backing, opts...)
+ if err != nil {
+ return nil, err
+ }
+ if idx == nil {
+ if v2r.Header.HasIndex() {
+ ir, err := v2r.IndexReader()
+ if err != nil {
+ return nil, err
+ }
+ idx, err = index.ReadFrom(ir)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ dr, err := v2r.DataReader()
+ if err != nil {
+ return nil, err
+ }
+ if idx, err = generateIndex(dr, opts...); err != nil {
+ return nil, err
+ }
+ }
+ }
+ b.backing, err = v2r.DataReader()
+ if err != nil {
+ return nil, err
+ }
+ b.idx = idx
+ return b, nil
+ default:
+ return nil, fmt.Errorf("unsupported car version: %v", version)
+ }
+}
+
+func readVersion(at io.ReaderAt, opts ...carv2.Option) (uint64, error) {
+ var rr io.Reader
+ switch r := at.(type) {
+ case io.Reader:
+ rr = r
+ default:
+ var err error
+ rr, err = internalio.NewOffsetReadSeeker(r, 0)
+ if err != nil {
+ return 0, err
+ }
+ }
+ return carv2.ReadVersion(rr, opts...)
+}
+
+func generateIndex(at io.ReaderAt, opts ...carv2.Option) (index.Index, error) {
+ var rs io.ReadSeeker
+ switch r := at.(type) {
+ case io.ReadSeeker:
+ rs = r
+ // The version may have been read from the given io.ReaderAt; therefore move back to the begining.
+ if _, err := rs.Seek(0, io.SeekStart); err != nil {
+ return nil, err
+ }
+ default:
+ var err error
+ rs, err = internalio.NewOffsetReadSeeker(r, 0)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // Note, we do not set any write options so that all write options fall back onto defaults.
+ return carv2.GenerateIndex(rs, opts...)
+}
+
+// OpenReadOnly opens a read-only blockstore from a CAR file (either v1 or v2), generating an index if it does not exist.
+// Note, the generated index if the index does not exist is ephemeral and only stored in memory.
+// See car.GenerateIndex and Index.Attach for persisting index onto a CAR file.
+func OpenReadOnly(path string, opts ...carv2.Option) (*ReadOnly, error) {
+ f, err := mmap.Open(path)
+ if err != nil {
+ return nil, err
+ }
+
+ robs, err := NewReadOnly(f, nil, opts...)
+ if err != nil {
+ return nil, err
+ }
+ robs.carv2Closer = f
+
+ return robs, nil
+}
+
+// Index gives direct access to the index.
+// You should never add records on your own there.
+func (b *ReadOnly) Index() index.Index {
+ return b.idx
+}
+
+// DeleteBlock is unsupported and always errors.
+func (b *ReadOnly) DeleteBlock(_ context.Context, _ cid.Cid) error {
+ return errReadOnly
+}
+
+// Has indicates if the store contains a block that corresponds to the given key.
+// This function always returns true for any given key with multihash.IDENTITY
+// code unless the StoreIdentityCIDs option is on, in which case it will defer
+// to the index to check for the existence of the block; the index may or may
+// not contain identity CIDs included in this CAR, depending on whether
+// StoreIdentityCIDs was on when the index was created. If the CAR is a CARv1
+// and StoreIdentityCIDs is on, then the index will contain identity CIDs and
+// this will always return true.
+func (b *ReadOnly) Has(ctx context.Context, key cid.Cid) (bool, error) {
+ if !b.opts.StoreIdentityCIDs {
+ // If we don't store identity CIDs then we can return them straight away as if they are here,
+ // otherwise we need to check for their existence.
+ // Note, we do this without locking, since there is no shared information to lock for in order to perform the check.
+ if _, ok, err := store.IsIdentity(key); err != nil {
+ return false, err
+ } else if ok {
+ return true, nil
+ }
+ }
+
+ b.mu.RLock()
+ defer b.mu.RUnlock()
+
+ if b.closed {
+ return false, errClosed
+ }
+
+ _, _, size, err := store.FindCid(
+ b.backing,
+ b.idx,
+ key,
+ b.opts.BlockstoreUseWholeCIDs,
+ b.opts.ZeroLengthSectionAsEOF,
+ b.opts.MaxAllowedSectionSize,
+ false,
+ )
+ if errors.Is(err, index.ErrNotFound) {
+ return false, nil
+ } else if err != nil {
+ return false, err
+ }
+ return size > -1, nil
+}
+
+// Get gets a block corresponding to the given key.
+// This function always returns the block for any given key with
+// multihash.IDENTITY code unless the StoreIdentityCIDs option is on, in which
+// case it will defer to the index to check for the existence of the block; the
+// index may or may not contain identity CIDs included in this CAR, depending on
+// whether StoreIdentityCIDs was on when the index was created. If the CAR is a
+// CARv1 and StoreIdentityCIDs is on, then the index will contain identity CIDs
+// and this will always return true.
+func (b *ReadOnly) Get(ctx context.Context, key cid.Cid) (blocks.Block, error) {
+ if !b.opts.StoreIdentityCIDs {
+ // If we don't store identity CIDs then we can return them straight away as if they are here,
+ // otherwise we need to check for their existence.
+ // Note, we do this without locking, since there is no shared information to lock for in order to perform the check.
+ if digest, ok, err := store.IsIdentity(key); err != nil {
+ return nil, err
+ } else if ok {
+ return blocks.NewBlockWithCid(digest, key)
+ }
+ }
+
+ b.mu.RLock()
+ defer b.mu.RUnlock()
+
+ if b.closed {
+ return nil, errClosed
+ }
+
+ data, _, _, err := store.FindCid(
+ b.backing,
+ b.idx,
+ key,
+ b.opts.BlockstoreUseWholeCIDs,
+ b.opts.ZeroLengthSectionAsEOF,
+ b.opts.MaxAllowedSectionSize,
+ true,
+ )
+ if errors.Is(err, index.ErrNotFound) {
+ return nil, format.ErrNotFound{Cid: key}
+ } else if err != nil {
+ return nil, err
+ }
+ return blocks.NewBlockWithCid(data, key)
+}
+
+// GetSize gets the size of an item corresponding to the given key.
+func (b *ReadOnly) GetSize(ctx context.Context, key cid.Cid) (int, error) {
+ // Check if the given CID has multihash.IDENTITY code
+ // Note, we do this without locking, since there is no shared information to lock for in order to perform the check.
+ if digest, ok, err := store.IsIdentity(key); err != nil {
+ return 0, err
+ } else if ok {
+ return len(digest), nil
+ }
+
+ b.mu.RLock()
+ defer b.mu.RUnlock()
+
+ if b.closed {
+ return 0, errClosed
+ }
+
+ _, _, size, err := store.FindCid(
+ b.backing,
+ b.idx,
+ key,
+ b.opts.BlockstoreUseWholeCIDs,
+ b.opts.ZeroLengthSectionAsEOF,
+ b.opts.MaxAllowedSectionSize,
+ false,
+ )
+ if errors.Is(err, index.ErrNotFound) {
+ return -1, format.ErrNotFound{Cid: key}
+ } else if err != nil {
+ return -1, err
+ }
+ return size, nil
+}
+
+// Put is not supported and always returns an error.
+func (b *ReadOnly) Put(context.Context, blocks.Block) error {
+ return errReadOnly
+}
+
+// PutMany is not supported and always returns an error.
+func (b *ReadOnly) PutMany(context.Context, []blocks.Block) error {
+ return errReadOnly
+}
+
+// WithAsyncErrorHandler returns a context with async error handling set to the given errHandler.
+// Any errors that occur during asynchronous operations of AllKeysChan will be passed to the given
+// handler.
+func WithAsyncErrorHandler(ctx context.Context, errHandler func(error)) context.Context {
+ return context.WithValue(ctx, asyncErrHandlerKey, errHandler)
+}
+
+// AllKeysChan returns the list of keys in the CAR data payload.
+// If the ctx is constructed using WithAsyncErrorHandler any errors that occur during asynchronous
+// retrieval of CIDs will be passed to the error handler function set in context.
+// Otherwise, errors will terminate the asynchronous operation silently.
+//
+// See WithAsyncErrorHandler
+func (b *ReadOnly) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) {
+ // We release the lock when the channel-sending goroutine stops.
+ // Note that we can't use a deferred unlock here,
+ // because if we return a nil error,
+ // we only want to unlock once the async goroutine has stopped.
+ b.mu.RLock()
+
+ if b.closed {
+ b.mu.RUnlock() // don't hold the mutex forever
+ return nil, errClosed
+ }
+
+ // TODO we may use this walk for populating the index, and we need to be able to iterate keys in this way somewhere for index generation. In general though, when it's asked for all keys from a blockstore with an index, we should iterate through the index when possible rather than linear reads through the full car.
+ rdr, err := internalio.NewOffsetReadSeeker(b.backing, 0)
+ if err != nil {
+ return nil, err
+ }
+ header, err := carv1.ReadHeader(rdr, b.opts.MaxAllowedHeaderSize)
+ if err != nil {
+ b.mu.RUnlock() // don't hold the mutex forever
+ return nil, fmt.Errorf("error reading car header: %w", err)
+ }
+ headerSize, err := carv1.HeaderSize(header)
+ if err != nil {
+ b.mu.RUnlock() // don't hold the mutex forever
+ return nil, err
+ }
+
+ // TODO: document this choice of 5, or use simpler buffering like 0 or 1.
+ ch := make(chan cid.Cid, 5)
+
+ // Seek to the end of header.
+ if _, err = rdr.Seek(int64(headerSize), io.SeekStart); err != nil {
+ b.mu.RUnlock() // don't hold the mutex forever
+ return nil, err
+ }
+
+ go func() {
+ defer b.mu.RUnlock()
+ defer close(ch)
+
+ for {
+ length, err := varint.ReadUvarint(rdr)
+ if err != nil {
+ if err != io.EOF {
+ maybeReportError(ctx, err)
+ }
+ return
+ }
+
+ // Null padding; by default it's an error.
+ if length == 0 {
+ if b.opts.ZeroLengthSectionAsEOF {
+ break
+ } else {
+ maybeReportError(ctx, errZeroLengthSection)
+ return
+ }
+ }
+
+ thisItemForNxt, err := rdr.Seek(0, io.SeekCurrent)
+ if err != nil {
+ maybeReportError(ctx, err)
+ return
+ }
+ _, c, err := cid.CidFromReader(rdr)
+ if err != nil {
+ maybeReportError(ctx, err)
+ return
+ }
+ if _, err := rdr.Seek(thisItemForNxt+int64(length), io.SeekStart); err != nil {
+ maybeReportError(ctx, err)
+ return
+ }
+
+ // If we're just using multihashes, flatten to the "raw" codec.
+ if !b.opts.BlockstoreUseWholeCIDs {
+ c = cid.NewCidV1(cid.Raw, c.Hash())
+ }
+
+ select {
+ case ch <- c:
+ case <-ctx.Done():
+ maybeReportError(ctx, ctx.Err())
+ return
+ }
+ }
+ }()
+ return ch, nil
+}
+
+// maybeReportError checks if an error handler is present in context associated to the key
+// asyncErrHandlerKey, and if preset it will pass the error to it.
+func maybeReportError(ctx context.Context, err error) {
+ value := ctx.Value(asyncErrHandlerKey)
+ if eh, _ := value.(func(error)); eh != nil {
+ eh(err)
+ }
+}
+
+// HashOnRead is currently unimplemented; hashing on reads never happens.
+func (b *ReadOnly) HashOnRead(bool) {
+ // TODO: implement before the final release?
+}
+
+// Roots returns the root CIDs of the backing CAR.
+func (b *ReadOnly) Roots() ([]cid.Cid, error) {
+ ors, err := internalio.NewOffsetReadSeeker(b.backing, 0)
+ if err != nil {
+ return nil, err
+ }
+ header, err := carv1.ReadHeader(ors, b.opts.MaxAllowedHeaderSize)
+ if err != nil {
+ return nil, fmt.Errorf("error reading car header: %w", err)
+ }
+ return header.Roots, nil
+}
+
+// Close closes the underlying reader if it was opened by OpenReadOnly.
+// After this call, the blockstore can no longer be used.
+//
+// Note that this call may block if any blockstore operations are currently in
+// progress, including an AllKeysChan that hasn't been fully consumed or cancelled.
+func (b *ReadOnly) Close() error {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ return b.closeWithoutMutex()
+}
+
+func (b *ReadOnly) closeWithoutMutex() error {
+ b.closed = true
+ if b.carv2Closer != nil {
+ return b.carv2Closer.Close()
+ }
+ return nil
+}
diff --git a/ipld/car/v2/blockstore/readonly_test.go b/ipld/car/v2/blockstore/readonly_test.go
new file mode 100644
index 0000000000..5d018b0318
--- /dev/null
+++ b/ipld/car/v2/blockstore/readonly_test.go
@@ -0,0 +1,396 @@
+package blockstore
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "os"
+ "testing"
+ "time"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ "github.com/ipfs/boxo/ipld/merkledag"
+ "github.com/ipfs/go-cid"
+ format "github.com/ipfs/go-ipld-format"
+ "github.com/multiformats/go-multicodec"
+ "github.com/multiformats/go-multihash"
+ "github.com/stretchr/testify/require"
+
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/store"
+)
+
+func TestReadOnlyGetReturnsBlockstoreNotFoundWhenCidDoesNotExist(t *testing.T) {
+ subject, err := OpenReadOnly("../testdata/sample-v1.car")
+ require.NoError(t, err)
+ nonExistingKey := merkledag.NewRawNode([]byte("lobstermuncher")).Block.Cid()
+
+ // Assert blockstore API returns blockstore.ErrNotFound
+ gotBlock, err := subject.Get(context.TODO(), nonExistingKey)
+ require.IsType(t, format.ErrNotFound{}, err)
+ require.Nil(t, gotBlock)
+}
+
+func TestReadOnly(t *testing.T) {
+ tests := []struct {
+ name string
+ v1OrV2path string
+ opts []carv2.Option
+ noIdCids bool
+ }{
+ {
+ "OpenedWithCarV1",
+ "../testdata/sample-v1.car",
+ []carv2.Option{UseWholeCIDs(true), carv2.StoreIdentityCIDs(true)},
+ // index is made, but identity CIDs are included so they'll be found
+ false,
+ },
+ {
+ "OpenedWithCarV1_NoIdentityCID",
+ "../testdata/sample-v1.car",
+ []carv2.Option{UseWholeCIDs(true)},
+ // index is made, identity CIDs are not included, but we always short-circuit when StoreIdentityCIDs(false)
+ false,
+ },
+ {
+ "OpenedWithCarV2",
+ "../testdata/sample-wrapped-v2.car",
+ []carv2.Option{UseWholeCIDs(true), carv2.StoreIdentityCIDs(true)},
+ // index already exists, but was made without identity CIDs, but opening with StoreIdentityCIDs(true) means we check the index
+ true,
+ },
+ {
+ "OpenedWithCarV2_NoIdentityCID",
+ "../testdata/sample-wrapped-v2.car",
+ []carv2.Option{UseWholeCIDs(true)},
+ // index already exists, it was made without identity CIDs, but we always short-circuit when StoreIdentityCIDs(false)
+ false,
+ },
+ {
+ "OpenedWithCarV1ZeroLenSection",
+ "../testdata/sample-v1-with-zero-len-section.car",
+ []carv2.Option{UseWholeCIDs(true), carv2.ZeroLengthSectionAsEOF(true)},
+ false,
+ },
+ {
+ "OpenedWithAnotherCarV1ZeroLenSection",
+ "../testdata/sample-v1-with-zero-len-section2.car",
+ []carv2.Option{UseWholeCIDs(true), carv2.ZeroLengthSectionAsEOF(true)},
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx := context.TODO()
+
+ subject, err := OpenReadOnly(tt.v1OrV2path, tt.opts...)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, subject.Close()) })
+
+ f, err := os.Open(tt.v1OrV2path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, f.Close()) })
+
+ reader, err := carv2.NewBlockReader(f, tt.opts...)
+ require.NoError(t, err)
+
+ // Assert roots match v1 payload.
+ wantRoots := reader.Roots
+ gotRoots, err := subject.Roots()
+ require.NoError(t, err)
+ require.Equal(t, wantRoots, gotRoots)
+
+ var wantCids []cid.Cid
+ for {
+ wantBlock, err := reader.Next()
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+
+ key := wantBlock.Cid()
+ wantCids = append(wantCids, key)
+
+ // Assert blockstore contains key.
+ has, err := subject.Has(ctx, key)
+ require.NoError(t, err)
+ if key.Prefix().MhType == uint64(multicodec.Identity) && tt.noIdCids {
+ // fixture wasn't made with StoreIdentityCIDs, but we opened it with StoreIdentityCIDs,
+ // so they aren't there to find
+ require.False(t, has)
+ } else {
+ require.True(t, has)
+ }
+
+ // Assert size matches block raw data length.
+ gotSize, err := subject.GetSize(ctx, key)
+ wantSize := len(wantBlock.RawData())
+ require.NoError(t, err)
+ require.Equal(t, wantSize, gotSize)
+
+ // Assert block itself matches v1 payload block.
+ if has {
+ gotBlock, err := subject.Get(ctx, key)
+ require.NoError(t, err)
+ require.Equal(t, wantBlock, gotBlock)
+ }
+
+ // Assert write operations error
+ require.Error(t, subject.Put(ctx, wantBlock))
+ require.Error(t, subject.PutMany(ctx, []blocks.Block{wantBlock}))
+ require.Error(t, subject.DeleteBlock(ctx, key))
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
+ defer cancel()
+
+ // Assert all cids in blockstore match v1 payload CIDs.
+ allKeysChan, err := subject.AllKeysChan(ctx)
+ require.NoError(t, err)
+ var gotCids []cid.Cid
+ for gotKey := range allKeysChan {
+ gotCids = append(gotCids, gotKey)
+ }
+ require.Equal(t, wantCids, gotCids)
+ })
+ }
+}
+
+func TestNewReadOnlyFailsOnUnknownVersion(t *testing.T) {
+ f, err := os.Open("../testdata/sample-rootless-v42.car")
+ require.NoError(t, err)
+ t.Cleanup(func() { f.Close() })
+ subject, err := NewReadOnly(f, nil)
+ require.Errorf(t, err, "unsupported car version: 42")
+ require.Nil(t, subject)
+}
+
+func TestReadOnlyAllKeysChanErrHandlerCalledOnTimeout(t *testing.T) {
+ expiredCtx, cancel := context.WithTimeout(context.Background(), -time.Millisecond)
+ t.Cleanup(cancel)
+
+ subject, err := OpenReadOnly("../testdata/sample-v1.car")
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, subject.Close()) })
+
+ // Make a channel to be able to select and block on until error handler is called.
+ errHandlerCalled := make(chan interface{})
+ expiredErrHandlingCtx := WithAsyncErrorHandler(expiredCtx, func(err error) {
+ defer close(errHandlerCalled)
+ require.EqualError(t, err, "context deadline exceeded")
+ })
+ _, err = subject.AllKeysChan(expiredErrHandlingCtx)
+ require.NoError(t, err)
+
+ // Assert error handler was called with required condition, waiting at most 3 seconds.
+ select {
+ case <-errHandlerCalled:
+ break
+ case <-time.After(time.Second * 3):
+ require.Fail(t, "error handler was not called within expected time window")
+ }
+}
+
+func TestReadOnlyAllKeysChanErrHandlerNeverCalled(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ errHandler func(err error)
+ wantCIDs []cid.Cid
+ }{
+ {
+ "ReadingValidCarV1ReturnsNoErrors",
+ "../testdata/sample-v1.car",
+ func(err error) {
+ require.Fail(t, "unexpected call", "error handler called unexpectedly with err: %v", err)
+ },
+ listCids(t, newV1ReaderFromV1File(t, "../testdata/sample-v1.car", false)),
+ },
+ {
+ "ReadingValidCarV2ReturnsNoErrors",
+ "../testdata/sample-wrapped-v2.car",
+ func(err error) {
+ require.Fail(t, "unexpected call", "error handler called unexpectedly with err: %v", err)
+ },
+ listCids(t, newV1ReaderFromV2File(t, "../testdata/sample-wrapped-v2.car", false)),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ subject, err := OpenReadOnly(tt.path, UseWholeCIDs(true))
+ require.NoError(t, err)
+ ctx := WithAsyncErrorHandler(context.Background(), tt.errHandler)
+ keysChan, err := subject.AllKeysChan(ctx)
+ require.NoError(t, err)
+ var gotCids []cid.Cid
+ for k := range keysChan {
+ gotCids = append(gotCids, k)
+ }
+ require.Equal(t, tt.wantCIDs, gotCids)
+ })
+ }
+}
+
+func listCids(t *testing.T, v1r *carv1.CarReader) (cids []cid.Cid) {
+ for {
+ block, err := v1r.Next()
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+ cids = append(cids, block.Cid())
+ }
+ return
+}
+
+func newV1ReaderFromV1File(t *testing.T, carv1Path string, zeroLenSectionAsEOF bool) *carv1.CarReader {
+ f, err := os.Open(carv1Path)
+ require.NoError(t, err)
+ t.Cleanup(func() { f.Close() })
+ v1r, err := newV1Reader(f, zeroLenSectionAsEOF)
+ require.NoError(t, err)
+ return v1r
+}
+
+func newV1ReaderFromV2File(t *testing.T, carv2Path string, zeroLenSectionAsEOF bool) *carv1.CarReader {
+ f, err := os.Open(carv2Path)
+ require.NoError(t, err)
+ t.Cleanup(func() { f.Close() })
+ v2r, err := carv2.NewReader(f)
+ require.NoError(t, err)
+ dr, err := v2r.DataReader()
+ require.NoError(t, err)
+ v1r, err := newV1Reader(dr, zeroLenSectionAsEOF)
+ require.NoError(t, err)
+ return v1r
+}
+
+func newV1Reader(r io.Reader, zeroLenSectionAsEOF bool) (*carv1.CarReader, error) {
+ if zeroLenSectionAsEOF {
+ return carv1.NewCarReaderWithZeroLengthSectionAsEOF(r)
+ }
+ return carv1.NewCarReader(r)
+}
+
+func TestReadOnlyErrorAfterClose(t *testing.T) {
+ bs, err := OpenReadOnly("../testdata/sample-v1.car")
+ ctx := context.TODO()
+ require.NoError(t, err)
+
+ roots, err := bs.Roots()
+ require.NoError(t, err)
+ _, err = bs.Has(ctx, roots[0])
+ require.NoError(t, err)
+ _, err = bs.Get(ctx, roots[0])
+ require.NoError(t, err)
+ _, err = bs.GetSize(ctx, roots[0])
+ require.NoError(t, err)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ _, err = bs.AllKeysChan(ctx)
+ require.NoError(t, err)
+ cancel() // to stop the AllKeysChan goroutine
+
+ bs.Close()
+
+ _, err = bs.Roots()
+ require.Error(t, err)
+ _, err = bs.Has(ctx, roots[0])
+ require.Error(t, err)
+ _, err = bs.Get(ctx, roots[0])
+ require.Error(t, err)
+ _, err = bs.GetSize(ctx, roots[0])
+ require.Error(t, err)
+ _, err = bs.AllKeysChan(ctx)
+ require.Error(t, err)
+
+ // TODO: test that closing blocks if an AllKeysChan operation is
+ // in progress.
+}
+
+func TestNewReadOnly_CarV1WithoutIndexWorksAsExpected(t *testing.T) {
+ carV1Bytes, err := os.ReadFile("../testdata/sample-v1.car")
+ require.NoError(t, err)
+
+ reader := bytes.NewReader(carV1Bytes)
+ v1r, err := carv1.NewCarReader(reader)
+ require.NoError(t, err)
+ require.Equal(t, uint64(1), v1r.Header.Version)
+
+ // Pick the first block in CARv1 as candidate to check `Get` works.
+ wantBlock, err := v1r.Next()
+ require.NoError(t, err)
+
+ // Seek back to the begining of the CARv1 payload.
+ _, err = reader.Seek(0, io.SeekStart)
+ require.NoError(t, err)
+
+ subject, err := NewReadOnly(reader, nil, UseWholeCIDs(true))
+ require.NoError(t, err)
+
+ // Require that the block is found via ReadOnly API and contetns are as expected.
+ gotBlock, err := subject.Get(context.TODO(), wantBlock.Cid())
+ require.NoError(t, err)
+ require.Equal(t, wantBlock, gotBlock)
+}
+
+func TestReadOnlyIndex(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ wantCIDs []cid.Cid
+ }{
+ {
+ "IndexCarV1",
+ "../testdata/sample-v1.car",
+ listCids(t, newV1ReaderFromV1File(t, "../testdata/sample-v1.car", false)),
+ },
+ {
+ "IndexCarV2",
+ "../testdata/sample-wrapped-v2.car",
+ listCids(t, newV1ReaderFromV2File(t, "../testdata/sample-wrapped-v2.car", false)),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ subject, err := OpenReadOnly(tt.path, UseWholeCIDs(true))
+ require.NoError(t, err)
+
+ idx := subject.Index()
+
+ for _, c := range tt.wantCIDs {
+ _, isIdentity, err := store.IsIdentity(c)
+ require.NoError(t, err)
+ if isIdentity {
+ // the index doesn't hold identity CIDs
+ continue
+ }
+ _, err = index.GetFirst(idx, c)
+ require.NoError(t, err)
+ }
+
+ if idx, ok := idx.(index.IterableIndex); ok {
+ expected := make([]multihash.Multihash, 0, len(tt.wantCIDs))
+ for _, c := range tt.wantCIDs {
+ _, isIdentity, err := store.IsIdentity(c)
+ require.NoError(t, err)
+ if isIdentity {
+ // the index doesn't hold identity CIDs
+ continue
+ }
+ expected = append(expected, c.Hash())
+ }
+
+ var got []multihash.Multihash
+ err = idx.ForEach(func(m multihash.Multihash, u uint64) error {
+ got = append(got, m)
+ return nil
+ })
+ require.NoError(t, err)
+ require.ElementsMatch(t, expected, got)
+ }
+ })
+ }
+}
diff --git a/ipld/car/v2/blockstore/readwrite.go b/ipld/car/v2/blockstore/readwrite.go
new file mode 100644
index 0000000000..918921ec93
--- /dev/null
+++ b/ipld/car/v2/blockstore/readwrite.go
@@ -0,0 +1,389 @@
+package blockstore
+
+import (
+ "context"
+ "fmt"
+ "os"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ blockstore "github.com/ipfs/boxo/blockstore"
+ "github.com/ipfs/go-cid"
+
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1/util"
+ internalio "github.com/ipfs/boxo/ipld/car/v2/internal/io"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/store"
+)
+
+var _ blockstore.Blockstore = (*ReadWrite)(nil)
+
+var (
+ errFinalized = fmt.Errorf("cannot write in a carv2 blockstore after finalize")
+)
+
+// ReadWrite implements a blockstore that stores blocks in CARv2 format.
+// Blocks put into the blockstore can be read back once they are successfully written.
+// This implementation is preferable for a write-heavy workload.
+// The blocks are written immediately on Put and PutAll calls, while the index is stored in memory
+// and updated incrementally.
+//
+// The Finalize function must be called once the putting blocks are finished.
+// Upon calling Finalize header is finalized and index is written out.
+// Once finalized, all read and write calls to this blockstore will result in errors.
+type ReadWrite struct {
+ ronly ReadOnly
+
+ f *os.File
+ dataWriter *internalio.OffsetWriteSeeker
+ idx *store.InsertionIndex
+ header carv2.Header
+
+ finalized bool // also protected by ronly.mu
+
+ opts carv2.Options
+}
+
+var WriteAsCarV1 = carv2.WriteAsCarV1
+var AllowDuplicatePuts = carv2.AllowDuplicatePuts
+
+// OpenReadWrite creates a new ReadWrite at the given path with a provided set of root CIDs and options.
+//
+// ReadWrite.Finalize must be called once putting and reading blocks are no longer needed.
+// Upon calling ReadWrite.Finalize the CARv2 header and index are written out onto the file and the
+// backing file is closed. Once finalized, all read and write calls to this blockstore will result
+// in errors. Note, ReadWrite.Finalize must be called on an open instance regardless of whether any
+// blocks were put or not.
+//
+// If a file at given path does not exist, the instantiation will write car.Pragma and data payload
+// header (i.e. the inner CARv1 header) onto the file before returning.
+//
+// When the given path already exists, the blockstore will attempt to resume from it.
+// On resumption the existing data sections in file are re-indexed, allowing the caller to continue
+// putting any remaining blocks without having to re-ingest blocks for which previous ReadWrite.Put
+// returned successfully.
+//
+// Resumption only works on files that were created by a previous instance of a ReadWrite
+// blockstore. This means a file created as a result of a successful call to OpenReadWrite can be
+// resumed from as long as write operations such as ReadWrite.Put, ReadWrite.PutMany returned
+// successfully. On resumption the roots argument and WithDataPadding option must match the
+// previous instantiation of ReadWrite blockstore that created the file. More explicitly, the file
+// resuming from must:
+// 1. start with a complete CARv2 car.Pragma.
+// 2. contain a complete CARv1 data header with root CIDs matching the CIDs passed to the
+// constructor, starting at offset optionally padded by WithDataPadding, followed by zero or
+// more complete data sections. If any corrupt data sections are present the resumption will fail.
+// Note, if set previously, the blockstore must use the same WithDataPadding option as before,
+// since this option is used to locate the CARv1 data payload.
+//
+// Note, resumption should be used with WithCidDeduplication, so that blocks that are successfully
+// written into the file are not re-written. Unless, the user explicitly wants duplicate blocks.
+//
+// Resuming from finalized files is allowed. However, resumption will regenerate the index
+// regardless by scanning every existing block in file.
+func OpenReadWrite(path string, roots []cid.Cid, opts ...carv2.Option) (*ReadWrite, error) {
+ f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o666) // TODO: Should the user be able to configure FileMode permissions?
+ if err != nil {
+ return nil, fmt.Errorf("could not open read/write file: %w", err)
+ }
+ // If construction of blockstore fails, make sure to close off the open file.
+ defer func() {
+ if err != nil {
+ f.Close()
+ }
+ }()
+ rwbs, err := OpenReadWriteFile(f, roots, opts...)
+ if err != nil {
+ return nil, err
+ }
+ // close the file when finalizing
+ rwbs.ronly.carv2Closer = rwbs.f
+ return rwbs, nil
+}
+
+// OpenReadWriteFile is similar as OpenReadWrite but lets you control the file lifecycle.
+// You are responsible for closing the given file.
+func OpenReadWriteFile(f *os.File, roots []cid.Cid, opts ...carv2.Option) (*ReadWrite, error) {
+ stat, err := f.Stat()
+ if err != nil {
+ // Note, we should not get an os.ErrNotExist here because the flags used to open file includes os.O_CREATE
+ return nil, err
+ }
+ // Try and resume by default if the file size is non-zero.
+ resume := stat.Size() != 0
+
+ // Instantiate block store.
+ // Set the header fileld before applying options since padding options may modify header.
+ rwbs := &ReadWrite{
+ f: f,
+ idx: store.NewInsertionIndex(),
+ header: carv2.NewHeader(0),
+ opts: carv2.ApplyOptions(opts...),
+ finalized: false,
+ }
+ rwbs.ronly.opts = rwbs.opts
+
+ if p := rwbs.opts.DataPadding; p > 0 {
+ rwbs.header = rwbs.header.WithDataPadding(p)
+ }
+ if p := rwbs.opts.IndexPadding; p > 0 {
+ rwbs.header = rwbs.header.WithIndexPadding(p)
+ }
+
+ offset := int64(rwbs.header.DataOffset)
+ if rwbs.opts.WriteAsCarV1 {
+ offset = 0
+ }
+ rwbs.dataWriter = internalio.NewOffsetWriter(rwbs.f, offset)
+ var v1r internalio.ReadSeekerAt
+ v1r, err = internalio.NewOffsetReadSeeker(rwbs.f, offset)
+ if err != nil {
+ return nil, err
+ }
+ rwbs.ronly.backing = v1r
+ rwbs.ronly.idx = rwbs.idx
+
+ if resume {
+ if err = store.ResumableVersion(f, rwbs.opts.WriteAsCarV1); err != nil {
+ return nil, err
+ }
+ if err = store.Resume(
+ f,
+ rwbs.ronly.backing,
+ rwbs.dataWriter,
+ rwbs.idx,
+ roots,
+ rwbs.header.DataOffset,
+ rwbs.opts.WriteAsCarV1,
+ rwbs.opts.MaxAllowedHeaderSize,
+ rwbs.opts.ZeroLengthSectionAsEOF,
+ ); err != nil {
+ return nil, err
+ }
+ } else {
+ if err = rwbs.initWithRoots(!rwbs.opts.WriteAsCarV1, roots); err != nil {
+ return nil, err
+ }
+ }
+
+ return rwbs, nil
+}
+
+func (b *ReadWrite) initWithRoots(v2 bool, roots []cid.Cid) error {
+ if v2 {
+ if _, err := b.f.WriteAt(carv2.Pragma, 0); err != nil {
+ return err
+ }
+ }
+ return carv1.WriteHeader(&carv1.CarHeader{Roots: roots, Version: 1}, b.dataWriter)
+}
+
+// Index gives direct access to the index.
+// You should never add records on your own there.
+func (b *ReadWrite) Index() index.Index {
+ return b.idx
+}
+
+// Put puts a given block to the underlying datastore
+func (b *ReadWrite) Put(ctx context.Context, blk blocks.Block) error {
+ // PutMany already checks b.ronly.closed.
+ return b.PutMany(ctx, []blocks.Block{blk})
+}
+
+// PutMany puts a slice of blocks at the same time using batching
+// capabilities of the underlying datastore whenever possible.
+func (b *ReadWrite) PutMany(ctx context.Context, blks []blocks.Block) error {
+ b.ronly.mu.Lock()
+ defer b.ronly.mu.Unlock()
+
+ if b.ronly.closed {
+ return errClosed
+ }
+ if b.finalized {
+ return errFinalized
+ }
+
+ for _, bl := range blks {
+ c := bl.Cid()
+
+ if should, err := store.ShouldPut(
+ b.idx,
+ c,
+ b.opts.MaxIndexCidSize,
+ b.opts.StoreIdentityCIDs,
+ b.opts.BlockstoreAllowDuplicatePuts,
+ b.opts.BlockstoreUseWholeCIDs,
+ ); err != nil {
+ return err
+ } else if !should {
+ continue
+ }
+
+ n := uint64(b.dataWriter.Position())
+ if err := util.LdWrite(b.dataWriter, c.Bytes(), bl.RawData()); err != nil {
+ return err
+ }
+ b.idx.InsertNoReplace(c, n)
+ }
+ return nil
+}
+
+// Discard closes this blockstore without finalizing its header and index.
+// After this call, the blockstore can no longer be used.
+//
+// Note that this call may block if any blockstore operations are currently in
+// progress, including an AllKeysChan that hasn't been fully consumed or cancelled.
+func (b *ReadWrite) Discard() {
+ // Same semantics as ReadOnly.Close, including allowing duplicate calls.
+ // The only difference is that our method is called Discard,
+ // to further clarify that we're not properly finalizing and writing a
+ // CARv2 file.
+ b.ronly.Close()
+}
+
+// Finalize finalizes this blockstore by writing the CARv2 header, along with flattened index
+// for more efficient subsequent read.
+// This is the equivalent to calling FinalizeReadOnly and Close.
+// After this call, the blockstore can no longer be used.
+func (b *ReadWrite) Finalize() error {
+ b.ronly.mu.Lock()
+ defer b.ronly.mu.Unlock()
+
+ for _, err := range []error{b.finalizeReadOnlyWithoutMutex(), b.closeWithoutMutex()} {
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// Finalize finalizes this blockstore by writing the CARv2 header, along with flattened index
+// for more efficient subsequent read, but keep it open read-only.
+// This call should be complemented later by a call to Close.
+func (b *ReadWrite) FinalizeReadOnly() error {
+ b.ronly.mu.Lock()
+ defer b.ronly.mu.Unlock()
+
+ return b.finalizeReadOnlyWithoutMutex()
+}
+
+func (b *ReadWrite) finalizeReadOnlyWithoutMutex() error {
+ if b.opts.WriteAsCarV1 {
+ // all blocks are already properly written to the CARv1 inner container and there's
+ // no additional finalization required at the end of the file for a complete v1
+ b.finalized = true
+ return nil
+ }
+
+ if b.ronly.closed {
+ // Allow duplicate Finalize calls, just like Close.
+ // Still error, just like ReadOnly.Close; it should be discarded.
+ return fmt.Errorf("called Finalize or FinalizeReadOnly on a closed blockstore")
+ }
+ if b.finalized {
+ return fmt.Errorf("called Finalize or FinalizeReadOnly on an already finalized blockstore")
+ }
+
+ b.finalized = true
+
+ return store.Finalize(b.f, b.header, b.idx, uint64(b.dataWriter.Position()), b.opts.StoreIdentityCIDs, b.opts.IndexCodec)
+}
+
+// Close closes the blockstore.
+// After this call, the blockstore can no longer be used.
+func (b *ReadWrite) Close() error {
+ b.ronly.mu.Lock()
+ defer b.ronly.mu.Unlock()
+
+ return b.closeWithoutMutex()
+}
+
+func (b *ReadWrite) closeWithoutMutex() error {
+ if !b.opts.WriteAsCarV1 && !b.finalized {
+ return fmt.Errorf("called Close without FinalizeReadOnly first")
+ }
+ if b.ronly.closed {
+ // Allow duplicate Close calls
+ // Still error, just like ReadOnly.Close; it should be discarded.
+ return fmt.Errorf("called Close on a closed blockstore")
+ }
+
+ if err := b.ronly.closeWithoutMutex(); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (b *ReadWrite) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) {
+ if ctx.Err() != nil {
+ return nil, ctx.Err()
+ }
+
+ b.ronly.mu.Lock()
+ defer b.ronly.mu.Unlock()
+
+ if b.ronly.closed {
+ return nil, errClosed
+ }
+
+ out := make(chan cid.Cid)
+
+ go func() {
+ defer close(out)
+ err := b.idx.ForEachCid(func(c cid.Cid, _ uint64) error {
+ if !b.opts.BlockstoreUseWholeCIDs {
+ c = cid.NewCidV1(cid.Raw, c.Hash())
+ }
+ select {
+ case out <- c:
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ return nil
+ })
+ if err != nil {
+ maybeReportError(ctx, err)
+ }
+ }()
+
+ return out, nil
+}
+
+func (b *ReadWrite) Has(ctx context.Context, key cid.Cid) (bool, error) {
+ b.ronly.mu.Lock()
+ defer b.ronly.mu.Unlock()
+
+ if b.ronly.closed {
+ return false, errClosed
+ }
+
+ return store.Has(
+ b.idx,
+ key,
+ b.opts.MaxIndexCidSize,
+ b.opts.StoreIdentityCIDs,
+ b.opts.BlockstoreAllowDuplicatePuts,
+ b.opts.BlockstoreUseWholeCIDs,
+ )
+}
+
+func (b *ReadWrite) Get(ctx context.Context, key cid.Cid) (blocks.Block, error) {
+ return b.ronly.Get(ctx, key)
+}
+
+func (b *ReadWrite) GetSize(ctx context.Context, key cid.Cid) (int, error) {
+ return b.ronly.GetSize(ctx, key)
+}
+
+func (b *ReadWrite) DeleteBlock(_ context.Context, _ cid.Cid) error {
+ return fmt.Errorf("ReadWrite blockstore does not support deleting blocks")
+}
+
+func (b *ReadWrite) HashOnRead(enable bool) {
+ b.ronly.HashOnRead(enable)
+}
+
+func (b *ReadWrite) Roots() ([]cid.Cid, error) {
+ return b.ronly.Roots()
+}
diff --git a/ipld/car/v2/blockstore/readwrite_test.go b/ipld/car/v2/blockstore/readwrite_test.go
new file mode 100644
index 0000000000..d024c33afd
--- /dev/null
+++ b/ipld/car/v2/blockstore/readwrite_test.go
@@ -0,0 +1,1177 @@
+package blockstore_test
+
+import (
+ "context"
+ "crypto/sha512"
+ "fmt"
+ "io"
+ "math/rand"
+ "os"
+ "path"
+ "path/filepath"
+ "sync"
+ "testing"
+ "time"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ "github.com/ipfs/boxo/ipld/merkledag"
+ "github.com/ipfs/go-cid"
+ cbor "github.com/ipfs/go-ipld-cbor"
+ format "github.com/ipfs/go-ipld-format"
+ "github.com/multiformats/go-multicodec"
+ "github.com/multiformats/go-multihash"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/blockstore"
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+)
+
+var (
+ rng = rand.New(rand.NewSource(1413))
+ oneTestBlockWithCidV1 = merkledag.NewRawNode([]byte("fish")).Block
+ anotherTestBlockWithCidV0 = blocks.NewBlock([]byte("barreleye"))
+)
+
+func TestReadWriteGetReturnsBlockstoreNotFoundWhenCidDoesNotExist(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "readwrite-err-not-found.car")
+ subject, err := blockstore.OpenReadWrite(path, []cid.Cid{})
+ t.Cleanup(func() { subject.Finalize() })
+ require.NoError(t, err)
+ nonExistingKey := merkledag.NewRawNode([]byte("undadasea")).Block.Cid()
+
+ // Assert blockstore API returns blockstore.ErrNotFound
+ gotBlock, err := subject.Get(context.TODO(), nonExistingKey)
+ require.IsType(t, format.ErrNotFound{}, err)
+ require.Nil(t, gotBlock)
+}
+
+func TestBlockstore(t *testing.T) {
+ originalCARv1Path := "../testdata/sample-v1.car"
+ originalCARv1ComparePath := "../testdata/sample-v1-noidentity.car"
+ originalCARv1ComparePathStat, err := os.Stat(originalCARv1ComparePath)
+ require.NoError(t, err)
+
+ variants := []struct {
+ name string
+ options []carv2.Option
+ expectedV1StartOffset int64
+ }{
+ // no options, expect a standard CARv2 with the noidentity inner CARv1
+ {"noopt_carv2", []carv2.Option{}, int64(carv2.PragmaSize + carv2.HeaderSize)},
+ // option to only write as a CARv1, expect the noidentity inner CARv1
+ {"carv1", []carv2.Option{blockstore.WriteAsCarV1(true)}, int64(0)},
+ }
+
+ for _, variant := range variants {
+ t.Run(variant.name, func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ f, err := os.Open(originalCARv1Path)
+ require.NoError(t, err)
+ t.Cleanup(func() { assert.NoError(t, f.Close()) })
+ r, err := carv1.NewCarReader(f)
+ require.NoError(t, err)
+
+ path := filepath.Join(t.TempDir(), fmt.Sprintf("readwrite_%s.car", variant.name))
+ ingester, err := blockstore.OpenReadWrite(path, r.Header.Roots, variant.options...)
+ require.NoError(t, err)
+ t.Cleanup(func() { ingester.Finalize() })
+
+ cids := make([]cid.Cid, 0)
+ var idCidCount int
+ for {
+ b, err := r.Next()
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+
+ err = ingester.Put(ctx, b)
+ require.NoError(t, err)
+ cids = append(cids, b.Cid())
+
+ // try reading a random one:
+ candidate := cids[rng.Intn(len(cids))]
+ if has, err := ingester.Has(ctx, candidate); !has || err != nil {
+ t.Fatalf("expected to find %s but didn't: %s", candidate, err)
+ }
+
+ dmh, err := multihash.Decode(b.Cid().Hash())
+ require.NoError(t, err)
+ if dmh.Code == multihash.IDENTITY {
+ idCidCount++
+ }
+ }
+
+ for _, c := range cids {
+ b, err := ingester.Get(ctx, c)
+ require.NoError(t, err)
+ if !b.Cid().Equals(c) {
+ t.Fatal("wrong item returned")
+ }
+ }
+
+ err = ingester.Finalize()
+ require.NoError(t, err)
+ robs, err := blockstore.OpenReadOnly(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { assert.NoError(t, robs.Close()) })
+
+ allKeysCh, err := robs.AllKeysChan(ctx)
+ require.NoError(t, err)
+ numKeysCh := 0
+ for c := range allKeysCh {
+ b, err := robs.Get(ctx, c)
+ require.NoError(t, err)
+ if !b.Cid().Equals(c) {
+ t.Fatal("wrong item returned")
+ }
+ numKeysCh++
+ }
+ expectedCidCount := len(cids) - idCidCount
+ require.Equal(t, expectedCidCount, numKeysCh, "AllKeysChan returned an unexpected amount of keys; expected %v but got %v", expectedCidCount, numKeysCh)
+
+ for _, c := range cids {
+ b, err := robs.Get(ctx, c)
+ require.NoError(t, err)
+ if !b.Cid().Equals(c) {
+ t.Fatal("wrong item returned")
+ }
+ }
+
+ wrote, err := os.Open(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, wrote.Close()) })
+ _, err = wrote.Seek(variant.expectedV1StartOffset, io.SeekStart)
+ require.NoError(t, err)
+ hasher := sha512.New()
+ gotWritten, err := io.Copy(hasher, io.LimitReader(wrote, originalCARv1ComparePathStat.Size()))
+ require.NoError(t, err)
+ gotSum := hasher.Sum(nil)
+
+ hasher.Reset()
+ originalCarV1, err := os.Open(originalCARv1ComparePath)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, originalCarV1.Close()) })
+ wantWritten, err := io.Copy(hasher, originalCarV1)
+ require.NoError(t, err)
+ wantSum := hasher.Sum(nil)
+
+ require.Equal(t, wantWritten, gotWritten)
+ require.Equal(t, wantSum, gotSum)
+ })
+ }
+}
+
+func TestBlockstorePutSameHashes(t *testing.T) {
+ tdir := t.TempDir()
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ // This blockstore allows duplicate puts,
+ // and identifies by multihash as per the default.
+ wbsAllowDups, err := blockstore.OpenReadWrite(
+ filepath.Join(tdir, "readwrite-allowdup.car"), nil,
+ blockstore.AllowDuplicatePuts(true),
+ )
+ require.NoError(t, err)
+ t.Cleanup(func() { wbsAllowDups.Finalize() })
+
+ // This blockstore deduplicates puts by CID.
+ wbsByCID, err := blockstore.OpenReadWrite(
+ filepath.Join(tdir, "readwrite-dedup-wholecid.car"), nil,
+ blockstore.UseWholeCIDs(true),
+ )
+ require.NoError(t, err)
+ t.Cleanup(func() { wbsByCID.Finalize() })
+
+ // This blockstore deduplicates puts by multihash.
+ wbsByHash, err := blockstore.OpenReadWrite(
+ filepath.Join(tdir, "readwrite-dedup-hash.car"), nil,
+ )
+ require.NoError(t, err)
+ t.Cleanup(func() { wbsByHash.Finalize() })
+
+ var blockList []blocks.Block
+
+ appendBlock := func(data []byte, version, codec uint64) {
+ c, err := cid.Prefix{
+ Version: version,
+ Codec: codec,
+ MhType: multihash.SHA2_256,
+ MhLength: -1,
+ }.Sum(data)
+ require.NoError(t, err)
+
+ block, err := blocks.NewBlockWithCid(data, c)
+ require.NoError(t, err)
+
+ blockList = append(blockList, block)
+ }
+
+ // Two raw blocks, meaning we have two unique multihashes.
+ // However, we have multiple CIDs for each multihash.
+ // We also have two duplicate CIDs.
+ data1 := []byte("foo bar")
+ appendBlock(data1, 0, cid.DagProtobuf)
+ appendBlock(data1, 1, cid.DagProtobuf)
+ appendBlock(data1, 1, cid.DagCBOR)
+ appendBlock(data1, 1, cid.DagCBOR) // duplicate CID
+
+ data2 := []byte("foo bar baz")
+ appendBlock(data2, 0, cid.DagProtobuf)
+ appendBlock(data2, 1, cid.DagProtobuf)
+ appendBlock(data2, 1, cid.DagProtobuf) // duplicate CID
+ appendBlock(data2, 1, cid.DagCBOR)
+
+ countBlocks := func(bs *blockstore.ReadWrite) int {
+ ch, err := bs.AllKeysChan(context.Background())
+ require.NoError(t, err)
+
+ n := 0
+ for c := range ch {
+ if c.Prefix().Codec == cid.Raw {
+ if bs == wbsByCID {
+ t.Error("expected blockstore with UseWholeCIDs to not flatten on AllKeysChan")
+ }
+ } else {
+ if bs != wbsByCID {
+ t.Error("expected blockstore without UseWholeCIDs to flatten on AllKeysChan")
+ }
+ }
+ n++
+ }
+ return n
+ }
+
+ putBlockList := func(bs *blockstore.ReadWrite) {
+ for i, block := range blockList {
+ // Has should never error here.
+ // The first block should be missing.
+ // Others might not, given the duplicate hashes.
+ has, err := bs.Has(ctx, block.Cid())
+ require.NoError(t, err)
+ if i == 0 {
+ require.False(t, has)
+ }
+
+ err = bs.Put(ctx, block)
+ require.NoError(t, err)
+
+ // Has, Get, and GetSize need to work right after a Put.
+ has, err = bs.Has(ctx, block.Cid())
+ require.NoError(t, err)
+ require.True(t, has)
+
+ got, err := bs.Get(ctx, block.Cid())
+ require.NoError(t, err)
+ require.Equal(t, block.Cid(), got.Cid())
+ require.Equal(t, block.RawData(), got.RawData())
+
+ size, err := bs.GetSize(ctx, block.Cid())
+ require.NoError(t, err)
+ require.Equal(t, len(block.RawData()), size)
+ }
+ }
+
+ putBlockList(wbsAllowDups)
+ require.Equal(t, len(blockList), countBlocks(wbsAllowDups))
+
+ err = wbsAllowDups.Finalize()
+ require.NoError(t, err)
+
+ // Put the same list of blocks to the blockstore that
+ // deduplicates by CID.
+ // We should end up with two fewer blocks,
+ // as two are entire CID duplicates.
+ putBlockList(wbsByCID)
+ require.Equal(t, len(blockList)-2, countBlocks(wbsByCID))
+
+ err = wbsByCID.Finalize()
+ require.NoError(t, err)
+
+ // Put the same list of blocks to the blockstore that
+ // deduplicates by CID.
+ // We should end up with just two blocks,
+ // as the original set of blocks only has two distinct multihashes.
+ putBlockList(wbsByHash)
+ require.Equal(t, 2, countBlocks(wbsByHash))
+
+ err = wbsByHash.Finalize()
+ require.NoError(t, err)
+}
+
+func TestBlockstoreConcurrentUse(t *testing.T) {
+ wbs, err := blockstore.OpenReadWrite(filepath.Join(t.TempDir(), "readwrite.car"), nil)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ require.NoError(t, err)
+ t.Cleanup(func() { wbs.Finalize() })
+
+ var wg sync.WaitGroup
+ for i := 0; i < 100; i++ {
+ data := []byte(fmt.Sprintf("data-%d", i))
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+
+ c, err := cid.Prefix{
+ Version: 1,
+ Codec: cid.Raw,
+ MhType: multihash.SHA2_256,
+ MhLength: -1,
+ }.Sum(data)
+ require.NoError(t, err)
+
+ block, err := blocks.NewBlockWithCid(data, c)
+ require.NoError(t, err)
+
+ has, err := wbs.Has(ctx, block.Cid())
+ require.NoError(t, err)
+ require.False(t, has)
+
+ err = wbs.Put(ctx, block)
+ require.NoError(t, err)
+
+ got, err := wbs.Get(ctx, block.Cid())
+ require.NoError(t, err)
+ require.Equal(t, data, got.RawData())
+ }()
+ }
+ wg.Wait()
+}
+
+type bufferReaderAt []byte
+
+func (b bufferReaderAt) ReadAt(p []byte, off int64) (int, error) {
+ if off >= int64(len(b)) {
+ return 0, io.EOF
+ }
+ return copy(p, b[off:]), nil
+}
+
+func TestBlockstoreNullPadding(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ paddedV1, err := os.ReadFile("../testdata/sample-v1-with-zero-len-section.car")
+ require.NoError(t, err)
+
+ rbs, err := blockstore.NewReadOnly(bufferReaderAt(paddedV1), nil,
+ carv2.ZeroLengthSectionAsEOF(true))
+ require.NoError(t, err)
+
+ roots, err := rbs.Roots()
+ require.NoError(t, err)
+
+ has, err := rbs.Has(ctx, roots[0])
+ require.NoError(t, err)
+ require.True(t, has)
+
+ allKeysCh, err := rbs.AllKeysChan(ctx)
+ require.NoError(t, err)
+ for c := range allKeysCh {
+ b, err := rbs.Get(ctx, c)
+ require.NoError(t, err)
+ if !b.Cid().Equals(c) {
+ t.Fatal("wrong item returned")
+ }
+ }
+}
+
+func TestBlockstoreResumption(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ v1f, err := os.Open("../testdata/sample-v1.car")
+ require.NoError(t, err)
+ t.Cleanup(func() { assert.NoError(t, v1f.Close()) })
+ r, err := carv1.NewCarReader(v1f)
+ require.NoError(t, err)
+
+ path := filepath.Join(t.TempDir(), "readwrite-resume.car")
+ // Create an incomplete CARv2 file with no blocks put.
+ subject, err := blockstore.OpenReadWrite(path, r.Header.Roots,
+ blockstore.UseWholeCIDs(true))
+ require.NoError(t, err)
+
+ // For each block resume on the same file, putting blocks one at a time.
+ var wantBlockCountSoFar, idCidCount int
+ wantBlocks := make(map[cid.Cid]blocks.Block)
+ for {
+ b, err := r.Next()
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+ wantBlockCountSoFar++
+ wantBlocks[b.Cid()] = b
+
+ dmh, err := multihash.Decode(b.Cid().Hash())
+ require.NoError(t, err)
+ if dmh.Code == multihash.IDENTITY {
+ idCidCount++
+ }
+
+ // 30% chance of subject failing; more concretely: re-instantiating blockstore with the same
+ // file without calling Finalize. The higher this percentage the slower the test runs
+ // considering the number of blocks in the original CARv1 test payload.
+ resume := rng.Float32() <= 0.3
+ // If testing resume case, then flip a coin to decide whether to finalize before blockstore
+ // re-instantiation or not. Note, both cases should work for resumption since we do not
+ // limit resumption to unfinalized files.
+ finalizeBeforeResumption := rng.Float32() <= 0.5
+ if resume {
+ if finalizeBeforeResumption {
+ require.NoError(t, subject.Finalize())
+ } else {
+ // Close off the open file and re-instantiate a new subject with resumption enabled.
+ // Note, we don't have to close the file for resumption to work.
+ // We do this to avoid resource leak during testing.
+ subject.Discard()
+ }
+ subject, err = blockstore.OpenReadWrite(path, r.Header.Roots,
+ blockstore.UseWholeCIDs(true))
+ require.NoError(t, err)
+ }
+ require.NoError(t, subject.Put(ctx, b))
+
+ // With 10% chance test read operations on an resumed read-write blockstore.
+ // We don't test on every put to reduce test runtime.
+ testRead := rng.Float32() <= 0.1
+ if testRead {
+ // Assert read operations on the read-write blockstore are as expected when resumed from an
+ // existing file
+ var gotBlockCountSoFar int
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ t.Cleanup(cancel)
+ keysChan, err := subject.AllKeysChan(ctx)
+ require.NoError(t, err)
+ for k := range keysChan {
+ has, err := subject.Has(ctx, k)
+ require.NoError(t, err)
+ require.True(t, has)
+ gotBlock, err := subject.Get(ctx, k)
+ require.NoError(t, err)
+ require.Equal(t, wantBlocks[k], gotBlock)
+ gotBlockCountSoFar++
+ }
+ // Assert the number of blocks in file are as expected calculated via AllKeysChan
+ require.Equal(t, wantBlockCountSoFar-idCidCount, gotBlockCountSoFar)
+ }
+ }
+ subject.Discard()
+
+ // Finalize the blockstore to complete partially written CARv2 file.
+ subject, err = blockstore.OpenReadWrite(path, r.Header.Roots,
+ blockstore.UseWholeCIDs(true))
+ require.NoError(t, err)
+ require.NoError(t, subject.Finalize())
+
+ // Assert resumed from file is a valid CARv2 with index.
+ v2f, err := os.Open(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { assert.NoError(t, v2f.Close()) })
+ v2r, err := carv2.NewReader(v2f)
+ require.NoError(t, err)
+ require.True(t, v2r.Header.HasIndex())
+
+ // Assert CARv1 payload in file matches the original CARv1 payload.
+ _, err = v1f.Seek(0, io.SeekStart)
+ require.NoError(t, err)
+ wantPayloadReader, err := carv1.NewCarReader(v1f)
+ require.NoError(t, err)
+
+ dr, err := v2r.DataReader()
+ require.NoError(t, err)
+ gotPayloadReader, err := carv1.NewCarReader(dr)
+ require.NoError(t, err)
+
+ require.Equal(t, wantPayloadReader.Header, gotPayloadReader.Header)
+ for {
+ wantNextBlock, wantErr := wantPayloadReader.Next()
+ if wantErr == io.EOF {
+ gotNextBlock, gotErr := gotPayloadReader.Next()
+ require.Equal(t, wantErr, gotErr)
+ require.Nil(t, gotNextBlock)
+ break
+ }
+ require.NoError(t, wantErr)
+
+ dmh, err := multihash.Decode(wantNextBlock.Cid().Hash())
+ require.NoError(t, err)
+ if dmh.Code == multihash.IDENTITY {
+ continue
+ }
+
+ gotNextBlock, gotErr := gotPayloadReader.Next()
+ require.NoError(t, gotErr)
+ require.Equal(t, wantNextBlock, gotNextBlock)
+ }
+
+ // Assert index in resumed from file is identical to index generated from the data payload portion of the generated CARv2 file.
+ _, err = v1f.Seek(0, io.SeekStart)
+ require.NoError(t, err)
+ ir, err := v2r.IndexReader()
+ require.NoError(t, err)
+ gotIdx, err := index.ReadFrom(ir)
+ require.NoError(t, err)
+ dr, err = v2r.DataReader()
+ require.NoError(t, err)
+ wantIdx, err := carv2.GenerateIndex(dr)
+ require.NoError(t, err)
+ require.Equal(t, wantIdx, gotIdx)
+}
+
+func TestBlockstoreResumptionIsSupportedOnFinalizedFile(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "readwrite-resume-finalized.car")
+ // Create an incomplete CARv2 file with no blocks put.
+ subject, err := blockstore.OpenReadWrite(path, []cid.Cid{})
+ require.NoError(t, err)
+ require.NoError(t, subject.Finalize())
+ subject, err = blockstore.OpenReadWrite(path, []cid.Cid{})
+ require.NoError(t, err)
+ t.Cleanup(func() { subject.Finalize() })
+}
+
+func TestReadWritePanicsOnlyWhenFinalized(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ oneTestBlockCid := oneTestBlockWithCidV1.Cid()
+ anotherTestBlockCid := anotherTestBlockWithCidV0.Cid()
+ wantRoots := []cid.Cid{oneTestBlockCid, anotherTestBlockCid}
+ path := filepath.Join(t.TempDir(), "readwrite-finalized-panic.car")
+
+ subject, err := blockstore.OpenReadWrite(path, wantRoots)
+ require.NoError(t, err)
+
+ require.NoError(t, subject.Put(ctx, oneTestBlockWithCidV1))
+ require.NoError(t, subject.Put(ctx, anotherTestBlockWithCidV0))
+
+ gotBlock, err := subject.Get(ctx, oneTestBlockCid)
+ require.NoError(t, err)
+ require.Equal(t, oneTestBlockWithCidV1, gotBlock)
+
+ gotSize, err := subject.GetSize(ctx, oneTestBlockCid)
+ require.NoError(t, err)
+ require.Equal(t, len(oneTestBlockWithCidV1.RawData()), gotSize)
+
+ gotRoots, err := subject.Roots()
+ require.NoError(t, err)
+ require.Equal(t, wantRoots, gotRoots)
+
+ has, err := subject.Has(ctx, oneTestBlockCid)
+ require.NoError(t, err)
+ require.True(t, has)
+
+ subject.HashOnRead(true)
+ // Delete should always error regardless of finalize
+ require.Error(t, subject.DeleteBlock(ctx, oneTestBlockCid))
+
+ require.NoError(t, subject.Finalize())
+ require.Error(t, subject.Finalize())
+
+ _, err = subject.Get(ctx, oneTestBlockCid)
+ require.Error(t, err)
+ _, err = subject.GetSize(ctx, anotherTestBlockCid)
+ require.Error(t, err)
+ _, err = subject.Has(ctx, anotherTestBlockCid)
+ require.Error(t, err)
+
+ require.Error(t, subject.Put(ctx, oneTestBlockWithCidV1))
+ require.Error(t, subject.PutMany(ctx, []blocks.Block{anotherTestBlockWithCidV0}))
+ _, err = subject.AllKeysChan(context.Background())
+ require.Error(t, err)
+ require.Error(t, subject.DeleteBlock(ctx, oneTestBlockCid))
+}
+
+func TestReadWriteWithPaddingWorksAsExpected(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ oneTestBlockCid := oneTestBlockWithCidV1.Cid()
+ anotherTestBlockCid := anotherTestBlockWithCidV0.Cid()
+ WantRoots := []cid.Cid{oneTestBlockCid, anotherTestBlockCid}
+ path := filepath.Join(t.TempDir(), "readwrite-with-padding.car")
+
+ wantCarV1Padding := uint64(1413)
+ wantIndexPadding := uint64(1314)
+ subject, err := blockstore.OpenReadWrite(
+ path,
+ WantRoots,
+ carv2.UseDataPadding(wantCarV1Padding),
+ carv2.UseIndexPadding(wantIndexPadding))
+ require.NoError(t, err)
+ require.NoError(t, subject.Put(ctx, oneTestBlockWithCidV1))
+ require.NoError(t, subject.Put(ctx, anotherTestBlockWithCidV0))
+ require.NoError(t, subject.Finalize())
+
+ // Assert CARv2 header contains right offsets.
+ gotCarV2, err := carv2.OpenReader(path)
+ t.Cleanup(func() { gotCarV2.Close() })
+ require.NoError(t, err)
+ wantCarV1Offset := carv2.PragmaSize + carv2.HeaderSize + wantCarV1Padding
+ wantIndexOffset := wantCarV1Offset + gotCarV2.Header.DataSize + wantIndexPadding
+ require.Equal(t, wantCarV1Offset, gotCarV2.Header.DataOffset)
+ require.Equal(t, wantIndexOffset, gotCarV2.Header.IndexOffset)
+ require.NoError(t, gotCarV2.Close())
+
+ f, err := os.Open(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { f.Close() })
+
+ // Assert reading CARv1 directly at offset and size is as expected.
+ gotCarV1, err := carv1.NewCarReader(io.NewSectionReader(f, int64(wantCarV1Offset), int64(gotCarV2.Header.DataSize)))
+ require.NoError(t, err)
+ require.Equal(t, WantRoots, gotCarV1.Header.Roots)
+ gotOneBlock, err := gotCarV1.Next()
+ require.NoError(t, err)
+ require.Equal(t, oneTestBlockWithCidV1, gotOneBlock)
+ gotAnotherBlock, err := gotCarV1.Next()
+ require.NoError(t, err)
+ require.Equal(t, anotherTestBlockWithCidV0, gotAnotherBlock)
+ _, err = gotCarV1.Next()
+ require.Equal(t, io.EOF, err)
+
+ // Assert reading index directly from file is parsable and has expected CIDs.
+ stat, err := f.Stat()
+ require.NoError(t, err)
+ indexSize := stat.Size() - int64(wantIndexOffset)
+ gotIdx, err := index.ReadFrom(io.NewSectionReader(f, int64(wantIndexOffset), indexSize))
+ require.NoError(t, err)
+ _, err = index.GetFirst(gotIdx, oneTestBlockCid)
+ require.NoError(t, err)
+ _, err = index.GetFirst(gotIdx, anotherTestBlockCid)
+ require.NoError(t, err)
+}
+
+func TestReadWriteResumptionFromNonV2FileIsError(t *testing.T) {
+ tmpPath := requireTmpCopy(t, "../testdata/sample-rootless-v42.car")
+ subject, err := blockstore.OpenReadWrite(tmpPath, []cid.Cid{})
+ require.EqualError(t, err, "cannot resume on CAR file with version 42")
+ require.Nil(t, subject)
+}
+
+func TestReadWriteResumptionMismatchingRootsIsError(t *testing.T) {
+ tmpPath := requireTmpCopy(t, "../testdata/sample-wrapped-v2.car")
+
+ origContent, err := os.ReadFile(tmpPath)
+ require.NoError(t, err)
+
+ badRoot, err := cid.NewPrefixV1(cid.Raw, multihash.SHA2_256).Sum([]byte("bad root"))
+ require.NoError(t, err)
+
+ subject, err := blockstore.OpenReadWrite(tmpPath, []cid.Cid{badRoot})
+ require.EqualError(t, err, "cannot resume on file with mismatching data header")
+ require.Nil(t, subject)
+
+ newContent, err := os.ReadFile(tmpPath)
+ require.NoError(t, err)
+
+ // Expect the bad file to be left untouched; check the size first.
+ // If the sizes mismatch, printing a huge diff would not help us.
+ require.Equal(t, len(origContent), len(newContent))
+ require.Equal(t, origContent, newContent)
+}
+
+func requireTmpCopy(t *testing.T, src string) string {
+ srcF, err := os.Open(src)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, srcF.Close()) }()
+ stats, err := srcF.Stat()
+ require.NoError(t, err)
+
+ dst := filepath.Join(t.TempDir(), stats.Name())
+ dstF, err := os.Create(dst)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, dstF.Close()) }()
+
+ _, err = io.Copy(dstF, srcF)
+ require.NoError(t, err)
+ return dst
+}
+
+func TestReadWriteResumptionFromFileWithDifferentCarV1PaddingIsError(t *testing.T) {
+ oneTestBlockCid := oneTestBlockWithCidV1.Cid()
+ WantRoots := []cid.Cid{oneTestBlockCid}
+ path := filepath.Join(t.TempDir(), "readwrite-resume-with-padding.car")
+
+ subject, err := blockstore.OpenReadWrite(
+ path,
+ WantRoots,
+ carv2.UseDataPadding(1413))
+ require.NoError(t, err)
+ require.NoError(t, subject.Put(context.TODO(), oneTestBlockWithCidV1))
+ require.NoError(t, subject.Finalize())
+
+ resumingSubject, err := blockstore.OpenReadWrite(
+ path,
+ WantRoots,
+ carv2.UseDataPadding(1314))
+ require.EqualError(t, err, "cannot resume from file with mismatched CARv1 offset; "+
+ "`WithDataPadding` option must match the padding on file. "+
+ "Expected padding value of 1413 but got 1314")
+ require.Nil(t, resumingSubject)
+}
+
+func TestReadWriteErrorAfterClose(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ root := blocks.NewBlock([]byte("foo"))
+ for _, closeMethod := range []func(*blockstore.ReadWrite){
+ (*blockstore.ReadWrite).Discard,
+ func(bs *blockstore.ReadWrite) { bs.Finalize() },
+ } {
+ path := filepath.Join(t.TempDir(), "readwrite.car")
+ bs, err := blockstore.OpenReadWrite(path, []cid.Cid{root.Cid()})
+ require.NoError(t, err)
+
+ err = bs.Put(ctx, root)
+ require.NoError(t, err)
+
+ roots, err := bs.Roots()
+ require.NoError(t, err)
+ _, err = bs.Has(ctx, roots[0])
+ require.NoError(t, err)
+ _, err = bs.Get(ctx, roots[0])
+ require.NoError(t, err)
+ _, err = bs.GetSize(ctx, roots[0])
+ require.NoError(t, err)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ _, err = bs.AllKeysChan(ctx)
+ require.NoError(t, err)
+ cancel() // to stop the AllKeysChan goroutine
+
+ closeMethod(bs)
+
+ _, err = bs.Roots()
+ require.Error(t, err)
+ _, err = bs.Has(ctx, roots[0])
+ require.Error(t, err)
+ _, err = bs.Get(ctx, roots[0])
+ require.Error(t, err)
+ _, err = bs.GetSize(ctx, roots[0])
+ require.Error(t, err)
+ _, err = bs.AllKeysChan(ctx)
+ require.Error(t, err)
+
+ err = bs.Put(ctx, root)
+ require.Error(t, err)
+
+ // TODO: test that closing blocks if an AllKeysChan operation is
+ // in progress.
+ }
+}
+
+func TestOpenReadWrite_WritesIdentityCIDsWhenOptionIsEnabled(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ path := filepath.Join(t.TempDir(), "readwrite-with-id-enabled.car")
+ subject, err := blockstore.OpenReadWrite(path, []cid.Cid{}, carv2.StoreIdentityCIDs(true))
+ require.NoError(t, err)
+
+ data := []byte("fish")
+ idmh, err := multihash.Sum(data, multihash.IDENTITY, -1)
+ require.NoError(t, err)
+ idCid := cid.NewCidV1(uint64(multicodec.Raw), idmh)
+
+ idBlock, err := blocks.NewBlockWithCid(data, idCid)
+ require.NoError(t, err)
+ err = subject.Put(ctx, idBlock)
+ require.NoError(t, err)
+
+ has, err := subject.Has(ctx, idCid)
+ require.NoError(t, err)
+ require.True(t, has)
+
+ gotBlock, err := subject.Get(ctx, idCid)
+ require.NoError(t, err)
+ require.Equal(t, idBlock, gotBlock)
+
+ keysChan, err := subject.AllKeysChan(context.Background())
+ require.NoError(t, err)
+ var i int
+ for c := range keysChan {
+ i++
+ require.Equal(t, idCid, c)
+ }
+ require.Equal(t, 1, i)
+
+ err = subject.Finalize()
+ require.NoError(t, err)
+
+ // Assert resulting CAR file indeed has the IDENTITY block.
+ f, err := os.Open(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, f.Close()) })
+
+ reader, err := carv2.NewBlockReader(f)
+ require.NoError(t, err)
+
+ gotBlock, err = reader.Next()
+ require.NoError(t, err)
+ require.Equal(t, idBlock, gotBlock)
+
+ next, err := reader.Next()
+ require.Equal(t, io.EOF, err)
+ require.Nil(t, next)
+
+ // Assert the id is indexed.
+ r, err := carv2.OpenReader(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, r.Close()) })
+ require.True(t, r.Header.HasIndex())
+
+ ir, err := r.IndexReader()
+ require.NoError(t, err)
+ require.NotNil(t, ir)
+
+ gotIdx, err := index.ReadFrom(ir)
+ require.NoError(t, err)
+
+ // Determine expected offset as the length of header plus one
+ dr, err := r.DataReader()
+ require.NoError(t, err)
+ header, err := carv1.ReadHeader(dr, carv1.DefaultMaxAllowedHeaderSize)
+ require.NoError(t, err)
+ object, err := cbor.DumpObject(header)
+ require.NoError(t, err)
+ expectedOffset := len(object) + 1
+
+ // Assert index is iterable and has exactly one record with expected multihash and offset.
+ switch idx := gotIdx.(type) {
+ case index.IterableIndex:
+ var i int
+ err := idx.ForEach(func(mh multihash.Multihash, offset uint64) error {
+ i++
+ require.Equal(t, idmh, mh)
+ require.Equal(t, uint64(expectedOffset), offset)
+ return nil
+ })
+ require.NoError(t, err)
+ require.Equal(t, 1, i)
+ default:
+ require.Failf(t, "unexpected index type", "wanted %v but got %v", multicodec.CarMultihashIndexSorted, idx.Codec())
+ }
+}
+
+func TestOpenReadWrite_ErrorsWhenWritingTooLargeOfACid(t *testing.T) {
+ maxAllowedCidSize := uint64(2)
+ path := filepath.Join(t.TempDir(), "readwrite-with-id-enabled-too-large.car")
+ subject, err := blockstore.OpenReadWrite(path, []cid.Cid{}, carv2.MaxIndexCidSize(maxAllowedCidSize))
+ t.Cleanup(subject.Discard)
+ require.NoError(t, err)
+
+ data := []byte("monsterlobster")
+ mh, err := multihash.Sum(data, multihash.SHA2_256, -1)
+ require.NoError(t, err)
+ bigCid := cid.NewCidV1(uint64(multicodec.Raw), mh)
+ bigCidLen := uint64(bigCid.ByteLen())
+ require.True(t, bigCidLen > maxAllowedCidSize)
+
+ bigBlock, err := blocks.NewBlockWithCid(data, bigCid)
+ require.NoError(t, err)
+ err = subject.Put(context.TODO(), bigBlock)
+ require.Equal(t, &carv2.ErrCidTooLarge{MaxSize: maxAllowedCidSize, CurrentSize: bigCidLen}, err)
+}
+
+func TestReadWrite_ReWritingCARv1WithIdentityCidIsIdenticalToOriginalWithOptionsEnabled(t *testing.T) {
+ originalCARv1Path := "../testdata/sample-v1.car"
+ originalCarV1, err := os.Open(originalCARv1Path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, originalCarV1.Close()) })
+
+ r, err := carv2.NewBlockReader(originalCarV1)
+ require.NoError(t, err)
+
+ path := filepath.Join(t.TempDir(), "readwrite-from-carv1-with-id-enabled.car")
+ subject, err := blockstore.OpenReadWrite(path, r.Roots, carv2.StoreIdentityCIDs(true))
+ require.NoError(t, err)
+ var idCidCount int
+ for {
+ next, err := r.Next()
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+ if next.Cid().Prefix().MhType == multihash.IDENTITY {
+ idCidCount++
+ }
+ err = subject.Put(context.TODO(), next)
+ require.NoError(t, err)
+ }
+ require.NotZero(t, idCidCount)
+ err = subject.Finalize()
+ require.NoError(t, err)
+
+ v2r, err := carv2.OpenReader(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, v2r.Close()) })
+
+ // Assert characteristics bit is set.
+ require.True(t, v2r.Header.Characteristics.IsFullyIndexed())
+
+ // Assert original CARv1 and generated innter CARv1 payload have the same SHA512 hash
+ // Note, we hash instead of comparing bytes to avoid excessive memory usage when sample CARv1 is large.
+
+ hasher := sha512.New()
+ dr, err := v2r.DataReader()
+ require.NoError(t, err)
+ gotWritten, err := io.Copy(hasher, dr)
+ require.NoError(t, err)
+ gotSum := hasher.Sum(nil)
+
+ hasher.Reset()
+ _, err = originalCarV1.Seek(0, io.SeekStart)
+ require.NoError(t, err)
+ wantWritten, err := io.Copy(hasher, originalCarV1)
+ require.NoError(t, err)
+ wantSum := hasher.Sum(nil)
+
+ require.Equal(t, wantWritten, gotWritten)
+ require.Equal(t, wantSum, gotSum)
+}
+
+func TestReadWriteOpenFile(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ dir := t.TempDir() // auto cleanup
+ f, err := os.CreateTemp(dir, "")
+ require.NoError(t, err)
+
+ root := blocks.NewBlock([]byte("foo"))
+
+ bs, err := blockstore.OpenReadWriteFile(f, []cid.Cid{root.Cid()})
+ require.NoError(t, err)
+
+ err = bs.Put(ctx, root)
+ require.NoError(t, err)
+
+ roots, err := bs.Roots()
+ require.NoError(t, err)
+ _, err = bs.Has(ctx, roots[0])
+ require.NoError(t, err)
+ _, err = bs.Get(ctx, roots[0])
+ require.NoError(t, err)
+ _, err = bs.GetSize(ctx, roots[0])
+ require.NoError(t, err)
+
+ err = bs.Finalize()
+ require.NoError(t, err)
+
+ _, err = f.Seek(0, 0)
+ require.NoError(t, err) // file should not be closed, let the caller do it
+
+ err = f.Close()
+ require.NoError(t, err)
+}
+
+func TestBlockstore_IdentityCidWithEmptyDataIsIndexed(t *testing.T) {
+ p := path.Join(t.TempDir(), "car-id-cid-empty.carv2")
+ var noData []byte
+
+ mh, err := multihash.Sum(noData, multihash.IDENTITY, -1)
+ require.NoError(t, err)
+ w, err := blockstore.OpenReadWrite(p, nil, carv2.StoreIdentityCIDs(true))
+ require.NoError(t, err)
+
+ blk, err := blocks.NewBlockWithCid(noData, cid.NewCidV1(cid.Raw, mh))
+ require.NoError(t, err)
+
+ err = w.Put(context.TODO(), blk)
+ require.NoError(t, err)
+ require.NoError(t, w.Finalize())
+
+ r, err := carv2.OpenReader(p)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, r.Close()) }()
+
+ dr, err := r.DataReader()
+ require.NoError(t, err)
+ header, err := carv1.ReadHeader(dr, carv1.DefaultMaxAllowedHeaderSize)
+ require.NoError(t, err)
+ wantOffset, err := carv1.HeaderSize(header)
+ require.NoError(t, err)
+
+ ir, err := r.IndexReader()
+ require.NoError(t, err)
+ idx, err := index.ReadFrom(ir)
+ require.NoError(t, err)
+
+ itidx, ok := idx.(index.IterableIndex)
+ require.True(t, ok)
+ var count int
+ err = itidx.ForEach(func(m multihash.Multihash, u uint64) error {
+ dm, err := multihash.Decode(m)
+ require.NoError(t, err)
+ require.Equal(t, multicodec.Identity, multicodec.Code(dm.Code))
+ require.Equal(t, 0, dm.Length)
+ require.Empty(t, dm.Digest)
+ require.Equal(t, wantOffset, u)
+ count++
+ return nil
+ })
+ require.NoError(t, err)
+ require.Equal(t, 1, count)
+}
+
+func TestBlockstoreFinalizeReadOnly(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ root := blocks.NewBlock([]byte("foo"))
+
+ p := filepath.Join(t.TempDir(), "readwrite.car")
+ bs, err := blockstore.OpenReadWrite(p, []cid.Cid{root.Cid()})
+ require.NoError(t, err)
+
+ err = bs.Put(ctx, root)
+ require.NoError(t, err)
+
+ roots, err := bs.Roots()
+ require.NoError(t, err)
+ _, err = bs.Has(ctx, roots[0])
+ require.NoError(t, err)
+ _, err = bs.Get(ctx, roots[0])
+ require.NoError(t, err)
+ _, err = bs.GetSize(ctx, roots[0])
+ require.NoError(t, err)
+ _, err = bs.AllKeysChan(ctx)
+ require.NoError(t, err)
+
+ // soft finalize, we can still read, but not write
+ err = bs.FinalizeReadOnly()
+ require.NoError(t, err)
+
+ _, err = bs.Roots()
+ require.NoError(t, err)
+ _, err = bs.Has(ctx, roots[0])
+ require.NoError(t, err)
+ _, err = bs.Get(ctx, roots[0])
+ require.NoError(t, err)
+ _, err = bs.GetSize(ctx, roots[0])
+ require.NoError(t, err)
+ _, err = bs.AllKeysChan(ctx)
+ require.NoError(t, err)
+
+ err = bs.Put(ctx, root)
+ require.Error(t, err)
+
+ // final close, nothing works anymore
+ err = bs.Close()
+ require.NoError(t, err)
+
+ _, err = bs.Roots()
+ require.Error(t, err)
+ _, err = bs.Has(ctx, roots[0])
+ require.Error(t, err)
+ _, err = bs.Get(ctx, roots[0])
+ require.Error(t, err)
+ _, err = bs.GetSize(ctx, roots[0])
+ require.Error(t, err)
+ _, err = bs.AllKeysChan(ctx)
+ require.Error(t, err)
+
+ err = bs.Put(ctx, root)
+ require.Error(t, err)
+}
+
+func TestWholeCID(t *testing.T) {
+ for _, whole := range []bool{true, false} {
+ whole := whole
+ t.Run(fmt.Sprintf("whole=%t", whole), func(t *testing.T) {
+ t.Parallel()
+ ctx := context.Background()
+ path := filepath.Join(t.TempDir(), fmt.Sprintf("writable_%t.car", whole))
+ rw, err := blockstore.OpenReadWrite(path, []cid.Cid{}, carv2.UseWholeCIDs(whole))
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, rw.Finalize()) })
+
+ require.NoError(t, rw.Put(ctx, oneTestBlockWithCidV1))
+ has, err := rw.Has(ctx, oneTestBlockWithCidV1.Cid())
+ require.NoError(t, err)
+ require.True(t, has)
+
+ pref := oneTestBlockWithCidV1.Cid().Prefix()
+ pref.Codec = cid.DagCBOR
+ pref.Version = 1
+ cpb1, err := pref.Sum(oneTestBlockWithCidV1.RawData())
+ require.NoError(t, err)
+
+ has, err = rw.Has(ctx, cpb1)
+ require.NoError(t, err)
+ require.Equal(t, has, !whole)
+
+ require.NoError(t, rw.Put(ctx, anotherTestBlockWithCidV0))
+ has, err = rw.Has(ctx, anotherTestBlockWithCidV0.Cid())
+ require.NoError(t, err)
+ require.True(t, has)
+ has, err = rw.Has(ctx, cpb1)
+ require.NoError(t, err)
+ require.Equal(t, has, !whole)
+
+ pref = anotherTestBlockWithCidV0.Cid().Prefix()
+ pref.Codec = cid.DagJSON
+ pref.Version = 1
+ cpb2, err := pref.Sum(anotherTestBlockWithCidV0.RawData())
+ require.NoError(t, err)
+
+ has, err = rw.Has(ctx, cpb2)
+ require.NoError(t, err)
+ require.Equal(t, has, !whole)
+ has, err = rw.Has(ctx, cpb1)
+ require.NoError(t, err)
+ require.Equal(t, has, !whole)
+ })
+ }
+}
+
+func TestReadWriteIndex(t *testing.T) {
+ tmpPath := requireTmpCopy(t, "../testdata/sample-wrapped-v2.car")
+
+ root := cid.MustParse("bafy2bzaced4ueelaegfs5fqu4tzsh6ywbbpfk3cxppupmxfdhbpbhzawfw5oy")
+ subject, err := blockstore.OpenReadWrite(tmpPath, []cid.Cid{root})
+ require.NoError(t, err)
+
+ defer func() {
+ err = subject.Finalize()
+ require.NoError(t, err)
+ }()
+
+ var wantCids []cid.Cid
+ var wantMh []multihash.Multihash
+ ch, err := subject.AllKeysChan(context.Background())
+ require.NoError(t, err)
+ for c := range ch {
+ wantCids = append(wantCids, c)
+ wantMh = append(wantMh, c.Hash())
+ }
+
+ idx := subject.Index()
+
+ for _, c := range wantCids {
+ _, err = index.GetFirst(idx, c)
+ require.NoError(t, err)
+ }
+
+ if idx, ok := idx.(index.IterableIndex); ok {
+ var got []multihash.Multihash
+ err = idx.ForEach(func(m multihash.Multihash, u uint64) error {
+ got = append(got, m)
+ return nil
+ })
+ require.NoError(t, err)
+ require.ElementsMatch(t, wantMh, got)
+ }
+}
diff --git a/ipld/car/v2/car.go b/ipld/car/v2/car.go
new file mode 100644
index 0000000000..571eb1140b
--- /dev/null
+++ b/ipld/car/v2/car.go
@@ -0,0 +1,193 @@
+package car
+
+import (
+ "encoding/binary"
+ "fmt"
+ "io"
+)
+
+const (
+ // PragmaSize is the size of the CARv2 pragma in bytes.
+ PragmaSize = 11
+ // HeaderSize is the fixed size of CARv2 header in number of bytes.
+ HeaderSize = 40
+ // CharacteristicsSize is the fixed size of Characteristics bitfield within CARv2 header in number of bytes.
+ CharacteristicsSize = 16
+)
+
+// The pragma of a CARv2, containing the version number.
+// This is a valid CARv1 header, with version number of 2 and no root CIDs.
+var Pragma = []byte{
+ 0x0a, // unit(10)
+ 0xa1, // map(1)
+ 0x67, // string(7)
+ 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, // "version"
+ 0x02, // uint(2)
+}
+
+type (
+ // Header represents the CARv2 header/pragma.
+ Header struct {
+ // 128-bit characteristics of this CARv2 file, such as order, deduplication, etc. Reserved for future use.
+ Characteristics Characteristics
+ // The byte-offset from the beginning of the CARv2 to the first byte of the CARv1 data payload.
+ DataOffset uint64
+ // The byte-length of the CARv1 data payload.
+ DataSize uint64
+ // 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.
+ IndexOffset uint64
+ }
+ // Characteristics is a bitfield placeholder for capturing the characteristics of a CARv2 such as order and determinism.
+ Characteristics struct {
+ Hi uint64
+ Lo uint64
+ }
+)
+
+// fullyIndexedCharPos is the position of Characteristics.Hi bit that specifies whether the index is a catalog af all CIDs or not.
+const fullyIndexedCharPos = 7 // left-most bit
+
+// WriteTo writes this characteristics to the given w.
+func (c Characteristics) WriteTo(w io.Writer) (n int64, err error) {
+ buf := make([]byte, 16)
+ binary.LittleEndian.PutUint64(buf[:8], c.Hi)
+ binary.LittleEndian.PutUint64(buf[8:], c.Lo)
+ written, err := w.Write(buf)
+ return int64(written), err
+}
+
+func (c *Characteristics) ReadFrom(r io.Reader) (int64, error) {
+ buf := make([]byte, CharacteristicsSize)
+ read, err := io.ReadFull(r, buf)
+ n := int64(read)
+ if err != nil {
+ return n, err
+ }
+ c.Hi = binary.LittleEndian.Uint64(buf[:8])
+ c.Lo = binary.LittleEndian.Uint64(buf[8:])
+ return n, nil
+}
+
+// IsFullyIndexed specifies whether the index of CARv2 represents a catalog of all CID segments.
+// See StoreIdentityCIDs
+func (c *Characteristics) IsFullyIndexed() bool {
+ return isBitSet(c.Hi, fullyIndexedCharPos)
+}
+
+// SetFullyIndexed sets whether of CARv2 represents a catalog of all CID segments.
+func (c *Characteristics) SetFullyIndexed(b bool) {
+ if b {
+ c.Hi = setBit(c.Hi, fullyIndexedCharPos)
+ } else {
+ c.Hi = unsetBit(c.Hi, fullyIndexedCharPos)
+ }
+}
+
+func setBit(n uint64, pos uint) uint64 {
+ n |= 1 << pos
+ return n
+}
+
+func unsetBit(n uint64, pos uint) uint64 {
+ mask := uint64(^(1 << pos))
+ n &= mask
+ return n
+}
+
+func isBitSet(n uint64, pos uint) bool {
+ bit := n & (1 << pos)
+ return bit > 0
+}
+
+// NewHeader instantiates a new CARv2 header, given the data size.
+func NewHeader(dataSize uint64) Header {
+ header := Header{
+ DataSize: dataSize,
+ }
+ header.DataOffset = PragmaSize + HeaderSize
+ header.IndexOffset = header.DataOffset + dataSize
+ return header
+}
+
+// WithIndexPadding sets the index offset from the beginning of the file for this header and returns
+// the header for convenient chained calls.
+// The index offset is calculated as the sum of PragmaSize, HeaderSize,
+// Header.DataSize, and the given padding.
+func (h Header) WithIndexPadding(padding uint64) Header {
+ h.IndexOffset = h.IndexOffset + padding
+ return h
+}
+
+// WithDataPadding sets the data payload byte-offset from the beginning of the file for this header
+// and returns the header for convenient chained calls.
+// The Data offset is calculated as the sum of PragmaSize, HeaderSize and the given padding.
+// The call to this function also shifts the Header.IndexOffset forward by the given padding.
+func (h Header) WithDataPadding(padding uint64) Header {
+ h.DataOffset = PragmaSize + HeaderSize + padding
+ h.IndexOffset = h.IndexOffset + padding
+ return h
+}
+
+func (h Header) WithDataSize(size uint64) Header {
+ h.DataSize = size
+ h.IndexOffset = size + h.IndexOffset
+ return h
+}
+
+// HasIndex indicates whether the index is present.
+func (h Header) HasIndex() bool {
+ return h.IndexOffset != 0
+}
+
+// WriteTo serializes this header as bytes and writes them using the given io.Writer.
+func (h Header) WriteTo(w io.Writer) (n int64, err error) {
+ wn, err := h.Characteristics.WriteTo(w)
+ n += wn
+ if err != nil {
+ return
+ }
+ buf := make([]byte, 24)
+ binary.LittleEndian.PutUint64(buf[:8], h.DataOffset)
+ binary.LittleEndian.PutUint64(buf[8:16], h.DataSize)
+ binary.LittleEndian.PutUint64(buf[16:], h.IndexOffset)
+ written, err := w.Write(buf)
+ n += int64(written)
+ return n, err
+}
+
+// ReadFrom populates fields of this header from the given r.
+func (h *Header) ReadFrom(r io.Reader) (int64, error) {
+ n, err := h.Characteristics.ReadFrom(r)
+ if err != nil {
+ return n, err
+ }
+ buf := make([]byte, 24)
+ read, err := io.ReadFull(r, buf)
+ n += int64(read)
+ if err != nil {
+ return n, err
+ }
+ dataOffset := binary.LittleEndian.Uint64(buf[:8])
+ dataSize := binary.LittleEndian.Uint64(buf[8:16])
+ indexOffset := binary.LittleEndian.Uint64(buf[16:])
+ // Assert the data payload offset validity.
+ // It must be at least 51 ( + ).
+ if int64(dataOffset) < PragmaSize+HeaderSize {
+ return n, fmt.Errorf("invalid data payload offset: %v", dataOffset)
+ }
+ // Assert the data size validity.
+ // It must be larger than zero.
+ // Technically, it should be at least 11 bytes (i.e. a valid CARv1 header with no roots) but
+ // we let further parsing of the header to signal invalid data payload header.
+ if int64(dataSize) <= 0 {
+ return n, fmt.Errorf("invalid data payload size: %v", dataSize)
+ }
+ // Assert the index offset validity.
+ if int64(indexOffset) < 0 {
+ return n, fmt.Errorf("invalid index offset: %v", indexOffset)
+ }
+ h.DataOffset = dataOffset
+ h.DataSize = dataSize
+ h.IndexOffset = indexOffset
+ return n, nil
+}
diff --git a/ipld/car/v2/car_test.go b/ipld/car/v2/car_test.go
new file mode 100644
index 0000000000..c5d35063c7
--- /dev/null
+++ b/ipld/car/v2/car_test.go
@@ -0,0 +1,247 @@
+package car_test
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCarV2PragmaLength(t *testing.T) {
+ tests := []struct {
+ name string
+ want interface{}
+ got interface{}
+ }{
+ {
+ "ActualSizeShouldBe11",
+ 11,
+ len(carv2.Pragma),
+ },
+ {
+ "ShouldStartWithVarint(10)",
+ carv2.Pragma[0],
+ 10,
+ },
+ }
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ assert.EqualValues(t, tt.want, tt.got, "CarV2Pragma got = %v, want %v", tt.got, tt.want)
+ })
+ }
+}
+
+func TestCarV2PragmaIsValidCarV1Header(t *testing.T) {
+ v1h, err := carv1.ReadHeader(bytes.NewReader(carv2.Pragma), carv1.DefaultMaxAllowedHeaderSize)
+ assert.NoError(t, err, "cannot decode pragma as CBOR with CARv1 header structure")
+ assert.Equal(t, &carv1.CarHeader{
+ Roots: nil,
+ Version: 2,
+ }, v1h, "CARv2 pragma must be a valid CARv1 header")
+}
+
+func TestHeader_WriteTo(t *testing.T) {
+ tests := []struct {
+ name string
+ target carv2.Header
+ wantWrite []byte
+ wantErr bool
+ }{
+ {
+ "HeaderWithEmptyCharacteristicsIsWrittenAsExpected",
+ carv2.Header{
+ Characteristics: carv2.Characteristics{},
+ DataOffset: 99,
+ },
+ []byte{
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x63, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ },
+ false,
+ },
+ {
+ "NonEmptyHeaderIsWrittenAsExpected",
+ carv2.Header{
+ Characteristics: carv2.Characteristics{
+ Hi: 1001, Lo: 1002,
+ },
+ DataOffset: 99,
+ DataSize: 100,
+ IndexOffset: 101,
+ },
+ []byte{
+ 0xe9, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xea, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x63, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x64, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x65, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ },
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ buf := &bytes.Buffer{}
+ written, err := tt.target.WriteTo(buf)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("WriteTo() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ gotWrite := buf.Bytes()
+ assert.Equal(t, tt.wantWrite, gotWrite, "Header.WriteTo() gotWrite = %v, wantWrite %v", gotWrite, tt.wantWrite)
+ assert.EqualValues(t, carv2.HeaderSize, uint64(len(gotWrite)), "WriteTo() CARv2 header length must always be %v bytes long", carv2.HeaderSize)
+ assert.EqualValues(t, carv2.HeaderSize, uint64(written), "WriteTo() CARv2 header byte count must always be %v bytes long", carv2.HeaderSize)
+ })
+ }
+}
+
+func TestHeader_ReadFrom(t *testing.T) {
+ tests := []struct {
+ name string
+ target []byte
+ wantHeader carv2.Header
+ wantErr bool
+ }{
+ {
+ "HeaderWithEmptyCharacteristicsIsWrittenAsExpected",
+ []byte{
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x63, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x64, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ },
+ carv2.Header{
+ Characteristics: carv2.Characteristics{},
+ DataOffset: 99,
+ DataSize: 100,
+ },
+ false,
+ },
+ {
+ "NonEmptyHeaderIsWrittenAsExpected",
+
+ []byte{
+ 0xe9, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0xea, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x63, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x64, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x65, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ },
+ carv2.Header{
+ Characteristics: carv2.Characteristics{
+ Hi: 1001, Lo: 1002,
+ },
+ DataOffset: 99,
+ DataSize: 100,
+ IndexOffset: 101,
+ },
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ gotHeader := carv2.Header{}
+ gotRead, err := gotHeader.ReadFrom(bytes.NewReader(tt.target))
+ assert.NoError(t, err)
+ assert.Equal(t, int64(carv2.HeaderSize), gotRead)
+ assert.Equal(t, tt.wantHeader, gotHeader)
+ })
+ }
+}
+
+func TestHeader_WithPadding(t *testing.T) {
+ tests := []struct {
+ name string
+ subject carv2.Header
+ wantCarV1Offset uint64
+ wantIndexOffset uint64
+ }{
+ {
+ "WhenNoPaddingOffsetsAreSumOfSizes",
+ carv2.NewHeader(123),
+ carv2.PragmaSize + carv2.HeaderSize,
+ carv2.PragmaSize + carv2.HeaderSize + 123,
+ },
+ {
+ "WhenOnlyPaddingCarV1BothOffsetsShift",
+ carv2.NewHeader(123).WithDataPadding(3),
+ carv2.PragmaSize + carv2.HeaderSize + 3,
+ carv2.PragmaSize + carv2.HeaderSize + 3 + 123,
+ },
+ {
+ "WhenPaddingBothCarV1AndIndexBothOffsetsShiftWithAdditionalIndexShift",
+ carv2.NewHeader(123).WithDataPadding(3).WithIndexPadding(7),
+ carv2.PragmaSize + carv2.HeaderSize + 3,
+ carv2.PragmaSize + carv2.HeaderSize + 3 + 123 + 7,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.EqualValues(t, tt.wantCarV1Offset, tt.subject.DataOffset)
+ assert.EqualValues(t, tt.wantIndexOffset, tt.subject.IndexOffset)
+ })
+ }
+}
+
+func TestNewHeaderHasExpectedValues(t *testing.T) {
+ wantCarV1Len := uint64(1413)
+ want := carv2.Header{
+ Characteristics: carv2.Characteristics{},
+ DataOffset: carv2.PragmaSize + carv2.HeaderSize,
+ DataSize: wantCarV1Len,
+ IndexOffset: carv2.PragmaSize + carv2.HeaderSize + wantCarV1Len,
+ }
+ got := carv2.NewHeader(wantCarV1Len)
+ assert.Equal(t, want, got, "NewHeader got = %v, want = %v", got, want)
+}
+
+func TestCharacteristics_StoreIdentityCIDs(t *testing.T) {
+ subject := carv2.Characteristics{}
+ require.False(t, subject.IsFullyIndexed())
+
+ subject.SetFullyIndexed(true)
+ require.True(t, subject.IsFullyIndexed())
+
+ var buf bytes.Buffer
+ written, err := subject.WriteTo(&buf)
+ require.NoError(t, err)
+ require.Equal(t, int64(16), written)
+ require.Equal(t, []byte{
+ 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ }, buf.Bytes())
+
+ var decodedSubject carv2.Characteristics
+ read, err := decodedSubject.ReadFrom(&buf)
+ require.NoError(t, err)
+ require.Equal(t, int64(16), read)
+ require.True(t, decodedSubject.IsFullyIndexed())
+
+ buf.Reset()
+ subject.SetFullyIndexed(false)
+ require.False(t, subject.IsFullyIndexed())
+
+ written, err = subject.WriteTo(&buf)
+ require.NoError(t, err)
+ require.Equal(t, int64(16), written)
+ require.Equal(t, []byte{
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ }, buf.Bytes())
+
+ var decodedSubjectAgain carv2.Characteristics
+ read, err = decodedSubjectAgain.ReadFrom(&buf)
+ require.NoError(t, err)
+ require.Equal(t, int64(16), read)
+ require.False(t, decodedSubjectAgain.IsFullyIndexed())
+}
diff --git a/ipld/car/v2/doc.go b/ipld/car/v2/doc.go
new file mode 100644
index 0000000000..b12266649a
--- /dev/null
+++ b/ipld/car/v2/doc.go
@@ -0,0 +1,8 @@
+// Package car allows inspecting and reading CAR files,
+// described at https://ipld.io/specs/transport/car/.
+// The entire library is geared towards the CARv2 spec,
+// but many of the APIs consuming CAR files also accept CARv1.
+//
+// The blockstore sub-package contains an implementation of the
+// go-ipfs-blockstore interface.
+package car
diff --git a/ipld/car/v2/errors.go b/ipld/car/v2/errors.go
new file mode 100644
index 0000000000..ee89e0b25a
--- /dev/null
+++ b/ipld/car/v2/errors.go
@@ -0,0 +1,18 @@
+package car
+
+import (
+ "fmt"
+)
+
+var _ (error) = (*ErrCidTooLarge)(nil)
+
+// ErrCidTooLarge signals that a CID is too large to include in CARv2 index.
+// See: MaxIndexCidSize.
+type ErrCidTooLarge struct {
+ MaxSize uint64
+ CurrentSize uint64
+}
+
+func (e *ErrCidTooLarge) Error() string {
+ return fmt.Sprintf("cid size is larger than max allowed (%d > %d)", e.CurrentSize, e.MaxSize)
+}
diff --git a/ipld/car/v2/errors_test.go b/ipld/car/v2/errors_test.go
new file mode 100644
index 0000000000..56e2c7a095
--- /dev/null
+++ b/ipld/car/v2/errors_test.go
@@ -0,0 +1,12 @@
+package car
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestNewErrCidTooLarge_ErrorContainsSizes(t *testing.T) {
+ subject := &ErrCidTooLarge{MaxSize: 1413, CurrentSize: 1414}
+ require.EqualError(t, subject, "cid size is larger than max allowed (1414 > 1413)")
+}
diff --git a/ipld/car/v2/example_test.go b/ipld/car/v2/example_test.go
new file mode 100644
index 0000000000..8b9288ebb1
--- /dev/null
+++ b/ipld/car/v2/example_test.go
@@ -0,0 +1,133 @@
+package car_test
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/blockstore"
+)
+
+func ExampleWrapV1File() {
+ // We have a sample CARv1 file.
+ // Wrap it as-is in a CARv2, with an index.
+ // Writing the result to testdata allows reusing that file in other tests,
+ // and also helps ensure that the result is deterministic.
+ src := "testdata/sample-v1.car"
+ tdir, err := os.MkdirTemp(os.TempDir(), "example-*")
+ if err != nil {
+ panic(err)
+ }
+ dst := filepath.Join(tdir, "wrapped-v2.car")
+ if err := carv2.WrapV1File(src, dst); err != nil {
+ panic(err)
+ }
+
+ // Open our new CARv2 file and show some info about it.
+ cr, err := carv2.OpenReader(dst)
+ if err != nil {
+ panic(err)
+ }
+ defer func() {
+ if err := cr.Close(); err != nil {
+ panic(err)
+ }
+ }()
+
+ roots, err := cr.Roots()
+ if err != nil {
+ panic(err)
+ }
+ fmt.Println("Roots:", roots)
+ fmt.Println("Has index:", cr.Header.HasIndex())
+
+ // Verify that the CARv1 remains exactly the same.
+ orig, err := os.ReadFile(src)
+ if err != nil {
+ panic(err)
+ }
+ dr, err := cr.DataReader()
+ if err != nil {
+ panic(err)
+ }
+ inner, err := io.ReadAll(dr)
+ if err != nil {
+ panic(err)
+ }
+ fmt.Println("Inner CARv1 is exactly the same:", bytes.Equal(orig, inner))
+
+ // Verify that the CARv2 works well with its index.
+ bs, err := blockstore.OpenReadOnly(dst)
+ if err != nil {
+ panic(err)
+ }
+ fmt.Println(bs.Get(context.TODO(), roots[0]))
+
+ // Output:
+ // Roots: [bafy2bzaced4ueelaegfs5fqu4tzsh6ywbbpfk3cxppupmxfdhbpbhzawfw5oy]
+ // Has index: true
+ // Inner CARv1 is exactly the same: true
+ // [Block bafy2bzaced4ueelaegfs5fqu4tzsh6ywbbpfk3cxppupmxfdhbpbhzawfw5oy]
+}
+
+// ExampleNewBlockReader instantiates a new BlockReader for a CARv1 file and its wrapped CARv2
+// version. For each file, it prints the version, the root CIDs and the first five block CIDs.
+// Note, the roots and first five block CIDs are identical in both files since both represent the
+// same root CIDs and data blocks.
+func ExampleNewBlockReader() {
+ for _, path := range []string{
+ "testdata/sample-v1.car",
+ "testdata/sample-wrapped-v2.car",
+ } {
+ fmt.Println("File:", path)
+ f, err := os.Open(path)
+ if err != nil {
+ panic(err)
+ }
+ br, err := carv2.NewBlockReader(f)
+ if err != nil {
+ panic(err)
+ }
+ defer func() {
+ if err := f.Close(); err != nil {
+ panic(err)
+ }
+ }()
+ fmt.Println("Version:", br.Version)
+ fmt.Println("Roots:", br.Roots)
+ fmt.Println("First 5 block CIDs:")
+ for i := 0; i < 5; i++ {
+ bl, err := br.Next()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("\t%v\n", bl.Cid())
+ }
+ }
+ // Output:
+ // File: testdata/sample-v1.car
+ // Version: 1
+ // Roots: [bafy2bzaced4ueelaegfs5fqu4tzsh6ywbbpfk3cxppupmxfdhbpbhzawfw5oy]
+ // First 5 block CIDs:
+ // bafy2bzaced4ueelaegfs5fqu4tzsh6ywbbpfk3cxppupmxfdhbpbhzawfw5oy
+ // bafy2bzaceaycv7jhaegckatnncu5yugzkrnzeqsppzegufr35lroxxnsnpspu
+ // bafy2bzaceb62wdepofqu34afqhbcn4a7jziwblt2ih5hhqqm6zitd3qpzhdp4
+ // bafy2bzaceb3utcspm5jqcdqpih3ztbaztv7yunzkiyfq7up7xmokpxemwgu5u
+ // bafy2bzacedjwekyjresrwjqj4n2r5bnuuu3klncgjo2r3slsp6wgqb37sz4ck
+ // File: testdata/sample-wrapped-v2.car
+ // Version: 2
+ // Roots: [bafy2bzaced4ueelaegfs5fqu4tzsh6ywbbpfk3cxppupmxfdhbpbhzawfw5oy]
+ // First 5 block CIDs:
+ // bafy2bzaced4ueelaegfs5fqu4tzsh6ywbbpfk3cxppupmxfdhbpbhzawfw5oy
+ // bafy2bzaceaycv7jhaegckatnncu5yugzkrnzeqsppzegufr35lroxxnsnpspu
+ // bafy2bzaceb62wdepofqu34afqhbcn4a7jziwblt2ih5hhqqm6zitd3qpzhdp4
+ // bafy2bzaceb3utcspm5jqcdqpih3ztbaztv7yunzkiyfq7up7xmokpxemwgu5u
+ // bafy2bzacedjwekyjresrwjqj4n2r5bnuuu3klncgjo2r3slsp6wgqb37sz4ck
+}
diff --git a/ipld/car/v2/fuzz_test.go b/ipld/car/v2/fuzz_test.go
new file mode 100644
index 0000000000..30ec4ff43a
--- /dev/null
+++ b/ipld/car/v2/fuzz_test.go
@@ -0,0 +1,141 @@
+//go:build go1.18
+
+package car_test
+
+import (
+ "bytes"
+ "encoding/hex"
+ "io"
+ "os"
+ "path/filepath"
+ "testing"
+
+ car "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+)
+
+// v1FixtureStr is a clean carv1 single-block, single-root CAR
+const v1FixtureStr = "3aa265726f6f747381d82a58250001711220151fe9e73c6267a7060c6f6c4cca943c236f4b196723489608edb42a8b8fa80b6776657273696f6e012c01711220151fe9e73c6267a7060c6f6c4cca943c236f4b196723489608edb42a8b8fa80ba165646f646779f5"
+
+func seedWithCarFiles(f *testing.F) {
+ fixture, err := hex.DecodeString(v1FixtureStr)
+ if err != nil {
+ f.Fatal(err)
+ }
+ f.Add(fixture)
+ files, err := filepath.Glob("testdata/*.car")
+ if err != nil {
+ f.Fatal(err)
+ }
+ for _, fname := range files {
+ func() {
+ file, err := os.Open(fname)
+ if err != nil {
+ f.Fatal(err)
+ }
+ defer file.Close()
+ data, err := io.ReadAll(file)
+ if err != nil {
+ f.Fatal(err)
+ }
+ f.Add(data)
+ }()
+ }
+}
+
+func FuzzBlockReader(f *testing.F) {
+ seedWithCarFiles(f)
+
+ f.Fuzz(func(t *testing.T, data []byte) {
+ r, err := car.NewBlockReader(bytes.NewReader(data))
+ if err != nil {
+ return
+ }
+
+ for {
+ _, err = r.Next()
+ if err == io.EOF {
+ return
+ }
+ }
+ })
+}
+
+func FuzzReader(f *testing.F) {
+ seedWithCarFiles(f)
+
+ f.Fuzz(func(t *testing.T, data []byte) {
+ subject, err := car.NewReader(bytes.NewReader(data))
+ if err != nil {
+ return
+ }
+
+ subject.Roots()
+ _, err = subject.IndexReader()
+ if err != nil {
+ return
+ }
+ dr, err := subject.DataReader()
+ if err != nil {
+ return
+ }
+ car.GenerateIndex(dr)
+ })
+}
+
+func FuzzInspect(f *testing.F) {
+ seedWithCarFiles(f)
+
+ f.Fuzz(func(t *testing.T, data []byte) {
+ reader, err := car.NewReader(bytes.NewReader(data))
+ if err != nil {
+ return
+ }
+
+ // Do differential fuzzing between Inspect and the normal parser
+ _, inspectErr := reader.Inspect(true)
+ if inspectErr == nil {
+ return
+ }
+
+ reader, err = car.NewReader(bytes.NewReader(data))
+ if err != nil {
+ t.Fatal("second NewReader on same data failed", err.Error())
+ }
+ i, err := reader.IndexReader()
+ if err != nil {
+ return
+ }
+ // FIXME: Once indexes are safe to parse, do not skip .car with index in the differential fuzzing.
+ if i == nil {
+ return
+ }
+ dr, err := reader.DataReader()
+ if err != nil {
+ return
+ }
+
+ _, err = carv1.ReadHeader(dr, carv1.DefaultMaxAllowedHeaderSize)
+ if err != nil {
+ return
+ }
+
+ blocks, err := car.NewBlockReader(dr)
+ if err != nil {
+ return
+ }
+
+ for {
+ _, err := blocks.Next()
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ // caught error as expected
+ return
+ }
+ }
+
+ t.Fatal("Inspect found error but we red this file correctly:", inspectErr.Error())
+ })
+}
diff --git a/ipld/car/v2/index/doc.go b/ipld/car/v2/index/doc.go
new file mode 100644
index 0000000000..b8062cc186
--- /dev/null
+++ b/ipld/car/v2/index/doc.go
@@ -0,0 +1,6 @@
+// package index provides indexing functionality for CARv1 data payload represented as a mapping of
+// CID to offset. This can then be used to implement random access over a CARv1.
+//
+// Index can be written or read using the following static functions: index.WriteTo and
+// index.ReadFrom.
+package index
diff --git a/ipld/car/v2/index/errors.go b/ipld/car/v2/index/errors.go
new file mode 100644
index 0000000000..1a10a98467
--- /dev/null
+++ b/ipld/car/v2/index/errors.go
@@ -0,0 +1,6 @@
+package index
+
+import "errors"
+
+// ErrNotFound signals a record is not found in the index.
+var ErrNotFound = errors.New("not found")
diff --git a/ipld/car/v2/index/example_test.go b/ipld/car/v2/index/example_test.go
new file mode 100644
index 0000000000..bef2e2e01a
--- /dev/null
+++ b/ipld/car/v2/index/example_test.go
@@ -0,0 +1,113 @@
+package index_test
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "reflect"
+
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+)
+
+// ExampleReadFrom unmarshalls an index from an indexed CARv2 file, and for each root CID prints the
+// offset at which its corresponding block starts relative to the wrapped CARv1 data payload.
+func ExampleReadFrom() {
+ // Open the CARv2 file
+ cr, err := carv2.OpenReader("../testdata/sample-wrapped-v2.car")
+ if err != nil {
+ panic(err)
+ }
+ defer cr.Close()
+
+ // Get root CIDs in the CARv1 file.
+ roots, err := cr.Roots()
+ if err != nil {
+ panic(err)
+ }
+
+ // Read and unmarshall index within CARv2 file.
+ ir, err := cr.IndexReader()
+ if err != nil {
+ panic(err)
+ }
+ idx, err := index.ReadFrom(ir)
+ if err != nil {
+ panic(err)
+ }
+
+ // For each root CID print the offset relative to CARv1 data payload.
+ for _, r := range roots {
+ offset, err := index.GetFirst(idx, r)
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("Frame with CID %v starts at offset %v relative to CARv1 data payload.\n", r, offset)
+ }
+
+ // Output:
+ // Frame with CID bafy2bzaced4ueelaegfs5fqu4tzsh6ywbbpfk3cxppupmxfdhbpbhzawfw5oy starts at offset 61 relative to CARv1 data payload.
+}
+
+// ExampleWriteTo unmarshalls an index from an indexed CARv2 file, and stores it as a separate
+// file on disk.
+func ExampleWriteTo() {
+ // Open the CARv2 file
+ src := "../testdata/sample-wrapped-v2.car"
+ cr, err := carv2.OpenReader(src)
+ if err != nil {
+ panic(err)
+ }
+ defer func() {
+ if err := cr.Close(); err != nil {
+ panic(err)
+ }
+ }()
+
+ // Read and unmarshall index within CARv2 file.
+ ir, err := cr.IndexReader()
+ if err != nil {
+ panic(err)
+ }
+ idx, err := index.ReadFrom(ir)
+ if err != nil {
+ panic(err)
+ }
+
+ // Store the index alone onto destination file.
+ f, err := os.CreateTemp(os.TempDir(), "example-index-*.carindex")
+ if err != nil {
+ panic(err)
+ }
+ defer func() {
+ if err := f.Close(); err != nil {
+ panic(err)
+ }
+ }()
+ _, err = index.WriteTo(idx, f)
+ if err != nil {
+ panic(err)
+ }
+
+ // Seek to the beginning of tile to read it back.
+ _, err = f.Seek(0, io.SeekStart)
+ if err != nil {
+ panic(err)
+ }
+
+ // Read and unmarshall the destination file as a separate index instance.
+ reReadIdx, err := index.ReadFrom(f)
+ if err != nil {
+ panic(err)
+ }
+
+ // Expect indices to be equal.
+ if reflect.DeepEqual(idx, reReadIdx) {
+ fmt.Printf("Saved index file matches the index embedded in CARv2 at %v.\n", src)
+ } else {
+ panic("expected to get the same index as the CARv2 file")
+ }
+
+ // Output:
+ // Saved index file matches the index embedded in CARv2 at ../testdata/sample-wrapped-v2.car.
+}
diff --git a/ipld/car/v2/index/index.go b/ipld/car/v2/index/index.go
new file mode 100644
index 0000000000..9a5e0d0841
--- /dev/null
+++ b/ipld/car/v2/index/index.go
@@ -0,0 +1,160 @@
+package index
+
+import (
+ "encoding/binary"
+ "fmt"
+ "io"
+
+ internalio "github.com/ipfs/boxo/ipld/car/v2/internal/io"
+ "github.com/ipfs/go-cid"
+
+ "github.com/multiformats/go-multicodec"
+ "github.com/multiformats/go-multihash"
+ "github.com/multiformats/go-varint"
+)
+
+// CarIndexNone is a sentinel value used as a multicodec code for the index indicating no index.
+const CarIndexNone = 0x300000
+
+type (
+ // Record is a pre-processed record of a car item and location.
+ Record struct {
+ cid.Cid
+ Offset uint64
+ }
+
+ // Index provides an interface for looking up byte offset of a given CID.
+ //
+ // Note that each indexing mechanism is free to match CIDs however it
+ // sees fit. For example, multicodec.CarIndexSorted only indexes
+ // multihash digests, meaning that Get and GetAll will find matching
+ // blocks even if the CID's encoding multicodec differs. Other index
+ // implementations might index the entire CID, the entire multihash, or
+ // just part of a multihash's digest.
+ //
+ // See: multicodec.CarIndexSorted, multicodec.CarMultihashIndexSorted
+ Index interface {
+ // Codec provides the multicodec code that the index implements.
+ //
+ // Note that this may return a reserved code if the index
+ // implementation is not defined in a spec.
+ Codec() multicodec.Code
+
+ // Marshal encodes the index in serial form.
+ Marshal(w io.Writer) (uint64, error)
+
+ // Unmarshal decodes the index from its serial form.
+ // Note, this function will copy the entire index into memory.
+ //
+ // Do not unmarshal index from untrusted CARv2 files. Instead, the index should be
+ // regenerated from the CARv2 data payload.
+ Unmarshal(r io.Reader) error
+
+ // Load inserts a number of records into the index.
+ // Note that Index will load all given records. Any filtering of the records such as
+ // exclusion of CIDs with multihash.IDENTITY code must occur prior to calling this function.
+ //
+ // Further, the actual information extracted and indexed from the given records entirely
+ // depends on the concrete index implementation.
+ // For example, some index implementations may only store partial multihashes.
+ Load([]Record) error
+
+ // GetAll looks up all blocks matching a given CID,
+ // calling a function for each one of their offsets.
+ //
+ // GetAll stops if the given function returns false,
+ // or there are no more offsets; whichever happens first.
+ //
+ // If no error occurred and the CID isn't indexed,
+ // meaning that no callbacks happen,
+ // ErrNotFound is returned.
+ GetAll(cid.Cid, func(uint64) bool) error
+ }
+
+ // IterableIndex is an index which support iterating over it's elements
+ IterableIndex interface {
+ Index
+
+ // ForEach takes a callback function that will be called
+ // on each entry in the index. The arguments to the callback are
+ // the multihash of the element, and the offset in the car file
+ // where the element appears.
+ //
+ // If the callback returns a non-nil error, the iteration is aborted,
+ // and the ForEach function returns the error to the user.
+ //
+ // An index may contain multiple offsets corresponding to the same multihash, e.g. via duplicate blocks.
+ // In such cases, the given function may be called multiple times with the same multihash but different offset.
+ //
+ // The order of calls to the given function is deterministic, but entirely index-specific.
+ ForEach(func(multihash.Multihash, uint64) error) error
+ }
+)
+
+// GetFirst is a wrapper over Index.GetAll, returning the offset for the first
+// matching indexed CID.
+func GetFirst(idx Index, key cid.Cid) (uint64, error) {
+ var firstOffset uint64
+ err := idx.GetAll(key, func(offset uint64) bool {
+ firstOffset = offset
+ return false
+ })
+ return firstOffset, err
+}
+
+// New constructs a new index corresponding to the given CAR index codec.
+func New(codec multicodec.Code) (Index, error) {
+ switch codec {
+ case multicodec.CarIndexSorted:
+ return newSorted(), nil
+ case multicodec.CarMultihashIndexSorted:
+ return NewMultihashSorted(), nil
+ default:
+ return nil, fmt.Errorf("unknwon index codec: %v", codec)
+ }
+}
+
+// WriteTo writes the given idx into w.
+// The written bytes include the index encoding.
+// This can then be read back using index.ReadFrom
+func WriteTo(idx Index, w io.Writer) (uint64, error) {
+ buf := make([]byte, binary.MaxVarintLen64)
+ b := varint.PutUvarint(buf, uint64(idx.Codec()))
+ n, err := w.Write(buf[:b])
+ if err != nil {
+ return uint64(n), err
+ }
+
+ l, err := idx.Marshal(w)
+ return uint64(n) + l, err
+}
+
+// ReadFrom reads index from r.
+// The reader decodes the index by reading the first byte to interpret the encoding.
+// Returns error if the encoding is not known.
+//
+// Attempting to read index data from untrusted sources is not recommended.
+// Instead, the index should be regenerated from the CARv2 data payload.
+func ReadFrom(r io.Reader) (Index, error) {
+ codec, err := ReadCodec(r)
+ if err != nil {
+ return nil, err
+ }
+ idx, err := New(codec)
+ if err != nil {
+ return nil, err
+ }
+ if err := idx.Unmarshal(r); err != nil {
+ return nil, err
+ }
+ return idx, nil
+}
+
+// ReadCodec reads the codec of the index by decoding the first varint read from r.
+func ReadCodec(r io.Reader) (multicodec.Code, error) {
+ code, err := varint.ReadUvarint(internalio.ToByteReader(r))
+ if err != nil {
+ return 0, err
+ }
+ return multicodec.Code(code), nil
+}
diff --git a/ipld/car/v2/index/index_test.go b/ipld/car/v2/index/index_test.go
new file mode 100644
index 0000000000..6dc2a5efde
--- /dev/null
+++ b/ipld/car/v2/index/index_test.go
@@ -0,0 +1,168 @@
+package index
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "path/filepath"
+ "testing"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1/util"
+ "github.com/multiformats/go-multicodec"
+ "github.com/multiformats/go-varint"
+ "github.com/stretchr/testify/require"
+)
+
+func TestNew(t *testing.T) {
+ tests := []struct {
+ name string
+ codec multicodec.Code
+ want Index
+ wantErr bool
+ }{
+ {
+ name: "CarSortedIndexCodecIsConstructed",
+ codec: multicodec.CarIndexSorted,
+ want: newSorted(),
+ },
+ {
+ name: "ValidMultiCodecButUnknwonToIndexIsError",
+ codec: multicodec.Cidv1,
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := New(tt.codec)
+ if tt.wantErr {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ require.Equal(t, tt.want, got)
+ }
+ })
+ }
+}
+
+func TestReadFrom(t *testing.T) {
+ idxf, err := os.Open("../testdata/sample-index.carindex")
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, idxf.Close()) })
+
+ subject, err := ReadFrom(idxf)
+ require.NoError(t, err)
+
+ _, err = idxf.Seek(0, io.SeekStart)
+ require.NoError(t, err)
+
+ idxf2, err := os.Open("../testdata/sample-multihash-index-sorted.carindex")
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, idxf2.Close()) })
+
+ subjectInAltFormat, err := ReadFrom(idxf)
+ require.NoError(t, err)
+
+ crf, err := os.Open("../testdata/sample-v1.car")
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, crf.Close()) })
+ cr, err := carv1.NewCarReader(crf)
+ require.NoError(t, err)
+
+ for {
+ wantBlock, err := cr.Next()
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+
+ wantCid := wantBlock.Cid()
+ // Get offset from the index for a CID and assert it exists
+ gotOffset, err := GetFirst(subject, wantCid)
+ require.NoError(t, err)
+ require.NotZero(t, gotOffset)
+
+ // Get offset from the index in alternative format for a CID and assert it exists
+ gotOffset2, err := GetFirst(subjectInAltFormat, wantCid)
+ require.NoError(t, err)
+ require.NotZero(t, gotOffset2)
+
+ // Seek to the offset on CARv1 file
+ _, err = crf.Seek(int64(gotOffset), io.SeekStart)
+ require.NoError(t, err)
+
+ // Read the fame at offset and assert the frame corresponds to the expected block.
+ gotCid, gotData, err := util.ReadNode(crf, false, carv1.DefaultMaxAllowedSectionSize)
+ require.NoError(t, err)
+ gotBlock, err := blocks.NewBlockWithCid(gotData, gotCid)
+ require.NoError(t, err)
+ require.Equal(t, wantBlock, gotBlock)
+ }
+}
+
+func TestWriteTo(t *testing.T) {
+ // Read sample index on file
+ idxf, err := os.Open("../testdata/sample-multihash-index-sorted.carindex")
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, idxf.Close()) })
+
+ // Unmarshall to get expected index
+ wantIdx, err := ReadFrom(idxf)
+ require.NoError(t, err)
+
+ // Write the same index out
+ dest := filepath.Join(t.TempDir(), "index-write-to-test.carindex")
+ destF, err := os.Create(dest)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, destF.Close()) })
+ _, err = WriteTo(wantIdx, destF)
+ require.NoError(t, err)
+
+ // Seek to the beginning of the written out file.
+ _, err = destF.Seek(0, io.SeekStart)
+ require.NoError(t, err)
+
+ // Read the written index back
+ gotIdx, err := ReadFrom(destF)
+ require.NoError(t, err)
+
+ // Assert they are equal
+ require.Equal(t, wantIdx, gotIdx)
+}
+
+func TestMarshalledIndexStartsWithCodec(t *testing.T) {
+
+ tests := []struct {
+ path string
+ codec multicodec.Code
+ }{
+ {
+ path: "../testdata/sample-multihash-index-sorted.carindex",
+ codec: multicodec.CarMultihashIndexSorted,
+ },
+ {
+ path: "../testdata/sample-index.carindex",
+ codec: multicodec.CarIndexSorted,
+ },
+ }
+ for _, test := range tests {
+ test := test
+ t.Run(test.codec.String(), func(t *testing.T) {
+ // Read sample index on file
+ idxf, err := os.Open(test.path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, idxf.Close()) })
+
+ // Unmarshall to get expected index
+ wantIdx, err := ReadFrom(idxf)
+ require.NoError(t, err)
+
+ // Assert the first two bytes are the corresponding multicodec code.
+ buf := new(bytes.Buffer)
+ _, err = WriteTo(wantIdx, buf)
+ require.NoError(t, err)
+ require.Equal(t, varint.ToUvarint(uint64(test.codec)), buf.Bytes()[:2])
+ })
+ }
+}
diff --git a/ipld/car/v2/index/indexsorted.go b/ipld/car/v2/index/indexsorted.go
new file mode 100644
index 0000000000..93b31e433c
--- /dev/null
+++ b/ipld/car/v2/index/indexsorted.go
@@ -0,0 +1,319 @@
+package index
+
+import (
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "sort"
+
+ internalio "github.com/ipfs/boxo/ipld/car/v2/internal/io"
+
+ "github.com/multiformats/go-multicodec"
+
+ "github.com/ipfs/go-cid"
+ "github.com/multiformats/go-multihash"
+)
+
+var _ Index = (*multiWidthIndex)(nil)
+
+type (
+ digestRecord struct {
+ digest []byte
+ index uint64
+ }
+ recordSet []digestRecord
+ singleWidthIndex struct {
+ width uint32
+ len uint64 // in struct, len is #items. when marshaled, it's saved as #bytes.
+ index []byte
+ }
+ multiWidthIndex map[uint32]singleWidthIndex
+)
+
+func (d digestRecord) write(buf []byte) {
+ n := copy(buf[:], d.digest)
+ binary.LittleEndian.PutUint64(buf[n:], d.index)
+}
+
+func (r recordSet) Len() int {
+ return len(r)
+}
+
+func (r recordSet) Less(i, j int) bool {
+ return bytes.Compare(r[i].digest, r[j].digest) < 0
+}
+
+func (r recordSet) Swap(i, j int) {
+ r[i], r[j] = r[j], r[i]
+}
+
+func (s *singleWidthIndex) Marshal(w io.Writer) (uint64, error) {
+ l := uint64(0)
+ if err := binary.Write(w, binary.LittleEndian, s.width); err != nil {
+ return 0, err
+ }
+ l += 4
+ if err := binary.Write(w, binary.LittleEndian, int64(len(s.index))); err != nil {
+ return l, err
+ }
+ l += 8
+ n, err := w.Write(s.index)
+ return l + uint64(n), err
+}
+
+func (s *singleWidthIndex) Unmarshal(r io.Reader) error {
+ var width uint32
+ if err := binary.Read(r, binary.LittleEndian, &width); err != nil {
+ if err == io.EOF {
+ return io.ErrUnexpectedEOF
+ }
+ return err
+ }
+ var dataLen uint64
+ if err := binary.Read(r, binary.LittleEndian, &dataLen); err != nil {
+ if err == io.EOF {
+ return io.ErrUnexpectedEOF
+ }
+ return err
+ }
+
+ if err := s.checkUnmarshalLengths(width, dataLen, 0); err != nil {
+ return err
+ }
+
+ buf := make([]byte, dataLen)
+ if _, err := io.ReadFull(r, buf); err != nil {
+ return err
+ }
+ s.index = buf
+ return nil
+}
+
+func (s *singleWidthIndex) checkUnmarshalLengths(width uint32, dataLen, extra uint64) error {
+ if width < 8 {
+ return errors.New("malformed index; width must be at least 8")
+ }
+ const maxWidth = 32 << 20 // 32MiB, to ~match the go-cid maximum
+ if width > maxWidth {
+ return errors.New("index too big; singleWidthIndex width is larger than allowed maximum")
+ }
+ oldDataLen, dataLen := dataLen, dataLen+extra
+ if oldDataLen > dataLen {
+ return errors.New("index too big; singleWidthIndex len is overflowing")
+ }
+ if int64(dataLen) < 0 {
+ return errors.New("index too big; singleWidthIndex len is overflowing int64")
+ }
+ s.width = width
+ s.len = dataLen / uint64(width)
+ return nil
+}
+
+func (s *singleWidthIndex) Less(i int, digest []byte) bool {
+ return bytes.Compare(digest[:], s.index[i*int(s.width):((i+1)*int(s.width)-8)]) <= 0
+}
+
+func (s *singleWidthIndex) GetAll(c cid.Cid, fn func(uint64) bool) error {
+ d, err := multihash.Decode(c.Hash())
+ if err != nil {
+ return err
+ }
+ return s.getAll(d.Digest, fn)
+}
+
+func (s *singleWidthIndex) getAll(d []byte, fn func(uint64) bool) error {
+ idx := sort.Search(int(s.len), func(i int) bool {
+ return s.Less(i, d)
+ })
+
+ var any bool
+ for ; uint64(idx) < s.len; idx++ {
+ digestStart := idx * int(s.width)
+ offsetEnd := (idx + 1) * int(s.width)
+ digestEnd := offsetEnd - 8
+ if bytes.Equal(d[:], s.index[digestStart:digestEnd]) {
+ any = true
+ offset := binary.LittleEndian.Uint64(s.index[digestEnd:offsetEnd])
+ if !fn(offset) {
+ // User signalled to stop searching; therefore, break.
+ break
+ }
+ } else {
+ // No more matches; therefore, break.
+ break
+ }
+ }
+ if !any {
+ return ErrNotFound
+ }
+ return nil
+}
+
+func (s *singleWidthIndex) Load(items []Record) error {
+ m := make(multiWidthIndex)
+ if err := m.Load(items); err != nil {
+ return err
+ }
+ if len(m) != 1 {
+ return fmt.Errorf("unexpected number of cid widths: %d", len(m))
+ }
+ for _, i := range m {
+ s.index = i.index
+ s.len = i.len
+ s.width = i.width
+ return nil
+ }
+ return nil
+}
+
+func (s *singleWidthIndex) forEachDigest(f func(digest []byte, offset uint64) error) error {
+ segmentCount := len(s.index) / int(s.width)
+ for i := 0; i < segmentCount; i++ {
+ digestStart := i * int(s.width)
+ offsetEnd := (i + 1) * int(s.width)
+ digestEnd := offsetEnd - 8
+ digest := s.index[digestStart:digestEnd]
+ offset := binary.LittleEndian.Uint64(s.index[digestEnd:offsetEnd])
+ if err := f(digest, offset); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (m *multiWidthIndex) GetAll(c cid.Cid, fn func(uint64) bool) error {
+ d, err := multihash.Decode(c.Hash())
+ if err != nil {
+ return err
+ }
+ if s, ok := (*m)[uint32(len(d.Digest)+8)]; ok {
+ return s.getAll(d.Digest, fn)
+ }
+ return ErrNotFound
+}
+
+func (m *multiWidthIndex) Codec() multicodec.Code {
+ return multicodec.CarIndexSorted
+}
+
+func (m *multiWidthIndex) Marshal(w io.Writer) (uint64, error) {
+ l := uint64(0)
+ if err := binary.Write(w, binary.LittleEndian, int32(len(*m))); err != nil {
+ return l, err
+ }
+ l += 4
+
+ // The widths are unique, but ranging over a map isn't deterministic.
+ // As per the CARv2 spec, we must order buckets by digest length.
+
+ widths := make([]uint32, 0, len(*m))
+ for width := range *m {
+ widths = append(widths, width)
+ }
+ sort.Slice(widths, func(i, j int) bool {
+ return widths[i] < widths[j]
+ })
+
+ for _, width := range widths {
+ bucket := (*m)[width]
+ n, err := bucket.Marshal(w)
+ l += n
+ if err != nil {
+ return l, err
+ }
+ }
+ return l, nil
+}
+
+func (m *multiWidthIndex) Unmarshal(r io.Reader) error {
+ reader := internalio.ToByteReadSeeker(r)
+ var l int32
+ if err := binary.Read(reader, binary.LittleEndian, &l); err != nil {
+ if err == io.EOF {
+ return io.ErrUnexpectedEOF
+ }
+ return err
+ }
+ sum, err := reader.Seek(0, io.SeekCurrent)
+ if err != nil {
+ return err
+ }
+ if int32(l) < 0 {
+ return errors.New("index too big; multiWidthIndex count is overflowing int32")
+ }
+ for i := 0; i < int(l); i++ {
+ s := singleWidthIndex{}
+ if err := s.Unmarshal(r); err != nil {
+ return err
+ }
+ n, err := reader.Seek(0, io.SeekCurrent)
+ if err != nil {
+ return err
+ }
+ oldSum := sum
+ sum += n
+ if sum < oldSum {
+ return errors.New("index too big; multiWidthIndex len is overflowing int64")
+ }
+ (*m)[s.width] = s
+ }
+ return nil
+}
+
+func (m *multiWidthIndex) Load(items []Record) error {
+ // Split cids on their digest length
+ idxs := make(map[int][]digestRecord)
+ for _, item := range items {
+ decHash, err := multihash.Decode(item.Hash())
+ if err != nil {
+ return err
+ }
+
+ digest := decHash.Digest
+ idx, ok := idxs[len(digest)]
+ if !ok {
+ idxs[len(digest)] = make([]digestRecord, 0)
+ idx = idxs[len(digest)]
+ }
+ idxs[len(digest)] = append(idx, digestRecord{digest, item.Offset})
+ }
+
+ // Sort each list. then write to compact form.
+ for width, lst := range idxs {
+ sort.Sort(recordSet(lst))
+ rcrdWdth := width + 8
+ compact := make([]byte, rcrdWdth*len(lst))
+ for off, itm := range lst {
+ itm.write(compact[off*rcrdWdth : (off+1)*rcrdWdth])
+ }
+ s := singleWidthIndex{
+ width: uint32(rcrdWdth),
+ len: uint64(len(lst)),
+ index: compact,
+ }
+ (*m)[uint32(width)+8] = s
+ }
+ return nil
+}
+
+func (m *multiWidthIndex) forEachDigest(f func(digest []byte, offset uint64) error) error {
+ sizes := make([]uint32, 0, len(*m))
+ for k := range *m {
+ sizes = append(sizes, k)
+ }
+ sort.Slice(sizes, func(i, j int) bool { return sizes[i] < sizes[j] })
+ for _, s := range sizes {
+ swi := (*m)[s]
+ if err := swi.forEachDigest(f); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func newSorted() Index {
+ m := make(multiWidthIndex)
+ return &m
+}
diff --git a/ipld/car/v2/index/indexsorted_test.go b/ipld/car/v2/index/indexsorted_test.go
new file mode 100644
index 0000000000..a0da42354a
--- /dev/null
+++ b/ipld/car/v2/index/indexsorted_test.go
@@ -0,0 +1,64 @@
+package index
+
+import (
+ "encoding/binary"
+ "testing"
+
+ "github.com/ipfs/boxo/ipld/merkledag"
+ "github.com/multiformats/go-multicodec"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSortedIndexCodec(t *testing.T) {
+ require.Equal(t, multicodec.CarIndexSorted, newSorted().Codec())
+}
+
+func TestIndexSorted_GetReturnsNotFoundWhenCidDoesNotExist(t *testing.T) {
+ nonExistingKey := merkledag.NewRawNode([]byte("lobstermuncher")).Block.Cid()
+ tests := []struct {
+ name string
+ subject Index
+ }{
+ {
+ "Sorted",
+ newSorted(),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ gotOffset, err := GetFirst(tt.subject, nonExistingKey)
+ require.Equal(t, ErrNotFound, err)
+ require.Equal(t, uint64(0), gotOffset)
+ })
+ }
+}
+
+func TestSingleWidthIndex_GetAll(t *testing.T) {
+ l := 4
+ width := 9
+ buf := make([]byte, width*l)
+
+ // Populate the index bytes as total of four records.
+ // The last record should not match the getAll.
+ for i := 0; i < l; i++ {
+ if i < l-1 {
+ buf[i*width] = 1
+ } else {
+ buf[i*width] = 2
+ }
+ binary.LittleEndian.PutUint64(buf[(i*width)+1:(i*width)+width], uint64(14))
+ }
+ subject := &singleWidthIndex{
+ width: 9,
+ len: uint64(l),
+ index: buf,
+ }
+
+ var foundCount int
+ err := subject.getAll([]byte{1}, func(u uint64) bool {
+ foundCount++
+ return true
+ })
+ require.NoError(t, err)
+ require.Equal(t, 3, foundCount)
+}
diff --git a/ipld/car/v2/index/mhindexsorted.go b/ipld/car/v2/index/mhindexsorted.go
new file mode 100644
index 0000000000..75ea21ecd5
--- /dev/null
+++ b/ipld/car/v2/index/mhindexsorted.go
@@ -0,0 +1,214 @@
+package index
+
+import (
+ "encoding/binary"
+ "errors"
+ "io"
+ "sort"
+
+ internalio "github.com/ipfs/boxo/ipld/car/v2/internal/io"
+
+ "github.com/ipfs/go-cid"
+ "github.com/multiformats/go-multicodec"
+ "github.com/multiformats/go-multihash"
+)
+
+var (
+ _ Index = (*MultihashIndexSorted)(nil)
+ _ IterableIndex = (*MultihashIndexSorted)(nil)
+)
+
+type (
+ // MultihashIndexSorted maps multihash code (i.e. hashing algorithm) to multiWidthCodedIndex.
+ MultihashIndexSorted map[uint64]*multiWidthCodedIndex
+ // multiWidthCodedIndex stores multihash code for each multiWidthIndex.
+ multiWidthCodedIndex struct {
+ multiWidthIndex
+ code uint64
+ }
+)
+
+func newMultiWidthCodedIndex() *multiWidthCodedIndex {
+ return &multiWidthCodedIndex{
+ multiWidthIndex: make(multiWidthIndex),
+ }
+}
+
+func (m *multiWidthCodedIndex) Marshal(w io.Writer) (uint64, error) {
+ if err := binary.Write(w, binary.LittleEndian, m.code); err != nil {
+ return 8, err
+ }
+ n, err := m.multiWidthIndex.Marshal(w)
+ return 8 + n, err
+}
+
+func (m *multiWidthCodedIndex) Unmarshal(r io.Reader) error {
+ if err := binary.Read(r, binary.LittleEndian, &m.code); err != nil {
+ if err == io.EOF {
+ return io.ErrUnexpectedEOF
+ }
+ return err
+ }
+ return m.multiWidthIndex.Unmarshal(r)
+}
+
+func (m *multiWidthCodedIndex) forEach(f func(mh multihash.Multihash, offset uint64) error) error {
+ return m.multiWidthIndex.forEachDigest(func(digest []byte, offset uint64) error {
+ mh, err := multihash.Encode(digest, m.code)
+ if err != nil {
+ return err
+ }
+ return f(mh, offset)
+ })
+}
+
+func (m *MultihashIndexSorted) Codec() multicodec.Code {
+ return multicodec.CarMultihashIndexSorted
+}
+
+func (m *MultihashIndexSorted) Marshal(w io.Writer) (uint64, error) {
+ if err := binary.Write(w, binary.LittleEndian, int32(len(*m))); err != nil {
+ return 4, err
+ }
+ // The codes are unique, but ranging over a map isn't deterministic.
+ // As per the CARv2 spec, we must order buckets by digest length.
+ // TODO update CARv2 spec to reflect this for the new index type.
+ codes := m.sortedMultihashCodes()
+ l := uint64(4)
+
+ for _, code := range codes {
+ mwci := (*m)[code]
+ n, err := mwci.Marshal(w)
+ l += n
+ if err != nil {
+ return l, err
+ }
+ }
+ return l, nil
+}
+
+func (m *MultihashIndexSorted) sortedMultihashCodes() []uint64 {
+ codes := make([]uint64, 0, len(*m))
+ for code := range *m {
+ codes = append(codes, code)
+ }
+ sort.Slice(codes, func(i, j int) bool {
+ return codes[i] < codes[j]
+ })
+ return codes
+}
+
+func (m *MultihashIndexSorted) Unmarshal(r io.Reader) error {
+ reader := internalio.ToByteReadSeeker(r)
+ var l int32
+ if err := binary.Read(reader, binary.LittleEndian, &l); err != nil {
+ if err == io.EOF {
+ return io.ErrUnexpectedEOF
+ }
+ return err
+ }
+ sum, err := reader.Seek(0, io.SeekCurrent)
+ if err != nil {
+ return err
+ }
+ if int32(l) < 0 {
+ return errors.New("index too big; MultihashIndexSorted count is overflowing int32")
+ }
+ for i := 0; i < int(l); i++ {
+ mwci := newMultiWidthCodedIndex()
+ if err := mwci.Unmarshal(r); err != nil {
+ return err
+ }
+ n, err := reader.Seek(0, io.SeekCurrent)
+ if err != nil {
+ return err
+ }
+ oldSum := sum
+ sum += n
+ if sum < oldSum {
+ return errors.New("index too big; MultihashIndexSorted len is overflowing int64")
+ }
+ m.put(mwci)
+ }
+ return nil
+}
+
+func (m *MultihashIndexSorted) put(mwci *multiWidthCodedIndex) {
+ (*m)[mwci.code] = mwci
+}
+
+func (m *MultihashIndexSorted) Load(records []Record) error {
+ // TODO optimize load by avoiding multihash decoding twice.
+ // This implementation decodes multihashes twice: once here to group by code, and once in the
+ // internals of multiWidthIndex to group by digest length. The code can be optimized by
+ // combining the grouping logic into one step where the multihash of a CID is only decoded once.
+ // The optimization would need refactoring of the IndexSorted compaction logic.
+
+ // Group records by multihash code
+ byCode := make(map[uint64][]Record)
+ for _, record := range records {
+ dmh, err := multihash.Decode(record.Hash())
+ if err != nil {
+ return err
+ }
+ code := dmh.Code
+ recsByCode, ok := byCode[code]
+ if !ok {
+ recsByCode = make([]Record, 0)
+ byCode[code] = recsByCode
+ }
+ byCode[code] = append(recsByCode, record)
+ }
+
+ // Load each record group.
+ for code, recsByCode := range byCode {
+ mwci := newMultiWidthCodedIndex()
+ mwci.code = code
+ if err := mwci.Load(recsByCode); err != nil {
+ return err
+ }
+ m.put(mwci)
+ }
+ return nil
+}
+
+func (m *MultihashIndexSorted) GetAll(cid cid.Cid, f func(uint64) bool) error {
+ hash := cid.Hash()
+ dmh, err := multihash.Decode(hash)
+ if err != nil {
+ return err
+ }
+ mwci, err := m.get(dmh)
+ if err != nil {
+ return err
+ }
+ return mwci.GetAll(cid, f)
+}
+
+// ForEach calls f for every multihash and its associated offset stored by this index.
+func (m *MultihashIndexSorted) ForEach(f func(mh multihash.Multihash, offset uint64) error) error {
+ sizes := make([]uint64, 0, len(*m))
+ for k := range *m {
+ sizes = append(sizes, k)
+ }
+ sort.Slice(sizes, func(i, j int) bool { return sizes[i] < sizes[j] })
+ for _, s := range sizes {
+ mwci := (*m)[s]
+ if err := mwci.forEach(f); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (m *MultihashIndexSorted) get(dmh *multihash.DecodedMultihash) (*multiWidthCodedIndex, error) {
+ if codedIdx, ok := (*m)[dmh.Code]; ok {
+ return codedIdx, nil
+ }
+ return nil, ErrNotFound
+}
+
+func NewMultihashSorted() *MultihashIndexSorted {
+ index := make(MultihashIndexSorted)
+ return &index
+}
diff --git a/ipld/car/v2/index/mhindexsorted_test.go b/ipld/car/v2/index/mhindexsorted_test.go
new file mode 100644
index 0000000000..1b0f2ab863
--- /dev/null
+++ b/ipld/car/v2/index/mhindexsorted_test.go
@@ -0,0 +1,113 @@
+package index_test
+
+import (
+ "bytes"
+ "fmt"
+ "math/rand"
+ "testing"
+
+ "github.com/multiformats/go-multicodec"
+
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+ "github.com/ipfs/go-cid"
+ "github.com/multiformats/go-multihash"
+ "github.com/stretchr/testify/require"
+)
+
+func TestMutilhashSortedIndex_Codec(t *testing.T) {
+ subject, err := index.New(multicodec.CarMultihashIndexSorted)
+ require.NoError(t, err)
+ require.Equal(t, multicodec.CarMultihashIndexSorted, subject.Codec())
+}
+
+func TestMultiWidthCodedIndex_MarshalUnmarshal(t *testing.T) {
+ rng := rand.New(rand.NewSource(1413))
+ records := generateIndexRecords(t, multihash.SHA2_256, rng)
+
+ // Create a new mh sorted index and load randomly generated records into it.
+ subject, err := index.New(multicodec.CarMultihashIndexSorted)
+ require.NoError(t, err)
+ err = subject.Load(records)
+ require.NoError(t, err)
+
+ // Marshal the index.
+ buf := new(bytes.Buffer)
+ _, err = subject.Marshal(buf)
+ require.NoError(t, err)
+
+ // Unmarshal it back to another instance of mh sorted index.
+ umSubject, err := index.New(multicodec.CarMultihashIndexSorted)
+ require.NoError(t, err)
+ err = umSubject.Unmarshal(buf)
+ require.NoError(t, err)
+
+ // Assert original records are present in both index instances with expected offset.
+ requireContainsAll(t, subject, records)
+ requireContainsAll(t, umSubject, records)
+}
+
+func TestMultiWidthCodedIndex_StableIterate(t *testing.T) {
+ rng := rand.New(rand.NewSource(1414))
+ records := generateIndexRecords(t, multihash.SHA2_256, rng)
+ records = append(records, generateIndexRecords(t, multihash.SHA2_512, rng)...)
+ records = append(records, generateIndexRecords(t, multihash.IDENTITY, rng)...)
+
+ // Create a new mh sorted index and load randomly generated records into it.
+ idx, err := index.New(multicodec.CarMultihashIndexSorted)
+ require.NoError(t, err)
+ err = idx.Load(records)
+ require.NoError(t, err)
+
+ subject, ok := idx.(index.IterableIndex)
+ require.True(t, ok)
+
+ mh := make([]multihash.Multihash, 0, len(records))
+ require.NoError(t, subject.ForEach(func(m multihash.Multihash, _ uint64) error {
+ mh = append(mh, m)
+ return nil
+ }))
+
+ for i := 0; i < 10; i++ {
+ candidate := make([]multihash.Multihash, 0, len(records))
+ require.NoError(t, subject.ForEach(func(m multihash.Multihash, _ uint64) error {
+ candidate = append(candidate, m)
+ return nil
+ }))
+ require.Equal(t, mh, candidate)
+ }
+}
+
+func generateIndexRecords(t *testing.T, hasherCode uint64, rng *rand.Rand) []index.Record {
+ var records []index.Record
+ recordCount := rng.Intn(99) + 1 // Up to 100 records
+ for i := 0; i < recordCount; i++ {
+ records = append(records, index.Record{
+ Cid: generateCidV1(t, hasherCode, rng),
+ Offset: rng.Uint64(),
+ })
+ }
+ return records
+}
+
+func generateCidV1(t *testing.T, hasherCode uint64, rng *rand.Rand) cid.Cid {
+ data := []byte(fmt.Sprintf("🌊d-%d", rng.Uint64()))
+ mh, err := multihash.Sum(data, hasherCode, -1)
+ require.NoError(t, err)
+ return cid.NewCidV1(cid.Raw, mh)
+}
+
+func requireContainsAll(t *testing.T, subject index.Index, nonIdentityRecords []index.Record) {
+ for _, r := range nonIdentityRecords {
+ wantCid := r.Cid
+ wantOffset := r.Offset
+
+ var gotOffsets []uint64
+ err := subject.GetAll(wantCid, func(o uint64) bool {
+ gotOffsets = append(gotOffsets, o)
+ return false
+ })
+ require.NoError(t, err)
+ require.Equal(t, 1, len(gotOffsets))
+ require.Equal(t, wantOffset, gotOffsets[0])
+ }
+}
diff --git a/ipld/car/v2/index_gen.go b/ipld/car/v2/index_gen.go
new file mode 100644
index 0000000000..5f1da03e16
--- /dev/null
+++ b/ipld/car/v2/index_gen.go
@@ -0,0 +1,231 @@
+package car
+
+import (
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+ internalio "github.com/ipfs/boxo/ipld/car/v2/internal/io"
+ "github.com/ipfs/go-cid"
+ "github.com/multiformats/go-multihash"
+ "github.com/multiformats/go-varint"
+)
+
+// GenerateIndex generates index for the given car payload reader.
+// The index can be stored in serialized format using index.WriteTo.
+//
+// Note, the index is re-generated every time even if the payload is in CARv2 format and already has
+// an index. To read existing index when available see ReadOrGenerateIndex.
+// See: LoadIndex.
+func GenerateIndex(v1r io.Reader, opts ...Option) (index.Index, error) {
+ wopts := ApplyOptions(opts...)
+ idx, err := index.New(wopts.IndexCodec)
+ if err != nil {
+ return nil, err
+ }
+ if err := LoadIndex(idx, v1r, opts...); err != nil {
+ return nil, err
+ }
+ return idx, nil
+}
+
+// LoadIndex populates idx with index records generated from r.
+// The r may be in CARv1 or CARv2 format.
+//
+// If the StoreIdentityCIDs option is set when calling LoadIndex, identity
+// CIDs will be included in the index. By default this option is off, and
+// identity CIDs will not be included in the index.
+//
+// Note, the index is re-generated every time even if r is in CARv2 format and already has an index.
+// To read existing index when available see ReadOrGenerateIndex.
+func LoadIndex(idx index.Index, r io.Reader, opts ...Option) error {
+ // Parse Options.
+ o := ApplyOptions(opts...)
+
+ reader := internalio.ToByteReadSeeker(r)
+ pragma, err := carv1.ReadHeader(r, o.MaxAllowedHeaderSize)
+ if err != nil {
+ return fmt.Errorf("error reading car header: %w", err)
+ }
+
+ var dataSize, dataOffset int64
+ switch pragma.Version {
+ case 1:
+ break
+ case 2:
+ // Read V2 header which should appear immediately after pragma according to CARv2 spec.
+ var v2h Header
+ _, err := v2h.ReadFrom(r)
+ if err != nil {
+ return err
+ }
+
+ // Sanity-check the CARv2 header
+ if v2h.DataOffset < HeaderSize {
+ return fmt.Errorf("malformed CARv2; data offset too small: %d", v2h.DataOffset)
+ }
+ if v2h.DataSize < 1 {
+ return fmt.Errorf("malformed CARv2; data payload size too small: %d", v2h.DataSize)
+ }
+
+ // Seek to the beginning of the inner CARv1 payload
+ _, err = reader.Seek(int64(v2h.DataOffset), io.SeekStart)
+ if err != nil {
+ return err
+ }
+
+ // Set dataSize and dataOffset which are then used during index loading logic to decide
+ // where to stop and adjust section offset respectively.
+ // Note that we could use a LimitReader here and re-define reader with it. However, it means
+ // the internalio.ToByteReadSeeker will be less efficient since LimitReader does not
+ // implement ByteReader nor ReadSeeker.
+ dataSize = int64(v2h.DataSize)
+ dataOffset = int64(v2h.DataOffset)
+
+ // Read the inner CARv1 header to skip it and sanity check it.
+ v1h, err := carv1.ReadHeader(reader, o.MaxAllowedHeaderSize)
+ if err != nil {
+ return err
+ }
+ if v1h.Version != 1 {
+ return fmt.Errorf("expected data payload header version of 1; got %d", v1h.Version)
+ }
+ default:
+ return fmt.Errorf("expected either version 1 or 2; got %d", pragma.Version)
+ }
+
+ // Record the start of each section, with first section starring from current position in the
+ // reader, i.e. right after the header, since we have only read the header so far.
+ var sectionOffset int64
+
+ // The Seek call below is equivalent to getting the reader.offset directly.
+ // We get it through Seek to only depend on APIs of a typical io.Seeker.
+ // This would also reduce refactoring in case the utility reader is moved.
+ if sectionOffset, err = reader.Seek(0, io.SeekCurrent); err != nil {
+ return err
+ }
+
+ // Subtract the data offset; if CARv1 this would be zero otherwise the value will come from the
+ // CARv2 header.
+ sectionOffset -= dataOffset
+
+ records := make([]index.Record, 0)
+ for {
+ // Read the section's length.
+ sectionLen, err := varint.ReadUvarint(reader)
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ return err
+ }
+
+ // Null padding; by default it's an error.
+ if sectionLen == 0 {
+ if o.ZeroLengthSectionAsEOF {
+ break
+ } else {
+ return fmt.Errorf("carv1 null padding not allowed by default; see ZeroLengthSectionAsEOF")
+ }
+ }
+
+ // Read the CID.
+ cidLen, c, err := cid.CidFromReader(reader)
+ if err != nil {
+ return err
+ }
+
+ if o.StoreIdentityCIDs || c.Prefix().MhType != multihash.IDENTITY {
+ if uint64(cidLen) > o.MaxIndexCidSize {
+ return &ErrCidTooLarge{MaxSize: o.MaxIndexCidSize, CurrentSize: uint64(cidLen)}
+ }
+ records = append(records, index.Record{Cid: c, Offset: uint64(sectionOffset)})
+ }
+
+ // Seek to the next section by skipping the block.
+ // The section length includes the CID, so subtract it.
+ remainingSectionLen := int64(sectionLen) - int64(cidLen)
+ if sectionOffset, err = reader.Seek(remainingSectionLen, io.SeekCurrent); err != nil {
+ return err
+ }
+ // Subtract the data offset which will be non-zero when reader represents a CARv2.
+ sectionOffset -= dataOffset
+
+ // Check if we have reached the end of data payload and if so treat it as an EOF.
+ // Note, dataSize will be non-zero only if we are reading from a CARv2.
+ if dataSize != 0 && sectionOffset >= dataSize {
+ break
+ }
+ }
+
+ if err := idx.Load(records); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// GenerateIndexFromFile walks a CAR file at the give path and generates an index of cid->byte offset.
+// The index can be stored using index.WriteTo. Both CARv1 and CARv2 formats are accepted.
+//
+// Note, the index is re-generated every time even if the given CAR file is in CARv2 format and
+// already has an index. To read existing index when available see ReadOrGenerateIndex.
+//
+// See: GenerateIndex.
+func GenerateIndexFromFile(path string, opts ...Option) (index.Index, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ return GenerateIndex(f, opts...)
+}
+
+// ReadOrGenerateIndex accepts both CARv1 and CARv2 formats, and reads or generates an index for it.
+// When the given reader is in CARv1 format an index is always generated.
+// For a payload in CARv2 format, an index is only generated if Header.HasIndex returns false.
+// An error is returned for all other formats, i.e. pragma with versions other than 1 or 2.
+//
+// Note, the returned index lives entirely in memory and will not depend on the
+// given reader to fulfill index lookup.
+func ReadOrGenerateIndex(rs io.ReadSeeker, opts ...Option) (index.Index, error) {
+ // Read version.
+ version, err := ReadVersion(rs, opts...)
+ if err != nil {
+ return nil, err
+ }
+ // Seek to the beginning, since reading the version changes the reader's offset.
+ if _, err := rs.Seek(0, io.SeekStart); err != nil {
+ return nil, err
+ }
+
+ switch version {
+ case 1:
+ // Simply generate the index, since there can't be a pre-existing one.
+ return GenerateIndex(rs, opts...)
+ case 2:
+ // Read CARv2 format
+ v2r, err := NewReader(internalio.ToReaderAt(rs), opts...)
+ if err != nil {
+ return nil, err
+ }
+ // If index is present, then no need to generate; decode and return it.
+ if v2r.Header.HasIndex() {
+ ir, err := v2r.IndexReader()
+ if err != nil {
+ return nil, err
+ }
+ return index.ReadFrom(ir)
+ }
+ // Otherwise, generate index from CARv1 payload wrapped within CARv2 format.
+ dr, err := v2r.DataReader()
+ if err != nil {
+ return nil, err
+ }
+ return GenerateIndex(dr, opts...)
+ default:
+ return nil, fmt.Errorf("unknown version %v", version)
+ }
+}
diff --git a/ipld/car/v2/index_gen_test.go b/ipld/car/v2/index_gen_test.go
new file mode 100644
index 0000000000..48d30b426b
--- /dev/null
+++ b/ipld/car/v2/index_gen_test.go
@@ -0,0 +1,274 @@
+package car_test
+
+import (
+ "io"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+ internalio "github.com/ipfs/boxo/ipld/car/v2/internal/io"
+ "github.com/ipfs/go-cid"
+ "github.com/multiformats/go-multicodec"
+ "github.com/multiformats/go-multihash"
+ "github.com/multiformats/go-varint"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGenerateIndex(t *testing.T) {
+ type testCase struct {
+ name string
+ carPath string
+ opts []carv2.Option
+ wantIndexer func(t *testing.T) index.Index
+ wantErr bool
+ }
+ tests := []testCase{
+ {
+ name: "CarV1IsIndexedAsExpected",
+ carPath: "testdata/sample-v1.car",
+ wantIndexer: func(t *testing.T) index.Index {
+ v1, err := os.Open("testdata/sample-v1.car")
+ require.NoError(t, err)
+ t.Cleanup(func() { assert.NoError(t, v1.Close()) })
+ want, err := carv2.GenerateIndex(v1)
+ require.NoError(t, err)
+ return want
+ },
+ },
+ {
+ name: "CarV2WithIndexIsReturnedAsExpected",
+ carPath: "testdata/sample-wrapped-v2.car",
+ wantIndexer: func(t *testing.T) index.Index {
+ v2, err := os.Open("testdata/sample-wrapped-v2.car")
+ require.NoError(t, err)
+ t.Cleanup(func() { assert.NoError(t, v2.Close()) })
+ reader, err := carv2.NewReader(v2)
+ require.NoError(t, err)
+ ir, err := reader.IndexReader()
+ require.NoError(t, err)
+ want, err := index.ReadFrom(ir)
+ require.NoError(t, err)
+ return want
+ },
+ },
+ {
+ name: "CarV1WithZeroLenSectionIsGeneratedAsExpected",
+ carPath: "testdata/sample-v1-with-zero-len-section.car",
+ opts: []carv2.Option{carv2.ZeroLengthSectionAsEOF(true)},
+ wantIndexer: func(t *testing.T) index.Index {
+ v1, err := os.Open("testdata/sample-v1-with-zero-len-section.car")
+ require.NoError(t, err)
+ t.Cleanup(func() { assert.NoError(t, v1.Close()) })
+ want, err := carv2.GenerateIndex(v1, carv2.ZeroLengthSectionAsEOF(true))
+ require.NoError(t, err)
+ return want
+ },
+ },
+ {
+ name: "AnotherCarV1WithZeroLenSectionIsGeneratedAsExpected",
+ carPath: "testdata/sample-v1-with-zero-len-section2.car",
+ opts: []carv2.Option{carv2.ZeroLengthSectionAsEOF(true)},
+ wantIndexer: func(t *testing.T) index.Index {
+ v1, err := os.Open("testdata/sample-v1-with-zero-len-section2.car")
+ require.NoError(t, err)
+ t.Cleanup(func() { assert.NoError(t, v1.Close()) })
+ want, err := carv2.GenerateIndex(v1, carv2.ZeroLengthSectionAsEOF(true))
+ require.NoError(t, err)
+ return want
+ },
+ },
+ {
+ name: "CarV1WithZeroLenSectionWithoutOptionIsError",
+ carPath: "testdata/sample-v1-with-zero-len-section.car",
+ wantErr: true,
+ },
+ {
+ name: "CarOtherThanV1OrV2IsError",
+ carPath: "testdata/sample-rootless-v42.car",
+ wantIndexer: func(t *testing.T) index.Index { return nil },
+ wantErr: true,
+ },
+ }
+
+ requireWant := func(tt testCase, got index.Index, gotErr error) {
+ if tt.wantErr {
+ require.Error(t, gotErr)
+ } else {
+ require.NoError(t, gotErr)
+ var want index.Index
+ if tt.wantIndexer != nil {
+ want = tt.wantIndexer(t)
+ }
+ if want == nil {
+ require.Nil(t, got)
+ } else {
+ require.Equal(t, want, got)
+ }
+ }
+ }
+
+ for _, tt := range tests {
+ t.Run("ReadOrGenerateIndex_"+tt.name, func(t *testing.T) {
+ carFile, err := os.Open(tt.carPath)
+ require.NoError(t, err)
+ t.Cleanup(func() { assert.NoError(t, carFile.Close()) })
+ got, gotErr := carv2.ReadOrGenerateIndex(carFile, tt.opts...)
+ requireWant(tt, got, gotErr)
+ })
+ t.Run("GenerateIndexFromFile_"+tt.name, func(t *testing.T) {
+ got, gotErr := carv2.GenerateIndexFromFile(tt.carPath, tt.opts...)
+ requireWant(tt, got, gotErr)
+ })
+ t.Run("LoadIndex_"+tt.name, func(t *testing.T) {
+ carFile, err := os.Open(tt.carPath)
+ require.NoError(t, err)
+ got, err := index.New(multicodec.CarMultihashIndexSorted)
+ require.NoError(t, err)
+ gotErr := carv2.LoadIndex(got, carFile, tt.opts...)
+ requireWant(tt, got, gotErr)
+ })
+ t.Run("GenerateIndex_"+tt.name, func(t *testing.T) {
+ carFile, err := os.Open(tt.carPath)
+ require.NoError(t, err)
+ got, gotErr := carv2.GenerateIndex(carFile, tt.opts...)
+ requireWant(tt, got, gotErr)
+ })
+ }
+}
+
+func TestMultihashIndexSortedConsistencyWithIndexSorted(t *testing.T) {
+ path := "testdata/sample-v1.car"
+
+ sortedIndex, err := carv2.GenerateIndexFromFile(path)
+ require.NoError(t, err)
+ require.Equal(t, multicodec.CarMultihashIndexSorted, sortedIndex.Codec())
+
+ f, err := os.Open(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, f.Close()) })
+ br, err := carv2.NewBlockReader(f)
+ require.NoError(t, err)
+
+ subject := generateMultihashSortedIndex(t, path)
+ for {
+ wantNext, err := br.Next()
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+
+ dmh, err := multihash.Decode(wantNext.Cid().Hash())
+ require.NoError(t, err)
+ if dmh.Code == multihash.IDENTITY {
+ continue
+ }
+
+ wantCid := wantNext.Cid()
+ var wantOffsets []uint64
+ err = sortedIndex.GetAll(wantCid, func(o uint64) bool {
+ wantOffsets = append(wantOffsets, o)
+ return false
+ })
+ require.NoError(t, err)
+
+ var gotOffsets []uint64
+ err = subject.GetAll(wantCid, func(o uint64) bool {
+ gotOffsets = append(gotOffsets, o)
+ return false
+ })
+
+ require.NoError(t, err)
+ require.Equal(t, wantOffsets, gotOffsets)
+ }
+}
+
+func TestMultihashSorted_ForEachIsConsistentWithGetAll(t *testing.T) {
+ path := "testdata/sample-v1.car"
+ f, err := os.Open(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, f.Close()) })
+
+ br, err := carv2.NewBlockReader(f)
+ require.NoError(t, err)
+ subject := generateMultihashSortedIndex(t, path)
+
+ gotForEach := make(map[string]uint64)
+ err = subject.ForEach(func(mh multihash.Multihash, offset uint64) error {
+ gotForEach[mh.String()] = offset
+ return nil
+ })
+ require.NoError(t, err)
+
+ for {
+ b, err := br.Next()
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+ c := b.Cid()
+ dmh, err := multihash.Decode(c.Hash())
+ require.NoError(t, err)
+ if dmh.Code == multihash.IDENTITY {
+ continue
+ }
+
+ wantMh := c.Hash()
+
+ var wantOffset uint64
+ err = subject.GetAll(c, func(u uint64) bool {
+ wantOffset = u
+ return false
+ })
+ require.NoError(t, err)
+
+ s := wantMh.String()
+ gotOffset, ok := gotForEach[s]
+ require.True(t, ok)
+ require.Equal(t, wantOffset, gotOffset)
+ }
+}
+
+func generateMultihashSortedIndex(t *testing.T, path string) *index.MultihashIndexSorted {
+ f, err := os.Open(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, f.Close()) })
+ reader := internalio.ToByteReadSeeker(f)
+ header, err := carv1.ReadHeader(reader, carv1.DefaultMaxAllowedHeaderSize)
+ require.NoError(t, err)
+ require.Equal(t, uint64(1), header.Version)
+
+ idx := index.NewMultihashSorted()
+ records := make([]index.Record, 0)
+
+ var sectionOffset int64
+ sectionOffset, err = reader.Seek(0, io.SeekCurrent)
+ require.NoError(t, err)
+
+ for {
+ sectionLen, err := varint.ReadUvarint(reader)
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+
+ if sectionLen == 0 {
+ break
+ }
+
+ cidLen, c, err := cid.CidFromReader(reader)
+ require.NoError(t, err)
+ records = append(records, index.Record{Cid: c, Offset: uint64(sectionOffset)})
+ remainingSectionLen := int64(sectionLen) - int64(cidLen)
+ sectionOffset, err = reader.Seek(remainingSectionLen, io.SeekCurrent)
+ require.NoError(t, err)
+ }
+
+ err = idx.Load(records)
+ require.NoError(t, err)
+
+ return idx
+}
diff --git a/ipld/car/v2/internal/carv1/car.go b/ipld/car/v2/internal/carv1/car.go
new file mode 100644
index 0000000000..469cb12747
--- /dev/null
+++ b/ipld/car/v2/internal/carv1/car.go
@@ -0,0 +1,284 @@
+package carv1
+
+import (
+ "context"
+ "fmt"
+ "io"
+
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1/util"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ internalio "github.com/ipfs/boxo/ipld/car/v2/internal/io"
+ "github.com/ipfs/boxo/ipld/merkledag"
+ cid "github.com/ipfs/go-cid"
+ cbor "github.com/ipfs/go-ipld-cbor"
+ format "github.com/ipfs/go-ipld-format"
+)
+
+const DefaultMaxAllowedHeaderSize uint64 = 32 << 20 // 32MiB
+const DefaultMaxAllowedSectionSize uint64 = 8 << 20 // 8MiB
+
+func init() {
+ cbor.RegisterCborType(CarHeader{})
+}
+
+type Store interface {
+ Put(context.Context, blocks.Block) error
+}
+
+type ReadStore interface {
+ Get(context.Context, cid.Cid) (blocks.Block, error)
+}
+
+type CarHeader struct {
+ Roots []cid.Cid
+ Version uint64
+}
+
+type carWriter struct {
+ ds format.NodeGetter
+ w io.Writer
+}
+
+func WriteCar(ctx context.Context, ds format.NodeGetter, roots []cid.Cid, w io.Writer) error {
+ h := &CarHeader{
+ Roots: roots,
+ Version: 1,
+ }
+
+ if err := WriteHeader(h, w); err != nil {
+ return fmt.Errorf("failed to write car header: %s", err)
+ }
+
+ cw := &carWriter{ds: ds, w: w}
+ seen := cid.NewSet()
+ for _, r := range roots {
+ if err := merkledag.Walk(ctx, cw.enumGetLinks, r, seen.Visit); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func ReadHeaderAt(at io.ReaderAt, maxReadBytes uint64) (*CarHeader, error) {
+ var rr io.Reader
+ switch r := at.(type) {
+ case io.Reader:
+ rr = r
+ default:
+ var err error
+ rr, err = internalio.NewOffsetReadSeeker(r, 0)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return ReadHeader(rr, maxReadBytes)
+}
+
+func ReadHeader(r io.Reader, maxReadBytes uint64) (*CarHeader, error) {
+ hb, err := util.LdRead(r, false, maxReadBytes)
+ if err != nil {
+ if err == util.ErrSectionTooLarge {
+ err = util.ErrHeaderTooLarge
+ }
+ return nil, err
+ }
+
+ var ch CarHeader
+ if err := cbor.DecodeInto(hb, &ch); err != nil {
+ return nil, fmt.Errorf("invalid header: %v", err)
+ }
+
+ return &ch, nil
+}
+
+func WriteHeader(h *CarHeader, w io.Writer) error {
+ hb, err := cbor.DumpObject(h)
+ if err != nil {
+ return err
+ }
+
+ return util.LdWrite(w, hb)
+}
+
+func HeaderSize(h *CarHeader) (uint64, error) {
+ hb, err := cbor.DumpObject(h)
+ if err != nil {
+ return 0, err
+ }
+
+ return util.LdSize(hb), nil
+}
+
+func (cw *carWriter) enumGetLinks(ctx context.Context, c cid.Cid) ([]*format.Link, error) {
+ nd, err := cw.ds.Get(ctx, c)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := cw.writeNode(ctx, nd); err != nil {
+ return nil, err
+ }
+
+ return nd.Links(), nil
+}
+
+func (cw *carWriter) writeNode(ctx context.Context, nd format.Node) error {
+ return util.LdWrite(cw.w, nd.Cid().Bytes(), nd.RawData())
+}
+
+type CarReader struct {
+ r io.Reader
+ Header *CarHeader
+ zeroLenAsEOF bool
+ maxAllowedSectionSize uint64
+}
+
+func NewCarReaderWithZeroLengthSectionAsEOF(r io.Reader) (*CarReader, error) {
+ return NewCarReaderWithoutDefaults(r, true, DefaultMaxAllowedHeaderSize, DefaultMaxAllowedSectionSize)
+}
+
+func NewCarReader(r io.Reader) (*CarReader, error) {
+ return NewCarReaderWithoutDefaults(r, false, DefaultMaxAllowedHeaderSize, DefaultMaxAllowedSectionSize)
+}
+
+func NewCarReaderWithoutDefaults(r io.Reader, zeroLenAsEOF bool, maxAllowedHeaderSize uint64, maxAllowedSectionSize uint64) (*CarReader, error) {
+ ch, err := ReadHeader(r, maxAllowedHeaderSize)
+ if err != nil {
+ return nil, err
+ }
+
+ if ch.Version != 1 {
+ return nil, fmt.Errorf("invalid car version: %d", ch.Version)
+ }
+
+ if len(ch.Roots) == 0 {
+ return nil, fmt.Errorf("empty car, no roots")
+ }
+
+ return &CarReader{
+ r: r,
+ Header: ch,
+ zeroLenAsEOF: zeroLenAsEOF,
+ maxAllowedSectionSize: maxAllowedSectionSize,
+ }, nil
+}
+
+func (cr *CarReader) Next() (blocks.Block, error) {
+ c, data, err := util.ReadNode(cr.r, cr.zeroLenAsEOF, cr.maxAllowedSectionSize)
+ if err != nil {
+ return nil, err
+ }
+
+ hashed, err := c.Prefix().Sum(data)
+ if err != nil {
+ return nil, err
+ }
+
+ if !hashed.Equals(c) {
+ return nil, fmt.Errorf("mismatch in content integrity, name: %s, data: %s", c, hashed)
+ }
+
+ return blocks.NewBlockWithCid(data, c)
+}
+
+type batchStore interface {
+ PutMany(context.Context, []blocks.Block) error
+}
+
+func LoadCar(s Store, r io.Reader) (*CarHeader, error) {
+ ctx := context.TODO()
+ cr, err := NewCarReader(r)
+ if err != nil {
+ return nil, err
+ }
+
+ if bs, ok := s.(batchStore); ok {
+ return loadCarFast(ctx, bs, cr)
+ }
+
+ return loadCarSlow(ctx, s, cr)
+}
+
+func loadCarFast(ctx context.Context, s batchStore, cr *CarReader) (*CarHeader, error) {
+ var buf []blocks.Block
+ for {
+ blk, err := cr.Next()
+ if err != nil {
+ if err == io.EOF {
+ if len(buf) > 0 {
+ if err := s.PutMany(ctx, buf); err != nil {
+ return nil, err
+ }
+ }
+ return cr.Header, nil
+ }
+ return nil, err
+ }
+
+ buf = append(buf, blk)
+
+ if len(buf) > 1000 {
+ if err := s.PutMany(ctx, buf); err != nil {
+ return nil, err
+ }
+ buf = buf[:0]
+ }
+ }
+}
+
+func loadCarSlow(ctx context.Context, s Store, cr *CarReader) (*CarHeader, error) {
+ for {
+ blk, err := cr.Next()
+ if err != nil {
+ if err == io.EOF {
+ return cr.Header, nil
+ }
+ return nil, err
+ }
+
+ if err := s.Put(ctx, blk); err != nil {
+ return nil, err
+ }
+ }
+}
+
+// Matches checks whether two headers match.
+// Two headers are considered matching if:
+// 1. They have the same version number, and
+// 2. They contain the same root CIDs in any order.
+//
+// Note, this function explicitly ignores the order of roots.
+// If order of roots matter use reflect.DeepEqual instead.
+func (h CarHeader) Matches(other CarHeader) bool {
+ if h.Version != other.Version {
+ return false
+ }
+ thisLen := len(h.Roots)
+ if thisLen != len(other.Roots) {
+ return false
+ }
+ // Headers with a single root are popular.
+ // Implement a fast execution path for popular cases.
+ if thisLen == 1 {
+ return h.Roots[0].Equals(other.Roots[0])
+ }
+
+ // Check other contains all roots.
+ // TODO: should this be optimised for cases where the number of roots are large since it has O(N^2) complexity?
+ for _, r := range h.Roots {
+ if !other.containsRoot(r) {
+ return false
+ }
+ }
+ return true
+}
+
+func (h *CarHeader) containsRoot(root cid.Cid) bool {
+ for _, r := range h.Roots {
+ if r.Equals(root) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/ipld/car/v2/internal/carv1/car_test.go b/ipld/car/v2/internal/carv1/car_test.go
new file mode 100644
index 0000000000..21602114e4
--- /dev/null
+++ b/ipld/car/v2/internal/carv1/car_test.go
@@ -0,0 +1,333 @@
+package carv1
+
+import (
+ "bytes"
+ "context"
+ "encoding/hex"
+ "io"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/ipfs/boxo/ipld/merkledag"
+ dstest "github.com/ipfs/boxo/ipld/merkledag/test"
+ cid "github.com/ipfs/go-cid"
+ format "github.com/ipfs/go-ipld-format"
+)
+
+func assertAddNodes(t *testing.T, ds format.DAGService, nds ...format.Node) {
+ for _, nd := range nds {
+ if err := ds.Add(context.Background(), nd); err != nil {
+ t.Fatal(err)
+ }
+ }
+}
+
+func TestRoundtrip(t *testing.T) {
+ dserv := dstest.Mock()
+ a := merkledag.NewRawNode([]byte("aaaa"))
+ b := merkledag.NewRawNode([]byte("bbbb"))
+ c := merkledag.NewRawNode([]byte("cccc"))
+
+ nd1 := &merkledag.ProtoNode{}
+ nd1.AddNodeLink("cat", a)
+
+ nd2 := &merkledag.ProtoNode{}
+ nd2.AddNodeLink("first", nd1)
+ nd2.AddNodeLink("dog", b)
+
+ nd3 := &merkledag.ProtoNode{}
+ nd3.AddNodeLink("second", nd2)
+ nd3.AddNodeLink("bear", c)
+
+ assertAddNodes(t, dserv, a, b, c, nd1, nd2, nd3)
+
+ buf := new(bytes.Buffer)
+ if err := WriteCar(context.Background(), dserv, []cid.Cid{nd3.Cid()}, buf); err != nil {
+ t.Fatal(err)
+ }
+
+ bserv := dstest.Bserv()
+ ch, err := LoadCar(bserv.Blockstore(), buf)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(ch.Roots) != 1 {
+ t.Fatal("should have one root")
+ }
+
+ if !ch.Roots[0].Equals(nd3.Cid()) {
+ t.Fatal("got wrong cid")
+ }
+
+ bs := bserv.Blockstore()
+ for _, nd := range []format.Node{a, b, c, nd1, nd2, nd3} {
+ has, err := bs.Has(context.TODO(), nd.Cid())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !has {
+ t.Fatal("should have cid in blockstore")
+ }
+ }
+}
+
+func TestEOFHandling(t *testing.T) {
+ // fixture is a clean single-block, single-root CAR
+ fixture, err := hex.DecodeString("3aa265726f6f747381d82a58250001711220151fe9e73c6267a7060c6f6c4cca943c236f4b196723489608edb42a8b8fa80b6776657273696f6e012c01711220151fe9e73c6267a7060c6f6c4cca943c236f4b196723489608edb42a8b8fa80ba165646f646779f5")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ load := func(t *testing.T, byts []byte) *CarReader {
+ cr, err := NewCarReader(bytes.NewReader(byts))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ blk, err := cr.Next()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if blk.Cid().String() != "bafyreiavd7u6opdcm6tqmddpnrgmvfb4enxuwglhenejmchnwqvixd5ibm" {
+ t.Fatal("unexpected CID")
+ }
+
+ return cr
+ }
+
+ t.Run("CleanEOF", func(t *testing.T) {
+ cr := load(t, fixture)
+
+ blk, err := cr.Next()
+ if err != io.EOF {
+ t.Fatal("Didn't get expected EOF")
+ }
+ if blk != nil {
+ t.Fatal("EOF returned expected block")
+ }
+ })
+
+ t.Run("BadVarint", func(t *testing.T) {
+ fixtureBadVarint := append(fixture, 160)
+ cr := load(t, fixtureBadVarint)
+
+ blk, err := cr.Next()
+ if err != io.ErrUnexpectedEOF {
+ t.Fatal("Didn't get unexpected EOF")
+ }
+ if blk != nil {
+ t.Fatal("EOF returned unexpected block")
+ }
+ })
+
+ t.Run("TruncatedBlock", func(t *testing.T) {
+ fixtureTruncatedBlock := append(fixture, 100, 0, 0)
+ cr := load(t, fixtureTruncatedBlock)
+
+ blk, err := cr.Next()
+ if err != io.ErrUnexpectedEOF {
+ t.Fatal("Didn't get unexpected EOF")
+ }
+ if blk != nil {
+ t.Fatal("EOF returned unexpected block")
+ }
+ })
+}
+
+func TestBadHeaders(t *testing.T) {
+ testCases := []struct {
+ name string
+ hex string
+ errStr string // either the whole error string
+ errPfx string // or just the prefix
+ }{
+ {
+ "{version:2}",
+ "0aa16776657273696f6e02",
+ "invalid car version: 2",
+ "",
+ },
+ {
+ // an unfortunate error because we don't use a pointer
+ "{roots:[baeaaaa3bmjrq]}",
+ "13a165726f6f747381d82a480001000003616263",
+ "invalid car version: 0",
+ "",
+ },
+ {
+ "{version:\"1\",roots:[baeaaaa3bmjrq]}",
+ "1da265726f6f747381d82a4800010000036162636776657273696f6e6131",
+ "", "invalid header: ",
+ },
+ {
+ "{version:1}",
+ "0aa16776657273696f6e01",
+ "empty car, no roots",
+ "",
+ },
+ {
+ "{version:1,roots:{cid:baeaaaa3bmjrq}}",
+ "20a265726f6f7473a163636964d82a4800010000036162636776657273696f6e01",
+ "",
+ "invalid header: ",
+ },
+ {
+ "{version:1,roots:[baeaaaa3bmjrq],blip:true}",
+ "22a364626c6970f565726f6f747381d82a4800010000036162636776657273696f6e01",
+ "",
+ "invalid header: ",
+ },
+ {
+ "[1,[]]",
+ "03820180",
+ "",
+ "invalid header: ",
+ },
+ {
+ // this is an unfortunate error, it'd be nice to catch it better but it's
+ // very unlikely we'd ever see this in practice
+ "null",
+ "01f6",
+ "",
+ "invalid car version: 0",
+ },
+ }
+
+ makeCar := func(t *testing.T, byts string) error {
+ fixture, err := hex.DecodeString(byts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = NewCarReader(bytes.NewReader(fixture))
+ return err
+ }
+
+ t.Run("Sanity check {version:1,roots:[baeaaaa3bmjrq]}", func(t *testing.T) {
+ err := makeCar(t, "1ca265726f6f747381d82a4800010000036162636776657273696f6e01")
+ if err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ err := makeCar(t, tc.hex)
+ if err == nil {
+ t.Fatal("expected error from bad header, didn't get one")
+ }
+ if tc.errStr != "" {
+ if err.Error() != tc.errStr {
+ t.Fatalf("bad error: %v", err)
+ }
+ } else {
+ if !strings.HasPrefix(err.Error(), tc.errPfx) {
+ t.Fatalf("bad error: %v", err)
+ }
+ }
+ })
+ }
+}
+
+func TestCarHeaderMatchess(t *testing.T) {
+ oneCid := merkledag.NewRawNode([]byte("fish")).Cid()
+ anotherCid := merkledag.NewRawNode([]byte("lobster")).Cid()
+ tests := []struct {
+ name string
+ one CarHeader
+ other CarHeader
+ want bool
+ }{
+ {
+ "SameVersionNilRootsIsMatching",
+ CarHeader{nil, 1},
+ CarHeader{nil, 1},
+ true,
+ },
+ {
+ "SameVersionEmptyRootsIsMatching",
+ CarHeader{[]cid.Cid{}, 1},
+ CarHeader{[]cid.Cid{}, 1},
+ true,
+ },
+ {
+ "SameVersionNonEmptySameRootsIsMatching",
+ CarHeader{[]cid.Cid{oneCid}, 1},
+ CarHeader{[]cid.Cid{oneCid}, 1},
+ true,
+ },
+ {
+ "SameVersionNonEmptySameRootsInDifferentOrderIsMatching",
+ CarHeader{[]cid.Cid{oneCid, anotherCid}, 1},
+ CarHeader{[]cid.Cid{anotherCid, oneCid}, 1},
+ true,
+ },
+ {
+ "SameVersionDifferentRootsIsNotMatching",
+ CarHeader{[]cid.Cid{oneCid}, 1},
+ CarHeader{[]cid.Cid{anotherCid}, 1},
+ false,
+ },
+ {
+ "DifferentVersionDifferentRootsIsNotMatching",
+ CarHeader{[]cid.Cid{oneCid}, 0},
+ CarHeader{[]cid.Cid{anotherCid}, 1},
+ false,
+ },
+ {
+ "MismatchingVersionIsNotMatching",
+ CarHeader{nil, 0},
+ CarHeader{nil, 1},
+ false,
+ },
+ {
+ "ZeroValueHeadersAreMatching",
+ CarHeader{},
+ CarHeader{},
+ true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := tt.one.Matches(tt.other)
+ require.Equal(t, tt.want, got, "Matches() = %v, want %v", got, tt.want)
+ })
+ }
+}
+
+func TestReadingZeroLengthSectionWithoutOptionSetIsError(t *testing.T) {
+ f, err := os.Open("../../testdata/sample-v1-with-zero-len-section.car")
+ require.NoError(t, err)
+ subject, err := NewCarReader(f)
+ require.NoError(t, err)
+
+ for {
+ _, err := subject.Next()
+ if err == io.EOF {
+ break
+ } else if err != nil {
+ require.EqualError(t, err, "varints malformed, could not reach the end")
+ return
+ }
+ }
+ require.Fail(t, "expected error when reading file with zero section without option set")
+}
+
+func TestReadingZeroLengthSectionWithOptionSetIsSuccess(t *testing.T) {
+ f, err := os.Open("../../testdata/sample-v1-with-zero-len-section.car")
+ require.NoError(t, err)
+ subject, err := NewCarReaderWithZeroLengthSectionAsEOF(f)
+ require.NoError(t, err)
+
+ for {
+ _, err := subject.Next()
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+ }
+}
diff --git a/ipld/car/v2/internal/carv1/doc.go b/ipld/car/v2/internal/carv1/doc.go
new file mode 100644
index 0000000000..821ca2f0aa
--- /dev/null
+++ b/ipld/car/v2/internal/carv1/doc.go
@@ -0,0 +1,2 @@
+// Forked from CARv1 to avoid dependency to ipld-prime 0.9.0 due to outstanding upgrades in filecoin.
+package carv1
diff --git a/ipld/car/v2/internal/carv1/util/util.go b/ipld/car/v2/internal/carv1/util/util.go
new file mode 100644
index 0000000000..eb20da6521
--- /dev/null
+++ b/ipld/car/v2/internal/carv1/util/util.go
@@ -0,0 +1,98 @@
+package util
+
+import (
+ "errors"
+ "io"
+
+ internalio "github.com/ipfs/boxo/ipld/car/v2/internal/io"
+
+ "github.com/multiformats/go-varint"
+
+ cid "github.com/ipfs/go-cid"
+)
+
+var ErrSectionTooLarge = errors.New("invalid section data, length of read beyond allowable maximum")
+var ErrHeaderTooLarge = errors.New("invalid header data, length of read beyond allowable maximum")
+
+type BytesReader interface {
+ io.Reader
+ io.ByteReader
+}
+
+func ReadNode(r io.Reader, zeroLenAsEOF bool, maxReadBytes uint64) (cid.Cid, []byte, error) {
+ data, err := LdRead(r, zeroLenAsEOF, maxReadBytes)
+ if err != nil {
+ return cid.Cid{}, nil, err
+ }
+
+ n, c, err := cid.CidFromBytes(data)
+ if err != nil {
+ return cid.Cid{}, nil, err
+ }
+
+ return c, data[n:], nil
+}
+
+func LdWrite(w io.Writer, d ...[]byte) error {
+ var sum uint64
+ for _, s := range d {
+ sum += uint64(len(s))
+ }
+
+ buf := make([]byte, 8)
+ n := varint.PutUvarint(buf, sum)
+ _, err := w.Write(buf[:n])
+ if err != nil {
+ return err
+ }
+
+ for _, s := range d {
+ _, err = w.Write(s)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func LdSize(d ...[]byte) uint64 {
+ var sum uint64
+ for _, s := range d {
+ sum += uint64(len(s))
+ }
+ s := varint.UvarintSize(sum)
+ return sum + uint64(s)
+}
+
+func LdReadSize(r io.Reader, zeroLenAsEOF bool, maxReadBytes uint64) (uint64, error) {
+ l, err := varint.ReadUvarint(internalio.ToByteReader(r))
+ if err != nil {
+ // If the length of bytes read is non-zero when the error is EOF then signal an unclean EOF.
+ if l > 0 && err == io.EOF {
+ return 0, io.ErrUnexpectedEOF
+ }
+ return 0, err
+ } else if l == 0 && zeroLenAsEOF {
+ return 0, io.EOF
+ }
+
+ if l > maxReadBytes { // Don't OOM
+ return 0, ErrSectionTooLarge
+ }
+ return l, nil
+}
+
+func LdRead(r io.Reader, zeroLenAsEOF bool, maxReadBytes uint64) ([]byte, error) {
+ l, err := LdReadSize(r, zeroLenAsEOF, maxReadBytes)
+ if err != nil {
+ return nil, err
+ }
+
+ buf := make([]byte, l)
+ if _, err := io.ReadFull(r, buf); err != nil {
+ return nil, err
+ }
+
+ return buf, nil
+}
diff --git a/ipld/car/v2/internal/carv1/util/util_test.go b/ipld/car/v2/internal/carv1/util/util_test.go
new file mode 100644
index 0000000000..594312f991
--- /dev/null
+++ b/ipld/car/v2/internal/carv1/util/util_test.go
@@ -0,0 +1,28 @@
+package util_test
+
+import (
+ "bytes"
+ crand "crypto/rand"
+ "math/rand"
+ "testing"
+
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1/util"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestLdSize(t *testing.T) {
+ for i := 0; i < 5; i++ {
+ var buf bytes.Buffer
+ data := make([][]byte, 5)
+ for j := 0; j < 5; j++ {
+ data[j] = make([]byte, rand.Intn(30))
+ _, err := crand.Read(data[j])
+ require.NoError(t, err)
+ }
+ size := util.LdSize(data...)
+ err := util.LdWrite(&buf, data...)
+ require.NoError(t, err)
+ require.Equal(t, uint64(len(buf.Bytes())), size)
+ }
+}
diff --git a/ipld/car/v2/internal/errsort/search.go b/ipld/car/v2/internal/errsort/search.go
new file mode 100644
index 0000000000..fb88617940
--- /dev/null
+++ b/ipld/car/v2/internal/errsort/search.go
@@ -0,0 +1,54 @@
+/*
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+package errsort
+
+// Search is like sort.Search but accepts an erroring closure.
+// If it errors the search is terminated immediately
+func Search(n int, f func(int) (bool, error)) (int, error) {
+ // Define f(-1) == false and f(n) == true.
+ // Invariant: f(i-1) == false, f(j) == true.
+ i, j := 0, n
+ for i < j {
+ h := int(uint(i+j) >> 1) // avoid overflow when computing h
+ // i ≤ h < j
+ less, err := f(h)
+ if err != nil {
+ return 0, err
+ }
+ if !less {
+ i = h + 1 // preserves f(i-1) == false
+ } else {
+ j = h // preserves f(j) == true
+ }
+ }
+ // i == j, f(i-1) == false, and f(j) (= f(i)) == true => answer is i.
+ return i, nil
+}
diff --git a/ipld/car/v2/internal/io/converter.go b/ipld/car/v2/internal/io/converter.go
new file mode 100644
index 0000000000..2e2844bd0e
--- /dev/null
+++ b/ipld/car/v2/internal/io/converter.go
@@ -0,0 +1,155 @@
+package io
+
+import (
+ "errors"
+ "io"
+ "sync"
+)
+
+var (
+ _ io.ByteReader = (*readerPlusByte)(nil)
+ _ io.ByteReader = (*readSeekerPlusByte)(nil)
+ _ io.ByteReader = (*discardingReadSeekerPlusByte)(nil)
+ _ io.ReadSeeker = (*discardingReadSeekerPlusByte)(nil)
+ _ io.ReadSeeker = (*readerAtSeeker)(nil)
+ _ io.ReaderAt = (*readSeekerAt)(nil)
+)
+
+type (
+ readerPlusByte struct {
+ io.Reader
+
+ byteBuf [1]byte // escapes via io.Reader.Read; preallocate
+ }
+
+ readSeekerPlusByte struct {
+ io.ReadSeeker
+
+ byteBuf [1]byte // escapes via io.Reader.Read; preallocate
+ }
+
+ discardingReadSeekerPlusByte struct {
+ io.Reader
+ offset int64
+
+ byteBuf [1]byte // escapes via io.Reader.Read; preallocate
+ }
+
+ ByteReadSeeker interface {
+ io.ReadSeeker
+ io.ByteReader
+ }
+
+ readSeekerAt struct {
+ rs io.ReadSeeker
+ mu sync.Mutex
+ }
+
+ readerAtSeeker struct {
+ ra io.ReaderAt
+ position int64
+ mu sync.Mutex
+ }
+)
+
+func ToByteReader(r io.Reader) io.ByteReader {
+ if br, ok := r.(io.ByteReader); ok {
+ return br
+ }
+ return &readerPlusByte{Reader: r}
+}
+
+func ToByteReadSeeker(r io.Reader) ByteReadSeeker {
+ if brs, ok := r.(ByteReadSeeker); ok {
+ return brs
+ }
+ if rs, ok := r.(io.ReadSeeker); ok {
+ return &readSeekerPlusByte{ReadSeeker: rs}
+ }
+ return &discardingReadSeekerPlusByte{Reader: r}
+}
+
+func ToReadSeeker(ra io.ReaderAt) io.ReadSeeker {
+ if rs, ok := ra.(io.ReadSeeker); ok {
+ return rs
+ }
+ return &readerAtSeeker{ra: ra}
+}
+
+func ToReaderAt(rs io.ReadSeeker) io.ReaderAt {
+ if ra, ok := rs.(io.ReaderAt); ok {
+ return ra
+ }
+ return &readSeekerAt{rs: rs}
+}
+
+func (rb *readerPlusByte) ReadByte() (byte, error) {
+ _, err := io.ReadFull(rb, rb.byteBuf[:])
+ return rb.byteBuf[0], err
+}
+
+func (rsb *readSeekerPlusByte) ReadByte() (byte, error) {
+ _, err := io.ReadFull(rsb, rsb.byteBuf[:])
+ return rsb.byteBuf[0], err
+}
+
+func (drsb *discardingReadSeekerPlusByte) ReadByte() (byte, error) {
+ _, err := io.ReadFull(drsb, drsb.byteBuf[:])
+ return drsb.byteBuf[0], err
+}
+
+func (drsb *discardingReadSeekerPlusByte) Read(p []byte) (read int, err error) {
+ read, err = drsb.Reader.Read(p)
+ drsb.offset += int64(read)
+ return
+}
+
+func (drsb *discardingReadSeekerPlusByte) Seek(offset int64, whence int) (int64, error) {
+ switch whence {
+ case io.SeekStart:
+ n := offset - drsb.offset
+ if n < 0 {
+ return 0, errors.New("unsupported rewind via whence: io.SeekStart")
+ }
+ _, err := io.CopyN(io.Discard, drsb, n)
+ return drsb.offset, err
+ case io.SeekCurrent:
+ _, err := io.CopyN(io.Discard, drsb, offset)
+ return drsb.offset, err
+ default:
+ return 0, errors.New("unsupported whence: io.SeekEnd")
+ }
+}
+
+func (ras *readerAtSeeker) Read(p []byte) (n int, err error) {
+ ras.mu.Lock()
+ defer ras.mu.Unlock()
+ n, err = ras.ra.ReadAt(p, ras.position)
+ ras.position += int64(n)
+ return n, err
+}
+
+func (ras *readerAtSeeker) Seek(offset int64, whence int) (int64, error) {
+ ras.mu.Lock()
+ defer ras.mu.Unlock()
+ switch whence {
+ case io.SeekStart:
+ ras.position = offset
+ case io.SeekCurrent:
+ ras.position += offset
+ case io.SeekEnd:
+ return 0, errors.New("unsupported whence: io.SeekEnd")
+ default:
+ return 0, errors.New("unsupported whence")
+ }
+ return ras.position, nil
+}
+
+func (rsa *readSeekerAt) ReadAt(p []byte, off int64) (n int, err error) {
+ rsa.mu.Lock()
+ defer rsa.mu.Unlock()
+ if _, err := rsa.rs.Seek(off, io.SeekStart); err != nil {
+ return 0, err
+ }
+ return rsa.rs.Read(p)
+}
diff --git a/ipld/car/v2/internal/io/offset_read_seeker.go b/ipld/car/v2/internal/io/offset_read_seeker.go
new file mode 100644
index 0000000000..b3899ab784
--- /dev/null
+++ b/ipld/car/v2/internal/io/offset_read_seeker.go
@@ -0,0 +1,122 @@
+package io
+
+import (
+ "errors"
+ "io"
+)
+
+var (
+ _ io.ReaderAt = (*offsetReadSeeker)(nil)
+ _ io.ReadSeeker = (*offsetReadSeeker)(nil)
+)
+
+// offsetReadSeeker implements Read, and ReadAt on a section
+// of an underlying io.ReaderAt.
+// The main difference between io.SectionReader and offsetReadSeeker is that
+// NewOffsetReadSeeker does not require the user to know the number of readable bytes.
+//
+// It also partially implements Seek, where the implementation panics if io.SeekEnd is passed.
+// This is because, offsetReadSeeker does not know the end of the file therefore cannot seek relative
+// to it.
+type offsetReadSeeker struct {
+ r io.ReaderAt
+ base int64
+ off int64
+ b [1]byte // avoid alloc in ReadByte
+}
+
+type ReadSeekerAt interface {
+ io.Reader
+ io.ReaderAt
+ io.Seeker
+ io.ByteReader
+}
+
+// NewOffsetReadSeeker returns an ReadSeekerAt that reads from r
+// starting offset offset off and stops with io.EOF when r reaches its end.
+// The Seek function will panic if whence io.SeekEnd is passed.
+func NewOffsetReadSeeker(r io.ReaderAt, off int64) (ReadSeekerAt, error) {
+ if or, ok := r.(*offsetReadSeeker); ok {
+ oldBase := or.base
+ newBase := or.base + off
+ if newBase < oldBase {
+ return nil, errors.New("NewOffsetReadSeeker overflow int64")
+ }
+ return &offsetReadSeeker{
+ r: or.r,
+ base: newBase,
+ off: newBase,
+ }, nil
+ }
+ return &offsetReadSeeker{
+ r: r,
+ base: off,
+ off: off,
+ }, nil
+}
+
+func (o *offsetReadSeeker) Read(p []byte) (n int, err error) {
+ n, err = o.r.ReadAt(p, o.off)
+ oldOffset := o.off
+ off := oldOffset + int64(n)
+ if off < oldOffset {
+ return 0, errors.New("ReadAt offset overflow")
+ }
+ o.off = off
+ return
+}
+
+func (o *offsetReadSeeker) ReadAt(p []byte, off int64) (n int, err error) {
+ if off < 0 {
+ return 0, io.EOF
+ }
+ oldOffset := off
+ off += o.base
+ if off < oldOffset {
+ return 0, errors.New("ReadAt offset overflow")
+ }
+ return o.r.ReadAt(p, off)
+}
+
+func (o *offsetReadSeeker) ReadByte() (byte, error) {
+ _, err := o.Read(o.b[:])
+ return o.b[0], err
+}
+
+func (o *offsetReadSeeker) Offset() int64 {
+ return o.off
+}
+
+func (o *offsetReadSeeker) Seek(offset int64, whence int) (int64, error) {
+ switch whence {
+ case io.SeekStart:
+ oldOffset := offset
+ off := offset + o.base
+ if off < oldOffset {
+ return 0, errors.New("Seek offset overflow")
+ }
+ o.off = off
+ case io.SeekCurrent:
+ oldOffset := o.off
+ if offset < 0 {
+ if -offset > oldOffset {
+ return 0, errors.New("Seek offset underflow")
+ }
+ o.off = oldOffset + offset
+ } else {
+ off := oldOffset + offset
+ if off < oldOffset {
+ return 0, errors.New("Seek offset overflow")
+ }
+ o.off = off
+ }
+ case io.SeekEnd:
+ panic("unsupported whence: SeekEnd")
+ }
+ return o.Position(), nil
+}
+
+// Position returns the current position of this reader relative to the initial offset.
+func (o *offsetReadSeeker) Position() int64 {
+ return o.off - o.base
+}
diff --git a/ipld/car/v2/internal/io/offset_write_seeker.go b/ipld/car/v2/internal/io/offset_write_seeker.go
new file mode 100644
index 0000000000..7e0f6ba58e
--- /dev/null
+++ b/ipld/car/v2/internal/io/offset_write_seeker.go
@@ -0,0 +1,41 @@
+package io
+
+import "io"
+
+var (
+ _ io.Writer = (*OffsetWriteSeeker)(nil)
+ _ io.WriteSeeker = (*OffsetWriteSeeker)(nil)
+)
+
+type OffsetWriteSeeker struct {
+ w io.WriterAt
+ base int64
+ offset int64
+}
+
+func NewOffsetWriter(w io.WriterAt, off int64) *OffsetWriteSeeker {
+ return &OffsetWriteSeeker{w, off, off}
+}
+
+func (ow *OffsetWriteSeeker) Write(b []byte) (n int, err error) {
+ n, err = ow.w.WriteAt(b, ow.offset)
+ ow.offset += int64(n)
+ return
+}
+
+func (ow *OffsetWriteSeeker) Seek(offset int64, whence int) (int64, error) {
+ switch whence {
+ case io.SeekStart:
+ ow.offset = offset + ow.base
+ case io.SeekCurrent:
+ ow.offset += offset
+ case io.SeekEnd:
+ panic("unsupported whence: SeekEnd")
+ }
+ return ow.Position(), nil
+}
+
+// Position returns the current position of this writer relative to the initial offset, i.e. the number of bytes written.
+func (ow *OffsetWriteSeeker) Position() int64 {
+ return ow.offset - ow.base
+}
diff --git a/ipld/car/v2/internal/loader/counting_loader.go b/ipld/car/v2/internal/loader/counting_loader.go
new file mode 100644
index 0000000000..e428993ea4
--- /dev/null
+++ b/ipld/car/v2/internal/loader/counting_loader.go
@@ -0,0 +1,61 @@
+package loader
+
+import (
+ "bytes"
+ "io"
+
+ "github.com/ipld/go-ipld-prime"
+ "github.com/ipld/go-ipld-prime/linking"
+ "github.com/multiformats/go-varint"
+)
+
+// counter tracks how much data has been read.
+type counter struct {
+ totalRead uint64
+}
+
+func (c *counter) Size() uint64 {
+ return c.totalRead
+}
+
+// ReadCounter provides an externally consumable interface to the
+// additional data tracked about the linksystem.
+type ReadCounter interface {
+ Size() uint64
+}
+
+type countingReader struct {
+ r io.Reader
+ c *counter
+}
+
+func (c *countingReader) Read(p []byte) (int, error) {
+ n, err := c.r.Read(p)
+ c.c.totalRead += uint64(n)
+ return n, err
+}
+
+// CountingLinkSystem wraps an ipld linksystem with to track the size of
+// data loaded in a `counter` object. Each time nodes are loaded from the
+// link system which trigger block reads, the size of the block as it would
+// appear in a CAR file is added to the counter (included the size of the
+// CID and the varint length for the block data).
+func CountingLinkSystem(ls ipld.LinkSystem) (ipld.LinkSystem, ReadCounter) {
+ c := counter{}
+ clc := ls
+ clc.StorageReadOpener = func(lc linking.LinkContext, l ipld.Link) (io.Reader, error) {
+ r, err := ls.StorageReadOpener(lc, l)
+ if err != nil {
+ return nil, err
+ }
+ buf := bytes.NewBuffer(nil)
+ n, err := buf.ReadFrom(r)
+ if err != nil {
+ return nil, err
+ }
+ size := varint.ToUvarint(uint64(n) + uint64(len(l.Binary())))
+ c.totalRead += uint64(len(size)) + uint64(len(l.Binary()))
+ return &countingReader{buf, &c}, nil
+ }
+ return clc, &c
+}
diff --git a/ipld/car/v2/internal/loader/writing_loader.go b/ipld/car/v2/internal/loader/writing_loader.go
new file mode 100644
index 0000000000..8be1af3f06
--- /dev/null
+++ b/ipld/car/v2/internal/loader/writing_loader.go
@@ -0,0 +1,126 @@
+package loader
+
+import (
+ "bytes"
+ "io"
+
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+ "github.com/ipfs/go-cid"
+ "github.com/ipld/go-ipld-prime"
+ "github.com/ipld/go-ipld-prime/linking"
+ "github.com/multiformats/go-multicodec"
+ "github.com/multiformats/go-varint"
+)
+
+type writerOutput struct {
+ w io.Writer
+ size uint64
+ code multicodec.Code
+ rcrds map[cid.Cid]index.Record
+}
+
+func (w *writerOutput) Size() uint64 {
+ return w.size
+}
+
+func (w *writerOutput) Index() (index.Index, error) {
+ idx, err := index.New(w.code)
+ if err != nil {
+ return nil, err
+ }
+ rcrds := make([]index.Record, 0, len(w.rcrds))
+ for _, r := range w.rcrds {
+ rcrds = append(rcrds, r)
+ }
+ if err := idx.Load(rcrds); err != nil {
+ return nil, err
+ }
+
+ return idx, nil
+}
+
+// An IndexTracker tracks the records loaded/written, calculate an
+// index based on them.
+type IndexTracker interface {
+ ReadCounter
+ Index() (index.Index, error)
+}
+
+type writingReader struct {
+ r io.Reader
+ len int64
+ cid string
+ wo *writerOutput
+}
+
+func (w *writingReader) Read(p []byte) (int, error) {
+ if w.wo != nil {
+ // write the cid
+ size := varint.ToUvarint(uint64(w.len) + uint64(len(w.cid)))
+ if _, err := w.wo.w.Write(size); err != nil {
+ return 0, err
+ }
+ if _, err := w.wo.w.Write([]byte(w.cid)); err != nil {
+ return 0, err
+ }
+ cpy := bytes.NewBuffer(w.r.(*bytes.Buffer).Bytes())
+ if _, err := cpy.WriteTo(w.wo.w); err != nil {
+ return 0, err
+ }
+ _, c, err := cid.CidFromBytes([]byte(w.cid))
+ if err != nil {
+ return 0, err
+ }
+ w.wo.rcrds[c] = index.Record{
+ Cid: c,
+ Offset: w.wo.size,
+ }
+ w.wo.size += uint64(w.len) + uint64(len(size)+len(w.cid))
+
+ w.wo = nil
+ }
+
+ return w.r.Read(p)
+}
+
+// TeeingLinkSystem wraps an IPLD.LinkSystem so that each time a block is loaded from it,
+// that block is also written as a CAR block to the provided io.Writer. Metadata
+// (the size of data written) is provided in the second return value.
+// The `initialOffset` is used to calculate the offsets recorded for the index, and will be
+//
+// included in the `.Size()` of the IndexTracker.
+//
+// An indexCodec of `index.CarIndexNoIndex` can be used to not track these offsets.
+func TeeingLinkSystem(ls ipld.LinkSystem, w io.Writer, initialOffset uint64, indexCodec multicodec.Code) (ipld.LinkSystem, IndexTracker) {
+ wo := writerOutput{
+ w: w,
+ size: initialOffset,
+ code: indexCodec,
+ rcrds: make(map[cid.Cid]index.Record),
+ }
+
+ tls := ls
+ tls.StorageReadOpener = func(lc linking.LinkContext, l ipld.Link) (io.Reader, error) {
+ _, c, err := cid.CidFromBytes([]byte(l.Binary()))
+ if err != nil {
+ return nil, err
+ }
+
+ // if we've already read this cid in this session, don't re-write it.
+ if _, ok := wo.rcrds[c]; ok {
+ return ls.StorageReadOpener(lc, l)
+ }
+
+ r, err := ls.StorageReadOpener(lc, l)
+ if err != nil {
+ return nil, err
+ }
+ buf := bytes.NewBuffer(nil)
+ n, err := buf.ReadFrom(r)
+ if err != nil {
+ return nil, err
+ }
+ return &writingReader{buf, n, l.Binary(), &wo}, nil
+ }
+ return tls, &wo
+}
diff --git a/ipld/car/v2/internal/store/identity.go b/ipld/car/v2/internal/store/identity.go
new file mode 100644
index 0000000000..d61a57c48d
--- /dev/null
+++ b/ipld/car/v2/internal/store/identity.go
@@ -0,0 +1,17 @@
+package store
+
+import (
+ "github.com/ipfs/go-cid"
+ "github.com/multiformats/go-multihash"
+)
+
+// IsIdentity inspects the CID and determines whether it is an IDENTITY CID.
+func IsIdentity(key cid.Cid) (digest []byte, ok bool, err error) {
+ dmh, err := multihash.Decode(key.Hash())
+ if err != nil {
+ return nil, false, err
+ }
+ ok = dmh.Code == multihash.IDENTITY
+ digest = dmh.Digest
+ return digest, ok, nil
+}
diff --git a/ipld/car/v2/internal/store/index.go b/ipld/car/v2/internal/store/index.go
new file mode 100644
index 0000000000..b29ec8e86b
--- /dev/null
+++ b/ipld/car/v2/internal/store/index.go
@@ -0,0 +1,108 @@
+package store
+
+import (
+ "bytes"
+ "io"
+
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1/util"
+ internalio "github.com/ipfs/boxo/ipld/car/v2/internal/io"
+ "github.com/ipfs/go-cid"
+ "github.com/multiformats/go-multicodec"
+ "github.com/multiformats/go-varint"
+)
+
+// FindCid can be used to either up the existence, size and offset of a block
+// if it exists in CAR as specified by the index; and optionally the data bytes
+// of the block.
+func FindCid(
+ reader io.ReaderAt,
+ idx index.Index,
+ key cid.Cid,
+ useWholeCids bool,
+ zeroLenAsEOF bool,
+ maxReadBytes uint64,
+ readBytes bool,
+) ([]byte, int64, int, error) {
+
+ var fnData []byte
+ var fnOffset int64
+ var fnLen int = -1
+ var fnErr error
+ err := idx.GetAll(key, func(offset uint64) bool {
+ reader, err := internalio.NewOffsetReadSeeker(reader, int64(offset))
+ if err != nil {
+ fnErr = err
+ return false
+ }
+ var readCid cid.Cid
+ if readBytes {
+ readCid, fnData, err = util.ReadNode(reader, zeroLenAsEOF, maxReadBytes)
+ if err != nil {
+ fnErr = err
+ return false
+ }
+ fnLen = len(fnData)
+ } else {
+ sectionLen, err := varint.ReadUvarint(reader)
+ if err != nil {
+ fnErr = err
+ return false
+ }
+ var cidLen int
+ cidLen, readCid, err = cid.CidFromReader(reader)
+ if err != nil {
+ fnErr = err
+ return false
+ }
+ fnLen = int(sectionLen) - cidLen
+ fnOffset = int64(offset) + reader.(interface{ Position() int64 }).Position()
+ }
+ if useWholeCids {
+ if !readCid.Equals(key) {
+ fnLen = -1
+ return true // continue looking
+ }
+ return false
+ } else {
+ if !bytes.Equal(readCid.Hash(), key.Hash()) {
+ // weird, bad index, continue looking
+ fnLen = -1
+ return true
+ }
+ return false
+ }
+ })
+ if err != nil {
+ return nil, -1, -1, err
+ }
+ if fnErr != nil {
+ return nil, -1, -1, fnErr
+ }
+ if fnLen == -1 {
+ return nil, -1, -1, index.ErrNotFound
+ }
+ return fnData, fnOffset, fnLen, nil
+}
+
+// Finalize will write the index to the writer at the offset specified in the header. It should only
+// be used for a CARv2 and when the CAR interface is being closed.
+func Finalize(writer io.WriterAt, header carv2.Header, idx *InsertionIndex, dataSize uint64, storeIdentityCIDs bool, indexCodec multicodec.Code) error {
+ // TODO check if add index option is set and don't write the index then set index offset to zero.
+ header = header.WithDataSize(dataSize)
+ header.Characteristics.SetFullyIndexed(storeIdentityCIDs)
+
+ // TODO if index not needed don't bother flattening it.
+ fi, err := idx.Flatten(indexCodec)
+ if err != nil {
+ return err
+ }
+ if _, err := index.WriteTo(fi, internalio.NewOffsetWriter(writer, int64(header.IndexOffset))); err != nil {
+ return err
+ }
+ if _, err := header.WriteTo(internalio.NewOffsetWriter(writer, carv2.PragmaSize)); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/ipld/car/v2/internal/store/indexcheck.go b/ipld/car/v2/internal/store/indexcheck.go
new file mode 100644
index 0000000000..89c563bb5b
--- /dev/null
+++ b/ipld/car/v2/internal/store/indexcheck.go
@@ -0,0 +1,84 @@
+package store
+
+import (
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/go-cid"
+)
+
+// ShouldPut returns true if the block should be put into the CAR according to the options provided
+// and the index. It returns false if the block should not be put into the CAR, either because it
+// is an identity block and StoreIdentityCIDs is false, or because it already exists and
+// BlockstoreAllowDuplicatePuts is false.
+func ShouldPut(
+ idx *InsertionIndex,
+ c cid.Cid,
+ maxIndexCidSize uint64,
+ storeIdentityCIDs bool,
+ blockstoreAllowDuplicatePuts bool,
+ blockstoreUseWholeCIDs bool,
+) (bool, error) {
+
+ // If StoreIdentityCIDs option is disabled then treat IDENTITY CIDs like IdStore.
+ if !storeIdentityCIDs {
+ // Check for IDENTITY CID. If IDENTITY, ignore and move to the next block.
+ if _, ok, err := IsIdentity(c); err != nil {
+ return false, err
+ } else if ok {
+ return false, nil
+ }
+ }
+
+ // Check if its size is too big.
+ // If larger than maximum allowed size, return error.
+ // Note, we need to check this regardless of whether we have IDENTITY CID or not.
+ // Since multhihash codes other than IDENTITY can result in large digests.
+ cSize := uint64(len(c.Bytes()))
+ if cSize > maxIndexCidSize {
+ return false, &carv2.ErrCidTooLarge{MaxSize: maxIndexCidSize, CurrentSize: cSize}
+ }
+
+ if !blockstoreAllowDuplicatePuts {
+ if blockstoreUseWholeCIDs {
+ has, err := idx.HasExactCID(c)
+ if err != nil {
+ return false, err
+ }
+ return !has, nil // deduplicated by CID
+ }
+ if !blockstoreUseWholeCIDs {
+ _, err := idx.Get(c)
+ if err == nil {
+ return false, nil // deduplicated by hash
+ }
+ }
+ }
+
+ return true, nil
+}
+
+// Has returns true if the block exists in in the store according to the various
+// rules associated with the options. Similar to ShouldPut, but for the simpler
+// Has() case.
+func Has(
+ idx *InsertionIndex,
+ c cid.Cid,
+ maxIndexCidSize uint64,
+ storeIdentityCIDs bool,
+ blockstoreAllowDuplicatePuts bool,
+ blockstoreUseWholeCIDs bool,
+) (bool, error) {
+
+ // If StoreIdentityCIDs option is disabled then treat IDENTITY CIDs like IdStore.
+ if !storeIdentityCIDs {
+ if _, ok, err := IsIdentity(c); err != nil {
+ return false, err
+ } else if ok {
+ return true, nil
+ }
+ }
+
+ if blockstoreUseWholeCIDs {
+ return idx.HasExactCID(c)
+ }
+ return idx.HasMultihash(c.Hash())
+}
diff --git a/ipld/car/v2/internal/store/insertionindex.go b/ipld/car/v2/internal/store/insertionindex.go
new file mode 100644
index 0000000000..a27844552d
--- /dev/null
+++ b/ipld/car/v2/internal/store/insertionindex.go
@@ -0,0 +1,264 @@
+package store
+
+import (
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+ "github.com/ipfs/go-cid"
+ "github.com/multiformats/go-multicodec"
+ "github.com/multiformats/go-multihash"
+ "github.com/petar/GoLLRB/llrb"
+ cbor "github.com/whyrusleeping/cbor/go"
+)
+
+// This index is intended to be efficient for random-access, in-memory lookups
+// and is not intended to be an index type that is attached to a CARv2.
+// See flatten() for conversion of this data to a known, existing index type.
+
+var (
+ errUnsupported = errors.New("not supported")
+ insertionIndexCodec = multicodec.Code(0x300003)
+)
+
+type InsertionIndex struct {
+ items llrb.LLRB
+}
+
+func NewInsertionIndex() *InsertionIndex {
+ return &InsertionIndex{}
+}
+
+type recordDigest struct {
+ digest []byte
+ index.Record
+}
+
+func (r recordDigest) Less(than llrb.Item) bool {
+ other, ok := than.(recordDigest)
+ if !ok {
+ return false
+ }
+ return bytes.Compare(r.digest, other.digest) < 0
+}
+
+func newRecordDigest(r index.Record) recordDigest {
+ d, err := multihash.Decode(r.Hash())
+ if err != nil {
+ panic(err)
+ }
+
+ return recordDigest{d.Digest, r}
+}
+
+func newRecordFromCid(c cid.Cid, at uint64) recordDigest {
+ d, err := multihash.Decode(c.Hash())
+ if err != nil {
+ panic(err)
+ }
+
+ return recordDigest{d.Digest, index.Record{Cid: c, Offset: at}}
+}
+
+func (ii *InsertionIndex) InsertNoReplace(key cid.Cid, n uint64) {
+ ii.items.InsertNoReplace(newRecordFromCid(key, n))
+}
+
+func (ii *InsertionIndex) Get(c cid.Cid) (uint64, error) {
+ record, err := ii.getRecord(c)
+ if err != nil {
+ return 0, err
+ }
+ return record.Offset, nil
+}
+
+func (ii *InsertionIndex) getRecord(c cid.Cid) (index.Record, error) {
+ d, err := multihash.Decode(c.Hash())
+ if err != nil {
+ return index.Record{}, err
+ }
+ entry := recordDigest{digest: d.Digest}
+ e := ii.items.Get(entry)
+ if e == nil {
+ return index.Record{}, index.ErrNotFound
+ }
+ r, ok := e.(recordDigest)
+ if !ok {
+ return index.Record{}, errUnsupported
+ }
+
+ return r.Record, nil
+}
+
+func (ii *InsertionIndex) GetAll(c cid.Cid, fn func(uint64) bool) error {
+ d, err := multihash.Decode(c.Hash())
+ if err != nil {
+ return err
+ }
+ entry := recordDigest{digest: d.Digest}
+
+ any := false
+ iter := func(i llrb.Item) bool {
+ existing := i.(recordDigest)
+ if !bytes.Equal(existing.digest, entry.digest) {
+ // We've already looked at all entries with matching digests.
+ return false
+ }
+ any = true
+ return fn(existing.Record.Offset)
+ }
+ ii.items.AscendGreaterOrEqual(entry, iter)
+ if !any {
+ return index.ErrNotFound
+ }
+ return nil
+}
+
+func (ii *InsertionIndex) Marshal(w io.Writer) (uint64, error) {
+ l := uint64(0)
+ if err := binary.Write(w, binary.LittleEndian, int64(ii.items.Len())); err != nil {
+ return l, err
+ }
+ l += 8
+
+ var err error
+ iter := func(i llrb.Item) bool {
+ if err = cbor.Encode(w, i.(recordDigest).Record); err != nil {
+ return false
+ }
+ return true
+ }
+ ii.items.AscendGreaterOrEqual(ii.items.Min(), iter)
+ return l, err
+}
+
+func (ii *InsertionIndex) Unmarshal(r io.Reader) error {
+ var length int64
+ if err := binary.Read(r, binary.LittleEndian, &length); err != nil {
+ return err
+ }
+ d := cbor.NewDecoder(r)
+ for i := int64(0); i < length; i++ {
+ var rec index.Record
+ if err := d.Decode(&rec); err != nil {
+ return err
+ }
+ ii.items.InsertNoReplace(newRecordDigest(rec))
+ }
+ return nil
+}
+
+func (ii *InsertionIndex) ForEach(f func(multihash.Multihash, uint64) error) error {
+ var err error
+ ii.items.AscendGreaterOrEqual(ii.items.Min(), func(i llrb.Item) bool {
+ r := i.(recordDigest).Record
+ err = f(r.Cid.Hash(), r.Offset)
+ return err == nil
+ })
+ return err
+}
+
+func (ii *InsertionIndex) ForEachCid(f func(cid.Cid, uint64) error) error {
+ var err error
+ ii.items.AscendGreaterOrEqual(ii.items.Min(), func(i llrb.Item) bool {
+ r := i.(recordDigest).Record
+ err = f(r.Cid, r.Offset)
+ return err == nil
+ })
+ return err
+}
+
+func (ii *InsertionIndex) Codec() multicodec.Code {
+ return insertionIndexCodec
+}
+
+func (ii *InsertionIndex) Load(rs []index.Record) error {
+ for _, r := range rs {
+ rec := newRecordDigest(r)
+ if rec.digest == nil {
+ return fmt.Errorf("invalid entry: %v", r)
+ }
+ ii.items.InsertNoReplace(rec)
+ }
+ return nil
+}
+
+// flatten returns a formatted index in the given codec for more efficient subsequent loading.
+func (ii *InsertionIndex) Flatten(codec multicodec.Code) (index.Index, error) {
+ si, err := index.New(codec)
+ if err != nil {
+ return nil, err
+ }
+ rcrds := make([]index.Record, ii.items.Len())
+
+ idx := 0
+ iter := func(i llrb.Item) bool {
+ rcrds[idx] = i.(recordDigest).Record
+ idx++
+ return true
+ }
+ ii.items.AscendGreaterOrEqual(ii.items.Min(), iter)
+
+ if err := si.Load(rcrds); err != nil {
+ return nil, err
+ }
+ return si, nil
+}
+
+// note that hasExactCID is very similar to GetAll,
+// but it's separate as it allows us to compare Record.Cid directly,
+// whereas GetAll just provides Record.Offset.
+
+func (ii *InsertionIndex) HasExactCID(c cid.Cid) (bool, error) {
+ d, err := multihash.Decode(c.Hash())
+ if err != nil {
+ return false, err
+ }
+ entry := recordDigest{digest: d.Digest}
+
+ found := false
+ iter := func(i llrb.Item) bool {
+ existing := i.(recordDigest)
+ if !bytes.Equal(existing.digest, entry.digest) {
+ // We've already looked at all entries with matching digests.
+ return false
+ }
+ if existing.Record.Cid == c {
+ // We found an exact match.
+ found = true
+ return false
+ }
+ // Continue looking in ascending order.
+ return true
+ }
+ ii.items.AscendGreaterOrEqual(entry, iter)
+ return found, nil
+}
+
+func (ii *InsertionIndex) HasMultihash(mh multihash.Multihash) (bool, error) {
+ d, err := multihash.Decode(mh)
+ if err != nil {
+ return false, err
+ }
+ entry := recordDigest{digest: d.Digest}
+
+ found := false
+ iter := func(i llrb.Item) bool {
+ existing := i.(recordDigest)
+ if !bytes.Equal(existing.digest, entry.digest) {
+ // We've already looked at all entries with matching digests.
+ return false
+ }
+ if bytes.Equal(existing.Record.Cid.Hash(), mh) {
+ found = true
+ return false
+ }
+ // Continue looking in ascending order.
+ return true
+ }
+ ii.items.AscendGreaterOrEqual(entry, iter)
+ return found, nil
+}
diff --git a/ipld/car/v2/internal/store/resume.go b/ipld/car/v2/internal/store/resume.go
new file mode 100644
index 0000000000..0ba94570a5
--- /dev/null
+++ b/ipld/car/v2/internal/store/resume.go
@@ -0,0 +1,198 @@
+package store
+
+import (
+ "errors"
+ "fmt"
+ "io"
+
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+ internalio "github.com/ipfs/boxo/ipld/car/v2/internal/io"
+ "github.com/ipfs/go-cid"
+ "github.com/multiformats/go-varint"
+)
+
+type ReaderWriterAt interface {
+ io.ReaderAt
+ io.WriterAt
+}
+
+// ResumableVersion performs two tasks - check if there is a valid header at the start of the,
+// reader, then check whether the version of that header matches what we expect.
+func ResumableVersion(reader io.Reader, writeAsV1 bool) error {
+ version, err := carv2.ReadVersion(reader)
+ if err != nil {
+ // The file is not a valid CAR file and cannot resume from it.
+ // Or the write must have failed before pragma was written.
+ return err
+ }
+
+ switch {
+ case version == 1 && writeAsV1:
+ case version == 2 && !writeAsV1:
+ default:
+ // The file is not the expected version and we cannot resume from it.
+ return fmt.Errorf("cannot resume on CAR file with version %v", version)
+ }
+ return nil
+}
+
+// Resume will attempt to resume a CARv2 or CARv1 file by checking that there exists an existing
+// CAR and that the CAR header details match what is being requested for resumption.
+// Resumption of a CARv2 involves "unfinalizing" the header by resetting it back to a bare state
+// and then truncating the file to remove the index. Truncation is important because it allows a
+// non-finalized CARv2 to be resumed from as the header won't contain the DataSize of the payload
+// body and if the file also contains an index, we cannot determine the end of the payload.
+// Therefore, when using a resumed, existing and finalized, CARv2, whose body may not extend
+// beyond the index and then closing without finalization (e.g. due to a crash), the file will no
+// longer be parseable because we won't have DataSize, and we won't be able to determine it by
+// parsing the payload to EOF.
+func Resume(
+ rw ReaderWriterAt,
+ dataReader io.ReaderAt,
+ dataWriter *internalio.OffsetWriteSeeker,
+ idx *InsertionIndex,
+ roots []cid.Cid,
+ dataOffset uint64,
+ v1 bool,
+ maxAllowedHeaderSize uint64,
+ zeroLengthSectionAsEOF bool,
+) error {
+
+ var headerInFile carv2.Header
+ var v1r internalio.ReadSeekerAt
+
+ if !v1 {
+ if _, ok := rw.(interface{ Truncate(size int64) error }); !ok {
+ return fmt.Errorf("cannot resume a CARv2 without the ability to truncate (e.g. an io.File)")
+ }
+
+ // Check if file was finalized by trying to read the CARv2 header.
+ // We check because if finalized the CARv1 reader behaviour needs to be adjusted since
+ // EOF will not signify end of CARv1 payload. i.e. index is most likely present.
+ r, err := internalio.NewOffsetReadSeeker(rw, carv2.PragmaSize)
+ if err != nil {
+ return err
+ }
+ _, err = headerInFile.ReadFrom(r)
+
+ // If reading CARv2 header succeeded, and CARv1 offset in header is not zero then the file is
+ // most-likely finalized. Check padding and truncate the file to remove index.
+ // Otherwise, carry on reading the v1 payload at offset determined from b.header.
+ if err == nil && headerInFile.DataOffset != 0 {
+ if headerInFile.DataOffset != dataOffset {
+ // Assert that the padding on file matches the given WithDataPadding option.
+ wantPadding := headerInFile.DataOffset - carv2.PragmaSize - carv2.HeaderSize
+ gotPadding := dataOffset - carv2.PragmaSize - carv2.HeaderSize
+ return fmt.Errorf(
+ "cannot resume from file with mismatched CARv1 offset; "+
+ "`WithDataPadding` option must match the padding on file. "+
+ "Expected padding value of %v but got %v", wantPadding, gotPadding,
+ )
+ } else if headerInFile.DataSize == 0 {
+ // If CARv1 size is zero, since CARv1 offset wasn't, then the CARv2 header was
+ // most-likely partially written. Since we write the header last in Finalize then the
+ // file most-likely contains the index and we cannot know where it starts, therefore
+ // can't resume.
+ return errors.New("corrupt CARv2 header; cannot resume from file")
+ }
+ }
+
+ v1r, err = internalio.NewOffsetReadSeeker(dataReader, 0)
+ if err != nil {
+ return err
+ }
+ } else {
+ var err error
+ v1r, err = internalio.NewOffsetReadSeeker(rw, 0)
+ if err != nil {
+ return err
+ }
+ }
+
+ header, err := carv1.ReadHeader(v1r, maxAllowedHeaderSize)
+ if err != nil {
+ // Cannot read the CARv1 header; the file is most likely corrupt.
+ return fmt.Errorf("error reading car header: %w", err)
+ }
+ if !header.Matches(carv1.CarHeader{Roots: roots, Version: 1}) {
+ // Cannot resume if version and root does not match.
+ return errors.New("cannot resume on file with mismatching data header")
+ }
+
+ if headerInFile.DataOffset != 0 {
+ // If header in file contains the size of car v1, then the index is most likely present.
+ // Since we will need to re-generate the index, as the one in file is flattened, truncate
+ // the file so that the Readonly.backing has the right set of bytes to deal with.
+ // This effectively means resuming from a finalized file will wipe its index even if there
+ // are no blocks put unless the user calls finalize.
+ if err := rw.(interface{ Truncate(size int64) error }).Truncate(int64(headerInFile.DataOffset + headerInFile.DataSize)); err != nil {
+ return err
+ }
+ }
+
+ if !v1 {
+ // Now that CARv2 header is present on file, clear it to avoid incorrect size and offset in
+ // header in case blocksotre is closed without finalization and is resumed from.
+ wat, ok := rw.(io.WriterAt)
+ if !ok { // how would we get this far??
+ return errors.New("cannot resume from file without io.WriterAt")
+ }
+ if _, err := new(carv2.Header).WriteTo(internalio.NewOffsetWriter(wat, carv2.PragmaSize)); err != nil {
+ return fmt.Errorf("could not un-finalize: %w", err)
+ }
+ }
+
+ // TODO See how we can reduce duplicate code here.
+ // The code here comes from car.GenerateIndex.
+ // Copied because we need to populate an insertindex, not a sorted index.
+ // Producing a sorted index via generate, then converting it to insertindex is not possible.
+ // Because Index interface does not expose internal records.
+ // This may be done as part of https://github.com/ipld/go-car/issues/95
+
+ offset, err := carv1.HeaderSize(header)
+ if err != nil {
+ return err
+ }
+ sectionOffset := int64(0)
+ if sectionOffset, err = v1r.Seek(int64(offset), io.SeekStart); err != nil {
+ return err
+ }
+
+ for {
+ // Grab the length of the section.
+ // Note that ReadUvarint wants a ByteReader.
+ length, err := varint.ReadUvarint(v1r)
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ return err
+ }
+
+ // Null padding; by default it's an error.
+ if length == 0 {
+ if zeroLengthSectionAsEOF {
+ break
+ } else {
+ return fmt.Errorf("carv1 null padding not allowed by default; see WithZeroLegthSectionAsEOF")
+ }
+ }
+
+ // Grab the CID.
+ n, c, err := cid.CidFromReader(v1r)
+ if err != nil {
+ return err
+ }
+ idx.InsertNoReplace(c, uint64(sectionOffset))
+
+ // Seek to the next section by skipping the block.
+ // The section length includes the CID, so subtract it.
+ if sectionOffset, err = v1r.Seek(int64(length)-int64(n), io.SeekCurrent); err != nil {
+ return err
+ }
+ }
+ // Seek to the end of last skipped block where the writer should resume writing.
+ _, err = dataWriter.Seek(sectionOffset, io.SeekStart)
+ return err
+}
diff --git a/ipld/car/v2/options.go b/ipld/car/v2/options.go
new file mode 100644
index 0000000000..5a8c5dc867
--- /dev/null
+++ b/ipld/car/v2/options.go
@@ -0,0 +1,240 @@
+package car
+
+import (
+ "math"
+
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+ "github.com/ipld/go-ipld-prime/traversal"
+ "github.com/multiformats/go-multicodec"
+
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+)
+
+// DefaultMaxIndexCidSize specifies the maximum size in byptes accepted as a section CID by CARv2 index.
+const DefaultMaxIndexCidSize = 2 << 10 // 2 KiB
+
+// DefaultMaxAllowedHeaderSize specifies the default maximum size that a CARv1
+// decode (including within a CARv2 container) will allow a header to be without
+// erroring. This is to prevent OOM errors where a header prefix includes a
+// too-large size specifier.
+// Currently set to 32 MiB.
+const DefaultMaxAllowedHeaderSize = carv1.DefaultMaxAllowedHeaderSize
+
+// DefaultMaxAllowedHeaderSize specifies the default maximum size that a CARv1
+// decode (including within a CARv2 container) will allow a section to be
+// without erroring. This is to prevent OOM errors where a section prefix
+// includes a too-large size specifier.
+// Typically IPLD blocks should be under 2 MiB (ideally under 1 MiB), so unless
+// atypical data is expected, this should not be a large value.
+// Currently set to 8 MiB.
+const DefaultMaxAllowedSectionSize = carv1.DefaultMaxAllowedSectionSize
+
+// Option describes an option which affects behavior when interacting with CAR files.
+type Option func(*Options)
+
+// ReadOption hints that an API wants options related only to reading CAR files.
+type ReadOption = Option
+
+// WriteOption hints that an API wants options related only to reading CAR files.
+type WriteOption = Option
+
+// ReadWriteOption is either a ReadOption or a WriteOption.
+// Deprecated: use Option instead.
+type ReadWriteOption = Option
+
+// Options holds the configured options after applying a number of
+// Option funcs.
+//
+// This type should not be used directly by end users; it's only exposed as a
+// side effect of Option.
+type Options struct {
+ DataPadding uint64
+ IndexPadding uint64
+ IndexCodec multicodec.Code
+ ZeroLengthSectionAsEOF bool
+ MaxIndexCidSize uint64
+ StoreIdentityCIDs bool
+
+ BlockstoreAllowDuplicatePuts bool
+ BlockstoreUseWholeCIDs bool
+ MaxTraversalLinks uint64
+ WriteAsCarV1 bool
+ TraversalPrototypeChooser traversal.LinkTargetNodePrototypeChooser
+ TrustedCAR bool
+
+ MaxAllowedHeaderSize uint64
+ MaxAllowedSectionSize uint64
+}
+
+// ApplyOptions applies given opts and returns the resulting Options.
+// This function should not be used directly by end users; it's only exposed as a
+// side effect of Option.
+func ApplyOptions(opt ...Option) Options {
+ opts := Options{
+ MaxTraversalLinks: math.MaxInt64, //default: traverse all
+ MaxAllowedHeaderSize: carv1.DefaultMaxAllowedHeaderSize,
+ MaxAllowedSectionSize: carv1.DefaultMaxAllowedSectionSize,
+ }
+ for _, o := range opt {
+ o(&opts)
+ }
+ // Set defaults for zero valued fields.
+ if opts.IndexCodec == 0 {
+ opts.IndexCodec = multicodec.CarMultihashIndexSorted
+ }
+ if opts.MaxIndexCidSize == 0 {
+ opts.MaxIndexCidSize = DefaultMaxIndexCidSize
+ }
+ return opts
+}
+
+// ZeroLengthSectionAsEOF sets whether to allow the CARv1 decoder to treat
+// a zero-length section as the end of the input CAR file. For example, this can
+// be useful to allow "null padding" after a CARv1 without knowing where the
+// padding begins.
+func ZeroLengthSectionAsEOF(enable bool) Option {
+ return func(o *Options) {
+ o.ZeroLengthSectionAsEOF = enable
+ }
+}
+
+// UseDataPadding sets the padding to be added between CARv2 header and its data payload on Finalize.
+func UseDataPadding(p uint64) Option {
+ return func(o *Options) {
+ o.DataPadding = p
+ }
+}
+
+// UseIndexPadding sets the padding between data payload and its index on Finalize.
+func UseIndexPadding(p uint64) Option {
+ return func(o *Options) {
+ o.IndexPadding = p
+ }
+}
+
+// UseIndexCodec sets the codec used for index generation.
+func UseIndexCodec(c multicodec.Code) Option {
+ return func(o *Options) {
+ o.IndexCodec = c
+ }
+}
+
+// WithoutIndex flags that no index should be included in generation.
+func WithoutIndex() Option {
+ return func(o *Options) {
+ o.IndexCodec = index.CarIndexNone
+ }
+}
+
+// StoreIdentityCIDs sets whether to persist sections that are referenced by
+// CIDs with multihash.IDENTITY digest.
+// When writing CAR files with this option, Characteristics.IsFullyIndexed will
+// be set.
+//
+// By default, the blockstore interface will always return true for Has() called
+// with identity CIDs, but when this option is turned on, it will defer to the
+// index.
+//
+// When creating an index (or loading a CARv1 as a blockstore), when this option
+// is on, identity CIDs will be included in the index.
+//
+// This option is disabled by default.
+func StoreIdentityCIDs(b bool) Option {
+ return func(o *Options) {
+ o.StoreIdentityCIDs = b
+ }
+}
+
+// MaxIndexCidSize specifies the maximum allowed size for indexed CIDs in bytes.
+// Indexing a CID with larger than the allowed size results in ErrCidTooLarge error.
+func MaxIndexCidSize(s uint64) Option {
+ return func(o *Options) {
+ o.MaxIndexCidSize = s
+ }
+}
+
+// WithTraversalPrototypeChooser specifies the prototype chooser that should be used
+// when performing traversals in writes from a linksystem.
+func WithTraversalPrototypeChooser(t traversal.LinkTargetNodePrototypeChooser) Option {
+ return func(o *Options) {
+ o.TraversalPrototypeChooser = t
+ }
+}
+
+// WithTrustedCAR specifies whether CIDs match the block data as they are read
+// from the CAR files.
+func WithTrustedCAR(t bool) Option {
+ return func(o *Options) {
+ o.TrustedCAR = t
+ }
+}
+
+// MaxAllowedHeaderSize overrides the default maximum size (of 32 MiB) that a
+// CARv1 decode (including within a CARv2 container) will allow a header to be
+// without erroring.
+func MaxAllowedHeaderSize(max uint64) Option {
+ return func(o *Options) {
+ o.MaxAllowedHeaderSize = max
+ }
+}
+
+// MaxAllowedSectionSize overrides the default maximum size (of 8 MiB) that a
+// CARv1 decode (including within a CARv2 container) will allow a header to be
+// without erroring.
+// Typically IPLD blocks should be under 2 MiB (ideally under 1 MiB), so unless
+// atypical data is expected, this should not be a large value.
+func MaxAllowedSectionSize(max uint64) Option {
+ return func(o *Options) {
+ o.MaxAllowedSectionSize = max
+ }
+}
+
+// --------------------------------------------------- storage interface options
+
+// UseWholeCIDs is a read option which makes a CAR storage interface (blockstore
+// or storage) identify blocks by whole CIDs, and not just their multihashes.
+// The default is to use multihashes, which matches the current semantics of
+// go-ipfs-blockstore v1.
+//
+// Enabling this option affects a number of methods, including read-only ones:
+//
+// • Get, Has, and HasSize will only return a block only if the entire CID is
+// present in the CAR file.
+//
+// • AllKeysChan will return the original whole CIDs, instead of with their
+// multicodec set to "raw" to just provide multihashes.
+//
+// • If AllowDuplicatePuts isn't set, Put and PutMany will deduplicate by the
+// whole CID, allowing different CIDs with equal multihashes.
+//
+// Note that this option only affects the storage interfaces (blockstore
+// or storage), and is ignored by the root go-car/v2 package.
+func UseWholeCIDs(enable bool) Option {
+ return func(o *Options) {
+ o.BlockstoreUseWholeCIDs = enable
+ }
+}
+
+// WriteAsCarV1 is a write option which makes a CAR interface (blockstore or
+// storage) write the output as a CARv1 only, with no CARv2 header or index.
+// Indexing is used internally during write but is discarded upon finalization.
+//
+// Note that this option only affects the storage interfaces (blockstore
+// or storage), and is ignored by the root go-car/v2 package.
+func WriteAsCarV1(asCarV1 bool) Option {
+ return func(o *Options) {
+ o.WriteAsCarV1 = asCarV1
+ }
+}
+
+// AllowDuplicatePuts is a write option which makes a CAR interface (blockstore
+// or storage) not deduplicate blocks in Put and PutMany. The default is to
+// deduplicate, which matches the current semantics of go-ipfs-blockstore v1.
+//
+// Note that this option only affects the storage interfaces (blockstore
+// or storage), and is ignored by the root go-car/v2 package.
+func AllowDuplicatePuts(allow bool) Option {
+ return func(o *Options) {
+ o.BlockstoreAllowDuplicatePuts = allow
+ }
+}
diff --git a/ipld/car/v2/options_test.go b/ipld/car/v2/options_test.go
new file mode 100644
index 0000000000..8964b981f5
--- /dev/null
+++ b/ipld/car/v2/options_test.go
@@ -0,0 +1,50 @@
+package car_test
+
+import (
+ "math"
+ "testing"
+
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/blockstore"
+ "github.com/multiformats/go-multicodec"
+ "github.com/stretchr/testify/require"
+)
+
+func TestApplyOptions_SetsExpectedDefaults(t *testing.T) {
+ require.Equal(t, carv2.Options{
+ IndexCodec: multicodec.CarMultihashIndexSorted,
+ MaxIndexCidSize: carv2.DefaultMaxIndexCidSize,
+ MaxTraversalLinks: math.MaxInt64,
+ MaxAllowedHeaderSize: 32 << 20,
+ MaxAllowedSectionSize: 8 << 20,
+ }, carv2.ApplyOptions())
+}
+
+func TestApplyOptions_AppliesOptions(t *testing.T) {
+ require.Equal(t,
+ carv2.Options{
+ DataPadding: 123,
+ IndexPadding: 456,
+ IndexCodec: multicodec.CarIndexSorted,
+ ZeroLengthSectionAsEOF: true,
+ MaxIndexCidSize: 789,
+ StoreIdentityCIDs: true,
+ BlockstoreAllowDuplicatePuts: true,
+ BlockstoreUseWholeCIDs: true,
+ MaxTraversalLinks: math.MaxInt64,
+ MaxAllowedHeaderSize: 101,
+ MaxAllowedSectionSize: 202,
+ },
+ carv2.ApplyOptions(
+ carv2.UseDataPadding(123),
+ carv2.UseIndexPadding(456),
+ carv2.UseIndexCodec(multicodec.CarIndexSorted),
+ carv2.ZeroLengthSectionAsEOF(true),
+ carv2.MaxIndexCidSize(789),
+ carv2.StoreIdentityCIDs(true),
+ carv2.MaxAllowedHeaderSize(101),
+ carv2.MaxAllowedSectionSize(202),
+ blockstore.AllowDuplicatePuts(true),
+ blockstore.UseWholeCIDs(true),
+ ))
+}
diff --git a/ipld/car/v2/reader.go b/ipld/car/v2/reader.go
new file mode 100644
index 0000000000..30092bc439
--- /dev/null
+++ b/ipld/car/v2/reader.go
@@ -0,0 +1,368 @@
+package car
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "math"
+
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1/util"
+ internalio "github.com/ipfs/boxo/ipld/car/v2/internal/io"
+ "github.com/ipfs/go-cid"
+ "github.com/multiformats/go-multicodec"
+ "github.com/multiformats/go-multihash"
+ "github.com/multiformats/go-varint"
+ "golang.org/x/exp/mmap"
+)
+
+// Reader represents a reader of CARv2.
+type Reader struct {
+ Header Header
+ Version uint64
+ r io.ReaderAt
+ roots []cid.Cid
+ opts Options
+ closer io.Closer
+}
+
+// OpenReader is a wrapper for NewReader which opens the file at path.
+func OpenReader(path string, opts ...Option) (*Reader, error) {
+ f, err := mmap.Open(path)
+ if err != nil {
+ return nil, err
+ }
+
+ r, err := NewReader(f, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ r.closer = f
+ return r, nil
+}
+
+// NewReader constructs a new reader that reads either CARv1 or CARv2 from the given r.
+// Upon instantiation, the reader inspects the payload and provides appropriate read operations
+// for both CARv1 and CARv2.
+//
+// Note that any other version other than 1 or 2 will result in an error. The caller may use
+// Reader.Version to get the actual version r represents. In the case where r represents a CARv1
+// Reader.Header will not be populated and is left as zero-valued.
+func NewReader(r io.ReaderAt, opts ...Option) (*Reader, error) {
+ cr := &Reader{
+ r: r,
+ }
+ cr.opts = ApplyOptions(opts...)
+
+ or, err := internalio.NewOffsetReadSeeker(r, 0)
+ if err != nil {
+ return nil, err
+ }
+ cr.Version, err = ReadVersion(or, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ if cr.Version != 1 && cr.Version != 2 {
+ return nil, fmt.Errorf("invalid car version: %d", cr.Version)
+ }
+
+ if cr.Version == 2 {
+ if err := cr.readV2Header(); err != nil {
+ return nil, err
+ }
+ }
+
+ return cr, nil
+}
+
+// Roots returns the root CIDs.
+// The root CIDs are extracted lazily from the data payload header.
+func (r *Reader) Roots() ([]cid.Cid, error) {
+ if r.roots != nil {
+ return r.roots, nil
+ }
+ dr, err := r.DataReader()
+ if err != nil {
+ return nil, err
+ }
+ header, err := carv1.ReadHeader(dr, r.opts.MaxAllowedHeaderSize)
+ if err != nil {
+ return nil, err
+ }
+ r.roots = header.Roots
+ return r.roots, nil
+}
+
+func (r *Reader) readV2Header() (err error) {
+ headerSection := io.NewSectionReader(r.r, PragmaSize, HeaderSize)
+ _, err = r.Header.ReadFrom(headerSection)
+ return
+}
+
+// SectionReader implements both io.ReadSeeker and io.ReaderAt.
+// It is the interface version of io.SectionReader, but note that the
+// implementation is not guaranteed to be an io.SectionReader.
+type SectionReader interface {
+ io.Reader
+ io.Seeker
+ io.ReaderAt
+}
+
+// DataReader provides a reader containing the data payload in CARv1 format.
+func (r *Reader) DataReader() (SectionReader, error) {
+ if r.Version == 2 {
+ return io.NewSectionReader(r.r, int64(r.Header.DataOffset), int64(r.Header.DataSize)), nil
+ }
+ return internalio.NewOffsetReadSeeker(r.r, 0)
+}
+
+// IndexReader provides an io.Reader containing the index for the data payload if the index is
+// present. Otherwise, returns nil.
+// Note, this function will always return nil if the backing payload represents a CARv1.
+func (r *Reader) IndexReader() (io.Reader, error) {
+ if r.Version == 1 || !r.Header.HasIndex() {
+ return nil, nil
+ }
+ return internalio.NewOffsetReadSeeker(r.r, int64(r.Header.IndexOffset))
+}
+
+// Stats is returned by an Inspect() call
+type Stats struct {
+ Version uint64
+ Header Header
+ Roots []cid.Cid
+ RootsPresent bool
+ BlockCount uint64
+ CodecCounts map[multicodec.Code]uint64
+ MhTypeCounts map[multicodec.Code]uint64
+ AvgCidLength uint64
+ MaxCidLength uint64
+ MinCidLength uint64
+ AvgBlockLength uint64
+ MaxBlockLength uint64
+ MinBlockLength uint64
+ IndexCodec multicodec.Code
+}
+
+// Inspect does a quick scan of a CAR, performing basic validation of the format
+// and returning a Stats object that provides a high-level description of the
+// contents of the CAR.
+// Inspect works for CARv1 and CARv2 contents. A CARv1 will return an
+// uninitialized Header value.
+//
+// If validateBlockHash is true, all block data in the payload will be hashed
+// and compared to the CID for that block and an error will return if there
+// is a mismatch. If false, block data will be skipped over and not checked.
+// Performing a full block hash validation is similar to using a BlockReader and
+// calling Next over all blocks.
+//
+// Inspect will perform a basic check of a CARv2 index, where present, but this
+// does not guarantee that the index is correct. Attempting to read index data
+// from untrusted sources is not recommended. If required, further validation of
+// an index can be performed by loading the index and performing a ForEach() and
+// sanity checking that the offsets are within the data payload section of the
+// CAR. However, re-generation of index data in this case is the recommended
+// course of action.
+//
+// Beyond the checks performed by Inspect, a valid / good CAR is somewhat
+// use-case dependent. Factors to consider include:
+//
+// - Bad indexes, including incorrect offsets, duplicate entries, or other
+// faulty data. Indexes should be re-generated, regardless, if you need to use
+// them and have any reason to not trust the source.
+//
+// - Blocks use codecs that your system doesn't have access to—which may mean
+// you can't traverse a DAG or use the contained data. Stats.CodecCounts
+// contains a list of codecs found in the CAR so this can be checked.
+//
+// - CIDs use multihashes that your system doesn't have access to—which will
+// mean you can't validate block hashes are correct (using validateBlockHash
+// in this case will result in a failure). Stats.MhTypeCounts contains a
+// list of multihashes found in the CAR so this can be checked.
+//
+// - The presence of IDENTITY CIDs, which may not be supported (or desired) by
+// the consumer of the CAR. Stats.CodecCounts can determine the presence
+// of IDENTITY CIDs.
+//
+// - Roots: the number of roots, duplicates, and whether they are related to the
+// blocks contained within the CAR. Stats contains a list of Roots and a
+// RootsPresent bool so further checks can be performed.
+//
+// - DAG completeness is not checked. Any properties relating to the DAG, or
+// DAGs contained within a CAR are the responsibility of the user to check.
+func (r *Reader) Inspect(validateBlockHash bool) (Stats, error) {
+ stats := Stats{
+ Version: r.Version,
+ Header: r.Header,
+ CodecCounts: make(map[multicodec.Code]uint64),
+ MhTypeCounts: make(map[multicodec.Code]uint64),
+ }
+
+ var totalCidLength uint64
+ var totalBlockLength uint64
+ var minCidLength uint64 = math.MaxUint64
+ var minBlockLength uint64 = math.MaxUint64
+
+ dr, err := r.DataReader()
+ if err != nil {
+ return Stats{}, err
+ }
+ bdr := internalio.ToByteReader(dr)
+
+ // read roots, not using Roots(), because we need the offset setup in the data trader
+ header, err := carv1.ReadHeader(dr, r.opts.MaxAllowedHeaderSize)
+ if err != nil {
+ return Stats{}, err
+ }
+ stats.Roots = header.Roots
+ var rootsPresentCount int
+ rootsPresent := make([]bool, len(stats.Roots))
+
+ // read block sections
+ for {
+ sectionLength, err := varint.ReadUvarint(bdr)
+ if err != nil {
+ if err == io.EOF {
+ // if the length of bytes read is non-zero when the error is EOF then signal an unclean EOF.
+ if sectionLength > 0 {
+ return Stats{}, io.ErrUnexpectedEOF
+ }
+ // otherwise, this is a normal ending
+ break
+ }
+ return Stats{}, err
+ }
+ if sectionLength == 0 && r.opts.ZeroLengthSectionAsEOF {
+ // normal ending for this read mode
+ break
+ }
+ if sectionLength > r.opts.MaxAllowedSectionSize {
+ return Stats{}, util.ErrSectionTooLarge
+ }
+
+ // decode just the CID bytes
+ cidLen, c, err := cid.CidFromReader(dr)
+ if err != nil {
+ return Stats{}, err
+ }
+
+ if sectionLength < uint64(cidLen) {
+ // this case is handled different in the normal ReadNode() path since it
+ // slurps in the whole section bytes and decodes CID from there - so an
+ // error should come from a failing io.ReadFull
+ return Stats{}, errors.New("section length shorter than CID length")
+ }
+
+ // is this a root block? (also account for duplicate root CIDs)
+ if rootsPresentCount < len(stats.Roots) {
+ for i, r := range stats.Roots {
+ if !rootsPresent[i] && c == r {
+ rootsPresent[i] = true
+ rootsPresentCount++
+ }
+ }
+ }
+
+ cp := c.Prefix()
+ codec := multicodec.Code(cp.Codec)
+ count := stats.CodecCounts[codec]
+ stats.CodecCounts[codec] = count + 1
+ mhtype := multicodec.Code(cp.MhType)
+ count = stats.MhTypeCounts[mhtype]
+ stats.MhTypeCounts[mhtype] = count + 1
+
+ blockLength := sectionLength - uint64(cidLen)
+
+ if validateBlockHash {
+ // Use multihash.SumStream to avoid having to copy the entire block content into memory.
+ // The SumStream uses a buffered copy to write bytes into the hasher which will take
+ // advantage of streaming hash calculation depending on the hash function.
+ // TODO: introduce SumStream in go-cid to simplify the code here.
+ blockReader := io.LimitReader(dr, int64(blockLength))
+ mhl := cp.MhLength
+ if mhtype == multicodec.Identity {
+ mhl = -1
+ }
+ mh, err := multihash.SumStream(blockReader, cp.MhType, mhl)
+ if err != nil {
+ return Stats{}, err
+ }
+ var gotCid cid.Cid
+ switch cp.Version {
+ case 0:
+ gotCid = cid.NewCidV0(mh)
+ case 1:
+ gotCid = cid.NewCidV1(cp.Codec, mh)
+ default:
+ return Stats{}, fmt.Errorf("invalid cid version: %d", cp.Version)
+ }
+ if !gotCid.Equals(c) {
+ return Stats{}, fmt.Errorf("mismatch in content integrity, expected: %s, got: %s", c, gotCid)
+ }
+ } else {
+ // otherwise, skip over it
+ if _, err := dr.Seek(int64(blockLength), io.SeekCurrent); err != nil {
+ return Stats{}, err
+ }
+ }
+
+ stats.BlockCount++
+ totalCidLength += uint64(cidLen)
+ totalBlockLength += blockLength
+ if uint64(cidLen) < minCidLength {
+ minCidLength = uint64(cidLen)
+ }
+ if uint64(cidLen) > stats.MaxCidLength {
+ stats.MaxCidLength = uint64(cidLen)
+ }
+ if blockLength < minBlockLength {
+ minBlockLength = blockLength
+ }
+ if blockLength > stats.MaxBlockLength {
+ stats.MaxBlockLength = blockLength
+ }
+ }
+
+ stats.RootsPresent = len(stats.Roots) == rootsPresentCount
+ if stats.BlockCount > 0 {
+ stats.MinCidLength = minCidLength
+ stats.MinBlockLength = minBlockLength
+ stats.AvgCidLength = totalCidLength / stats.BlockCount
+ stats.AvgBlockLength = totalBlockLength / stats.BlockCount
+ }
+
+ if stats.Version != 1 && stats.Header.HasIndex() {
+ idxr, err := r.IndexReader()
+ if err != nil {
+ return Stats{}, err
+ }
+ stats.IndexCodec, err = index.ReadCodec(idxr)
+ if err != nil {
+ return Stats{}, err
+ }
+ }
+
+ return stats, nil
+}
+
+// Close closes the underlying reader if it was opened by OpenReader.
+func (r *Reader) Close() error {
+ if r.closer != nil {
+ return r.closer.Close()
+ }
+ return nil
+}
+
+// ReadVersion reads the version from the pragma.
+// This function accepts both CARv1 and CARv2 payloads.
+func ReadVersion(r io.Reader, opts ...Option) (uint64, error) {
+ o := ApplyOptions(opts...)
+ header, err := carv1.ReadHeader(r, o.MaxAllowedHeaderSize)
+ if err != nil {
+ return 0, err
+ }
+ return header.Version, nil
+}
diff --git a/ipld/car/v2/reader_test.go b/ipld/car/v2/reader_test.go
new file mode 100644
index 0000000000..75a4c274f3
--- /dev/null
+++ b/ipld/car/v2/reader_test.go
@@ -0,0 +1,592 @@
+package car_test
+
+import (
+ "bytes"
+ "encoding/hex"
+ "io"
+ "os"
+ "strings"
+ "testing"
+
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+ "github.com/ipfs/go-cid"
+ "github.com/multiformats/go-multicodec"
+ "github.com/stretchr/testify/require"
+)
+
+func TestReadVersion(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ want uint64
+ wantErr bool
+ }{
+ {
+ name: "CarV1VersionIsOne",
+ path: "testdata/sample-v1.car",
+ want: 1,
+ },
+ {
+ name: "CarV2VersionIsTwo",
+ path: "testdata/sample-rw-bs-v2.car",
+ want: 2,
+ },
+ {
+ name: "CarV1VersionWithZeroLenSectionIsOne",
+ path: "testdata/sample-v1-with-zero-len-section.car",
+ want: 1,
+ },
+ {
+ name: "AnotherCarV1VersionWithZeroLenSectionIsOne",
+ path: "testdata/sample-v1-with-zero-len-section2.car",
+ want: 1,
+ },
+ {
+ name: "WrappedCarV1InCarV2VersionIsTwo",
+ path: "testdata/sample-wrapped-v2.car",
+ want: 2,
+ },
+ {
+ name: "FutureVersionWithCorrectPragmaIsAsExpected",
+ path: "testdata/sample-rootless-v42.car",
+ want: 42,
+ },
+ {
+ name: "CarV1WithValidHeaderButCorruptSectionIsOne",
+ path: "testdata/sample-v1-tailing-corrupt-section.car",
+ want: 1,
+ },
+ {
+ name: "CarV2WithValidHeaderButCorruptSectionAndIndexIsTwo",
+ path: "testdata/sample-v2-corrupt-data-and-index.car",
+ want: 2,
+ },
+ {
+ name: "CarFileWithCorruptPragmaIsError",
+ path: "testdata/sample-corrupt-pragma.car",
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ f, err := os.Open(tt.path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, f.Close()) })
+
+ got, err := carv2.ReadVersion(f)
+ if tt.wantErr {
+ require.Error(t, err, "ReadVersion() error = %v, wantErr %v", err, tt.wantErr)
+ } else {
+ require.NoError(t, err)
+ require.Equal(t, tt.want, got, "ReadVersion() got = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestReaderFailsOnUnknownVersion(t *testing.T) {
+ _, err := carv2.OpenReader("testdata/sample-rootless-v42.car")
+ require.EqualError(t, err, "invalid car version: 42")
+}
+
+func TestReaderFailsOnCorruptPragma(t *testing.T) {
+ _, err := carv2.OpenReader("testdata/sample-corrupt-pragma.car")
+ require.EqualError(t, err, "unexpected EOF")
+}
+
+func TestReader_WithCarV1Consistency(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ zerLenAsEOF bool
+ }{
+ {
+ name: "CarV1WithoutZeroLengthSection",
+ path: "testdata/sample-v1.car",
+ },
+ {
+ name: "CarV1WithZeroLenSection",
+ path: "testdata/sample-v1-with-zero-len-section.car",
+ zerLenAsEOF: true,
+ },
+ {
+ name: "AnotherCarV1WithZeroLenSection",
+ path: "testdata/sample-v1-with-zero-len-section2.car",
+ zerLenAsEOF: true,
+ },
+ {
+ name: "CarV1WithZeroLenSectionWithoutOption",
+ path: "testdata/sample-v1-with-zero-len-section.car",
+ },
+ {
+ name: "AnotherCarV1WithZeroLenSectionWithoutOption",
+ path: "testdata/sample-v1-with-zero-len-section2.car",
+ },
+ {
+ name: "CorruptCarV1",
+ path: "testdata/sample-v1-tailing-corrupt-section.car",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ subject, err := carv2.OpenReader(tt.path, carv2.ZeroLengthSectionAsEOF(tt.zerLenAsEOF))
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, subject.Close()) })
+ wantReader := requireNewCarV1ReaderFromV1File(t, tt.path, tt.zerLenAsEOF)
+
+ require.Equal(t, uint64(1), subject.Version)
+ gotRoots, err := subject.Roots()
+ require.NoError(t, err)
+ require.Equal(t, wantReader.Header.Roots, gotRoots)
+ ir, err := subject.IndexReader()
+ require.Nil(t, ir)
+ require.NoError(t, err)
+ })
+ }
+}
+
+func TestReader_WithCarV2Consistency(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ zerLenAsEOF bool
+ }{
+ {
+ name: "CarV2WrappingV1",
+ path: "testdata/sample-wrapped-v2.car",
+ },
+ {
+ name: "CarV2ProducedByBlockstore",
+ path: "testdata/sample-rw-bs-v2.car",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ subject, err := carv2.OpenReader(tt.path, carv2.ZeroLengthSectionAsEOF(tt.zerLenAsEOF))
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, subject.Close()) })
+ wantReader := requireNewCarV1ReaderFromV2File(t, tt.path, tt.zerLenAsEOF)
+
+ require.Equal(t, uint64(2), subject.Version)
+ gotRoots, err := subject.Roots()
+ require.NoError(t, err)
+ require.Equal(t, wantReader.Header.Roots, gotRoots)
+
+ gotIndexReader, err := subject.IndexReader()
+ require.NoError(t, err)
+ require.NotNil(t, gotIndexReader)
+ gotIndex, err := index.ReadFrom(gotIndexReader)
+ require.NoError(t, err)
+ dr, err := subject.DataReader()
+ require.NoError(t, err)
+ wantIndex, err := carv2.GenerateIndex(dr)
+ require.NoError(t, err)
+ require.Equal(t, wantIndex, gotIndex)
+ })
+ }
+}
+
+func TestOpenReader_DoesNotPanicForReadersCreatedBeforeClosure(t *testing.T) {
+ subject, err := carv2.OpenReader("testdata/sample-wrapped-v2.car")
+ require.NoError(t, err)
+ dReaderBeforeClosure, err := subject.DataReader()
+ require.NoError(t, err)
+ iReaderBeforeClosure, err := subject.IndexReader()
+ require.NoError(t, err)
+ require.NoError(t, subject.Close())
+
+ buf := make([]byte, 1)
+ panicTest := func(r io.Reader) {
+ _, err := r.Read(buf)
+ require.EqualError(t, err, "mmap: closed")
+ }
+
+ require.NotPanics(t, func() { panicTest(dReaderBeforeClosure) })
+ require.NotPanics(t, func() { panicTest(iReaderBeforeClosure) })
+}
+
+func TestOpenReader_DoesNotPanicForReadersCreatedAfterClosure(t *testing.T) {
+ subject, err := carv2.OpenReader("testdata/sample-wrapped-v2.car")
+ require.NoError(t, err)
+ require.NoError(t, subject.Close())
+ dReaderAfterClosure, err := subject.DataReader()
+ require.NoError(t, err)
+ iReaderAfterClosure, err := subject.IndexReader()
+ require.NoError(t, err)
+
+ buf := make([]byte, 1)
+ panicTest := func(r io.Reader) {
+ _, err := r.Read(buf)
+ require.EqualError(t, err, "mmap: closed")
+ }
+
+ require.NotPanics(t, func() { panicTest(dReaderAfterClosure) })
+ require.NotPanics(t, func() { panicTest(iReaderAfterClosure) })
+}
+
+func TestReader_ReturnsNilWhenThereIsNoIndex(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ }{
+ {
+ name: "IndexlessCarV2",
+ path: "testdata/sample-v2-indexless.car",
+ },
+ {
+ name: "CarV1",
+ path: "testdata/sample-v1.car",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ subject, err := carv2.OpenReader(tt.path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, subject.Close()) })
+ ir, err := subject.IndexReader()
+ require.NoError(t, err)
+ require.Nil(t, ir)
+ })
+ }
+}
+
+func requireNewCarV1ReaderFromV2File(t *testing.T, carV12Path string, zerLenAsEOF bool) *carv1.CarReader {
+ f, err := os.Open(carV12Path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, f.Close()) })
+
+ _, err = f.Seek(carv2.PragmaSize, io.SeekStart)
+ require.NoError(t, err)
+ header := carv2.Header{}
+ _, err = header.ReadFrom(f)
+ require.NoError(t, err)
+ return requireNewCarV1Reader(t, io.NewSectionReader(f, int64(header.DataOffset), int64(header.DataSize)), zerLenAsEOF)
+}
+
+func requireNewCarV1ReaderFromV1File(t *testing.T, carV1Path string, zerLenAsEOF bool) *carv1.CarReader {
+ f, err := os.Open(carV1Path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, f.Close()) })
+ return requireNewCarV1Reader(t, f, zerLenAsEOF)
+}
+
+func requireNewCarV1Reader(t *testing.T, r io.Reader, zerLenAsEOF bool) *carv1.CarReader {
+ var cr *carv1.CarReader
+ var err error
+ if zerLenAsEOF {
+ cr, err = carv1.NewCarReaderWithZeroLengthSectionAsEOF(r)
+ } else {
+ cr, err = carv1.NewCarReader(r)
+ }
+ require.NoError(t, err)
+ return cr
+}
+
+func TestInspect(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ carHex string
+ zerLenAsEOF bool
+ expectedStats carv2.Stats
+ }{
+ {
+ name: "IndexlessCarV2",
+ path: "testdata/sample-v2-indexless.car",
+ expectedStats: carv2.Stats{
+ Version: 2,
+ Header: carv2.Header{
+ Characteristics: carv2.Characteristics{0, 0},
+ DataOffset: 51,
+ DataSize: 479907,
+ IndexOffset: 0,
+ },
+ Roots: []cid.Cid{mustCidDecode("bafy2bzaced4ueelaegfs5fqu4tzsh6ywbbpfk3cxppupmxfdhbpbhzawfw5oy")},
+ RootsPresent: true,
+ AvgBlockLength: 417, // 417.6644423260248
+ MinBlockLength: 1,
+ MaxBlockLength: 1342,
+ AvgCidLength: 37, // 37.86939942802669
+ MinCidLength: 14,
+ MaxCidLength: 38,
+ BlockCount: 1049,
+ CodecCounts: map[multicodec.Code]uint64{
+ multicodec.Raw: 6,
+ multicodec.DagCbor: 1043,
+ },
+ MhTypeCounts: map[multicodec.Code]uint64{
+ multicodec.Identity: 6,
+ multicodec.Blake2b256: 1043,
+ },
+ },
+ },
+ {
+ // same payload as IndexlessCarV2, so only difference is the Version & Header
+ name: "CarV1",
+ path: "testdata/sample-v1.car",
+ expectedStats: carv2.Stats{
+ Version: 1,
+ Header: carv2.Header{},
+ Roots: []cid.Cid{mustCidDecode("bafy2bzaced4ueelaegfs5fqu4tzsh6ywbbpfk3cxppupmxfdhbpbhzawfw5oy")},
+ RootsPresent: true,
+ AvgBlockLength: 417, // 417.6644423260248
+ MinBlockLength: 1,
+ MaxBlockLength: 1342,
+ AvgCidLength: 37, // 37.86939942802669
+ MinCidLength: 14,
+ MaxCidLength: 38,
+ BlockCount: 1049,
+ CodecCounts: map[multicodec.Code]uint64{
+ multicodec.Raw: 6,
+ multicodec.DagCbor: 1043,
+ },
+ MhTypeCounts: map[multicodec.Code]uint64{
+ multicodec.Identity: 6,
+ multicodec.Blake2b256: 1043,
+ },
+ },
+ },
+ {
+ // same payload as IndexlessCarV2, so only difference is the Header
+ name: "CarV2ProducedByBlockstore",
+ path: "testdata/sample-rw-bs-v2.car",
+ expectedStats: carv2.Stats{
+ Version: 2,
+ Header: carv2.Header{
+ DataOffset: 1464,
+ DataSize: 273,
+ IndexOffset: 1737,
+ },
+ Roots: []cid.Cid{
+ mustCidDecode("bafkreifuosuzujyf4i6psbneqtwg2fhplc2wxptc5euspa2gn3bwhnihfu"),
+ mustCidDecode("bafkreifc4hca3inognou377hfhvu2xfchn2ltzi7yu27jkaeujqqqdbjju"),
+ mustCidDecode("bafkreig5lvr4l6b4fr3un4xvzeyt3scevgsqjgrhlnwxw2unwbn5ro276u"),
+ },
+ RootsPresent: true,
+ BlockCount: 3,
+ CodecCounts: map[multicodec.Code]uint64{multicodec.Raw: 3},
+ MhTypeCounts: map[multicodec.Code]uint64{multicodec.Sha2_256: 3},
+ AvgCidLength: 36,
+ MaxCidLength: 36,
+ MinCidLength: 36,
+ AvgBlockLength: 6,
+ MaxBlockLength: 9,
+ MinBlockLength: 4,
+ IndexCodec: multicodec.CarMultihashIndexSorted,
+ },
+ },
+ // same as CarV1 but with a zero-byte EOF to test options
+ {
+ name: "CarV1VersionWithZeroLenSectionIsOne",
+ path: "testdata/sample-v1-with-zero-len-section.car",
+ zerLenAsEOF: true,
+ expectedStats: carv2.Stats{
+ Version: 1,
+ Header: carv2.Header{},
+ Roots: []cid.Cid{mustCidDecode("bafy2bzaced4ueelaegfs5fqu4tzsh6ywbbpfk3cxppupmxfdhbpbhzawfw5oy")},
+ RootsPresent: true,
+ AvgBlockLength: 417, // 417.6644423260248
+ MinBlockLength: 1,
+ MaxBlockLength: 1342,
+ AvgCidLength: 37, // 37.86939942802669
+ MinCidLength: 14,
+ MaxCidLength: 38,
+ BlockCount: 1049,
+ CodecCounts: map[multicodec.Code]uint64{
+ multicodec.Raw: 6,
+ multicodec.DagCbor: 1043,
+ },
+ MhTypeCounts: map[multicodec.Code]uint64{
+ multicodec.Identity: 6,
+ multicodec.Blake2b256: 1043,
+ },
+ },
+ },
+ {
+ // A case where this _could_ be a valid CAR if we allowed identity CIDs
+ // and not matching block contents to exist, there's no block bytes in
+ // this. It will only fail if you don't validate the CID matches the,
+ // bytes (see TestInspectError for that case).
+ name: "IdentityCID",
+ // 47 {version:1,roots:[identity cid]} 25 identity cid (dag-json {"identity":"block"})
+ carHex: "2f a265726f6f747381d82a581a0001a90200147b226964656e74697479223a22626c6f636b227d6776657273696f6e01 19 01a90200147b226964656e74697479223a22626c6f636b227d",
+ expectedStats: carv2.Stats{
+ Version: 1,
+ Roots: []cid.Cid{mustCidDecode("baguqeaaupmrgszdfnz2gs5dzei5ceytmn5rwwit5")},
+ RootsPresent: true,
+ BlockCount: 1,
+ CodecCounts: map[multicodec.Code]uint64{multicodec.DagJson: 1},
+ MhTypeCounts: map[multicodec.Code]uint64{multicodec.Identity: 1},
+ AvgCidLength: 25,
+ MaxCidLength: 25,
+ MinCidLength: 25,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var reader *carv2.Reader
+ var err error
+ if tt.path != "" {
+ reader, err = carv2.OpenReader(tt.path, carv2.ZeroLengthSectionAsEOF(tt.zerLenAsEOF))
+ require.NoError(t, err)
+ } else {
+ byts, err := hex.DecodeString(strings.ReplaceAll(tt.carHex, " ", ""))
+ require.NoError(t, err)
+ reader, err = carv2.NewReader(bytes.NewReader(byts), carv2.ZeroLengthSectionAsEOF(tt.zerLenAsEOF))
+ require.NoError(t, err)
+ }
+ t.Cleanup(func() { require.NoError(t, reader.Close()) })
+ stats, err := reader.Inspect(false)
+ require.NoError(t, err)
+ require.Equal(t, tt.expectedStats, stats)
+ })
+ }
+}
+
+func TestInspectError(t *testing.T) {
+ tests := []struct {
+ name string
+ carHex string
+ expectedOpenError string
+ expectedInspectError string
+ validateBlockHash bool
+ }{
+ {
+ name: "BadCidV0",
+ carHex: "3aa265726f6f747381d8305825000130302030303030303030303030303030303030303030303030303030303030303030306776657273696f6e010130",
+ expectedInspectError: "expected 1 as the cid version number, got: 48",
+ },
+ {
+ name: "BadHeaderLength",
+ carHex: "e0e0e0e0a7060c6f6c4cca943c236f4b196723489608edb42a8b8fa80b6776657273696f6e19",
+ expectedOpenError: "invalid header data, length of read beyond allowable maximum",
+ },
+ {
+ name: "BadSectionLength",
+ carHex: "11a265726f6f7473806776657273696f6e01e0e0e0e0a7060155122001d448afd928065458cf670b60f5a594d735af0172c8d67f22a81680132681ca00000000000000000000",
+ expectedInspectError: "invalid section data, length of read beyond allowable maximum",
+ },
+ {
+ name: "BadSectionLength2",
+ carHex: "3aa265726f6f747381d8305825000130302030303030303030303030303030303030303030303030303030303030303030306776657273696f6e01200130302030303030303030303030303030303030303030303030303030303030303030303030303030303030",
+ expectedInspectError: "section length shorter than CID length",
+ validateBlockHash: true,
+ },
+ {
+ name: "BadSectionLength3",
+ carHex: "11a265726f6f7473f66776657273696f6e0180",
+ expectedInspectError: "unexpected EOF",
+ },
+ {
+ name: "BadBlockHash(SanityCheck)", // this should pass because we don't ask the CID be validated even though it doesn't match
+ // header cid data
+ carHex: "11a265726f6f7473806776657273696f6e 012e0155122001d448afd928065458cf670b60f5a594d735af0172c8d67f22a81680132681ca ffffffffffffffffffff",
+ },
+ {
+ name: "BadBlockHash", // same as above, but we ask for CID validation
+ // header cid data
+ carHex: "11a265726f6f7473806776657273696f6e 012e0155122001d448afd928065458cf670b60f5a594d735af0172c8d67f22a81680132681ca ffffffffffffffffffff",
+ validateBlockHash: true,
+ expectedInspectError: "mismatch in content integrity, expected: bafkreiab2rek7wjiazkfrt3hbnqpljmu24226alszdlh6ivic2abgjubzi, got: bafkreiaaqoxrddiyuy6gxnks6ioqytxhq5a7tchm2mm5htigznwiljukmm",
+ },
+ {
+ name: "IdentityCID", // a case where this _could_ be a valid CAR if we allowed identity CIDs and not matching block contents to exist, there's no block bytes in this
+ // 47 {version:1,roots:[identity cid]} 25 identity cid (dag-json {"identity":"block"})
+ carHex: "2f a265726f6f747381d82a581a0001a90200147b226964656e74697479223a22626c6f636b227d6776657273696f6e01 19 01a90200147b226964656e74697479223a22626c6f636b227d",
+ validateBlockHash: true,
+ expectedInspectError: "mismatch in content integrity, expected: baguqeaaupmrgszdfnz2gs5dzei5ceytmn5rwwit5, got: baguqeaaa",
+ },
+ // the bad index tests are manually constructed from this single-block CARv2 by adjusting the Uint32 and Uint64 values in the index:
+ // pragma carv2 header carv1 icodec count codec count (swi) width dataLen mh offset
+ // 0aa16776657273696f6e02 00000000000000000000000000000000330000000000000041000000000000007400000000000000 11a265726f6f7473806776657273696f6e012e0155122001d448afd928065458cf670b60f5a594d735af0172c8d67f22a81680132681ca00000000000000000000 8108 01000000 1200000000000000 01000000 28000000 2800000000000000 01d448afd928065458cf670b60f5a594d735af0172c8d67f22a81680132681ca 1200000000000000
+ // we don't test any further into the index, to do that, a user should do a ForEach across the loaded index (and sanity check the offsets)
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ car, _ := hex.DecodeString(strings.ReplaceAll(tt.carHex, " ", ""))
+ reader, err := carv2.NewReader(bytes.NewReader(car))
+ if tt.expectedOpenError != "" {
+ require.Error(t, err)
+ require.Equal(t, tt.expectedOpenError, err.Error())
+ return
+ } else {
+ require.NoError(t, err)
+ }
+ t.Cleanup(func() { require.NoError(t, reader.Close()) })
+ _, err = reader.Inspect(tt.validateBlockHash)
+ if tt.expectedInspectError != "" {
+ require.Error(t, err)
+ require.Equal(t, tt.expectedInspectError, err.Error())
+ } else {
+ require.NoError(t, err)
+ }
+ })
+ }
+}
+
+func TestIndex_ReadFromCorruptIndex(t *testing.T) {
+ tests := []struct {
+ name string
+ givenCarHex string
+ wantErr string
+ }{
+ {
+ name: "BadIndexCountOverflow",
+ // pragma carv2 header carv1 icodec count codec count (swi) width dataLen mh offset
+ givenCarHex: "0aa16776657273696f6e02 00000000000000000000000000000000330000000000000041000000000000007400000000000000 11a265726f6f7473806776657273696f6e012e0155122001d448afd928065458cf670b60f5a594d735af0172c8d67f22a81680132681ca00000000000000000000 8108 ffffffff 1200000000000000 01000000 28000000 2800000000000000 01d448afd928065458cf670b60f5a594d735af0172c8d67f22a81680132681ca 1200000000000000",
+ wantErr: "index too big; MultihashIndexSorted count is overflowing int32",
+ },
+ {
+ name: "BadIndexCountTooMany",
+ // pragma carv2 header carv1 icodec count codec count (swi) width dataLen mh offset
+ givenCarHex: "0aa16776657273696f6e02 00000000000000000000000000000000330000000000000041000000000000007400000000000000 11a265726f6f7473806776657273696f6e012e0155122001d448afd928065458cf670b60f5a594d735af0172c8d67f22a81680132681ca00000000000000000000 8108 ffffff7f 1200000000000000 01000000 28000000 2800000000000000 01d448afd928065458cf670b60f5a594d735af0172c8d67f22a81680132681ca 1200000000000000",
+ wantErr: "unexpected EOF",
+ },
+ {
+ name: "BadIndexMultiWidthOverflow",
+ // pragma carv2 header carv1 icodec count codec count (swi) width dataLen mh offset
+ givenCarHex: "0aa16776657273696f6e02 00000000000000000000000000000000330000000000000041000000000000007400000000000000 11a265726f6f7473806776657273696f6e012e0155122001d448afd928065458cf670b60f5a594d735af0172c8d67f22a81680132681ca00000000000000000000 8108 01000000 1200000000000000 ffffffff 28000000 2800000000000000 01d448afd928065458cf670b60f5a594d735af0172c8d67f22a81680132681ca 1200000000000000",
+ wantErr: "index too big; multiWidthIndex count is overflowing int32",
+ },
+ {
+ name: "BadIndexMultiWidthTooMany",
+ // pragma carv2 header carv1 icodec count codec count (swi) width dataLen mh offset
+ givenCarHex: "0aa16776657273696f6e02 00000000000000000000000000000000330000000000000041000000000000007400000000000000 11a265726f6f7473806776657273696f6e012e0155122001d448afd928065458cf670b60f5a594d735af0172c8d67f22a81680132681ca00000000000000000000 8108 01000000 1200000000000000 ffffff7f 28000000 2800000000000000 01d448afd928065458cf670b60f5a594d735af0172c8d67f22a81680132681ca 1200000000000000",
+ wantErr: "unexpected EOF",
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ car, _ := hex.DecodeString(strings.ReplaceAll(test.givenCarHex, " ", ""))
+ reader, err := carv2.NewReader(bytes.NewReader(car))
+ require.NoError(t, err)
+
+ ir, err := reader.IndexReader()
+ require.NoError(t, err)
+ require.NotNil(t, ir)
+
+ gotIdx, err := index.ReadFrom(ir)
+ if test.wantErr == "" {
+ require.NoError(t, err)
+ require.NotNil(t, gotIdx)
+ } else {
+ require.Error(t, err)
+ require.Equal(t, test.wantErr, err.Error())
+ require.Nil(t, gotIdx)
+ }
+ })
+ }
+}
+
+func mustCidDecode(s string) cid.Cid {
+ c, err := cid.Decode(s)
+ if err != nil {
+ panic(err)
+ }
+ return c
+}
diff --git a/ipld/car/v2/selective.go b/ipld/car/v2/selective.go
new file mode 100644
index 0000000000..33ac016bcd
--- /dev/null
+++ b/ipld/car/v2/selective.go
@@ -0,0 +1,289 @@
+package car
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "math"
+ "os"
+
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/loader"
+ "github.com/ipfs/go-cid"
+ ipld "github.com/ipld/go-ipld-prime"
+ "github.com/ipld/go-ipld-prime/datamodel"
+ "github.com/ipld/go-ipld-prime/linking"
+ cidlink "github.com/ipld/go-ipld-prime/linking/cid"
+ "github.com/ipld/go-ipld-prime/node/basicnode"
+ "github.com/ipld/go-ipld-prime/traversal"
+ "github.com/ipld/go-ipld-prime/traversal/selector"
+)
+
+// ErrSizeMismatch is returned when a written traversal realizes the written header size does not
+// match the actual number of car bytes written.
+var ErrSizeMismatch = fmt.Errorf("car-error-sizemismatch")
+
+// ErrOffsetImpossible is returned when specified paddings or offsets of either a wrapped carv1
+// or index cannot be satisfied based on the data being written.
+var ErrOffsetImpossible = fmt.Errorf("car-error-offsetimpossible")
+
+// MaxTraversalLinks changes the allowed number of links a selector traversal
+// can execute before failing.
+//
+// Note that setting this option may cause an error to be returned from selector
+// execution when building a SelectiveCar.
+func MaxTraversalLinks(MaxTraversalLinks uint64) Option {
+ return func(sco *Options) {
+ sco.MaxTraversalLinks = MaxTraversalLinks
+ }
+}
+
+// NewSelectiveWriter walks through the proposed dag traversal to learn its total size in order to be able to
+// stream out a car to a writer in the expected traversal order in one go.
+func NewSelectiveWriter(ctx context.Context, ls *ipld.LinkSystem, root cid.Cid, selector ipld.Node, opts ...Option) (Writer, error) {
+ cls, cntr := loader.CountingLinkSystem(*ls)
+
+ c1h := carv1.CarHeader{Roots: []cid.Cid{root}, Version: 1}
+ headSize, err := carv1.HeaderSize(&c1h)
+ if err != nil {
+ return nil, err
+ }
+ if err := traverse(ctx, &cls, root, selector, ApplyOptions(opts...)); err != nil {
+ return nil, err
+ }
+ tc := traversalCar{
+ size: headSize + cntr.Size(),
+ ctx: ctx,
+ root: root,
+ selector: selector,
+ ls: ls,
+ opts: ApplyOptions(opts...),
+ }
+ return &tc, nil
+}
+
+// TraverseToFile writes a car file matching a given root and selector to the
+// path at `destination` using one read of each block.
+func TraverseToFile(ctx context.Context, ls *ipld.LinkSystem, root cid.Cid, selector ipld.Node, destination string, opts ...Option) error {
+ tc := traversalCar{
+ size: 0,
+ ctx: ctx,
+ root: root,
+ selector: selector,
+ ls: ls,
+ opts: ApplyOptions(opts...),
+ }
+
+ fp, err := os.Create(destination)
+ if err != nil {
+ return err
+ }
+ defer fp.Close()
+
+ _, err = tc.WriteTo(fp)
+ if err != nil {
+ return err
+ }
+
+ // fix header size.
+ if _, err = fp.Seek(0, 0); err != nil {
+ return err
+ }
+
+ tc.size = uint64(tc.size)
+ if _, err = tc.WriteV2Header(fp); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// TraverseV1 walks through the proposed dag traversal and writes a carv1 to the provided io.Writer
+func TraverseV1(ctx context.Context, ls *ipld.LinkSystem, root cid.Cid, selector ipld.Node, writer io.Writer, opts ...Option) (uint64, error) {
+ opts = append(opts, WithoutIndex())
+ tc := traversalCar{
+ size: 0,
+ ctx: ctx,
+ root: root,
+ selector: selector,
+ ls: ls,
+ opts: ApplyOptions(opts...),
+ }
+
+ len, _, err := tc.WriteV1(writer)
+ return len, err
+}
+
+// Writer is an interface allowing writing a car prepared by PrepareTraversal
+type Writer interface {
+ io.WriterTo
+}
+
+var _ Writer = (*traversalCar)(nil)
+
+type traversalCar struct {
+ size uint64
+ ctx context.Context
+ root cid.Cid
+ selector ipld.Node
+ ls *ipld.LinkSystem
+ opts Options
+}
+
+func (tc *traversalCar) WriteTo(w io.Writer) (int64, error) {
+ n, err := tc.WriteV2Header(w)
+ if err != nil {
+ return n, err
+ }
+ v1s, idx, err := tc.WriteV1(w)
+ n += int64(v1s)
+
+ if err != nil {
+ return n, err
+ }
+
+ // index padding, then index
+ if tc.opts.IndexCodec != index.CarIndexNone {
+ if tc.opts.IndexPadding > 0 {
+ buf := make([]byte, tc.opts.IndexPadding)
+ pn, err := w.Write(buf)
+ n += int64(pn)
+ if err != nil {
+ return n, err
+ }
+ }
+ in, err := index.WriteTo(idx, w)
+ n += int64(in)
+ if err != nil {
+ return n, err
+ }
+ }
+
+ return n, err
+}
+
+func (tc *traversalCar) WriteV2Header(w io.Writer) (int64, error) {
+ n, err := w.Write(Pragma)
+ if err != nil {
+ return int64(n), err
+ }
+
+ h := NewHeader(tc.size)
+ if p := tc.opts.DataPadding; p > 0 {
+ h = h.WithDataPadding(p)
+ }
+ if p := tc.opts.IndexPadding; p > 0 {
+ h = h.WithIndexPadding(p)
+ }
+ if tc.opts.IndexCodec == index.CarIndexNone {
+ h.IndexOffset = 0
+ }
+ hn, err := h.WriteTo(w)
+ if err != nil {
+ return int64(n) + hn, err
+ }
+ hn += int64(n)
+
+ // We include the initial data padding after the carv2 header
+ if h.DataOffset > uint64(hn) {
+ // TODO: buffer writes if this needs to be big.
+ buf := make([]byte, h.DataOffset-uint64(hn))
+ n, err = w.Write(buf)
+ hn += int64(n)
+ if err != nil {
+ return hn, err
+ }
+ } else if h.DataOffset < uint64(hn) {
+ return hn, ErrOffsetImpossible
+ }
+
+ return hn, nil
+}
+
+func (tc *traversalCar) WriteV1(w io.Writer) (uint64, index.Index, error) {
+ // write the v1 header
+ c1h := carv1.CarHeader{Roots: []cid.Cid{tc.root}, Version: 1}
+ if err := carv1.WriteHeader(&c1h, w); err != nil {
+ return 0, nil, err
+ }
+ v1Size, err := carv1.HeaderSize(&c1h)
+ if err != nil {
+ return v1Size, nil, err
+ }
+
+ // write the block.
+ wls, writer := loader.TeeingLinkSystem(*tc.ls, w, v1Size, tc.opts.IndexCodec)
+ err = traverse(tc.ctx, &wls, tc.root, tc.selector, tc.opts)
+ v1Size = writer.Size()
+ if err != nil {
+ return v1Size, nil, err
+ }
+ if tc.size != 0 && tc.size != v1Size {
+ return v1Size, nil, ErrSizeMismatch
+ }
+ tc.size = v1Size
+
+ if tc.opts.IndexCodec == index.CarIndexNone {
+ return v1Size, nil, nil
+ }
+ idx, err := writer.Index()
+ return v1Size, idx, err
+}
+
+func traverse(ctx context.Context, ls *ipld.LinkSystem, root cid.Cid, s ipld.Node, opts Options) error {
+ sel, err := selector.CompileSelector(s)
+ if err != nil {
+ return err
+ }
+
+ chooser := func(_ ipld.Link, _ linking.LinkContext) (ipld.NodePrototype, error) {
+ return basicnode.Prototype.Any, nil
+ }
+ if opts.TraversalPrototypeChooser != nil {
+ chooser = opts.TraversalPrototypeChooser
+ }
+
+ progress := traversal.Progress{
+ Cfg: &traversal.Config{
+ Ctx: ctx,
+ LinkSystem: *ls,
+ LinkTargetNodePrototypeChooser: chooser,
+ LinkVisitOnlyOnce: !opts.BlockstoreAllowDuplicatePuts,
+ },
+ }
+ if opts.MaxTraversalLinks < math.MaxInt64 {
+ progress.Budget = &traversal.Budget{
+ NodeBudget: math.MaxInt64,
+ LinkBudget: int64(opts.MaxTraversalLinks),
+ }
+ }
+
+ lnk := cidlink.Link{Cid: root}
+ ls.TrustedStorage = true
+ rp, err := chooser(lnk, ipld.LinkContext{})
+ if err != nil {
+ return err
+ }
+ rootNode, err := ls.Load(ipld.LinkContext{}, lnk, rp)
+ if err != nil {
+ return fmt.Errorf("root blk load failed: %s", err)
+ }
+ err = progress.WalkMatching(rootNode, sel, func(_ traversal.Progress, node ipld.Node) error {
+ if lbn, ok := node.(datamodel.LargeBytesNode); ok {
+ s, err := lbn.AsLargeBytes()
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(io.Discard, s)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ return fmt.Errorf("walk failed: %s", err)
+ }
+ return nil
+}
diff --git a/ipld/car/v2/selective_test.go b/ipld/car/v2/selective_test.go
new file mode 100644
index 0000000000..02afde0eb0
--- /dev/null
+++ b/ipld/car/v2/selective_test.go
@@ -0,0 +1,137 @@
+package car_test
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "os"
+ "path"
+ "testing"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/blockstore"
+ "github.com/ipfs/go-cid"
+ "github.com/ipfs/go-unixfsnode"
+ "github.com/ipfs/go-unixfsnode/data/builder"
+ dagpb "github.com/ipld/go-codec-dagpb"
+ "github.com/ipld/go-ipld-prime/datamodel"
+ "github.com/ipld/go-ipld-prime/linking"
+ cidlink "github.com/ipld/go-ipld-prime/linking/cid"
+ basicnode "github.com/ipld/go-ipld-prime/node/basic"
+ "github.com/ipld/go-ipld-prime/storage/bsadapter"
+ sb "github.com/ipld/go-ipld-prime/traversal/selector/builder"
+ selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse"
+ "github.com/stretchr/testify/require"
+
+ _ "github.com/ipld/go-ipld-prime/codec/dagcbor"
+ _ "github.com/ipld/go-ipld-prime/codec/raw"
+)
+
+func TestPrepareTraversal(t *testing.T) {
+ from, err := blockstore.OpenReadOnly("testdata/sample-unixfs-v2.car")
+ require.NoError(t, err)
+ ls := cidlink.DefaultLinkSystem()
+ bsa := bsadapter.Adapter{Wrapped: from}
+ ls.SetReadStorage(&bsa)
+
+ rts, _ := from.Roots()
+ writer, err := car.NewSelectiveWriter(context.Background(), &ls, rts[0], selectorparse.CommonSelector_ExploreAllRecursively)
+ require.NoError(t, err)
+
+ buf := bytes.Buffer{}
+ n, err := writer.WriteTo(&buf)
+ require.NoError(t, err)
+ require.Equal(t, int64(len(buf.Bytes())), n)
+
+ fi, _ := os.Stat("testdata/sample-unixfs-v2.car")
+ require.Equal(t, fi.Size(), n)
+
+ // Headers should be equal
+ h1, _ := car.OpenReader("testdata/sample-unixfs-v2.car")
+ h1h := bytes.Buffer{}
+ h1h.Write(car.Pragma)
+ h1.Header.WriteTo(&h1h)
+ require.Equal(t, buf.Bytes()[:h1h.Len()], h1h.Bytes())
+}
+
+func TestFileTraversal(t *testing.T) {
+ from, err := blockstore.OpenReadOnly("testdata/sample-unixfs-v2.car")
+ require.NoError(t, err)
+ ls := cidlink.DefaultLinkSystem()
+ bsa := bsadapter.Adapter{Wrapped: from}
+ ls.SetReadStorage(&bsa)
+
+ rts, _ := from.Roots()
+ outDir := t.TempDir()
+ err = car.TraverseToFile(context.Background(), &ls, rts[0], selectorparse.CommonSelector_ExploreAllRecursively, path.Join(outDir, "out.car"))
+ require.NoError(t, err)
+
+ require.FileExists(t, path.Join(outDir, "out.car"))
+
+ fa, _ := os.Stat("testdata/sample-unixfs-v2.car")
+ fb, _ := os.Stat(path.Join(outDir, "out.car"))
+ require.Equal(t, fa.Size(), fb.Size())
+}
+
+func TestV1Traversal(t *testing.T) {
+ from, err := blockstore.OpenReadOnly("testdata/sample-v1.car")
+ require.NoError(t, err)
+ ls := cidlink.DefaultLinkSystem()
+ bsa := bsadapter.Adapter{Wrapped: from}
+ ls.SetReadStorage(&bsa)
+
+ rts, _ := from.Roots()
+ w := bytes.NewBuffer(nil)
+ n, err := car.TraverseV1(context.Background(), &ls, rts[0], selectorparse.CommonSelector_ExploreAllRecursively, w)
+ require.NoError(t, err)
+ require.Equal(t, int64(len(w.Bytes())), int64(n))
+
+ fa, _ := os.Stat("testdata/sample-v1.car")
+ require.Equal(t, fa.Size(), int64(n))
+}
+
+func TestPartialTraversal(t *testing.T) {
+ store := cidlink.Memory{Bag: make(map[string][]byte)}
+ ls := cidlink.DefaultLinkSystem()
+ ls.StorageReadOpener = store.OpenRead
+ ls.StorageWriteOpener = store.OpenWrite
+ unixfsnode.AddUnixFSReificationToLinkSystem(&ls)
+
+ // write a unixfs file.
+ initBuf := bytes.Buffer{}
+ _, _ = initBuf.Write(make([]byte, 1000000))
+ rt, _, err := builder.BuildUnixFSFile(&initBuf, "", &ls)
+ require.NoError(t, err)
+
+ // read a subset of the file.
+ _, rts, err := cid.CidFromBytes([]byte(rt.Binary()))
+ require.NoError(t, err)
+ ssb := sb.NewSelectorSpecBuilder(basicnode.Prototype.Any)
+ sel := ssb.ExploreInterpretAs("unixfs", ssb.MatcherSubset(0, 256*1000))
+ buf := bytes.Buffer{}
+ chooser := dagpb.AddSupportToChooser(func(l datamodel.Link, lc linking.LinkContext) (datamodel.NodePrototype, error) {
+ return basicnode.Prototype.Any, nil
+ })
+ _, err = car.TraverseV1(context.Background(), &ls, rts, sel.Node(), &buf, car.WithTraversalPrototypeChooser(chooser))
+ require.NoError(t, err)
+
+ fb := len(buf.Bytes())
+ require.Less(t, fb, 1000000)
+
+ loaded, err := car.NewBlockReader(&buf)
+ require.NoError(t, err)
+ fnd := make(map[cid.Cid]struct{})
+ var b blocks.Block
+ for err == nil {
+ b, err = loaded.Next()
+ if err == io.EOF {
+ break
+ }
+ if _, ok := fnd[b.Cid()]; ok {
+ require.Fail(t, "duplicate block present", b.Cid())
+ }
+ fnd[b.Cid()] = struct{}{}
+ }
+ require.Equal(t, 2, len(fnd))
+}
diff --git a/ipld/car/v2/storage/doc.go b/ipld/car/v2/storage/doc.go
new file mode 100644
index 0000000000..21e049b21b
--- /dev/null
+++ b/ipld/car/v2/storage/doc.go
@@ -0,0 +1,75 @@
+// Package storage provides a CAR abstraction for the
+// github.com/ipld/go-ipld-prime/storage interfaces in the form of a StorageCar.
+//
+// THIS PACKAGE IS EXPERIMENTAL. Breaking changes may be introduced in
+// semver-minor releases before this package stabilizes. Use with caution and
+// prefer the blockstore API if stability is required.
+//
+// StorageCar as ReadableStorage provides basic Get and Has operations. It also
+// implements StreamingReadableStorage for the more efficient GetStreaming
+// operation which is easily supported by the CAR format.
+//
+// StorageCar as WritableStorage provides the Put operation. It does not
+// implement StreamingWritableStorage because the CAR format requires CIDs to
+// be written before the blocks themselves, which is not possible with
+// StreamingWritableStorage without buffering. Therefore, the PutStream function
+// in github.com/ipld/go-ipld-prime/storage will provide equivalent
+// functionality if it were to be implemented here.
+//
+// StorageCar can be used with an IPLD LinkSystem, defined by
+// github.com/ipld/go-ipld-prime/linking, with the
+// linking.SetReadStorage and linking.SetWriteStorage functions, to provide
+// read and/or write to and/or from a CAR format as required.
+//
+// The focus of the StorageCar interfaces is to use the minimal possible IO
+// interface for the operation(s) being performed.
+//
+// • OpenReadable requires an io.ReaderAt as seeking is required for
+// random-access reads as a ReadableStore.
+//
+// • NewWritable requires an io.Writer when used to write a CARv1 as this format
+// can be written in a continuous stream as blocks are written through a
+// WritableStore (i.e. when the WriteAsCarV1 option is turned on). When used to
+// write a CARv2, the default mode, a random-access io.WriterAt is required as
+// the CARv2 header must be written after the payload is finalized and index
+// written in order to indicate payload location in the output. The plain
+// Writable store may be used to stream CARv1 contents without buffering;
+// only storing CIDs in memory for de-duplication (where required) and to still
+// allow Has operations.
+//
+// • NewReadableWritable requires an io.ReaderAt and an io.Writer as it combines
+// the functionality of a NewWritable with OpenReadable, being able to random-
+// access read any written blocks.
+//
+// • OpenReadableWritable requires an io.ReaderAt, an io.Writer and an
+// io.WriterAt as it extends the NewReadableWritable functionality with the
+// ability to resume an existing CAR. In addition, if the CAR being resumed is
+// a CARv2, the IO object being provided must have a Truncate() method (e.g.
+// an io.File) in order to properly manage CAR lifecycle and avoid writing a
+// corrupt CAR.
+//
+// The following options are available to customize the behavior of the
+// StorageCar:
+//
+// • WriteAsCarV1
+//
+// • StoreIdentityCIDs
+//
+// • AllowDuplicatePuts
+//
+// • UseWholeCIDs
+//
+// • ZeroLengthSectionAsEOF
+//
+// • UseIndexCodec
+//
+// • UseDataPadding
+//
+// • UseIndexPadding
+//
+// • MaxIndexCidSize
+//
+// • MaxAllowedHeaderSize
+//
+// • MaxAllowedSectionSize
+package storage
diff --git a/ipld/car/v2/storage/notfound.go b/ipld/car/v2/storage/notfound.go
new file mode 100644
index 0000000000..82153e2f2e
--- /dev/null
+++ b/ipld/car/v2/storage/notfound.go
@@ -0,0 +1,39 @@
+package storage
+
+import "github.com/ipfs/go-cid"
+
+// compatible with the go-ipld-format ErrNotFound, match against
+// interface{NotFound() bool}
+// this could go into go-ipld-prime, but for now we'll just exercise the
+// feature-test pattern
+
+type ErrNotFound struct {
+ Cid cid.Cid
+}
+
+func (e ErrNotFound) Error() string {
+ if e.Cid == cid.Undef {
+ return "ipld: could not find node"
+ }
+ return "ipld: could not find " + e.Cid.String()
+}
+
+func (e ErrNotFound) NotFound() bool {
+ return true
+}
+
+func (e ErrNotFound) Is(err error) bool {
+ switch err.(type) {
+ case ErrNotFound:
+ return true
+ default:
+ return false
+ }
+}
+
+func IsNotFound(err error) bool {
+ if nf, ok := err.(interface{ NotFound() bool }); ok {
+ return nf.NotFound()
+ }
+ return false
+}
diff --git a/ipld/car/v2/storage/storage.go b/ipld/car/v2/storage/storage.go
new file mode 100644
index 0000000000..46156e8dfe
--- /dev/null
+++ b/ipld/car/v2/storage/storage.go
@@ -0,0 +1,505 @@
+package storage
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "sync"
+
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1/util"
+ internalio "github.com/ipfs/boxo/ipld/car/v2/internal/io"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/store"
+ "github.com/ipfs/go-cid"
+ ipldstorage "github.com/ipld/go-ipld-prime/storage"
+)
+
+var errClosed = errors.New("cannot use a CARv2 storage after closing")
+
+type ReaderAtWriterAt interface {
+ io.ReaderAt
+ io.Writer
+ io.WriterAt
+}
+
+type ReadableCar interface {
+ ipldstorage.ReadableStorage
+ ipldstorage.StreamingReadableStorage
+ Roots() []cid.Cid
+}
+
+// WritableCar is compatible with storage.WritableStorage but also returns
+// the roots of the CAR. It does not implement ipld.StreamingWritableStorage
+// as the CAR format does not support streaming data followed by its CID, so
+// any streaming implementation would perform buffering and copy the
+// existing storage.PutStream() implementation.
+type WritableCar interface {
+ ipldstorage.WritableStorage
+ Roots() []cid.Cid
+ Finalize() error
+}
+
+var _ ReadableCar = (*StorageCar)(nil)
+var _ WritableCar = (*StorageCar)(nil)
+
+type StorageCar struct {
+ idx index.Index
+ reader io.ReaderAt
+ writer positionedWriter
+ dataWriter *internalio.OffsetWriteSeeker
+ header carv2.Header
+ roots []cid.Cid
+ opts carv2.Options
+
+ closed bool
+ mu sync.RWMutex
+}
+
+type positionedWriter interface {
+ io.Writer
+ Position() int64
+}
+
+// OpenReadable opens a CARv1 or CARv2 file for reading as a ReadableStorage
+// and StreamingReadableStorage as defined by
+// github.com/ipld/go-ipld-prime/storage.
+//
+// The returned ReadableStorage is compatible with a linksystem SetReadStorage
+// method as defined by github.com/ipld/go-ipld-prime/linking
+// to provide a block source backed by a CAR.
+//
+// When opening a CAR, an initial scan is performed to generate an index, or
+// load an index from a CARv2 index where available. This index data is kept in
+// memory while the CAR is being used in order to provide efficient random
+// Get access to blocks and Has operations.
+//
+// The Readable supports StreamingReadableStorage, which allows for efficient
+// GetStreaming operations straight out of the underlying CAR where the
+// linksystem can make use of it.
+func OpenReadable(reader io.ReaderAt, opts ...carv2.Option) (ReadableCar, error) {
+ sc := &StorageCar{opts: carv2.ApplyOptions(opts...)}
+
+ rr := internalio.ToReadSeeker(reader)
+ header, err := carv1.ReadHeader(rr, sc.opts.MaxAllowedHeaderSize)
+ if err != nil {
+ return nil, err
+ }
+ switch header.Version {
+ case 1:
+ sc.roots = header.Roots
+ sc.reader = reader
+ rr.Seek(0, io.SeekStart)
+ sc.idx = store.NewInsertionIndex()
+ if err := carv2.LoadIndex(sc.idx, rr, opts...); err != nil {
+ return nil, err
+ }
+ case 2:
+ v2r, err := carv2.NewReader(reader, opts...)
+ if err != nil {
+ return nil, err
+ }
+ sc.roots, err = v2r.Roots()
+ if err != nil {
+ return nil, err
+ }
+ if v2r.Header.HasIndex() {
+ ir, err := v2r.IndexReader()
+ if err != nil {
+ return nil, err
+ }
+ sc.idx, err = index.ReadFrom(ir)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ dr, err := v2r.DataReader()
+ if err != nil {
+ return nil, err
+ }
+ sc.idx = store.NewInsertionIndex()
+ if err := carv2.LoadIndex(sc.idx, dr, opts...); err != nil {
+ return nil, err
+ }
+ }
+ if sc.reader, err = v2r.DataReader(); err != nil {
+ return nil, err
+ }
+ default:
+ return nil, fmt.Errorf("unsupported CAR version: %v", header.Version)
+ }
+
+ return sc, nil
+}
+
+// NewWritable creates a new WritableStorage as defined by
+// github.com/ipld/go-ipld-prime/storage that writes a CARv1 or CARv2 format to
+// the given io.Writer.
+//
+// The returned WritableStorage is compatible with a linksystem SetWriteStorage
+// method as defined by github.com/ipld/go-ipld-prime/linking
+// to provide a block sink backed by a CAR.
+//
+// The WritableStorage supports Put operations, which will write
+// blocks to the CAR in the order they are received.
+//
+// When writing a CARv2 format (the default), the provided writer must be
+// compatible with io.WriterAt in order to provide random access as the CARv2
+// header must be written after the blocks in order to indicate the size of the
+// CARv2 data payload.
+//
+// A CARv1 (generated using the WriteAsCarV1 option) only requires an io.Writer
+// and can therefore stream CAR contents as blocks are written while still
+// providing Has operations and the ability to avoid writing duplicate blocks
+// as required.
+//
+// When writing a CARv2 format, it is important to call the Finalize method on
+// the returned WritableStorage in order to write the CARv2 header and index.
+func NewWritable(writer io.Writer, roots []cid.Cid, opts ...carv2.Option) (WritableCar, error) {
+ sc, err := newWritable(writer, roots, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return sc.init()
+}
+
+func newWritable(writer io.Writer, roots []cid.Cid, opts ...carv2.Option) (*StorageCar, error) {
+ sc := &StorageCar{
+ writer: &positionTrackingWriter{w: writer},
+ idx: store.NewInsertionIndex(),
+ header: carv2.NewHeader(0),
+ opts: carv2.ApplyOptions(opts...),
+ roots: roots,
+ }
+
+ if p := sc.opts.DataPadding; p > 0 {
+ sc.header = sc.header.WithDataPadding(p)
+ }
+ if p := sc.opts.IndexPadding; p > 0 {
+ sc.header = sc.header.WithIndexPadding(p)
+ }
+
+ offset := int64(sc.header.DataOffset)
+ if sc.opts.WriteAsCarV1 {
+ offset = 0
+ }
+
+ if writerAt, ok := writer.(io.WriterAt); ok {
+ sc.dataWriter = internalio.NewOffsetWriter(writerAt, offset)
+ } else {
+ if !sc.opts.WriteAsCarV1 {
+ return nil, fmt.Errorf("cannot write as CARv2 to a non-seekable writer")
+ }
+ }
+
+ return sc, nil
+}
+
+func newReadableWritable(rw ReaderAtWriterAt, roots []cid.Cid, opts ...carv2.Option) (*StorageCar, error) {
+ sc, err := newWritable(rw, roots, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ sc.reader = rw
+ if !sc.opts.WriteAsCarV1 {
+ sc.reader, err = internalio.NewOffsetReadSeeker(rw, int64(sc.header.DataOffset))
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return sc, nil
+}
+
+// NewReadableWritable creates a new StorageCar that is able to provide both
+// StorageReader and StorageWriter functionality.
+//
+// The returned StorageCar is compatible with a linksystem SetReadStorage and
+// SetWriteStorage methods as defined by github.com/ipld/go-ipld-prime/linking.
+//
+// When writing a CARv2 format, it is important to call the Finalize method on
+// the returned WritableStorage in order to write the CARv2 header and index.
+func NewReadableWritable(rw ReaderAtWriterAt, roots []cid.Cid, opts ...carv2.Option) (*StorageCar, error) {
+ sc, err := newReadableWritable(rw, roots, opts...)
+ if err != nil {
+ return nil, err
+ }
+ if _, err := sc.init(); err != nil {
+ return nil, err
+ }
+ return sc, nil
+}
+
+// OpenReadableWritable creates a new StorageCar that is able to provide both
+// StorageReader and StorageWriter functionality.
+//
+// The returned StorageCar is compatible with a linksystem SetReadStorage and
+// SetWriteStorage methods as defined by github.com/ipld/go-ipld-prime/linking.
+//
+// It attempts to resume a CARv2 file that was previously written to by
+// NewWritable, or NewReadableWritable.
+func OpenReadableWritable(rw ReaderAtWriterAt, roots []cid.Cid, opts ...carv2.Option) (*StorageCar, error) {
+ sc, err := newReadableWritable(rw, roots, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ // attempt to resume
+ rs, err := internalio.NewOffsetReadSeeker(rw, 0)
+ if err != nil {
+ return nil, err
+ }
+ if err := store.ResumableVersion(rs, sc.opts.WriteAsCarV1); err != nil {
+ return nil, err
+ }
+ if err := store.Resume(
+ rw,
+ sc.reader,
+ sc.dataWriter,
+ sc.idx.(*store.InsertionIndex),
+ roots,
+ sc.header.DataOffset,
+ sc.opts.WriteAsCarV1,
+ sc.opts.MaxAllowedHeaderSize,
+ sc.opts.ZeroLengthSectionAsEOF,
+ ); err != nil {
+ return nil, err
+ }
+ return sc, nil
+}
+
+func (sc *StorageCar) init() (WritableCar, error) {
+ if !sc.opts.WriteAsCarV1 {
+ if _, err := sc.writer.Write(carv2.Pragma); err != nil {
+ return nil, err
+ }
+ }
+ var w io.Writer = sc.dataWriter
+ if sc.dataWriter == nil {
+ w = sc.writer
+ }
+ if err := carv1.WriteHeader(&carv1.CarHeader{Roots: sc.roots, Version: 1}, w); err != nil {
+ return nil, err
+ }
+ return sc, nil
+}
+
+// Roots returns the roots of the CAR.
+func (sc *StorageCar) Roots() []cid.Cid {
+ return sc.roots
+}
+
+// Put adds a block to the CAR, where the block is identified by the given CID
+// provided in string form. The keyStr value must be a valid CID binary string
+// (not a multibase string representation), i.e. generated with CID#KeyString().
+func (sc *StorageCar) Put(ctx context.Context, keyStr string, data []byte) error {
+ keyCid, err := cid.Cast([]byte(keyStr))
+ if err != nil {
+ return fmt.Errorf("bad CID key: %w", err)
+ }
+
+ sc.mu.Lock()
+ defer sc.mu.Unlock()
+
+ if sc.closed {
+ return errClosed
+ }
+
+ idx, ok := sc.idx.(*store.InsertionIndex)
+ if !ok || sc.writer == nil {
+ return fmt.Errorf("cannot put into a read-only CAR")
+ }
+
+ if should, err := store.ShouldPut(
+ idx,
+ keyCid,
+ sc.opts.MaxIndexCidSize,
+ sc.opts.StoreIdentityCIDs,
+ sc.opts.BlockstoreAllowDuplicatePuts,
+ sc.opts.BlockstoreUseWholeCIDs,
+ ); err != nil {
+ return err
+ } else if !should {
+ return nil
+ }
+
+ w := sc.writer
+ if sc.dataWriter != nil {
+ w = sc.dataWriter
+ }
+ n := uint64(w.Position())
+ if err := util.LdWrite(w, keyCid.Bytes(), data); err != nil {
+ return err
+ }
+ idx.InsertNoReplace(keyCid, n)
+
+ return nil
+}
+
+// Has returns true if the CAR contains a block identified by the given CID
+// provided in string form. The keyStr value must be a valid CID binary string
+// (not a multibase string representation), i.e. generated with CID#KeyString().
+func (sc *StorageCar) Has(ctx context.Context, keyStr string) (bool, error) {
+ keyCid, err := cid.Cast([]byte(keyStr))
+ if err != nil {
+ return false, fmt.Errorf("bad CID key: %w", err)
+ }
+
+ sc.mu.RLock()
+ defer sc.mu.RUnlock()
+
+ if sc.closed {
+ return false, errClosed
+ }
+
+ if idx, ok := sc.idx.(*store.InsertionIndex); ok && sc.writer != nil {
+ // writable CAR, fast path using InsertionIndex
+ return store.Has(
+ idx,
+ keyCid,
+ sc.opts.MaxIndexCidSize,
+ sc.opts.StoreIdentityCIDs,
+ sc.opts.BlockstoreAllowDuplicatePuts,
+ sc.opts.BlockstoreUseWholeCIDs,
+ )
+ }
+
+ if !sc.opts.StoreIdentityCIDs {
+ // If we don't store identity CIDs then we can return them straight away as if they are here,
+ // otherwise we need to check for their existence.
+ // Note, we do this without locking, since there is no shared information to lock for in order to perform the check.
+ if _, ok, err := store.IsIdentity(keyCid); err != nil {
+ return false, err
+ } else if ok {
+ return true, nil
+ }
+ }
+
+ _, _, size, err := store.FindCid(
+ sc.reader,
+ sc.idx,
+ keyCid,
+ sc.opts.BlockstoreUseWholeCIDs,
+ sc.opts.ZeroLengthSectionAsEOF,
+ sc.opts.MaxAllowedSectionSize,
+ false,
+ )
+ if errors.Is(err, index.ErrNotFound) {
+ return false, nil
+ } else if err != nil {
+ return false, err
+ }
+ return size > -1, nil
+}
+
+// Get returns the block bytes identified by the given CID provided in string
+// form. The keyStr value must be a valid CID binary string (not a multibase
+// string representation), i.e. generated with CID#KeyString().
+func (sc *StorageCar) Get(ctx context.Context, keyStr string) ([]byte, error) {
+ rdr, err := sc.GetStream(ctx, keyStr)
+ if err != nil {
+ return nil, err
+ }
+ return io.ReadAll(rdr)
+}
+
+// GetStream returns a stream of the block bytes identified by the given CID
+// provided in string form. The keyStr value must be a valid CID binary string
+// (not a multibase string representation), i.e. generated with CID#KeyString().
+func (sc *StorageCar) GetStream(ctx context.Context, keyStr string) (io.ReadCloser, error) {
+ if sc.reader == nil {
+ return nil, fmt.Errorf("cannot read from a write-only CAR")
+ }
+
+ keyCid, err := cid.Cast([]byte(keyStr))
+ if err != nil {
+ return nil, fmt.Errorf("bad CID key: %w", err)
+ }
+
+ if !sc.opts.StoreIdentityCIDs {
+ // If we don't store identity CIDs then we can return them straight away as if they are here,
+ // otherwise we need to check for their existence.
+ // Note, we do this without locking, since there is no shared information to lock for in order to perform the check.
+ if digest, ok, err := store.IsIdentity(keyCid); err != nil {
+ return nil, err
+ } else if ok {
+ return io.NopCloser(bytes.NewReader(digest)), nil
+ }
+ }
+
+ sc.mu.RLock()
+ defer sc.mu.RUnlock()
+
+ if sc.closed {
+ return nil, errClosed
+ }
+
+ _, offset, size, err := store.FindCid(
+ sc.reader,
+ sc.idx,
+ keyCid,
+ sc.opts.BlockstoreUseWholeCIDs,
+ sc.opts.ZeroLengthSectionAsEOF,
+ sc.opts.MaxAllowedSectionSize,
+ false,
+ )
+ if errors.Is(err, index.ErrNotFound) {
+ return nil, ErrNotFound{Cid: keyCid}
+ } else if err != nil {
+ return nil, err
+ }
+ return io.NopCloser(io.NewSectionReader(sc.reader, offset, int64(size))), nil
+}
+
+// Finalize writes the CAR index to the underlying writer if the CAR being
+// written is a CARv2. It also writes a finalized CARv2 header which details
+// payload location. This should be called on a writable StorageCar in order to
+// avoid data loss.
+func (sc *StorageCar) Finalize() error {
+ idx, ok := sc.idx.(*store.InsertionIndex)
+ if !ok || sc.writer == nil {
+ // ignore this, it's not writable
+ return nil
+ }
+
+ if sc.opts.WriteAsCarV1 {
+ return nil
+ }
+
+ wat, ok := sc.writer.(*positionTrackingWriter).w.(io.WriterAt)
+ if !ok { // should should already be checked at construction if this is a writable
+ return fmt.Errorf("cannot finalize a CARv2 without an io.WriterAt")
+ }
+
+ sc.mu.Lock()
+ defer sc.mu.Unlock()
+
+ if sc.closed {
+ // Allow duplicate Finalize calls, just like Close.
+ // Still error, just like ReadOnly.Close; it should be discarded.
+ return fmt.Errorf("called Finalize on a closed storage CAR")
+ }
+
+ sc.closed = true
+
+ return store.Finalize(wat, sc.header, idx, uint64(sc.dataWriter.Position()), sc.opts.StoreIdentityCIDs, sc.opts.IndexCodec)
+}
+
+type positionTrackingWriter struct {
+ w io.Writer
+ offset int64
+}
+
+func (ptw *positionTrackingWriter) Write(p []byte) (int, error) {
+ written, err := ptw.w.Write(p)
+ ptw.offset += int64(written)
+ return written, err
+}
+
+func (ptw *positionTrackingWriter) Position() int64 {
+ return ptw.offset
+}
diff --git a/ipld/car/v2/storage/storage_test.go b/ipld/car/v2/storage/storage_test.go
new file mode 100644
index 0000000000..8278462623
--- /dev/null
+++ b/ipld/car/v2/storage/storage_test.go
@@ -0,0 +1,1276 @@
+package storage_test
+
+// TODO: test readable can't write and writable can't read
+
+import (
+ "bytes"
+ "context"
+ "crypto/sha512"
+ "errors"
+ "fmt"
+ "io"
+ "math/rand"
+ "os"
+ "path/filepath"
+ "sync"
+ "testing"
+ "time"
+
+ carv2 "github.com/ipfs/boxo/ipld/car/v2"
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+ "github.com/ipfs/boxo/ipld/car/v2/storage"
+ "github.com/ipfs/go-cid"
+ "github.com/multiformats/go-multicodec"
+ "github.com/multiformats/go-multihash"
+ "github.com/stretchr/testify/require"
+)
+
+var rng = rand.New(rand.NewSource(1413))
+var rngLk sync.Mutex
+
+func TestReadable(t *testing.T) {
+ tests := []struct {
+ name string
+ inputPath string
+ opts []carv2.Option
+ noIdCids bool
+ }{
+ {
+ "OpenedWithCarV1",
+ "../testdata/sample-v1.car",
+ []carv2.Option{carv2.UseWholeCIDs(true), carv2.StoreIdentityCIDs(true)},
+ false,
+ },
+ {
+ "OpenedWithCarV1_NoIdentityCID",
+ "../testdata/sample-v1.car",
+ []carv2.Option{carv2.UseWholeCIDs(true)},
+ false,
+ },
+ {
+ "OpenedWithCarV2",
+ "../testdata/sample-wrapped-v2.car",
+ []carv2.Option{carv2.UseWholeCIDs(true), carv2.StoreIdentityCIDs(true)},
+ // index already exists, but was made without identity CIDs, but opening with StoreIdentityCIDs(true) means we check the index
+ true,
+ },
+ {
+ "OpenedWithCarV2_NoIdentityCID",
+ "../testdata/sample-wrapped-v2.car",
+ []carv2.Option{carv2.UseWholeCIDs(true)},
+ false,
+ },
+ {
+ "OpenedWithCarV1ZeroLenSection",
+ "../testdata/sample-v1-with-zero-len-section.car",
+ []carv2.Option{carv2.UseWholeCIDs(true), carv2.ZeroLengthSectionAsEOF(true)},
+ false,
+ },
+ {
+ "OpenedWithAnotherCarV1ZeroLenSection",
+ "../testdata/sample-v1-with-zero-len-section2.car",
+ []carv2.Option{carv2.UseWholeCIDs(true), carv2.ZeroLengthSectionAsEOF(true)},
+ false,
+ },
+ {
+ "IndexlessV2",
+ "../testdata/sample-v2-indexless.car",
+ []carv2.Option{carv2.UseWholeCIDs(true)},
+ false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ // Setup new StorageCar
+ inputReader, err := os.Open(tt.inputPath)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, inputReader.Close()) })
+ readable, err := storage.OpenReadable(inputReader, tt.opts...)
+ require.NoError(t, err)
+
+ // Setup BlockReader to compare against
+ actualReader, err := os.Open(tt.inputPath)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, actualReader.Close()) })
+ actual, err := carv2.NewBlockReader(actualReader, tt.opts...)
+ require.NoError(t, err)
+
+ // Assert roots match v1 payload.
+ require.Equal(t, actual.Roots, readable.Roots())
+
+ for {
+ wantBlock, err := actual.Next()
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+
+ key := wantBlock.Cid()
+
+ // Assert StorageCar contains key.
+ has, err := readable.Has(ctx, key.KeyString())
+ require.NoError(t, err)
+ if key.Prefix().MhType == uint64(multicodec.Identity) && tt.noIdCids {
+ // fixture wasn't made with StoreIdentityCIDs, but we opened it with StoreIdentityCIDs,
+ // so they aren't there to find
+ require.False(t, has)
+ } else {
+ require.True(t, has)
+ }
+
+ // Assert block itself matches v1 payload block.
+ if has {
+ gotBlock, err := readable.Get(ctx, key.KeyString())
+ require.NoError(t, err)
+ require.Equal(t, wantBlock.RawData(), gotBlock)
+
+ reader, err := readable.GetStream(ctx, key.KeyString())
+ require.NoError(t, err)
+ data, err := io.ReadAll(reader)
+ require.NoError(t, err)
+ require.Equal(t, wantBlock.RawData(), data)
+ }
+ }
+
+ // test not exists
+ c := randCid()
+ has, err := readable.Has(ctx, c.KeyString())
+ require.NoError(t, err)
+ require.False(t, has)
+
+ _, err = readable.Get(ctx, c.KeyString())
+ require.True(t, errors.Is(err, storage.ErrNotFound{}))
+ require.True(t, storage.IsNotFound(err))
+ require.Contains(t, err.Error(), c.String())
+
+ // random identity, should only find this if we _don't_ store identity CIDs
+ storeIdentity := carv2.ApplyOptions(tt.opts...).StoreIdentityCIDs
+ c = randIdentityCid()
+
+ has, err = readable.Has(ctx, c.KeyString())
+ require.NoError(t, err)
+ require.Equal(t, !storeIdentity, has)
+
+ got, err := readable.Get(ctx, c.KeyString())
+ if !storeIdentity {
+ require.NoError(t, err)
+ mh, err := multihash.Decode(c.Hash())
+ require.NoError(t, err)
+ require.Equal(t, mh.Digest, got)
+ } else {
+ require.True(t, errors.Is(err, storage.ErrNotFound{}))
+ require.True(t, storage.IsNotFound(err))
+ require.Contains(t, err.Error(), c.String())
+ }
+ })
+ }
+}
+
+func TestReadableBadVersion(t *testing.T) {
+ f, err := os.Open("../testdata/sample-rootless-v42.car")
+ require.NoError(t, err)
+ t.Cleanup(func() { f.Close() })
+ subject, err := storage.OpenReadable(f)
+ require.Errorf(t, err, "unsupported car version: 42")
+ require.Nil(t, subject)
+}
+
+func TestWritable(t *testing.T) {
+ originalCarV1Path := "../testdata/sample-v1.car"
+
+ variants := []struct {
+ name string
+ compareCarV1 string
+ options []carv2.Option
+ expectedV1StartOffset int64
+ }{
+ {"carv2_noopt", "sample-v1-noidentity.car", []carv2.Option{}, int64(carv2.PragmaSize + carv2.HeaderSize)},
+ {"carv2_identity", "sample-v1.car", []carv2.Option{carv2.StoreIdentityCIDs(true)}, int64(carv2.PragmaSize + carv2.HeaderSize)},
+ {"carv1", "sample-v1-noidentity.car", []carv2.Option{carv2.WriteAsCarV1(true)}, int64(0)},
+ {"carv1_identity", "sample-v1.car", []carv2.Option{carv2.WriteAsCarV1(true), carv2.StoreIdentityCIDs(true)}, int64(0)},
+ }
+
+ for _, mode := range []string{"WithRead", "WithoutRead"} {
+ t.Run(mode, func(t *testing.T) {
+ for _, variant := range variants {
+ t.Run(variant.name, func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ opts := carv2.ApplyOptions(variant.options...)
+
+ // Setup input file using standard CarV1 reader
+ srcFile, err := os.Open(originalCarV1Path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, srcFile.Close()) })
+ r, err := carv1.NewCarReader(srcFile)
+ require.NoError(t, err)
+
+ path := filepath.Join(t.TempDir(), fmt.Sprintf("writable_%s_%s.car", mode, variant.name))
+ var dstFile *os.File
+
+ var writable *storage.StorageCar
+ if mode == "WithoutRead" {
+ dstFile, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
+ require.NoError(t, err)
+ t.Cleanup(func() { dstFile.Close() })
+ var writer io.Writer = &writerOnly{dstFile}
+ if !opts.WriteAsCarV1 {
+ writer = &writerAtOnly{dstFile}
+ }
+ w, err := storage.NewWritable(writer, r.Header.Roots, variant.options...)
+ require.NoError(t, err)
+ writable = w.(*storage.StorageCar)
+ } else {
+ dstFile, err = os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
+ require.NoError(t, err)
+ t.Cleanup(func() { dstFile.Close() })
+ writable, err = storage.NewReadableWritable(dstFile, r.Header.Roots, variant.options...)
+ require.NoError(t, err)
+ }
+
+ require.Equal(t, r.Header.Roots, writable.Roots())
+
+ cids := make([]cid.Cid, 0)
+ var idCidCount int
+ for {
+ // read from source
+ b, err := r.Next()
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+
+ // write to dest
+ err = writable.Put(ctx, b.Cid().KeyString(), b.RawData())
+ require.NoError(t, err)
+ cids = append(cids, b.Cid())
+
+ dmh, err := multihash.Decode(b.Cid().Hash())
+ require.NoError(t, err)
+ if dmh.Code == multihash.IDENTITY {
+ idCidCount++
+ }
+
+ if mode == "WithRead" {
+ // writable is a ReadableWritable / StorageCar
+
+ // read back out the one we just wrote
+ gotBlock, err := writable.Get(ctx, b.Cid().KeyString())
+ require.NoError(t, err)
+ require.Equal(t, b.RawData(), gotBlock)
+
+ reader, err := writable.GetStream(ctx, b.Cid().KeyString())
+ require.NoError(t, err)
+ data, err := io.ReadAll(reader)
+ require.NoError(t, err)
+ require.Equal(t, b.RawData(), data)
+
+ // try reading a random one:
+ candIndex := rng.Intn(len(cids))
+ var candidate cid.Cid
+ for _, c := range cids {
+ if candIndex == 0 {
+ candidate = c
+ break
+ }
+ candIndex--
+ }
+ has, err := writable.Has(ctx, candidate.KeyString())
+ require.NoError(t, err)
+ require.True(t, has)
+
+ // not exists
+ c := randCid()
+ has, err = writable.Has(ctx, c.KeyString())
+ require.NoError(t, err)
+ require.False(t, has)
+ _, err = writable.Get(ctx, c.KeyString())
+ require.True(t, errors.Is(err, storage.ErrNotFound{}))
+ require.True(t, storage.IsNotFound(err))
+ require.Contains(t, err.Error(), c.String())
+
+ // random identity, should only find this if we _don't_ store identity CIDs
+ c = randIdentityCid()
+ has, err = writable.Has(ctx, c.KeyString())
+ require.NoError(t, err)
+ require.Equal(t, !opts.StoreIdentityCIDs, has)
+
+ got, err := writable.Get(ctx, c.KeyString())
+ if !opts.StoreIdentityCIDs {
+ require.NoError(t, err)
+ mh, err := multihash.Decode(c.Hash())
+ require.NoError(t, err)
+ require.Equal(t, mh.Digest, got)
+ } else {
+ require.True(t, errors.Is(err, storage.ErrNotFound{}))
+ require.True(t, storage.IsNotFound(err))
+ require.Contains(t, err.Error(), c.String())
+ }
+ }
+ }
+
+ err = writable.Finalize()
+ require.NoError(t, err)
+
+ err = dstFile.Close()
+ require.NoError(t, err)
+
+ // test header version using carv2 reader
+ reopen, err := os.Open(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, reopen.Close()) })
+ rd, err := carv2.NewReader(reopen)
+ require.NoError(t, err)
+ require.Equal(t, opts.WriteAsCarV1, rd.Version == 1)
+
+ // now compare the binary contents of the written file to the expected file
+ comparePath := filepath.Join("../testdata/", variant.compareCarV1)
+ compareStat, err := os.Stat(comparePath)
+ require.NoError(t, err)
+
+ wrote, err := os.Open(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, wrote.Close()) })
+ _, err = wrote.Seek(variant.expectedV1StartOffset, io.SeekStart)
+ require.NoError(t, err)
+ hasher := sha512.New()
+ gotWritten, err := io.Copy(hasher, io.LimitReader(wrote, compareStat.Size()))
+ require.NoError(t, err)
+ gotSum := hasher.Sum(nil)
+
+ hasher.Reset()
+ compareV1, err := os.Open(comparePath)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, compareV1.Close()) })
+ wantWritten, err := io.Copy(hasher, compareV1)
+ require.NoError(t, err)
+ wantSum := hasher.Sum(nil)
+
+ require.Equal(t, wantWritten, gotWritten)
+ require.Equal(t, wantSum, gotSum)
+ })
+ }
+ })
+ }
+}
+
+func TestCannotWriteableV2WithoutWriterAt(t *testing.T) {
+ w, err := storage.NewWritable(&writerOnly{os.Stdout}, []cid.Cid{})
+ require.Error(t, err)
+ require.Nil(t, w)
+}
+
+func TestErrorsWhenWritingCidTooLarge(t *testing.T) {
+ maxAllowedCidSize := uint64(20)
+
+ path := filepath.Join(t.TempDir(), "writable-with-id-enabled-too-large.car")
+ out, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, out.Close()) })
+ subject, err := storage.NewWritable(out, []cid.Cid{}, carv2.MaxIndexCidSize(maxAllowedCidSize))
+ require.NoError(t, err)
+
+ // normal block but shorten the CID to make it acceptable
+ testCid, testData := randBlock()
+ mh, err := multihash.Decode(testCid.Hash())
+ require.NoError(t, err)
+ dig := mh.Digest[:10]
+ shortMh, err := multihash.Encode(dig, mh.Code)
+ require.NoError(t, err)
+ testCid = cid.NewCidV1(mh.Code, shortMh)
+
+ err = subject.Put(context.TODO(), testCid.KeyString(), testData)
+ require.NoError(t, err)
+
+ // standard CID but too long for options
+ testCid, testData = randBlock()
+ err = subject.Put(context.TODO(), testCid.KeyString(), testData)
+ require.Equal(t, &carv2.ErrCidTooLarge{MaxSize: maxAllowedCidSize, CurrentSize: uint64(testCid.ByteLen())}, err)
+}
+
+func TestConcurrentUse(t *testing.T) {
+ dst, err := os.OpenFile(filepath.Join(t.TempDir(), "readwrite.car"), os.O_CREATE|os.O_RDWR, 0644)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, dst.Close()) })
+ wbs, err := storage.NewReadableWritable(dst, nil)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ require.NoError(t, err)
+ t.Cleanup(func() { wbs.Finalize() })
+
+ var wg sync.WaitGroup
+ for i := 0; i < 100; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+
+ testCid, testData := randBlock()
+
+ has, err := wbs.Has(ctx, testCid.KeyString())
+ require.NoError(t, err)
+ require.False(t, has)
+
+ err = wbs.Put(ctx, testCid.KeyString(), testData)
+ require.NoError(t, err)
+
+ got, err := wbs.Get(ctx, testCid.KeyString())
+ require.NoError(t, err)
+ require.Equal(t, testData, got)
+ }()
+ }
+ wg.Wait()
+}
+
+func TestNullPadding(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ paddedV1, err := os.ReadFile("../testdata/sample-v1-with-zero-len-section.car")
+ require.NoError(t, err)
+
+ readable, err := storage.OpenReadable(bufferReaderAt(paddedV1), carv2.ZeroLengthSectionAsEOF(true))
+ require.NoError(t, err)
+
+ roots := readable.Roots()
+ require.Len(t, roots, 1)
+ has, err := readable.Has(ctx, roots[0].KeyString())
+ require.NoError(t, err)
+ require.True(t, has)
+
+ actual, err := carv2.NewBlockReader(bytes.NewReader(paddedV1), carv2.ZeroLengthSectionAsEOF(true))
+ require.NoError(t, err)
+
+ for {
+ wantBlock, err := actual.Next()
+ if err == io.EOF {
+ break
+ }
+ require.NoError(t, err)
+ b, err := readable.Get(ctx, wantBlock.Cid().KeyString())
+ require.NoError(t, err)
+ require.Equal(t, wantBlock.RawData(), b)
+ }
+}
+
+func TestPutSameHashes(t *testing.T) {
+ tdir := t.TempDir()
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ // This writable allows duplicate puts, and identifies by multihash as per the default.
+ pathAllowDups := filepath.Join(tdir, "writable-allowdup.car")
+ dstAllowDups, err := os.OpenFile(pathAllowDups, os.O_CREATE|os.O_RDWR, 0644)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, dstAllowDups.Close()) })
+ wbsAllowDups, err := storage.NewReadableWritable(dstAllowDups, nil, carv2.AllowDuplicatePuts(true))
+ require.NoError(t, err)
+
+ // This writable deduplicates puts by CID.
+ pathByCID := filepath.Join(tdir, "writable-dedup-wholecid.car")
+ dstByCID, err := os.OpenFile(pathByCID, os.O_CREATE|os.O_RDWR, 0644)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, dstByCID.Close()) })
+ wbsByCID, err := storage.NewReadableWritable(dstByCID, nil, carv2.UseWholeCIDs(true))
+ require.NoError(t, err)
+
+ // This writable deduplicates puts by multihash
+ pathByHash := filepath.Join(tdir, "writable-dedup-byhash.car")
+ dstByHash, err := os.OpenFile(pathByHash, os.O_CREATE|os.O_RDWR, 0644)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, dstByHash.Close()) })
+ wbsByHash, err := storage.NewReadableWritable(dstByHash, nil)
+ require.NoError(t, err)
+
+ var blockList []struct {
+ cid cid.Cid
+ data []byte
+ }
+
+ appendBlock := func(data []byte, version, codec uint64) {
+ c, err := cid.Prefix{
+ Version: version,
+ Codec: codec,
+ MhType: multihash.SHA2_256,
+ MhLength: -1,
+ }.Sum(data)
+ require.NoError(t, err)
+ blockList = append(blockList, struct {
+ cid cid.Cid
+ data []byte
+ }{c, data})
+ }
+
+ // Two raw blocks, meaning we have two unique multihashes.
+ // However, we have multiple CIDs for each multihash.
+ // We also have two duplicate CIDs.
+ data1 := []byte("foo bar")
+ appendBlock(data1, 0, cid.DagProtobuf)
+ appendBlock(data1, 1, cid.DagProtobuf)
+ appendBlock(data1, 1, cid.DagCBOR)
+ appendBlock(data1, 1, cid.DagCBOR) // duplicate CID
+
+ data2 := []byte("foo bar baz")
+ appendBlock(data2, 0, cid.DagProtobuf)
+ appendBlock(data2, 1, cid.DagProtobuf)
+ appendBlock(data2, 1, cid.DagProtobuf) // duplicate CID
+ appendBlock(data2, 1, cid.DagCBOR)
+
+ countBlocks := func(path string) int {
+ f, err := os.Open(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, f.Close()) })
+ rdr, err := carv2.NewBlockReader(f)
+ require.NoError(t, err)
+
+ n := 0
+ for {
+ _, err := rdr.Next()
+ if err == io.EOF {
+ break
+ }
+ n++
+ }
+ return n
+ }
+
+ putBlockList := func(writable *storage.StorageCar) {
+ for i, block := range blockList {
+ // Has should never error here.
+ // The first block should be missing.
+ // Others might not, given the duplicate hashes.
+ has, err := writable.Has(ctx, block.cid.KeyString())
+ require.NoError(t, err)
+ if i == 0 {
+ require.False(t, has)
+ }
+
+ err = writable.Put(ctx, block.cid.KeyString(), block.data)
+ require.NoError(t, err)
+
+ // Has and Get need to work right after a Put
+ has, err = writable.Has(ctx, block.cid.KeyString())
+ require.NoError(t, err)
+ require.True(t, has)
+
+ got, err := writable.Get(ctx, block.cid.KeyString())
+ require.NoError(t, err)
+ require.Equal(t, block.data, got)
+ }
+ }
+
+ putBlockList(wbsAllowDups)
+ err = wbsAllowDups.Finalize()
+ require.NoError(t, err)
+ require.Equal(t, len(blockList), countBlocks(pathAllowDups))
+
+ // Put the same list of blocks to the CAR that deduplicates by CID.
+ // We should end up with two fewer blocks, as two are entire CID duplicates.
+ putBlockList(wbsByCID)
+ err = wbsByCID.Finalize()
+ require.NoError(t, err)
+ require.Equal(t, len(blockList)-2, countBlocks(pathByCID))
+
+ // Put the same list of blocks to the CAR that deduplicates by CID.
+ // We should end up with just two blocks, as the original set of blocks only
+ // has two distinct multihashes.
+ putBlockList(wbsByHash)
+ err = wbsByHash.Finalize()
+ require.NoError(t, err)
+ require.Equal(t, 2, countBlocks(pathByHash))
+}
+
+func TestReadableCantWrite(t *testing.T) {
+ inp, err := os.Open("../testdata/sample-v1.car")
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, inp.Close()) })
+ readable, err := storage.OpenReadable(inp)
+ require.NoError(t, err)
+ require.ErrorContains(t, readable.(*storage.StorageCar).Put(context.Background(), randCid().KeyString(), []byte("bar")), "read-only")
+ // Finalize() is nonsense for a readable, but it should be safe
+ require.NoError(t, readable.(*storage.StorageCar).Finalize())
+}
+
+func TestWritableCantRead(t *testing.T) {
+ // an io.Writer with no io.WriterAt capabilities
+ path := filepath.Join(t.TempDir(), "writable.car")
+ out, err := os.Create(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, out.Close()) })
+
+ // This should fail because the writer is not an io.WriterAt
+ _, err = storage.NewWritable(&writerOnly{out}, nil)
+ require.ErrorContains(t, err, "CARv2")
+ require.ErrorContains(t, err, "non-seekable")
+
+ writable, err := storage.NewWritable(&writerOnly{out}, nil, carv2.WriteAsCarV1(true))
+ require.NoError(t, err)
+
+ _, err = writable.(*storage.StorageCar).Get(context.Background(), randCid().KeyString())
+ require.ErrorContains(t, err, "write-only")
+
+ _, err = writable.(*storage.StorageCar).GetStream(context.Background(), randCid().KeyString())
+ require.ErrorContains(t, err, "write-only")
+
+ require.NoError(t, writable.Finalize())
+}
+
+func TestReadWriteWithPaddingWorksAsExpected(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ testCid1, testData1 := randBlock()
+ testCid2, testData2 := randBlock()
+
+ wantRoots := []cid.Cid{testCid1, testCid2}
+ path := filepath.Join(t.TempDir(), "readwrite-with-padding.car")
+ writer, err := os.Create(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, writer.Close()) })
+
+ wantCarV1Padding := uint64(1413)
+ wantIndexPadding := uint64(1314)
+ subject, err := storage.NewReadableWritable(
+ writer,
+ wantRoots,
+ carv2.UseDataPadding(wantCarV1Padding),
+ carv2.UseIndexPadding(wantIndexPadding))
+ require.NoError(t, err)
+ require.NoError(t, subject.Put(ctx, testCid1.KeyString(), testData1))
+ require.NoError(t, subject.Put(ctx, testCid2.KeyString(), testData2))
+ require.NoError(t, subject.Finalize())
+
+ // Assert CARv2 header contains right offsets.
+ gotCarV2, err := carv2.OpenReader(path)
+ t.Cleanup(func() { gotCarV2.Close() })
+ require.NoError(t, err)
+ wantCarV1Offset := carv2.PragmaSize + carv2.HeaderSize + wantCarV1Padding
+ wantIndexOffset := wantCarV1Offset + gotCarV2.Header.DataSize + wantIndexPadding
+ require.Equal(t, wantCarV1Offset, gotCarV2.Header.DataOffset)
+ require.Equal(t, wantIndexOffset, gotCarV2.Header.IndexOffset)
+ require.NoError(t, gotCarV2.Close())
+
+ f, err := os.Open(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { f.Close() })
+
+ // Assert reading CARv1 directly at offset and size is as expected.
+ gotCarV1, err := carv1.NewCarReader(io.NewSectionReader(f, int64(wantCarV1Offset), int64(gotCarV2.Header.DataSize)))
+ require.NoError(t, err)
+ require.Equal(t, wantRoots, gotCarV1.Header.Roots)
+ gotBlock, err := gotCarV1.Next()
+ require.NoError(t, err)
+ require.Equal(t, testCid1, gotBlock.Cid())
+ require.Equal(t, testData1, gotBlock.RawData())
+ gotBlock, err = gotCarV1.Next()
+ require.NoError(t, err)
+ require.Equal(t, testCid2, gotBlock.Cid())
+ require.Equal(t, testData2, gotBlock.RawData())
+
+ _, err = gotCarV1.Next()
+ require.Equal(t, io.EOF, err)
+
+ // Assert reading index directly from file is parsable and has expected CIDs.
+ stat, err := f.Stat()
+ require.NoError(t, err)
+ indexSize := stat.Size() - int64(wantIndexOffset)
+ gotIdx, err := index.ReadFrom(io.NewSectionReader(f, int64(wantIndexOffset), indexSize))
+ require.NoError(t, err)
+ _, err = index.GetFirst(gotIdx, testCid1)
+ require.NoError(t, err)
+ _, err = index.GetFirst(gotIdx, testCid2)
+ require.NoError(t, err)
+}
+
+func TestResumption(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ srcPath := "../testdata/sample-v1.car"
+
+ v1f, err := os.Open(srcPath)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, v1f.Close()) })
+ rd, err := carv2.NewReader(v1f)
+ require.NoError(t, err)
+ roots, err := rd.Roots()
+ require.NoError(t, err)
+
+ blockSource := func() <-chan simpleBlock {
+ v1f, err := os.Open(srcPath)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, v1f.Close()) })
+ r, err := carv1.NewCarReader(v1f)
+ require.NoError(t, err)
+ ret := make(chan simpleBlock)
+
+ go func() {
+ for {
+ b, err := r.Next()
+ if err == io.EOF {
+ close(ret)
+ break
+ }
+ require.NoError(t, err)
+ ret <- simpleBlock{cid: b.Cid(), data: b.RawData()}
+ }
+ }()
+
+ return ret
+ }
+
+ path := filepath.Join(t.TempDir(), "readwrite-resume.car")
+ writer, err := os.Create(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, writer.Close()) })
+ // Create an incomplete CARv2 file with no blocks put.
+ subject, err := storage.NewReadableWritable(writer, roots, carv2.UseWholeCIDs(true))
+ require.NoError(t, err)
+
+ // For each block resume on the same file, putting blocks one at a time.
+ var wantBlockCountSoFar, idCidCount int
+ wantBlocks := make(map[cid.Cid]simpleBlock)
+ for b := range blockSource() {
+ wantBlockCountSoFar++
+ wantBlocks[b.cid] = b
+
+ dmh, err := multihash.Decode(b.cid.Hash())
+ require.NoError(t, err)
+ if dmh.Code == multihash.IDENTITY {
+ idCidCount++
+ }
+
+ // 30% chance of subject failing; more concretely: re-instantiating the StorageCar with the same
+ // file without calling Finalize. The higher this percentage the slower the test runs
+ // considering the number of blocks in the original CARv1 test payload.
+ resume := rng.Float32() <= 0.3
+ // If testing resume case, then flip a coin to decide whether to finalize before the StorageCar
+ // re-instantiation or not. Note, both cases should work for resumption since we do not
+ // limit resumption to unfinalized files.
+ finalizeBeforeResumption := rng.Float32() <= 0.5
+ if resume {
+ if finalizeBeforeResumption {
+ require.NoError(t, subject.Finalize())
+ }
+
+ _, err := writer.Seek(0, io.SeekStart)
+ require.NoError(t, err)
+ subject, err = storage.OpenReadableWritable(writer, roots, carv2.UseWholeCIDs(true))
+ require.NoError(t, err)
+ }
+ require.NoError(t, subject.Put(ctx, b.cid.KeyString(), b.data))
+
+ // With 10% chance test read operations on an resumed read-write StorageCar.
+ // We don't test on every put to reduce test runtime.
+ testRead := rng.Float32() <= 0.1
+ if testRead {
+ // Assert read operations on the read-write StorageCar are as expected when resumed from an
+ // existing file
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ t.Cleanup(cancel)
+ for k, wantBlock := range wantBlocks {
+ has, err := subject.Has(ctx, k.KeyString())
+ require.NoError(t, err)
+ require.True(t, has)
+ gotBlock, err := subject.Get(ctx, k.KeyString())
+ require.NoError(t, err)
+ require.Equal(t, wantBlock.data, gotBlock)
+ }
+ // Assert the number of blocks in file are as expected calculated via AllKeysChan
+ require.Equal(t, wantBlockCountSoFar, len(wantBlocks))
+ }
+ }
+
+ // Finalize the StorageCar to complete partially written CARv2 file.
+ subject, err = storage.OpenReadableWritable(writer, roots, carv2.UseWholeCIDs(true))
+ require.NoError(t, err)
+ require.NoError(t, subject.Finalize())
+
+ // Assert resumed from file is a valid CARv2 with index.
+ v2f, err := os.Open(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, v2f.Close()) })
+ v2r, err := carv2.NewReader(v2f)
+ require.NoError(t, err)
+ require.True(t, v2r.Header.HasIndex())
+
+ // Assert CARv1 payload in file matches the original CARv1 payload.
+ _, err = v1f.Seek(0, io.SeekStart)
+ require.NoError(t, err)
+ wantPayloadReader, err := carv1.NewCarReader(v1f)
+ require.NoError(t, err)
+
+ dr, err := v2r.DataReader()
+ require.NoError(t, err)
+ gotPayloadReader, err := carv1.NewCarReader(dr)
+ require.NoError(t, err)
+
+ require.Equal(t, wantPayloadReader.Header, gotPayloadReader.Header)
+ for {
+ wantNextBlock, wantErr := wantPayloadReader.Next()
+ if wantErr == io.EOF {
+ gotNextBlock, gotErr := gotPayloadReader.Next()
+ require.Equal(t, wantErr, gotErr)
+ require.Nil(t, gotNextBlock)
+ break
+ }
+ require.NoError(t, wantErr)
+
+ dmh, err := multihash.Decode(wantNextBlock.Cid().Hash())
+ require.NoError(t, err)
+ if dmh.Code == multihash.IDENTITY {
+ continue
+ }
+
+ gotNextBlock, gotErr := gotPayloadReader.Next()
+ require.NoError(t, gotErr)
+ require.Equal(t, wantNextBlock, gotNextBlock)
+ }
+
+ // Assert index in resumed from file is identical to index generated from the data payload portion of the generated CARv2 file.
+ _, err = v1f.Seek(0, io.SeekStart)
+ require.NoError(t, err)
+ ir, err := v2r.IndexReader()
+ require.NoError(t, err)
+ gotIdx, err := index.ReadFrom(ir)
+ require.NoError(t, err)
+ dr, err = v2r.DataReader()
+ require.NoError(t, err)
+ wantIdx, err := carv2.GenerateIndex(dr)
+ require.NoError(t, err)
+ require.Equal(t, wantIdx, gotIdx)
+}
+
+func TestResumptionV1(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ srcPath := "../testdata/sample-v1.car"
+
+ v1f, err := os.Open(srcPath)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, v1f.Close()) })
+ rd, err := carv2.NewReader(v1f)
+ require.NoError(t, err)
+ roots, err := rd.Roots()
+ require.NoError(t, err)
+
+ blockSource := func() <-chan simpleBlock {
+ v1f, err := os.Open(srcPath)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, v1f.Close()) })
+ r, err := carv1.NewCarReader(v1f)
+ require.NoError(t, err)
+ ret := make(chan simpleBlock)
+
+ go func() {
+ for {
+ b, err := r.Next()
+ if err == io.EOF {
+ close(ret)
+ break
+ }
+ require.NoError(t, err)
+ ret <- simpleBlock{cid: b.Cid(), data: b.RawData()}
+ }
+ }()
+
+ return ret
+ }
+
+ path := filepath.Join(t.TempDir(), "readwrite-resume-v1.car")
+ writer, err := os.Create(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, writer.Close()) })
+ // Create an incomplete CARv2 file with no blocks put.
+ subject, err := storage.NewReadableWritable(writer, roots, carv2.UseWholeCIDs(true), carv2.WriteAsCarV1(true))
+ require.NoError(t, err)
+
+ // For each block resume on the same file, putting blocks one at a time.
+ var wantBlockCountSoFar, idCidCount int
+ wantBlocks := make(map[cid.Cid]simpleBlock)
+ for b := range blockSource() {
+ wantBlockCountSoFar++
+ wantBlocks[b.cid] = b
+
+ dmh, err := multihash.Decode(b.cid.Hash())
+ require.NoError(t, err)
+ if dmh.Code == multihash.IDENTITY {
+ idCidCount++
+ }
+
+ // 30% chance of subject failing; more concretely: re-instantiating the StorageCar with the same
+ // file without calling Finalize. The higher this percentage the slower the test runs
+ // considering the number of blocks in the original CARv1 test payload.
+ resume := rng.Float32() <= 0.3
+ // If testing resume case, then flip a coin to decide whether to finalize before the StorageCar
+ // re-instantiation or not. Note, both cases should work for resumption since we do not
+ // limit resumption to unfinalized files.
+ finalizeBeforeResumption := rng.Float32() <= 0.5
+ if resume {
+ if finalizeBeforeResumption {
+ require.NoError(t, subject.Finalize())
+ }
+
+ _, err := writer.Seek(0, io.SeekStart)
+ require.NoError(t, err)
+ subject, err = storage.OpenReadableWritable(writer, roots, carv2.UseWholeCIDs(true), carv2.WriteAsCarV1(true))
+ require.NoError(t, err)
+ }
+ require.NoError(t, subject.Put(ctx, b.cid.KeyString(), b.data))
+
+ // With 10% chance test read operations on an resumed read-write StorageCar.
+ // We don't test on every put to reduce test runtime.
+ testRead := rng.Float32() <= 0.1
+ if testRead {
+ // Assert read operations on the read-write StorageCar are as expected when resumed from an
+ // existing file
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ t.Cleanup(cancel)
+ for k, wantBlock := range wantBlocks {
+ has, err := subject.Has(ctx, k.KeyString())
+ require.NoError(t, err)
+ require.True(t, has)
+ gotBlock, err := subject.Get(ctx, k.KeyString())
+ require.NoError(t, err)
+ require.Equal(t, wantBlock.data, gotBlock)
+ }
+ // Assert the number of blocks in file are as expected calculated via AllKeysChan
+ require.Equal(t, wantBlockCountSoFar, len(wantBlocks))
+ }
+ }
+
+ // Finalize the StorageCar to complete partially written CARv2 file.
+ subject, err = storage.OpenReadableWritable(writer, roots, carv2.UseWholeCIDs(true), carv2.WriteAsCarV1(true))
+ require.NoError(t, err)
+ require.NoError(t, subject.Finalize())
+
+ // Assert resumed from file is a valid CARv2 with index.
+ v2f, err := os.Open(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, v2f.Close()) })
+ v2r, err := carv2.NewReader(v2f)
+ require.NoError(t, err)
+ require.False(t, v2r.Header.HasIndex())
+ require.Equal(t, uint64(1), v2r.Version)
+
+ _, err = v1f.Seek(0, io.SeekStart)
+ require.NoError(t, err)
+ wantPayloadReader, err := carv1.NewCarReader(v1f)
+ require.NoError(t, err)
+
+ dr, err := v2r.DataReader() // since this is a v1 we're just reading from the top with this
+ require.NoError(t, err)
+ gotPayloadReader, err := carv1.NewCarReader(dr)
+ require.NoError(t, err)
+
+ require.Equal(t, wantPayloadReader.Header, gotPayloadReader.Header)
+ for {
+ wantNextBlock, wantErr := wantPayloadReader.Next()
+ if wantErr == io.EOF {
+ gotNextBlock, gotErr := gotPayloadReader.Next()
+ require.Equal(t, wantErr, gotErr)
+ require.Nil(t, gotNextBlock)
+ break
+ }
+ require.NoError(t, wantErr)
+
+ dmh, err := multihash.Decode(wantNextBlock.Cid().Hash())
+ require.NoError(t, err)
+ if dmh.Code == multihash.IDENTITY {
+ continue
+ }
+
+ gotNextBlock, gotErr := gotPayloadReader.Next()
+ require.NoError(t, gotErr)
+ require.Equal(t, wantNextBlock, gotNextBlock)
+ }
+}
+
+func TestResumptionIsSupportedOnFinalizedFile(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "readwrite-resume-finalized.car")
+ v2f, err := os.Create(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, v2f.Close()) })
+ // Create an incomplete CARv2 file with no blocks put.
+ subject, err := storage.NewReadableWritable(v2f, []cid.Cid{})
+ require.NoError(t, err)
+ require.NoError(t, subject.Finalize())
+
+ reopen, err := os.OpenFile(path, os.O_RDWR, 0o666)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, reopen.Close()) })
+ subject, err = storage.NewReadableWritable(reopen, []cid.Cid{})
+ require.NoError(t, err)
+ t.Cleanup(func() { subject.Finalize() })
+}
+
+func TestReadWriteErrorsOnlyWhenFinalized(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ testCid1, testData1 := randBlock()
+ testCid2, testData2 := randBlock()
+
+ wantRoots := []cid.Cid{testCid1, testCid2}
+ path := filepath.Join(t.TempDir(), "readwrite-finalized-panic.car")
+ writer, err := os.Create(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, writer.Close()) })
+
+ subject, err := storage.NewReadableWritable(writer, wantRoots)
+ require.NoError(t, err)
+
+ require.NoError(t, subject.Put(ctx, testCid1.KeyString(), testData1))
+ require.NoError(t, subject.Put(ctx, testCid2.KeyString(), testData2))
+
+ gotBlock, err := subject.Get(ctx, testCid1.KeyString())
+ require.NoError(t, err)
+ require.Equal(t, testData1, gotBlock)
+
+ gotRoots := subject.Roots()
+ require.Equal(t, wantRoots, gotRoots)
+
+ has, err := subject.Has(ctx, testCid1.KeyString())
+ require.NoError(t, err)
+ require.True(t, has)
+
+ require.NoError(t, subject.Finalize())
+ require.Error(t, subject.Finalize())
+
+ _, ok := (interface{})(subject).(io.Closer)
+ require.False(t, ok)
+
+ _, err = subject.Get(ctx, testCid1.KeyString())
+ require.Error(t, err)
+ require.Error(t, err)
+ _, err = subject.Has(ctx, testCid2.KeyString())
+ require.Error(t, err)
+
+ require.Error(t, subject.Put(ctx, testCid1.KeyString(), testData1))
+}
+
+func TestReadWriteResumptionMismatchingRootsIsError(t *testing.T) {
+ tmpPath := requireTmpCopy(t, "../testdata/sample-wrapped-v2.car")
+
+ origContent, err := os.ReadFile(tmpPath)
+ require.NoError(t, err)
+
+ badRoot := randCid()
+ writer, err := os.OpenFile(tmpPath, os.O_RDWR, 0o666)
+ require.NoError(t, err)
+ t.Cleanup(func() { writer.Close() })
+ subject, err := storage.OpenReadableWritable(writer, []cid.Cid{badRoot})
+ require.EqualError(t, err, "cannot resume on file with mismatching data header")
+ require.Nil(t, subject)
+ require.NoError(t, writer.Close())
+
+ newContent, err := os.ReadFile(tmpPath)
+ require.NoError(t, err)
+
+ // Expect the bad file to be left untouched; check the size first.
+ // If the sizes mismatch, printing a huge diff would not help us.
+ require.Equal(t, len(origContent), len(newContent))
+ require.Equal(t, origContent, newContent)
+}
+
+func requireTmpCopy(t *testing.T, src string) string {
+ srcF, err := os.Open(src)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, srcF.Close()) }()
+ stats, err := srcF.Stat()
+ require.NoError(t, err)
+
+ dst := filepath.Join(t.TempDir(), stats.Name())
+ dstF, err := os.Create(dst)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, dstF.Close()) }()
+
+ _, err = io.Copy(dstF, srcF)
+ require.NoError(t, err)
+ return dst
+}
+
+func TestReadWriteResumptionFromFileWithDifferentCarV1PaddingIsError(t *testing.T) {
+ testCid1, testData1 := randBlock()
+ wantRoots := []cid.Cid{testCid1}
+ path := filepath.Join(t.TempDir(), "readwrite-resume-with-padding.car")
+ writer, err := os.Create(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, writer.Close()) })
+ subject, err := storage.NewReadableWritable(
+ writer,
+ wantRoots,
+ carv2.UseDataPadding(1413))
+ require.NoError(t, err)
+ require.NoError(t, subject.Put(context.TODO(), testCid1.KeyString(), testData1))
+ require.NoError(t, subject.Finalize())
+
+ subject, err = storage.OpenReadableWritable(
+ writer,
+ wantRoots,
+ carv2.UseDataPadding(1314))
+ require.EqualError(t, err, "cannot resume from file with mismatched CARv1 offset; "+
+ "`WithDataPadding` option must match the padding on file. "+
+ "Expected padding value of 1413 but got 1314")
+ require.Nil(t, subject)
+}
+
+func TestOperationsErrorWithBadCidStrings(t *testing.T) {
+ testCid, testData := randBlock()
+ path := filepath.Join(t.TempDir(), "badkeys.car")
+ writer, err := os.Create(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, writer.Close()) })
+ subject, err := storage.NewReadableWritable(writer, []cid.Cid{})
+ require.NoError(t, err)
+
+ require.NoError(t, subject.Put(context.TODO(), testCid.KeyString(), testData))
+ require.ErrorContains(t, subject.Put(context.TODO(), fmt.Sprintf("%s/nope", testCid.KeyString()), testData), "bad CID key")
+ require.ErrorContains(t, subject.Put(context.TODO(), "nope", testData), "bad CID key")
+
+ has, err := subject.Has(context.TODO(), testCid.KeyString())
+ require.NoError(t, err)
+ require.True(t, has)
+ has, err = subject.Has(context.TODO(), fmt.Sprintf("%s/nope", testCid.KeyString()))
+ require.ErrorContains(t, err, "bad CID key")
+ require.False(t, has)
+ has, err = subject.Has(context.TODO(), "nope")
+ require.ErrorContains(t, err, "bad CID key")
+ require.False(t, has)
+
+ got, err := subject.Get(context.TODO(), testCid.KeyString())
+ require.NoError(t, err)
+ require.NotNil(t, got)
+ got, err = subject.Get(context.TODO(), fmt.Sprintf("%s/nope", testCid.KeyString()))
+ require.ErrorContains(t, err, "bad CID key")
+ require.Nil(t, got)
+ got, err = subject.Get(context.TODO(), "nope")
+ require.ErrorContains(t, err, "bad CID key")
+ require.Nil(t, got)
+}
+
+func TestWholeCID(t *testing.T) {
+ for _, whole := range []bool{true, false} {
+ whole := whole
+ t.Run(fmt.Sprintf("whole=%t", whole), func(t *testing.T) {
+ t.Parallel()
+ ctx := context.Background()
+ path := filepath.Join(t.TempDir(), fmt.Sprintf("writable_%t.car", whole))
+ out, err := os.Create(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, out.Close()) })
+ store, err := storage.NewReadableWritable(out, []cid.Cid{}, carv2.UseWholeCIDs(whole))
+ require.NoError(t, err)
+
+ c1, b1 := randBlock()
+ c2, b2 := randBlock()
+
+ require.NoError(t, store.Put(ctx, c1.KeyString(), b1))
+ has, err := store.Has(ctx, c1.KeyString())
+ require.NoError(t, err)
+ require.True(t, has)
+
+ pref := c1.Prefix()
+ pref.Codec = cid.DagProtobuf
+ pref.Version = 1
+ cpb1, err := pref.Sum(b1)
+ require.NoError(t, err)
+
+ has, err = store.Has(ctx, cpb1.KeyString())
+ require.NoError(t, err)
+ require.Equal(t, has, !whole)
+
+ require.NoError(t, store.Put(ctx, c2.KeyString(), b1))
+ has, err = store.Has(ctx, c2.KeyString())
+ require.NoError(t, err)
+ require.True(t, has)
+ has, err = store.Has(ctx, cpb1.KeyString())
+ require.NoError(t, err)
+ require.Equal(t, has, !whole)
+
+ pref = c2.Prefix()
+ pref.Codec = cid.DagProtobuf
+ pref.Version = 1
+ cpb2, err := pref.Sum(b2)
+ require.NoError(t, err)
+
+ has, err = store.Has(ctx, cpb2.KeyString())
+ require.NoError(t, err)
+ require.Equal(t, has, !whole)
+ has, err = store.Has(ctx, cpb1.KeyString())
+ require.NoError(t, err)
+ require.Equal(t, has, !whole)
+ })
+ }
+}
+
+type writerOnly struct {
+ io.Writer
+}
+
+func (w *writerOnly) Write(p []byte) (n int, err error) {
+ return w.Writer.Write(p)
+}
+
+type writerAtOnly struct {
+ *os.File
+}
+
+func (w *writerAtOnly) WriteAt(p []byte, off int64) (n int, err error) {
+ return w.File.WriteAt(p, off)
+}
+
+func (w *writerAtOnly) Write(p []byte) (n int, err error) {
+ return w.File.Write(p)
+}
+
+func randBlock() (cid.Cid, []byte) {
+ data := make([]byte, 1024)
+ rngLk.Lock()
+ rng.Read(data)
+ rngLk.Unlock()
+ h, err := multihash.Sum(data, multihash.SHA2_512, -1)
+ if err != nil {
+ panic(err)
+ }
+ return cid.NewCidV1(cid.Raw, h), data
+}
+
+func randCid() cid.Cid {
+ b := make([]byte, 32)
+ rngLk.Lock()
+ rng.Read(b)
+ rngLk.Unlock()
+ mh, _ := multihash.Encode(b, multihash.SHA2_256)
+ return cid.NewCidV1(cid.DagProtobuf, mh)
+}
+
+func randIdentityCid() cid.Cid {
+ b := make([]byte, 32)
+ rngLk.Lock()
+ rng.Read(b)
+ rngLk.Unlock()
+ mh, _ := multihash.Encode(b, multihash.IDENTITY)
+ return cid.NewCidV1(cid.Raw, mh)
+}
+
+type bufferReaderAt []byte
+
+func (b bufferReaderAt) ReadAt(p []byte, off int64) (int, error) {
+ if off >= int64(len(b)) {
+ return 0, io.EOF
+ }
+ return copy(p, b[off:]), nil
+}
+
+type simpleBlock struct {
+ cid cid.Cid
+ data []byte
+}
diff --git a/ipld/car/v2/testdata/fuzz/FuzzBlockReader/c3c7eedeb4968a5b3131371ba9f5138a38c882a7fa06456595998e740a9f5a14 b/ipld/car/v2/testdata/fuzz/FuzzBlockReader/c3c7eedeb4968a5b3131371ba9f5138a38c882a7fa06456595998e740a9f5a14
new file mode 100644
index 0000000000..ae9262d491
--- /dev/null
+++ b/ipld/car/v2/testdata/fuzz/FuzzBlockReader/c3c7eedeb4968a5b3131371ba9f5138a38c882a7fa06456595998e740a9f5a14
@@ -0,0 +1,2 @@
+go test fuzz v1
+[]byte("\xff\x80\xaa\x95\xa6sion\x01,\x01q\x12 \x15\x1f\xe9\xe7 dataSize {
+ // Truncate to the expected size to assure the resulting file is a correctly sized CARv1.
+ err = dst.Truncate(written)
+ }
+
+ return err
+}
+
+// AttachIndex attaches a given index to an existing CARv2 file at given path and offset.
+func AttachIndex(path string, idx index.Index, offset uint64) error {
+ // TODO: instead of offset, maybe take padding?
+ // TODO: check that the given path is indeed a CARv2.
+ // TODO: update CARv2 header according to the offset at which index is written out.
+ out, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o640)
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+ indexWriter := internalio.NewOffsetWriter(out, int64(offset))
+ _, err = index.WriteTo(idx, indexWriter)
+ return err
+}
+
+// ReplaceRootsInFile replaces the root CIDs in CAR file at given path with the given roots.
+// This function accepts both CARv1 and CARv2 files.
+//
+// Note that the roots are only replaced if their total serialized size exactly matches the total
+// serialized size of existing roots in CAR file.
+func ReplaceRootsInFile(path string, roots []cid.Cid, opts ...Option) (err error) {
+ f, err := os.OpenFile(path, os.O_RDWR, 0o666)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ // Close file and override return error type if it is nil.
+ if cerr := f.Close(); err == nil {
+ err = cerr
+ }
+ }()
+
+ options := ApplyOptions(opts...)
+
+ // Read header or pragma; note that both are a valid CARv1 header.
+ header, err := carv1.ReadHeader(f, options.MaxAllowedHeaderSize)
+ if err != nil {
+ return err
+ }
+
+ var currentSize int64
+ var newHeaderOffset int64
+ switch header.Version {
+ case 1:
+ // When the given file is a CARv1 :
+ // 1. The offset at which the new header should be written is zero (newHeaderOffset = 0)
+ // 2. The current header size is equal to the number of bytes read, and
+ //
+ // Note that we explicitly avoid using carv1.HeaderSize to determine the current header size.
+ // This is based on the fact that carv1.ReadHeader does not read any extra bytes.
+ // Therefore, we can avoid extra allocations of carv1.HeaderSize to determine size by simply
+ // counting the bytes read so far.
+ currentSize, err = f.Seek(0, io.SeekCurrent)
+ if err != nil {
+ return err
+ }
+ case 2:
+ // When the given file is a CARv2 :
+ // 1. The offset at which the new header should be written is carv2.Header.DataOffset
+ // 2. The inner CARv1 header size is equal to the number of bytes read minus carv2.Header.DataOffset
+ var v2h Header
+ if _, err = v2h.ReadFrom(f); err != nil {
+ return err
+ }
+ newHeaderOffset = int64(v2h.DataOffset)
+ if _, err = f.Seek(newHeaderOffset, io.SeekStart); err != nil {
+ return err
+ }
+ var innerV1Header *carv1.CarHeader
+ innerV1Header, err = carv1.ReadHeader(f, options.MaxAllowedHeaderSize)
+ if err != nil {
+ return err
+ }
+ if innerV1Header.Version != 1 {
+ err = fmt.Errorf("invalid data payload header: expected version 1, got %d", innerV1Header.Version)
+ }
+ var readSoFar int64
+ readSoFar, err = f.Seek(0, io.SeekCurrent)
+ if err != nil {
+ return err
+ }
+ currentSize = readSoFar - newHeaderOffset
+ default:
+ err = fmt.Errorf("invalid car version: %d", header.Version)
+ return err
+ }
+
+ newHeader := &carv1.CarHeader{
+ Roots: roots,
+ Version: 1,
+ }
+ // Serialize the new header straight up instead of using carv1.HeaderSize.
+ // Because, carv1.HeaderSize serialises it to calculate size anyway.
+ // By serializing straight up we get the replacement bytes and size.
+ // Otherwise, we end up serializing the new header twice:
+ // once through carv1.HeaderSize, and
+ // once to write it out.
+ var buf bytes.Buffer
+ if err = carv1.WriteHeader(newHeader, &buf); err != nil {
+ return err
+ }
+ // Assert the header sizes are consistent.
+ newSize := int64(buf.Len())
+ if currentSize != newSize {
+ return fmt.Errorf("current header size (%d) must match replacement header size (%d)", currentSize, newSize)
+ }
+ // Seek to the offset at which the new header should be written.
+ if _, err = f.Seek(newHeaderOffset, io.SeekStart); err != nil {
+ return err
+ }
+ _, err = f.Write(buf.Bytes())
+ return err
+}
diff --git a/ipld/car/v2/writer_test.go b/ipld/car/v2/writer_test.go
new file mode 100644
index 0000000000..517a820dbb
--- /dev/null
+++ b/ipld/car/v2/writer_test.go
@@ -0,0 +1,282 @@
+package car
+
+import (
+ "context"
+ "io"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/ipfs/boxo/ipld/car/v2/index"
+ "github.com/ipfs/boxo/ipld/car/v2/internal/carv1"
+ "github.com/stretchr/testify/require"
+
+ "github.com/ipfs/boxo/ipld/merkledag"
+ dstest "github.com/ipfs/boxo/ipld/merkledag/test"
+ "github.com/ipfs/go-cid"
+ format "github.com/ipfs/go-ipld-format"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestWrapV1(t *testing.T) {
+ // Produce a CARv1 file to test wrapping with.
+ dagSvc := dstest.Mock()
+ src := filepath.Join(t.TempDir(), "unwrapped-test-v1.car")
+ sf, err := os.Create(src)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, sf.Close()) })
+ require.NoError(t, carv1.WriteCar(context.Background(), dagSvc, generateRootCid(t, dagSvc), sf))
+
+ // Wrap the test CARv1 file
+ dest := filepath.Join(t.TempDir(), "wrapped-test-v1.car")
+ df, err := os.Create(dest)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, df.Close()) })
+ _, err = sf.Seek(0, io.SeekStart)
+ require.NoError(t, err)
+ require.NoError(t, WrapV1(sf, df))
+
+ // Assert wrapped file is valid CARv2 with CARv1 data payload matching the original CARv1 file.
+ subject, err := OpenReader(dest)
+ t.Cleanup(func() { require.NoError(t, subject.Close()) })
+ require.NoError(t, err)
+
+ // Assert CARv1 data payloads are identical.
+ _, err = sf.Seek(0, io.SeekStart)
+ require.NoError(t, err)
+ wantPayload, err := io.ReadAll(sf)
+ require.NoError(t, err)
+ dr, err := subject.DataReader()
+ require.NoError(t, err)
+ gotPayload, err := io.ReadAll(dr)
+ require.NoError(t, err)
+ require.Equal(t, wantPayload, gotPayload)
+
+ // Assert embedded index in CARv2 is same as index generated from the original CARv1.
+ wantIdx, err := GenerateIndexFromFile(src)
+ require.NoError(t, err)
+ ir, err := subject.IndexReader()
+ require.NoError(t, err)
+ gotIdx, err := index.ReadFrom(ir)
+ require.NoError(t, err)
+ require.Equal(t, wantIdx, gotIdx)
+}
+
+func TestExtractV1(t *testing.T) {
+ // Produce a CARv1 file to test.
+ dagSvc := dstest.Mock()
+ v1Src := filepath.Join(t.TempDir(), "original-test-v1.car")
+ v1f, err := os.Create(v1Src)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, v1f.Close()) })
+ require.NoError(t, carv1.WriteCar(context.Background(), dagSvc, generateRootCid(t, dagSvc), v1f))
+ _, err = v1f.Seek(0, io.SeekStart)
+ require.NoError(t, err)
+ wantV1, err := io.ReadAll(v1f)
+ require.NoError(t, err)
+
+ // Wrap the produced CARv1 into a CARv2 to use for testing.
+ v2path := filepath.Join(t.TempDir(), "wrapped-for-extract-test-v2.car")
+ require.NoError(t, WrapV1File(v1Src, v2path))
+
+ // Assert extract from CARv2 file is as expected.
+ dstPath := filepath.Join(t.TempDir(), "extract-file-test-v1.car")
+ require.NoError(t, ExtractV1File(v2path, dstPath))
+ gotFromFile, err := os.ReadFile(dstPath)
+ require.NoError(t, err)
+ require.Equal(t, wantV1, gotFromFile)
+
+ // Assert extract from CARv2 file in-place is as expected
+ require.NoError(t, ExtractV1File(v2path, v2path))
+ gotFromInPlaceFile, err := os.ReadFile(v2path)
+ require.NoError(t, err)
+ require.Equal(t, wantV1, gotFromInPlaceFile)
+}
+
+func TestExtractV1WithUnknownVersionIsError(t *testing.T) {
+ dstPath := filepath.Join(t.TempDir(), "extract-dst-file-test-v42.car")
+ err := ExtractV1File("testdata/sample-rootless-v42.car", dstPath)
+ require.EqualError(t, err, "source version must be 2; got: 42")
+}
+
+func TestExtractV1FromACarV1IsError(t *testing.T) {
+ dstPath := filepath.Join(t.TempDir(), "extract-dst-file-test-v1.car")
+ err := ExtractV1File("testdata/sample-v1.car", dstPath)
+ require.Equal(t, ErrAlreadyV1, err)
+}
+
+func generateRootCid(t *testing.T, adder format.NodeAdder) []cid.Cid {
+ // TODO convert this into a utility testing lib that takes an rng and generates a random DAG with some threshold for depth/breadth.
+ this := merkledag.NewRawNode([]byte("fish"))
+ that := merkledag.NewRawNode([]byte("lobster"))
+ other := merkledag.NewRawNode([]byte("🌊"))
+
+ one := &merkledag.ProtoNode{}
+ assertAddNodeLink(t, one, this, "fishmonger")
+
+ another := &merkledag.ProtoNode{}
+ assertAddNodeLink(t, another, one, "barreleye")
+ assertAddNodeLink(t, another, that, "🐡")
+
+ andAnother := &merkledag.ProtoNode{}
+ assertAddNodeLink(t, andAnother, another, "🍤")
+
+ assertAddNodes(t, adder, this, that, other, one, another, andAnother)
+ return []cid.Cid{andAnother.Cid()}
+}
+
+func assertAddNodeLink(t *testing.T, pn *merkledag.ProtoNode, fn format.Node, name string) {
+ assert.NoError(t, pn.AddNodeLink(name, fn))
+}
+
+func assertAddNodes(t *testing.T, adder format.NodeAdder, nds ...format.Node) {
+ for _, nd := range nds {
+ assert.NoError(t, adder.Add(context.Background(), nd))
+ }
+}
+
+func TestReplaceRootsInFile(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ roots []cid.Cid
+ wantErrMsg string
+ }{
+ {
+ name: "CorruptPragmaIsRejected",
+ path: "testdata/sample-corrupt-pragma.car",
+ wantErrMsg: "unexpected EOF",
+ },
+ {
+ name: "CARv42IsRejected",
+ path: "testdata/sample-rootless-v42.car",
+ wantErrMsg: "invalid car version: 42",
+ },
+ {
+ name: "CARv1RootsOfDifferentSizeAreNotReplaced",
+ path: "testdata/sample-v1.car",
+ wantErrMsg: "current header size (61) must match replacement header size (18)",
+ },
+ {
+ name: "CARv2RootsOfDifferentSizeAreNotReplaced",
+ path: "testdata/sample-wrapped-v2.car",
+ wantErrMsg: "current header size (61) must match replacement header size (18)",
+ },
+ {
+ name: "CARv1NonEmptyRootsOfDifferentSizeAreNotReplaced",
+ path: "testdata/sample-v1.car",
+ roots: []cid.Cid{requireDecodedCid(t, "QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n")},
+ wantErrMsg: "current header size (61) must match replacement header size (57)",
+ },
+ {
+ name: "CARv1ZeroLenNonEmptyRootsOfDifferentSizeAreNotReplaced",
+ path: "testdata/sample-v1-with-zero-len-section.car",
+ roots: []cid.Cid{merkledag.NewRawNode([]byte("fish")).Cid()},
+ wantErrMsg: "current header size (61) must match replacement header size (59)",
+ },
+ {
+ name: "CARv2NonEmptyRootsOfDifferentSizeAreNotReplaced",
+ path: "testdata/sample-wrapped-v2.car",
+ roots: []cid.Cid{merkledag.NewRawNode([]byte("fish")).Cid()},
+ wantErrMsg: "current header size (61) must match replacement header size (59)",
+ },
+ {
+ name: "CARv2IndexlessNonEmptyRootsOfDifferentSizeAreNotReplaced",
+ path: "testdata/sample-v2-indexless.car",
+ roots: []cid.Cid{merkledag.NewRawNode([]byte("fish")).Cid()},
+ wantErrMsg: "current header size (61) must match replacement header size (59)",
+ },
+ {
+ name: "CARv1SameSizeRootsAreReplaced",
+ path: "testdata/sample-v1.car",
+ roots: []cid.Cid{requireDecodedCid(t, "bafy2bzaced4ueelaegfs5fqu4tzsh6ywbbpfk3cxppupmxfdhbpbhzawfw5od")},
+ },
+ {
+ name: "CARv2SameSizeRootsAreReplaced",
+ path: "testdata/sample-wrapped-v2.car",
+ roots: []cid.Cid{requireDecodedCid(t, "bafy2bzaced4ueelaegfs5fqu4tzsh6ywbbpfk3cxppupmxfdhbpbhzawfw5oi")},
+ },
+ {
+ name: "CARv2IndexlessSameSizeRootsAreReplaced",
+ path: "testdata/sample-v2-indexless.car",
+ roots: []cid.Cid{requireDecodedCid(t, "bafy2bzaced4ueelaegfs5fqu4tzsh6ywbbpfk3cxppupmxfdhbpbhzawfw5oi")},
+ },
+ {
+ name: "CARv1ZeroLenSameSizeRootsAreReplaced",
+ path: "testdata/sample-v1-with-zero-len-section.car",
+ roots: []cid.Cid{requireDecodedCid(t, "bafy2bzaced4ueelaegfs5fqu4tzsh6ywbbpfk3cxppupmxfdhbpbhzawfw5o5")},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Make a copy of input files to preserve original for comparison.
+ // This also avoids modification files in testdata.
+ tmpCopy := requireTmpCopy(t, tt.path)
+ err := ReplaceRootsInFile(tmpCopy, tt.roots)
+ if tt.wantErrMsg != "" {
+ require.EqualError(t, err, tt.wantErrMsg)
+ return
+ }
+ require.NoError(t, err)
+
+ original, err := os.Open(tt.path)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, original.Close()) }()
+
+ target, err := os.Open(tmpCopy)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, target.Close()) }()
+
+ // Assert file size has not changed.
+ wantStat, err := original.Stat()
+ require.NoError(t, err)
+ gotStat, err := target.Stat()
+ require.NoError(t, err)
+ require.Equal(t, wantStat.Size(), gotStat.Size())
+
+ wantReader, err := NewBlockReader(original, ZeroLengthSectionAsEOF(true))
+ require.NoError(t, err)
+ gotReader, err := NewBlockReader(target, ZeroLengthSectionAsEOF(true))
+ require.NoError(t, err)
+
+ // Assert roots are replaced.
+ require.Equal(t, tt.roots, gotReader.Roots)
+
+ // Assert data blocks are identical.
+ for {
+ wantNext, wantErr := wantReader.Next()
+ gotNext, gotErr := gotReader.Next()
+ if wantErr == io.EOF {
+ require.Equal(t, io.EOF, gotErr)
+ break
+ }
+ require.NoError(t, wantErr)
+ require.NoError(t, gotErr)
+ require.Equal(t, wantNext, gotNext)
+ }
+ })
+ }
+}
+
+func requireDecodedCid(t *testing.T, s string) cid.Cid {
+ decoded, err := cid.Decode(s)
+ require.NoError(t, err)
+ return decoded
+}
+
+func requireTmpCopy(t *testing.T, src string) string {
+ srcF, err := os.Open(src)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, srcF.Close()) }()
+ stats, err := srcF.Stat()
+ require.NoError(t, err)
+
+ dst := filepath.Join(t.TempDir(), stats.Name())
+ dstF, err := os.Create(dst)
+ require.NoError(t, err)
+ defer func() { require.NoError(t, dstF.Close()) }()
+
+ _, err = io.Copy(dstF, srcF)
+ require.NoError(t, err)
+ return dst
+}
diff --git a/ipld/merkledag/coding.go b/ipld/merkledag/coding.go
new file mode 100644
index 0000000000..e88f2dd325
--- /dev/null
+++ b/ipld/merkledag/coding.go
@@ -0,0 +1,218 @@
+package merkledag
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ cid "github.com/ipfs/go-cid"
+ format "github.com/ipfs/go-ipld-format"
+ pb "github.com/ipfs/boxo/ipld/merkledag/pb"
+ dagpb "github.com/ipld/go-codec-dagpb"
+ ipld "github.com/ipld/go-ipld-prime"
+ "github.com/ipld/go-ipld-prime/fluent/qp"
+ cidlink "github.com/ipld/go-ipld-prime/linking/cid"
+)
+
+// Make sure the user doesn't upgrade this file.
+// We need to check *here* as well as inside the `pb` package *just* in case the
+// user replaces *all* go files in that package.
+const _ = pb.DoNotUpgradeFileEverItWillChangeYourHashes
+
+// for now, we use a PBNode intermediate thing.
+// because native go objects are nice.
+
+// pbLinkSlice is a slice of pb.PBLink, similar to LinkSlice but for sorting the
+// PB form
+type pbLinkSlice []*pb.PBLink
+
+func (pbls pbLinkSlice) Len() int { return len(pbls) }
+func (pbls pbLinkSlice) Swap(a, b int) { pbls[a], pbls[b] = pbls[b], pbls[a] }
+func (pbls pbLinkSlice) Less(a, b int) bool { return *pbls[a].Name < *pbls[b].Name }
+
+// unmarshal decodes raw data into a *Node instance.
+// The conversion uses an intermediate PBNode.
+func unmarshal(encodedBytes []byte) (*ProtoNode, error) {
+ nb := dagpb.Type.PBNode.NewBuilder()
+ if err := dagpb.DecodeBytes(nb, encodedBytes); err != nil {
+ return nil, err
+ }
+ nd := nb.Build()
+ return fromImmutableNode(&immutableProtoNode{encodedBytes, nd.(dagpb.PBNode)}), nil
+}
+
+func fromImmutableNode(encoded *immutableProtoNode) *ProtoNode {
+ n := new(ProtoNode)
+ n.encoded = encoded
+ if n.encoded.PBNode.Data.Exists() {
+ n.data = n.encoded.PBNode.Data.Must().Bytes()
+ }
+ numLinks := n.encoded.PBNode.Links.Length()
+ // links may not be sorted after deserialization, but we don't change
+ // them until we mutate this node since we're representing the current,
+ // as-serialized state
+ n.links = make([]*format.Link, numLinks)
+ linkAllocs := make([]format.Link, numLinks)
+ for i := int64(0); i < numLinks; i++ {
+ next := n.encoded.PBNode.Links.Lookup(i)
+ name := ""
+ if next.FieldName().Exists() {
+ name = next.FieldName().Must().String()
+ }
+ c := next.FieldHash().Link().(cidlink.Link).Cid
+ size := uint64(0)
+ if next.FieldTsize().Exists() {
+ size = uint64(next.FieldTsize().Must().Int())
+ }
+ link := &linkAllocs[i]
+ link.Name = name
+ link.Size = size
+ link.Cid = c
+ n.links[i] = link
+ }
+ // we don't set n.linksDirty because the order of the links list from
+ // serialized form needs to be stable, until we start mutating the ProtoNode
+ return n
+}
+func (n *ProtoNode) marshalImmutable() (*immutableProtoNode, error) {
+ links := n.Links()
+ nd, err := qp.BuildMap(dagpb.Type.PBNode, 2, func(ma ipld.MapAssembler) {
+ qp.MapEntry(ma, "Links", qp.List(int64(len(links)), func(la ipld.ListAssembler) {
+ for _, link := range links {
+ // it shouldn't be possible to get here with an undefined CID, but in
+ // case it is we're going to drop this link from the encoded form
+ // entirely
+ if link.Cid.Defined() {
+ qp.ListEntry(la, qp.Map(3, func(ma ipld.MapAssembler) {
+ qp.MapEntry(ma, "Hash", qp.Link(cidlink.Link{Cid: link.Cid}))
+ qp.MapEntry(ma, "Name", qp.String(link.Name))
+ sz := int64(link.Size)
+ if sz < 0 { // overflow, >MaxInt64 is almost certainly an error
+ sz = 0
+ }
+ qp.MapEntry(ma, "Tsize", qp.Int(sz))
+ }))
+ }
+ }
+ }))
+ if n.data != nil {
+ qp.MapEntry(ma, "Data", qp.Bytes(n.data))
+ }
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // 1KiB can be allocated on the stack, and covers most small nodes
+ // without having to grow the buffer and cause allocations.
+ enc := make([]byte, 0, 1024)
+
+ enc, err = dagpb.AppendEncode(enc, nd)
+ if err != nil {
+ return nil, err
+ }
+ return &immutableProtoNode{enc, nd.(dagpb.PBNode)}, nil
+}
+
+// Marshal encodes a *Node instance into a new byte slice.
+// The conversion uses an intermediate PBNode.
+func (n *ProtoNode) Marshal() ([]byte, error) {
+ enc, err := n.marshalImmutable()
+ if err != nil {
+ return nil, err
+ }
+ return enc.encoded, nil
+}
+
+// GetPBNode converts *ProtoNode into it's protocol buffer variant.
+// If you plan on mutating the data of the original node, it is recommended
+// that you call ProtoNode.Copy() before calling ProtoNode.GetPBNode()
+func (n *ProtoNode) GetPBNode() *pb.PBNode {
+ pbn := &pb.PBNode{}
+ if len(n.links) > 0 {
+ pbn.Links = make([]*pb.PBLink, len(n.links))
+ }
+
+ for i, l := range n.links {
+ pbn.Links[i] = &pb.PBLink{}
+ pbn.Links[i].Name = &l.Name
+ pbn.Links[i].Tsize = &l.Size
+ if l.Cid.Defined() {
+ pbn.Links[i].Hash = l.Cid.Bytes()
+ }
+ }
+
+ // Ensure links are sorted prior to encode, regardless of `linksDirty`. They
+ // may not have come sorted if we deserialized a badly encoded form that
+ // didn't have links already sorted.
+ sort.Stable(pbLinkSlice(pbn.Links))
+
+ if len(n.data) > 0 {
+ pbn.Data = n.data
+ }
+ return pbn
+}
+
+// EncodeProtobuf returns the encoded raw data version of a Node instance.
+// It may use a cached encoded version, unless the force flag is given.
+func (n *ProtoNode) EncodeProtobuf(force bool) ([]byte, error) {
+ if n.encoded == nil || n.linksDirty || force {
+ if n.linksDirty {
+ // there was a mutation involving links, make sure we sort before we build
+ // and cache a `Node` form that captures the current state
+ sort.Stable(LinkSlice(n.links))
+ n.linksDirty = false
+ }
+ n.cached = cid.Undef
+ var err error
+ n.encoded, err = n.marshalImmutable()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if !n.cached.Defined() {
+ c, err := n.CidBuilder().Sum(n.encoded.encoded)
+ if err != nil {
+ return nil, err
+ }
+
+ n.cached = c
+ }
+
+ return n.encoded.encoded, nil
+}
+
+// DecodeProtobuf decodes raw data and returns a new Node instance.
+func DecodeProtobuf(encoded []byte) (*ProtoNode, error) {
+ n, err := unmarshal(encoded)
+ if err != nil {
+ return nil, fmt.Errorf("incorrectly formatted merkledag node: %s", err)
+ }
+ return n, nil
+}
+
+// DecodeProtobufBlock is a block decoder for protobuf IPLD nodes conforming to
+// node.DecodeBlockFunc
+func DecodeProtobufBlock(b blocks.Block) (format.Node, error) {
+ c := b.Cid()
+ if c.Type() != cid.DagProtobuf {
+ return nil, fmt.Errorf("this function can only decode protobuf nodes")
+ }
+
+ decnd, err := DecodeProtobuf(b.RawData())
+ if err != nil {
+ if strings.Contains(err.Error(), "Unmarshal failed") {
+ return nil, fmt.Errorf("the block referred to by '%s' was not a valid merkledag node", c)
+ }
+ return nil, fmt.Errorf("failed to decode Protocol Buffers: %v", err)
+ }
+
+ decnd.cached = c
+ decnd.builder = c.Prefix()
+ return decnd, nil
+}
+
+// Type assertion
+var _ format.DecodeBlockFunc = DecodeProtobufBlock
diff --git a/ipld/merkledag/coding_test.go b/ipld/merkledag/coding_test.go
new file mode 100644
index 0000000000..7e94990566
--- /dev/null
+++ b/ipld/merkledag/coding_test.go
@@ -0,0 +1,52 @@
+package merkledag_test
+
+import (
+ "bytes"
+ "fmt"
+ "testing"
+
+ cid "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+ "github.com/ipfs/boxo/ipld/merkledag"
+)
+
+var benchInput []byte
+
+func init() {
+ someData := bytes.Repeat([]byte("some plaintext data\n"), 10)
+ // make a test CID -- doesn't matter just to add as a link
+ someCid, _ := cid.Cast([]byte{1, 85, 0, 5, 0, 1, 2, 3, 4})
+
+ node := &merkledag.ProtoNode{}
+ node.SetData(someData)
+ for i := 0; i < 10; i++ {
+ node.AddRawLink(fmt.Sprintf("%d", i), &ipld.Link{
+ Size: 10,
+ Cid: someCid,
+ })
+ }
+
+ enc, err := node.EncodeProtobuf(true)
+ if err != nil {
+ panic(err)
+ }
+ benchInput = enc
+}
+
+func BenchmarkRoundtrip(b *testing.B) {
+ b.ReportAllocs()
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ node, err := merkledag.DecodeProtobuf(benchInput)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ enc, err := node.EncodeProtobuf(true)
+ if err != nil {
+ b.Fatal(err)
+ }
+ _ = enc
+ }
+ })
+}
diff --git a/ipld/merkledag/dagutils/diff.go b/ipld/merkledag/dagutils/diff.go
new file mode 100644
index 0000000000..49a3ae627b
--- /dev/null
+++ b/ipld/merkledag/dagutils/diff.go
@@ -0,0 +1,204 @@
+package dagutils
+
+import (
+ "context"
+ "fmt"
+ "path"
+
+ "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+)
+
+// ChangeType denotes type of change in Change
+type ChangeType int
+
+// These constants define the changes that can be applied to a DAG.
+const (
+ Add ChangeType = iota
+ Remove
+ Mod
+)
+
+// Change represents a change to a DAG and contains a reference to the old and
+// new CIDs.
+type Change struct {
+ Type ChangeType
+ Path string
+ Before cid.Cid
+ After cid.Cid
+}
+
+// String prints a human-friendly line about a change.
+func (c *Change) String() string {
+ switch c.Type {
+ case Add:
+ return fmt.Sprintf("Added %s at %s", c.After.String(), c.Path)
+ case Remove:
+ return fmt.Sprintf("Removed %s from %s", c.Before.String(), c.Path)
+ case Mod:
+ return fmt.Sprintf("Changed %s to %s at %s", c.Before.String(), c.After.String(), c.Path)
+ default:
+ panic("nope")
+ }
+}
+
+// ApplyChange applies the requested changes to the given node in the given dag.
+func ApplyChange(ctx context.Context, ds ipld.DAGService, nd *dag.ProtoNode, cs []*Change) (*dag.ProtoNode, error) {
+ e := NewDagEditor(nd, ds)
+ for _, c := range cs {
+ switch c.Type {
+ case Add:
+ child, err := ds.Get(ctx, c.After)
+ if err != nil {
+ return nil, err
+ }
+
+ childpb, ok := child.(*dag.ProtoNode)
+ if !ok {
+ return nil, dag.ErrNotProtobuf
+ }
+
+ err = e.InsertNodeAtPath(ctx, c.Path, childpb, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ case Remove:
+ err := e.RmLink(ctx, c.Path)
+ if err != nil {
+ return nil, err
+ }
+
+ case Mod:
+ err := e.RmLink(ctx, c.Path)
+ if err != nil {
+ return nil, err
+ }
+ child, err := ds.Get(ctx, c.After)
+ if err != nil {
+ return nil, err
+ }
+
+ childpb, ok := child.(*dag.ProtoNode)
+ if !ok {
+ return nil, dag.ErrNotProtobuf
+ }
+
+ err = e.InsertNodeAtPath(ctx, c.Path, childpb, nil)
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ return e.Finalize(ctx, ds)
+}
+
+// Diff returns a set of changes that transform node 'a' into node 'b'.
+// It only traverses links in the following cases:
+// 1. two node's links number are greater than 0.
+// 2. both of two nodes are ProtoNode.
+// Otherwise, it compares the cid and emits a Mod change object.
+func Diff(ctx context.Context, ds ipld.DAGService, a, b ipld.Node) ([]*Change, error) {
+ if a.Cid() == b.Cid() {
+ return []*Change{}, nil
+ }
+
+ cleanA, okA := a.Copy().(*dag.ProtoNode)
+ cleanB, okB := b.Copy().(*dag.ProtoNode)
+
+ linksA := a.Links()
+ linksB := b.Links()
+
+ if !okA || !okB || (len(linksA) == 0 && len(linksB) == 0) {
+ return []*Change{{Type: Mod, Before: a.Cid(), After: b.Cid()}}, nil
+ }
+
+ var out []*Change
+ for _, linkA := range linksA {
+ linkB, _, err := b.ResolveLink([]string{linkA.Name})
+ if err != nil {
+ continue
+ }
+
+ cleanA.RemoveNodeLink(linkA.Name)
+ cleanB.RemoveNodeLink(linkA.Name)
+
+ if linkA.Cid == linkB.Cid {
+ continue
+ }
+
+ nodeA, err := linkA.GetNode(ctx, ds)
+ if err != nil {
+ return nil, err
+ }
+
+ nodeB, err := linkB.GetNode(ctx, ds)
+ if err != nil {
+ return nil, err
+ }
+
+ sub, err := Diff(ctx, ds, nodeA, nodeB)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, c := range sub {
+ c.Path = path.Join(linkA.Name, c.Path)
+ }
+
+ out = append(out, sub...)
+ }
+
+ for _, l := range cleanA.Links() {
+ out = append(out, &Change{Type: Remove, Path: l.Name, Before: l.Cid})
+ }
+
+ for _, l := range cleanB.Links() {
+ out = append(out, &Change{Type: Add, Path: l.Name, After: l.Cid})
+ }
+
+ return out, nil
+}
+
+// Conflict represents two incompatible changes and is returned by MergeDiffs().
+type Conflict struct {
+ A *Change
+ B *Change
+}
+
+// MergeDiffs takes two slice of changes and adds them to a single slice.
+// When a Change from b happens to the same path of an existing change in a,
+// a conflict is created and b is not added to the merged slice.
+// A slice of Conflicts is returned and contains pointers to the
+// Changes involved (which share the same path).
+func MergeDiffs(a, b []*Change) ([]*Change, []Conflict) {
+ paths := make(map[string]*Change)
+ for _, c := range b {
+ paths[c.Path] = c
+ }
+
+ var changes []*Change
+ var conflicts []Conflict
+
+ // NOTE: we avoid iterating over maps here to ensure iteration order is determistic. We
+ // include changes from a first, then b.
+ for _, changeA := range a {
+ if changeB, ok := paths[changeA.Path]; ok {
+ conflicts = append(conflicts, Conflict{changeA, changeB})
+ } else {
+ changes = append(changes, changeA)
+ }
+ delete(paths, changeA.Path)
+ }
+
+ for _, c := range b {
+ if _, ok := paths[c.Path]; ok {
+ changes = append(changes, c)
+ }
+ }
+
+ return changes, conflicts
+}
diff --git a/ipld/merkledag/dagutils/diff_test.go b/ipld/merkledag/dagutils/diff_test.go
new file mode 100644
index 0000000000..692e8cc704
--- /dev/null
+++ b/ipld/merkledag/dagutils/diff_test.go
@@ -0,0 +1,127 @@
+package dagutils
+
+import (
+ "context"
+ "testing"
+
+ cid "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+ mdtest "github.com/ipfs/boxo/ipld/merkledag/test"
+)
+
+func TestMergeDiffs(t *testing.T) {
+ node1 := dag.NodeWithData([]byte("one"))
+ node2 := dag.NodeWithData([]byte("two"))
+ node3 := dag.NodeWithData([]byte("three"))
+ node4 := dag.NodeWithData([]byte("four"))
+
+ changesA := []*Change{
+ {Add, "one", cid.Cid{}, node1.Cid()},
+ {Remove, "two", node2.Cid(), cid.Cid{}},
+ {Mod, "three", node3.Cid(), node4.Cid()},
+ }
+
+ changesB := []*Change{
+ {Mod, "two", node2.Cid(), node3.Cid()},
+ {Add, "four", cid.Cid{}, node4.Cid()},
+ }
+
+ changes, conflicts := MergeDiffs(changesA, changesB)
+ if len(changes) != 3 {
+ t.Fatal("unexpected merge changes")
+ }
+
+ expect := []*Change{
+ changesA[0],
+ changesA[2],
+ changesB[1],
+ }
+
+ for i, change := range changes {
+ if change.Type != expect[i].Type {
+ t.Error("unexpected diff change type")
+ }
+
+ if change.Path != expect[i].Path {
+ t.Error("unexpected diff change path")
+ }
+
+ if change.Before != expect[i].Before {
+ t.Error("unexpected diff change before")
+ }
+
+ if change.After != expect[i].After {
+ t.Error("unexpected diff change before")
+ }
+ }
+
+ if len(conflicts) != 1 {
+ t.Fatal("unexpected merge conflicts")
+ }
+
+ if conflicts[0].A != changesA[1] {
+ t.Error("unexpected merge conflict a")
+ }
+
+ if conflicts[0].B != changesB[0] {
+ t.Error("unexpected merge conflict b")
+ }
+}
+
+func TestDiff(t *testing.T) {
+ ctx := context.Background()
+ ds := mdtest.Mock()
+
+ rootA := &dag.ProtoNode{}
+ rootB := &dag.ProtoNode{}
+
+ child1 := dag.NodeWithData([]byte("one"))
+ child2 := dag.NodeWithData([]byte("two"))
+ child3 := dag.NodeWithData([]byte("three"))
+ child4 := dag.NodeWithData([]byte("four"))
+
+ rootA.AddNodeLink("one", child1)
+ rootA.AddNodeLink("two", child2)
+
+ rootB.AddNodeLink("one", child3)
+ rootB.AddNodeLink("four", child4)
+
+ nodes := []ipld.Node{child1, child2, child3, child4, rootA, rootB}
+ if err := ds.AddMany(ctx, nodes); err != nil {
+ t.Fatal("failed to add nodes")
+ }
+
+ changes, err := Diff(ctx, ds, rootA, rootB)
+ if err != nil {
+ t.Fatal("unexpected diff error")
+ }
+
+ if len(changes) != 3 {
+ t.Fatal("unexpected diff changes")
+ }
+
+ expect := []Change{
+ {Mod, "one", child1.Cid(), child3.Cid()},
+ {Remove, "two", child2.Cid(), cid.Cid{}},
+ {Add, "four", cid.Cid{}, child4.Cid()},
+ }
+
+ for i, change := range changes {
+ if change.Type != expect[i].Type {
+ t.Error("unexpected diff change type")
+ }
+
+ if change.Path != expect[i].Path {
+ t.Error("unexpected diff change path")
+ }
+
+ if change.Before != expect[i].Before {
+ t.Error("unexpected diff change before")
+ }
+
+ if change.After != expect[i].After {
+ t.Error("unexpected diff change before")
+ }
+ }
+}
diff --git a/ipld/merkledag/dagutils/diffenum.go b/ipld/merkledag/dagutils/diffenum.go
new file mode 100644
index 0000000000..75f86b952d
--- /dev/null
+++ b/ipld/merkledag/dagutils/diffenum.go
@@ -0,0 +1,99 @@
+package dagutils
+
+import (
+ "context"
+ "fmt"
+
+ cid "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+
+ mdag "github.com/ipfs/boxo/ipld/merkledag"
+)
+
+// DiffEnumerate fetches every object in the graph pointed to by 'to' that is
+// not in 'from'. This can be used to more efficiently fetch a graph if you can
+// guarantee you already have the entirety of 'from'
+func DiffEnumerate(ctx context.Context, dserv ipld.NodeGetter, from, to cid.Cid) error {
+ fnd, err := dserv.Get(ctx, from)
+ if err != nil {
+ return fmt.Errorf("get %s: %s", from, err)
+ }
+
+ tnd, err := dserv.Get(ctx, to)
+ if err != nil {
+ return fmt.Errorf("get %s: %s", to, err)
+ }
+
+ diff := getLinkDiff(fnd, tnd)
+
+ sset := cid.NewSet()
+ for _, c := range diff {
+ // Since we're already assuming we have everything in the 'from' graph,
+ // add all those cids to our 'already seen' set to avoid potentially
+ // enumerating them later
+ if c.bef.Defined() {
+ sset.Add(c.bef)
+ }
+ }
+ for _, c := range diff {
+ if !c.bef.Defined() {
+ if sset.Has(c.aft) {
+ continue
+ }
+ err := mdag.Walk(ctx, mdag.GetLinksDirect(dserv), c.aft, sset.Visit, mdag.Concurrent())
+ if err != nil {
+ return err
+ }
+ } else {
+ err := DiffEnumerate(ctx, dserv, c.bef, c.aft)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+// if both bef and aft are not nil, then that signifies bef was replaces with aft.
+// if bef is nil and aft is not, that means aft was newly added
+// if aft is nil and bef is not, that means bef was deleted
+type diffpair struct {
+ bef, aft cid.Cid
+}
+
+// getLinkDiff returns a changeset between nodes 'a' and 'b'. Currently does
+// not log deletions as our usecase doesnt call for this.
+func getLinkDiff(a, b ipld.Node) []diffpair {
+ ina := make(map[string]*ipld.Link)
+ inb := make(map[string]*ipld.Link)
+ var aonly []cid.Cid
+ for _, l := range b.Links() {
+ inb[l.Cid.KeyString()] = l
+ }
+ for _, l := range a.Links() {
+ var key = l.Cid.KeyString()
+ ina[key] = l
+ if inb[key] == nil {
+ aonly = append(aonly, l.Cid)
+ }
+ }
+
+ var out []diffpair
+ var aindex int
+
+ for _, l := range b.Links() {
+ if ina[l.Cid.KeyString()] != nil {
+ continue
+ }
+
+ if aindex < len(aonly) {
+ out = append(out, diffpair{bef: aonly[aindex], aft: l.Cid})
+ aindex++
+ } else {
+ out = append(out, diffpair{aft: l.Cid})
+ continue
+ }
+ }
+ return out
+}
diff --git a/ipld/merkledag/dagutils/diffenum_test.go b/ipld/merkledag/dagutils/diffenum_test.go
new file mode 100644
index 0000000000..b96e6f759b
--- /dev/null
+++ b/ipld/merkledag/dagutils/diffenum_test.go
@@ -0,0 +1,249 @@
+package dagutils
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+ mdtest "github.com/ipfs/boxo/ipld/merkledag/test"
+)
+
+func buildNode(name string, desc map[string]ndesc, out map[string]ipld.Node) ipld.Node {
+ this := desc[name]
+ nd := new(dag.ProtoNode)
+ nd.SetData([]byte(name))
+ for k, v := range this {
+ child, ok := out[v]
+ if !ok {
+ child = buildNode(v, desc, out)
+ out[v] = child
+ }
+
+ if err := nd.AddNodeLink(k, child); err != nil {
+ panic(err)
+ }
+ }
+
+ return nd
+}
+
+type ndesc map[string]string
+
+func mkGraph(desc map[string]ndesc) map[string]ipld.Node {
+ out := make(map[string]ipld.Node)
+ for name := range desc {
+ if _, ok := out[name]; ok {
+ continue
+ }
+
+ out[name] = buildNode(name, desc, out)
+ }
+ return out
+}
+
+var tg1 = map[string]ndesc{
+ "a1": {
+ "foo": "b",
+ },
+ "b": {},
+ "a2": {
+ "foo": "b",
+ "bar": "c",
+ },
+ "c": {},
+}
+
+var tg2 = map[string]ndesc{
+ "a1": {
+ "foo": "b",
+ },
+ "b": {},
+ "a2": {
+ "foo": "b",
+ "bar": "c",
+ },
+ "c": {"baz": "d"},
+ "d": {},
+}
+
+var tg3 = map[string]ndesc{
+ "a1": {
+ "foo": "b",
+ "bar": "c",
+ },
+ "b": {},
+ "a2": {
+ "foo": "b",
+ "bar": "d",
+ },
+ "c": {},
+ "d": {},
+}
+
+var tg4 = map[string]ndesc{
+ "a1": {
+ "key1": "b",
+ "key2": "c",
+ },
+ "a2": {
+ "key1": "b",
+ "key2": "d",
+ },
+}
+
+var tg5 = map[string]ndesc{
+ "a1": {
+ "key1": "a",
+ "key2": "b",
+ },
+ "a2": {
+ "key1": "c",
+ "key2": "d",
+ },
+}
+
+func TestNameMatching(t *testing.T) {
+ nds := mkGraph(tg4)
+
+ diff := getLinkDiff(nds["a1"], nds["a2"])
+ if len(diff) != 1 {
+ t.Fatal(fmt.Errorf("node diff didn't match by name"))
+ }
+}
+
+func TestNameMatching2(t *testing.T) {
+ nds := mkGraph(tg5)
+
+ diff := getLinkDiff(nds["a1"], nds["a2"])
+ if len(diff) != 2 {
+ t.Fatal(fmt.Errorf("incorrect number of link diff elements"))
+ }
+ if !(diff[0].bef.Equals(nds["a1"].Links()[0].Cid) && diff[0].aft.Equals(nds["a2"].Links()[0].Cid)) {
+ t.Fatal(fmt.Errorf("node diff didn't match by name"))
+ }
+}
+
+func TestDiffEnumBasic(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ nds := mkGraph(tg1)
+
+ ds := mdtest.Mock()
+ lgds := &getLogger{ds: ds}
+
+ for _, nd := range nds {
+ err := ds.Add(ctx, nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ err := DiffEnumerate(ctx, lgds, nds["a1"].Cid(), nds["a2"].Cid())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = assertCidList(lgds.log, []cid.Cid{nds["a1"].Cid(), nds["a2"].Cid(), nds["c"].Cid()})
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+type getLogger struct {
+ ds ipld.NodeGetter
+ log []cid.Cid
+}
+
+func (gl *getLogger) Get(ctx context.Context, c cid.Cid) (ipld.Node, error) {
+ nd, err := gl.ds.Get(ctx, c)
+ if err != nil {
+ return nil, err
+ }
+ gl.log = append(gl.log, c)
+ return nd, nil
+}
+
+func (gl *getLogger) GetMany(ctx context.Context, cids []cid.Cid) <-chan *ipld.NodeOption {
+ outCh := make(chan *ipld.NodeOption, len(cids))
+ nds := gl.ds.GetMany(ctx, cids)
+ for no := range nds {
+ if no.Err == nil {
+ gl.log = append(gl.log, no.Node.Cid())
+ }
+ select {
+ case outCh <- no:
+ default:
+ panic("too many responses")
+ }
+ }
+ return nds
+}
+
+func assertCidList(a, b []cid.Cid) error {
+ if len(a) != len(b) {
+ return fmt.Errorf("got different number of cids than expected")
+ }
+ for i, c := range a {
+ if !c.Equals(b[i]) {
+ return fmt.Errorf("expected %s, got %s", c, b[i])
+ }
+ }
+ return nil
+}
+
+func TestDiffEnumFail(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ nds := mkGraph(tg2)
+
+ ds := mdtest.Mock()
+ lgds := &getLogger{ds: ds}
+
+ for _, s := range []string{"a1", "a2", "b", "c"} {
+ err := ds.Add(ctx, nds[s])
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ err := DiffEnumerate(ctx, lgds, nds["a1"].Cid(), nds["a2"].Cid())
+ if !ipld.IsNotFound(err) {
+ t.Fatal("expected err not found")
+ }
+
+ err = assertCidList(lgds.log, []cid.Cid{nds["a1"].Cid(), nds["a2"].Cid(), nds["c"].Cid()})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+}
+
+func TestDiffEnumRecurse(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ nds := mkGraph(tg3)
+
+ ds := mdtest.Mock()
+ lgds := &getLogger{ds: ds}
+
+ for _, s := range []string{"a1", "a2", "b", "c", "d"} {
+ err := ds.Add(ctx, nds[s])
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ err := DiffEnumerate(ctx, lgds, nds["a1"].Cid(), nds["a2"].Cid())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = assertCidList(lgds.log, []cid.Cid{nds["a1"].Cid(), nds["a2"].Cid(), nds["c"].Cid(), nds["d"].Cid()})
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/ipld/merkledag/dagutils/utils.go b/ipld/merkledag/dagutils/utils.go
new file mode 100644
index 0000000000..4f6500b25c
--- /dev/null
+++ b/ipld/merkledag/dagutils/utils.go
@@ -0,0 +1,234 @@
+package dagutils
+
+import (
+ "context"
+ "errors"
+ "strings"
+
+ bserv "github.com/ipfs/boxo/blockservice"
+ bstore "github.com/ipfs/boxo/blockstore"
+ offline "github.com/ipfs/boxo/exchange/offline"
+ ds "github.com/ipfs/go-datastore"
+ syncds "github.com/ipfs/go-datastore/sync"
+ ipld "github.com/ipfs/go-ipld-format"
+
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+)
+
+// Editor represents a ProtoNode tree editor and provides methods to
+// modify it.
+type Editor struct {
+ root *dag.ProtoNode
+
+ // tmp is a temporary in memory (for now) dagstore for all of the
+ // intermediary nodes to be stored in
+ tmp ipld.DAGService
+
+ // src is the dagstore with *all* of the data on it, it is used to pull
+ // nodes from for modification (nil is a valid value)
+ src ipld.DAGService
+}
+
+// NewMemoryDagService returns a new, thread-safe in-memory DAGService.
+func NewMemoryDagService() ipld.DAGService {
+ // build mem-datastore for editor's intermediary nodes
+ bs := bstore.NewBlockstore(syncds.MutexWrap(ds.NewMapDatastore()))
+ bsrv := bserv.New(bs, offline.Exchange(bs))
+ return dag.NewDAGService(bsrv)
+}
+
+// NewDagEditor returns an ProtoNode editor.
+//
+// * root is the node to be modified
+// * source is the dagstore to pull nodes from (optional)
+func NewDagEditor(root *dag.ProtoNode, source ipld.DAGService) *Editor {
+ return &Editor{
+ root: root,
+ tmp: NewMemoryDagService(),
+ src: source,
+ }
+}
+
+// GetNode returns the a copy of the root node being edited.
+func (e *Editor) GetNode() *dag.ProtoNode {
+ return e.root.Copy().(*dag.ProtoNode)
+}
+
+// GetDagService returns the DAGService used by this editor.
+func (e *Editor) GetDagService() ipld.DAGService {
+ return e.tmp
+}
+
+func addLink(ctx context.Context, ds ipld.DAGService, root *dag.ProtoNode, childname string, childnd ipld.Node) (*dag.ProtoNode, error) {
+ if childname == "" {
+ return nil, errors.New("cannot create link with no name")
+ }
+
+ // ensure that the node we are adding is in the dagservice
+ err := ds.Add(ctx, childnd)
+ if err != nil {
+ return nil, err
+ }
+
+ _ = ds.Remove(ctx, root.Cid())
+
+ // ensure no link with that name already exists
+ _ = root.RemoveNodeLink(childname) // ignore error, only option is ErrNotFound
+
+ if err := root.AddNodeLink(childname, childnd); err != nil {
+ return nil, err
+ }
+
+ if err := ds.Add(ctx, root); err != nil {
+ return nil, err
+ }
+ return root, nil
+}
+
+// InsertNodeAtPath inserts a new node in the tree and replaces the current root with the new one.
+func (e *Editor) InsertNodeAtPath(ctx context.Context, pth string, toinsert ipld.Node, create func() *dag.ProtoNode) error {
+ splpath := strings.Split(pth, "/")
+ nd, err := e.insertNodeAtPath(ctx, e.root, splpath, toinsert, create)
+ if err != nil {
+ return err
+ }
+ e.root = nd
+ return nil
+}
+
+func (e *Editor) insertNodeAtPath(ctx context.Context, root *dag.ProtoNode, path []string, toinsert ipld.Node, create func() *dag.ProtoNode) (*dag.ProtoNode, error) {
+ if len(path) == 1 {
+ return addLink(ctx, e.tmp, root, path[0], toinsert)
+ }
+
+ nd, err := root.GetLinkedProtoNode(ctx, e.tmp, path[0])
+ if err != nil {
+ // if 'create' is true, we create directories on the way down as needed
+ if err == dag.ErrLinkNotFound && create != nil {
+ nd = create()
+ err = nil // no longer an error case
+ } else if ipld.IsNotFound(err) {
+ // try finding it in our source dagstore
+ nd, err = root.GetLinkedProtoNode(ctx, e.src, path[0])
+ }
+
+ // if we receive an ErrNotFound, then our second 'GetLinkedNode' call
+ // also fails, we want to error out
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ ndprime, err := e.insertNodeAtPath(ctx, nd, path[1:], toinsert, create)
+ if err != nil {
+ return nil, err
+ }
+
+ _ = e.tmp.Remove(ctx, root.Cid())
+
+ _ = root.RemoveNodeLink(path[0])
+ err = root.AddNodeLink(path[0], ndprime)
+ if err != nil {
+ return nil, err
+ }
+
+ err = e.tmp.Add(ctx, root)
+ if err != nil {
+ return nil, err
+ }
+
+ return root, nil
+}
+
+// RmLink removes the link with the given name and updates the root node of
+// the editor.
+func (e *Editor) RmLink(ctx context.Context, pth string) error {
+ splpath := strings.Split(pth, "/")
+ nd, err := e.rmLink(ctx, e.root, splpath)
+ if err != nil {
+ return err
+ }
+ e.root = nd
+ return nil
+}
+
+func (e *Editor) rmLink(ctx context.Context, root *dag.ProtoNode, path []string) (*dag.ProtoNode, error) {
+ if len(path) == 1 {
+ // base case, remove node in question
+ err := root.RemoveNodeLink(path[0])
+ if err != nil {
+ return nil, err
+ }
+
+ err = e.tmp.Add(ctx, root)
+ if err != nil {
+ return nil, err
+ }
+
+ return root, nil
+ }
+
+ // search for node in both tmp dagstore and source dagstore
+ nd, err := root.GetLinkedProtoNode(ctx, e.tmp, path[0])
+ if ipld.IsNotFound(err) {
+ nd, err = root.GetLinkedProtoNode(ctx, e.src, path[0])
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ nnode, err := e.rmLink(ctx, nd, path[1:])
+ if err != nil {
+ return nil, err
+ }
+
+ _ = e.tmp.Remove(ctx, root.Cid())
+
+ _ = root.RemoveNodeLink(path[0])
+ err = root.AddNodeLink(path[0], nnode)
+ if err != nil {
+ return nil, err
+ }
+
+ err = e.tmp.Add(ctx, root)
+ if err != nil {
+ return nil, err
+ }
+
+ return root, nil
+}
+
+// Finalize writes the new DAG to the given DAGService and returns the modified
+// root node.
+func (e *Editor) Finalize(ctx context.Context, ds ipld.DAGService) (*dag.ProtoNode, error) {
+ nd := e.GetNode()
+ err := copyDag(ctx, nd, e.tmp, ds)
+ return nd, err
+}
+
+func copyDag(ctx context.Context, nd ipld.Node, from, to ipld.DAGService) error {
+ // TODO(#4609): make this batch.
+ err := to.Add(ctx, nd)
+ if err != nil {
+ return err
+ }
+
+ for _, lnk := range nd.Links() {
+ child, err := lnk.GetNode(ctx, from)
+ if err != nil {
+ if ipld.IsNotFound(err) {
+ // not found means we didnt modify it, and it should
+ // already be in the target datastore
+ continue
+ }
+ return err
+ }
+
+ err = copyDag(ctx, child, from, to)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/ipld/merkledag/dagutils/utils_test.go b/ipld/merkledag/dagutils/utils_test.go
new file mode 100644
index 0000000000..3fd3e823fe
--- /dev/null
+++ b/ipld/merkledag/dagutils/utils_test.go
@@ -0,0 +1,114 @@
+package dagutils
+
+import (
+ "context"
+ "strings"
+ "testing"
+
+ "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+ mdtest "github.com/ipfs/boxo/ipld/merkledag/test"
+)
+
+func TestAddLink(t *testing.T) {
+ ctx, context := context.WithCancel(context.Background())
+ defer context()
+
+ ds := mdtest.Mock()
+ fishnode := dag.NodeWithData([]byte("fishcakes!"))
+
+ err := ds.Add(ctx, fishnode)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nd := new(dag.ProtoNode)
+ nnode, err := addLink(ctx, ds, nd, "fish", fishnode)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fnprime, err := nnode.GetLinkedNode(ctx, ds, "fish")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fnpkey := fnprime.Cid()
+ if !fnpkey.Equals(fishnode.Cid()) {
+ t.Fatal("wrong child node found!")
+ }
+}
+
+func assertNodeAtPath(t *testing.T, ds ipld.DAGService, root *dag.ProtoNode, pth string, exp cid.Cid) {
+ parts := strings.Split(pth, "/")
+ cur := root
+ for _, e := range parts {
+ nxt, err := cur.GetLinkedProtoNode(context.Background(), ds, e)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cur = nxt
+ }
+
+ curc := cur.Cid()
+ if !curc.Equals(exp) {
+ t.Fatal("node not as expected at end of path")
+ }
+}
+
+func TestInsertNode(t *testing.T) {
+ root := new(dag.ProtoNode)
+ e := NewDagEditor(root, nil)
+
+ testInsert(t, e, "a", "anodefortesting", false, "")
+ testInsert(t, e, "a/b", "data", false, "")
+ testInsert(t, e, "a/b/c/d/e", "blah", false, "no link by that name")
+ testInsert(t, e, "a/b/c/d/e", "foo", true, "")
+ testInsert(t, e, "a/b/c/d/f", "baz", true, "")
+ testInsert(t, e, "a/b/c/d/f", "bar", true, "")
+
+ testInsert(t, e, "", "bar", true, "cannot create link with no name")
+ testInsert(t, e, "////", "slashes", true, "cannot create link with no name")
+
+ c := e.GetNode().Cid()
+
+ if c.String() != "QmZ8yeT9uD6ouJPNAYt62XffYuXBT6b4mP4obRSE9cJrSt" {
+ t.Fatal("output was different than expected: ", c)
+ }
+}
+
+func testInsert(t *testing.T, e *Editor, path, data string, create bool, experr string) {
+ child := dag.NodeWithData([]byte(data))
+ err := e.tmp.Add(context.Background(), child)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var c func() *dag.ProtoNode
+ if create {
+ c = func() *dag.ProtoNode {
+ return &dag.ProtoNode{}
+ }
+ }
+
+ err = e.InsertNodeAtPath(context.Background(), path, child, c)
+ if experr != "" {
+ var got string
+ if err != nil {
+ got = err.Error()
+ }
+ if got != experr {
+ t.Fatalf("expected '%s' but got '%s'", experr, got)
+ }
+ return
+ }
+
+ if err != nil {
+ t.Fatal(err, path, data, create, experr)
+ }
+
+ assertNodeAtPath(t, e.tmp, e.root, path, child.Cid())
+}
diff --git a/ipld/merkledag/errservice.go b/ipld/merkledag/errservice.go
new file mode 100644
index 0000000000..f4607615a4
--- /dev/null
+++ b/ipld/merkledag/errservice.go
@@ -0,0 +1,47 @@
+package merkledag
+
+import (
+ "context"
+
+ cid "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+// ErrorService implements ipld.DAGService, returning 'Err' for every call.
+type ErrorService struct {
+ Err error
+}
+
+var _ ipld.DAGService = (*ErrorService)(nil)
+
+// Add returns the cs.Err.
+func (cs *ErrorService) Add(ctx context.Context, nd ipld.Node) error {
+ return cs.Err
+}
+
+// AddMany returns the cs.Err.
+func (cs *ErrorService) AddMany(ctx context.Context, nds []ipld.Node) error {
+ return cs.Err
+}
+
+// Get returns the cs.Err.
+func (cs *ErrorService) Get(ctx context.Context, c cid.Cid) (ipld.Node, error) {
+ return nil, cs.Err
+}
+
+// GetMany many returns the cs.Err.
+func (cs *ErrorService) GetMany(ctx context.Context, cids []cid.Cid) <-chan *ipld.NodeOption {
+ ch := make(chan *ipld.NodeOption)
+ close(ch)
+ return ch
+}
+
+// Remove returns the cs.Err.
+func (cs *ErrorService) Remove(ctx context.Context, c cid.Cid) error {
+ return cs.Err
+}
+
+// RemoveMany returns the cs.Err.
+func (cs *ErrorService) RemoveMany(ctx context.Context, cids []cid.Cid) error {
+ return cs.Err
+}
diff --git a/ipld/merkledag/merkledag.go b/ipld/merkledag/merkledag.go
new file mode 100644
index 0000000000..3b18917948
--- /dev/null
+++ b/ipld/merkledag/merkledag.go
@@ -0,0 +1,580 @@
+// Package merkledag implements the IPFS Merkle DAG data structures.
+package merkledag
+
+import (
+ "context"
+ "fmt"
+ "sync"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ bserv "github.com/ipfs/boxo/blockservice"
+ cid "github.com/ipfs/go-cid"
+ ipldcbor "github.com/ipfs/go-ipld-cbor"
+ format "github.com/ipfs/go-ipld-format"
+ legacy "github.com/ipfs/go-ipld-legacy"
+ dagpb "github.com/ipld/go-codec-dagpb"
+
+ // blank import is used to register the IPLD raw codec
+ _ "github.com/ipld/go-ipld-prime/codec/raw"
+ basicnode "github.com/ipld/go-ipld-prime/node/basic"
+)
+
+// TODO: We should move these registrations elsewhere. Really, most of the IPLD
+// functionality should go in a `go-ipld` repo but that will take a lot of work
+// and design.
+func init() {
+ format.Register(cid.DagProtobuf, DecodeProtobufBlock)
+ format.Register(cid.Raw, DecodeRawBlock)
+ format.Register(cid.DagCBOR, ipldcbor.DecodeBlock)
+
+ legacy.RegisterCodec(cid.DagProtobuf, dagpb.Type.PBNode, ProtoNodeConverter)
+ legacy.RegisterCodec(cid.Raw, basicnode.Prototype.Bytes, RawNodeConverter)
+}
+
+// contextKey is a type to use as value for the ProgressTracker contexts.
+type contextKey string
+
+const progressContextKey contextKey = "progress"
+
+// NewDAGService constructs a new DAGService (using the default implementation).
+// Note that the default implementation is also an ipld.LinkGetter.
+func NewDAGService(bs bserv.BlockService) *dagService {
+ return &dagService{Blocks: bs}
+}
+
+// dagService is an IPFS Merkle DAG service.
+// - the root is virtual (like a forest)
+// - stores nodes' data in a BlockService
+// TODO: should cache Nodes that are in memory, and be
+//
+// able to free some of them when vm pressure is high
+type dagService struct {
+ Blocks bserv.BlockService
+}
+
+// Add adds a node to the dagService, storing the block in the BlockService
+func (n *dagService) Add(ctx context.Context, nd format.Node) error {
+ if n == nil { // FIXME remove this assertion. protect with constructor invariant
+ return fmt.Errorf("dagService is nil")
+ }
+
+ return n.Blocks.AddBlock(ctx, nd)
+}
+
+func (n *dagService) AddMany(ctx context.Context, nds []format.Node) error {
+ blks := make([]blocks.Block, len(nds))
+ for i, nd := range nds {
+ blks[i] = nd
+ }
+ return n.Blocks.AddBlocks(ctx, blks)
+}
+
+// Get retrieves a node from the dagService, fetching the block in the BlockService
+func (n *dagService) Get(ctx context.Context, c cid.Cid) (format.Node, error) {
+ if n == nil {
+ return nil, fmt.Errorf("dagService is nil")
+ }
+
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ b, err := n.Blocks.GetBlock(ctx, c)
+ if err != nil {
+ return nil, err
+ }
+
+ return legacy.DecodeNode(ctx, b)
+}
+
+// GetLinks return the links for the node, the node doesn't necessarily have
+// to exist locally.
+func (n *dagService) GetLinks(ctx context.Context, c cid.Cid) ([]*format.Link, error) {
+ if c.Type() == cid.Raw {
+ return nil, nil
+ }
+ node, err := n.Get(ctx, c)
+ if err != nil {
+ return nil, err
+ }
+ return node.Links(), nil
+}
+
+func (n *dagService) Remove(ctx context.Context, c cid.Cid) error {
+ return n.Blocks.DeleteBlock(ctx, c)
+}
+
+// RemoveMany removes multiple nodes from the DAG. It will likely be faster than
+// removing them individually.
+//
+// This operation is not atomic. If it returns an error, some nodes may or may
+// not have been removed.
+func (n *dagService) RemoveMany(ctx context.Context, cids []cid.Cid) error {
+ // TODO(#4608): make this batch all the way down.
+ for _, c := range cids {
+ if err := n.Blocks.DeleteBlock(ctx, c); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// GetLinksDirect creates a function to get the links for a node, from
+// the node, bypassing the LinkService. If the node does not exist
+// locally (and can not be retrieved) an error will be returned.
+func GetLinksDirect(serv format.NodeGetter) GetLinks {
+ return func(ctx context.Context, c cid.Cid) ([]*format.Link, error) {
+ nd, err := serv.Get(ctx, c)
+ if err != nil {
+ return nil, err
+ }
+ return nd.Links(), nil
+ }
+}
+
+type sesGetter struct {
+ bs *bserv.Session
+}
+
+// Get gets a single node from the DAG.
+func (sg *sesGetter) Get(ctx context.Context, c cid.Cid) (format.Node, error) {
+ blk, err := sg.bs.GetBlock(ctx, c)
+
+ if err != nil {
+ return nil, err
+ }
+
+ return legacy.DecodeNode(ctx, blk)
+}
+
+// GetMany gets many nodes at once, batching the request if possible.
+func (sg *sesGetter) GetMany(ctx context.Context, keys []cid.Cid) <-chan *format.NodeOption {
+ return getNodesFromBG(ctx, sg.bs, keys)
+}
+
+// WrapSession wraps a blockservice session to satisfy the format.NodeGetter interface
+func WrapSession(s *bserv.Session) format.NodeGetter {
+ return &sesGetter{s}
+}
+
+// Session returns a NodeGetter using a new session for block fetches.
+func (n *dagService) Session(ctx context.Context) format.NodeGetter {
+ return WrapSession(bserv.NewSession(ctx, n.Blocks))
+}
+
+// FetchGraph fetches all nodes that are children of the given node
+func FetchGraph(ctx context.Context, root cid.Cid, serv format.DAGService) error {
+ return FetchGraphWithDepthLimit(ctx, root, -1, serv)
+}
+
+// FetchGraphWithDepthLimit fetches all nodes that are children to the given
+// node down to the given depth. maxDepth=0 means "only fetch root",
+// maxDepth=1 means "fetch root and its direct children" and so on...
+// maxDepth=-1 means unlimited.
+func FetchGraphWithDepthLimit(ctx context.Context, root cid.Cid, depthLim int, serv format.DAGService) error {
+ var ng format.NodeGetter = NewSession(ctx, serv)
+
+ set := make(map[cid.Cid]int)
+
+ // Visit function returns true when:
+ // * The element is not in the set and we're not over depthLim
+ // * The element is in the set but recorded depth is deeper
+ // than currently seen (if we find it higher in the tree we'll need
+ // to explore deeper than before).
+ // depthLim = -1 means we only return true if the element is not in the
+ // set.
+ visit := func(c cid.Cid, depth int) bool {
+ oldDepth, ok := set[c]
+
+ if (ok && depthLim < 0) || (depthLim >= 0 && depth > depthLim) {
+ return false
+ }
+
+ if !ok || oldDepth > depth {
+ set[c] = depth
+ return true
+ }
+ return false
+ }
+
+ // If we have a ProgressTracker, we wrap the visit function to handle it
+ v, _ := ctx.Value(progressContextKey).(*ProgressTracker)
+ if v == nil {
+ return WalkDepth(ctx, GetLinksDirect(ng), root, visit, Concurrent())
+ }
+
+ visitProgress := func(c cid.Cid, depth int) bool {
+ if visit(c, depth) {
+ v.Increment()
+ return true
+ }
+ return false
+ }
+ return WalkDepth(ctx, GetLinksDirect(ng), root, visitProgress, Concurrent())
+}
+
+// GetMany gets many nodes from the DAG at once.
+//
+// This method may not return all requested nodes (and may or may not return an
+// error indicating that it failed to do so. It is up to the caller to verify
+// that it received all nodes.
+func (n *dagService) GetMany(ctx context.Context, keys []cid.Cid) <-chan *format.NodeOption {
+ return getNodesFromBG(ctx, n.Blocks, keys)
+}
+
+func dedupKeys(keys []cid.Cid) []cid.Cid {
+ set := cid.NewSet()
+ for _, c := range keys {
+ set.Add(c)
+ }
+ if set.Len() == len(keys) {
+ return keys
+ }
+ return set.Keys()
+}
+
+func getNodesFromBG(ctx context.Context, bs bserv.BlockGetter, keys []cid.Cid) <-chan *format.NodeOption {
+ keys = dedupKeys(keys)
+
+ out := make(chan *format.NodeOption, len(keys))
+ blocks := bs.GetBlocks(ctx, keys)
+ var count int
+
+ go func() {
+ defer close(out)
+ for {
+ select {
+ case b, ok := <-blocks:
+ if !ok {
+ if count != len(keys) {
+ out <- &format.NodeOption{Err: fmt.Errorf("failed to fetch all nodes")}
+ }
+ return
+ }
+
+ nd, err := legacy.DecodeNode(ctx, b)
+ if err != nil {
+ out <- &format.NodeOption{Err: err}
+ return
+ }
+
+ out <- &format.NodeOption{Node: nd}
+ count++
+
+ case <-ctx.Done():
+ out <- &format.NodeOption{Err: ctx.Err()}
+ return
+ }
+ }
+ }()
+ return out
+}
+
+// GetLinks is the type of function passed to the EnumerateChildren function(s)
+// for getting the children of an IPLD node.
+type GetLinks func(context.Context, cid.Cid) ([]*format.Link, error)
+
+// GetLinksWithDAG returns a GetLinks function that tries to use the given
+// NodeGetter as a LinkGetter to get the children of a given IPLD node. This may
+// allow us to traverse the DAG without actually loading and parsing the node in
+// question (if we already have the links cached).
+func GetLinksWithDAG(ng format.NodeGetter) GetLinks {
+ return func(ctx context.Context, c cid.Cid) ([]*format.Link, error) {
+ return format.GetLinks(ctx, ng, c)
+ }
+}
+
+// defaultConcurrentFetch is the default maximum number of concurrent fetches
+// that 'fetchNodes' will start at a time
+const defaultConcurrentFetch = 32
+
+// walkOptions represent the parameters of a graph walking algorithm
+type walkOptions struct {
+ SkipRoot bool
+ Concurrency int
+ ErrorHandler func(c cid.Cid, err error) error
+}
+
+// WalkOption is a setter for walkOptions
+type WalkOption func(*walkOptions)
+
+func (wo *walkOptions) addHandler(handler func(c cid.Cid, err error) error) {
+ if wo.ErrorHandler != nil {
+ wo.ErrorHandler = func(c cid.Cid, err error) error {
+ return handler(c, wo.ErrorHandler(c, err))
+ }
+ } else {
+ wo.ErrorHandler = handler
+ }
+}
+
+// SkipRoot is a WalkOption indicating that the root node should skipped
+func SkipRoot() WalkOption {
+ return func(walkOptions *walkOptions) {
+ walkOptions.SkipRoot = true
+ }
+}
+
+// Concurrent is a WalkOption indicating that node fetching should be done in
+// parallel, with the default concurrency factor.
+// NOTE: When using that option, the walk order is *not* guarantee.
+// NOTE: It *does not* make multiple concurrent calls to the passed `visit` function.
+func Concurrent() WalkOption {
+ return func(walkOptions *walkOptions) {
+ walkOptions.Concurrency = defaultConcurrentFetch
+ }
+}
+
+// Concurrency is a WalkOption indicating that node fetching should be done in
+// parallel, with a specific concurrency factor.
+// NOTE: When using that option, the walk order is *not* guarantee.
+// NOTE: It *does not* make multiple concurrent calls to the passed `visit` function.
+func Concurrency(worker int) WalkOption {
+ return func(walkOptions *walkOptions) {
+ walkOptions.Concurrency = worker
+ }
+}
+
+// IgnoreErrors is a WalkOption indicating that the walk should attempt to
+// continue even when an error occur.
+func IgnoreErrors() WalkOption {
+ return func(walkOptions *walkOptions) {
+ walkOptions.addHandler(func(c cid.Cid, err error) error {
+ return nil
+ })
+ }
+}
+
+// IgnoreMissing is a WalkOption indicating that the walk should continue when
+// a node is missing.
+func IgnoreMissing() WalkOption {
+ return func(walkOptions *walkOptions) {
+ walkOptions.addHandler(func(c cid.Cid, err error) error {
+ if format.IsNotFound(err) {
+ return nil
+ }
+ return err
+ })
+ }
+}
+
+// OnMissing is a WalkOption adding a callback that will be triggered on a missing
+// node.
+func OnMissing(callback func(c cid.Cid)) WalkOption {
+ return func(walkOptions *walkOptions) {
+ walkOptions.addHandler(func(c cid.Cid, err error) error {
+ if format.IsNotFound(err) {
+ callback(c)
+ }
+ return err
+ })
+ }
+}
+
+// OnError is a WalkOption adding a custom error handler.
+// If this handler return a nil error, the walk will continue.
+func OnError(handler func(c cid.Cid, err error) error) WalkOption {
+ return func(walkOptions *walkOptions) {
+ walkOptions.addHandler(handler)
+ }
+}
+
+// WalkGraph will walk the dag in order (depth first) starting at the given root.
+func Walk(ctx context.Context, getLinks GetLinks, c cid.Cid, visit func(cid.Cid) bool, options ...WalkOption) error {
+ visitDepth := func(c cid.Cid, depth int) bool {
+ return visit(c)
+ }
+
+ return WalkDepth(ctx, getLinks, c, visitDepth, options...)
+}
+
+// WalkDepth walks the dag starting at the given root and passes the current
+// depth to a given visit function. The visit function can be used to limit DAG
+// exploration.
+func WalkDepth(ctx context.Context, getLinks GetLinks, c cid.Cid, visit func(cid.Cid, int) bool, options ...WalkOption) error {
+ opts := &walkOptions{}
+ for _, opt := range options {
+ opt(opts)
+ }
+
+ if opts.Concurrency > 1 {
+ return parallelWalkDepth(ctx, getLinks, c, visit, opts)
+ } else {
+ return sequentialWalkDepth(ctx, getLinks, c, 0, visit, opts)
+ }
+}
+
+func sequentialWalkDepth(ctx context.Context, getLinks GetLinks, root cid.Cid, depth int, visit func(cid.Cid, int) bool, options *walkOptions) error {
+ if !(options.SkipRoot && depth == 0) {
+ if !visit(root, depth) {
+ return nil
+ }
+ }
+
+ links, err := getLinks(ctx, root)
+ if err != nil && options.ErrorHandler != nil {
+ err = options.ErrorHandler(root, err)
+ }
+ if err != nil {
+ return err
+ }
+
+ for _, lnk := range links {
+ if err := sequentialWalkDepth(ctx, getLinks, lnk.Cid, depth+1, visit, options); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// ProgressTracker is used to show progress when fetching nodes.
+type ProgressTracker struct {
+ Total int
+ lk sync.Mutex
+}
+
+// DeriveContext returns a new context with value "progress" derived from
+// the given one.
+func (p *ProgressTracker) DeriveContext(ctx context.Context) context.Context {
+ return context.WithValue(ctx, progressContextKey, p)
+}
+
+// Increment adds one to the total progress.
+func (p *ProgressTracker) Increment() {
+ p.lk.Lock()
+ defer p.lk.Unlock()
+ p.Total++
+}
+
+// Value returns the current progress.
+func (p *ProgressTracker) Value() int {
+ p.lk.Lock()
+ defer p.lk.Unlock()
+ return p.Total
+}
+
+func parallelWalkDepth(ctx context.Context, getLinks GetLinks, root cid.Cid, visit func(cid.Cid, int) bool, options *walkOptions) error {
+ type cidDepth struct {
+ cid cid.Cid
+ depth int
+ }
+
+ type linksDepth struct {
+ links []*format.Link
+ depth int
+ }
+
+ feed := make(chan cidDepth)
+ out := make(chan linksDepth)
+ done := make(chan struct{})
+
+ var visitlk sync.Mutex
+ var wg sync.WaitGroup
+
+ errChan := make(chan error)
+ fetchersCtx, cancel := context.WithCancel(ctx)
+ defer wg.Wait()
+ defer cancel()
+ for i := 0; i < options.Concurrency; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for cdepth := range feed {
+ ci := cdepth.cid
+ depth := cdepth.depth
+
+ var shouldVisit bool
+
+ // bypass the root if needed
+ if !(options.SkipRoot && depth == 0) {
+ visitlk.Lock()
+ shouldVisit = visit(ci, depth)
+ visitlk.Unlock()
+ } else {
+ shouldVisit = true
+ }
+
+ if shouldVisit {
+ links, err := getLinks(ctx, ci)
+ if err != nil && options.ErrorHandler != nil {
+ err = options.ErrorHandler(root, err)
+ }
+ if err != nil {
+ select {
+ case errChan <- err:
+ case <-fetchersCtx.Done():
+ }
+ return
+ }
+
+ outLinks := linksDepth{
+ links: links,
+ depth: depth + 1,
+ }
+
+ select {
+ case out <- outLinks:
+ case <-fetchersCtx.Done():
+ return
+ }
+ }
+ select {
+ case done <- struct{}{}:
+ case <-fetchersCtx.Done():
+ }
+ }
+ }()
+ }
+ defer close(feed)
+
+ send := feed
+ var todoQueue []cidDepth
+ var inProgress int
+
+ next := cidDepth{
+ cid: root,
+ depth: 0,
+ }
+
+ for {
+ select {
+ case send <- next:
+ inProgress++
+ if len(todoQueue) > 0 {
+ next = todoQueue[0]
+ todoQueue = todoQueue[1:]
+ } else {
+ next = cidDepth{}
+ send = nil
+ }
+ case <-done:
+ inProgress--
+ if inProgress == 0 && !next.cid.Defined() {
+ return nil
+ }
+ case linksDepth := <-out:
+ for _, lnk := range linksDepth.links {
+ cd := cidDepth{
+ cid: lnk.Cid,
+ depth: linksDepth.depth,
+ }
+
+ if !next.cid.Defined() {
+ next = cd
+ send = feed
+ } else {
+ todoQueue = append(todoQueue, cd)
+ }
+ }
+ case err := <-errChan:
+ return err
+
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+}
+
+var _ format.LinkGetter = &dagService{}
+var _ format.NodeGetter = &dagService{}
+var _ format.NodeGetter = &sesGetter{}
+var _ format.DAGService = &dagService{}
diff --git a/ipld/merkledag/merkledag_test.go b/ipld/merkledag/merkledag_test.go
new file mode 100644
index 0000000000..58c8d1f95b
--- /dev/null
+++ b/ipld/merkledag/merkledag_test.go
@@ -0,0 +1,1232 @@
+package merkledag_test
+
+import (
+ "bytes"
+ "context"
+ "crypto/rand"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "math"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ . "github.com/ipfs/boxo/ipld/merkledag"
+ mdpb "github.com/ipfs/boxo/ipld/merkledag/pb"
+ dstest "github.com/ipfs/boxo/ipld/merkledag/test"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ bserv "github.com/ipfs/boxo/blockservice"
+ bstest "github.com/ipfs/boxo/blockservice/test"
+ cid "github.com/ipfs/go-cid"
+ offline "github.com/ipfs/boxo/exchange/offline"
+ u "github.com/ipfs/boxo/util"
+ ipld "github.com/ipfs/go-ipld-format"
+ prime "github.com/ipld/go-ipld-prime"
+ mh "github.com/multiformats/go-multihash"
+)
+
+var someCid cid.Cid = func() cid.Cid {
+ c, _ := cid.Cast([]byte{1, 85, 0, 5, 0, 1, 2, 3, 4})
+ return c
+}()
+
+// makeDepthTestingGraph makes a small DAG with two levels. The level-two
+// nodes are both children of the root and of one of the level 1 nodes.
+// This is meant to test the Walk*Depth functions.
+func makeDepthTestingGraph(t *testing.T, ds ipld.DAGService) ipld.Node {
+ root := NodeWithData(nil)
+ l11 := NodeWithData([]byte("leve1_node1"))
+ l12 := NodeWithData([]byte("leve1_node2"))
+ l21 := NodeWithData([]byte("leve2_node1"))
+ l22 := NodeWithData([]byte("leve2_node2"))
+ l23 := NodeWithData([]byte("leve2_node3"))
+
+ l11.AddNodeLink(l21.Cid().String(), l21)
+ l11.AddNodeLink(l22.Cid().String(), l22)
+ l11.AddNodeLink(l23.Cid().String(), l23)
+
+ root.AddNodeLink(l11.Cid().String(), l11)
+ root.AddNodeLink(l12.Cid().String(), l12)
+ root.AddNodeLink(l23.Cid().String(), l23)
+
+ ctx := context.Background()
+ for _, n := range []ipld.Node{l23, l22, l21, l12, l11, root} {
+ err := ds.Add(ctx, n)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ return root
+}
+
+// Check that all children of root are in the given set and in the datastore
+func traverseAndCheck(t *testing.T, root ipld.Node, ds ipld.DAGService, hasF func(c cid.Cid) bool) {
+ // traverse dag and check
+ for _, lnk := range root.Links() {
+ c := lnk.Cid
+ if !hasF(c) {
+ t.Fatal("missing key in set! ", lnk.Cid.String())
+ }
+ child, err := ds.Get(context.Background(), c)
+ if err != nil {
+ t.Fatal(err)
+ }
+ traverseAndCheck(t, child, ds, hasF)
+ }
+}
+
+type brokenBuilder struct{}
+
+func (brokenBuilder) Sum([]byte) (cid.Cid, error) { return cid.Undef, errors.New("Nope!") }
+func (brokenBuilder) GetCodec() uint64 { return 0 }
+func (b brokenBuilder) WithCodec(uint64) cid.Builder { return b }
+
+// builder that will pass the basic SetCidBuilder tests but fail otherwise
+type sneakyBrokenBuilder struct{}
+
+func (sneakyBrokenBuilder) Sum(data []byte) (cid.Cid, error) {
+ if len(data) == 256 {
+ return V1CidPrefix().Sum(data)
+ }
+ return cid.Undef, errors.New("Nope!")
+}
+func (sneakyBrokenBuilder) GetCodec() uint64 { return 0 }
+func (b sneakyBrokenBuilder) WithCodec(uint64) cid.Builder { return b }
+
+func TestBadBuilderEncode(t *testing.T) {
+ n := NodeWithData([]byte("boop"))
+
+ t.Run("good builder sanity check", func(t *testing.T) {
+ if _, err := n.EncodeProtobuf(false); err != nil {
+ t.Fatal(err)
+ }
+ if err := n.SetCidBuilder(
+ &cid.Prefix{
+ MhType: mh.SHA2_256,
+ MhLength: -1,
+ Version: 1,
+ Codec: cid.DagProtobuf,
+ },
+ ); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ t.Run("hasher we can't use, should error", func(t *testing.T) {
+ if err := n.SetCidBuilder(
+ &cid.Prefix{
+ MhType: mh.SHA2_256_TRUNC254_PADDED,
+ MhLength: 256,
+ Version: 1,
+ Codec: cid.DagProtobuf,
+ },
+ ); err == nil {
+ t.Fatal("expected SetCidBuilder to error on unusable hasher")
+ }
+ if _, err := n.EncodeProtobuf(false); err != nil {
+ t.Fatalf("expected EncodeProtobuf to use safe CidBuilder: %v", err)
+ }
+ })
+
+ t.Run("broken custom builder, should error", func(t *testing.T) {
+ if err := n.SetCidBuilder(brokenBuilder{}); err == nil {
+ t.Fatal("expected SetCidBuilder to error on unusable hasher")
+ }
+ if _, err := n.EncodeProtobuf(false); err != nil {
+ t.Fatalf("expected EncodeProtobuf to use safe CidBuilder: %v", err)
+ }
+ })
+
+ t.Run("broken custom builder as pointer, should error", func(t *testing.T) {
+ if err := n.SetCidBuilder(&brokenBuilder{}); err == nil {
+ t.Fatal("expected SetCidBuilder to error on unusable hasher")
+ }
+ if _, err := n.EncodeProtobuf(false); err != nil {
+ t.Fatalf("expected EncodeProtobuf to use safe CidBuilder: %v", err)
+ }
+ })
+
+ t.Run("broken sneaky custom builder, should error", func(t *testing.T) {
+ if err := n.SetCidBuilder(sneakyBrokenBuilder{}); err != nil {
+ t.Fatalf("expected SetCidBuilder to not error with sneaky custom builder: %v", err)
+ }
+ if _, err := n.EncodeProtobuf(false); err == nil {
+ t.Fatal("expected EncodeProtobuf to fail using the sneaky custom builder")
+ }
+ if len(n.RawData()) != 0 {
+ t.Fatal("expected RawData to return zero-byte slice")
+ }
+ if n.Cid().String() != "bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku" {
+ t.Fatal("expected Cid to return the zero dag-pb CID")
+ }
+ if n.String() != "bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku" {
+ t.Fatal("expected String to return the zero dag-pb CID string")
+ }
+ })
+}
+
+func TestLinkChecking(t *testing.T) {
+ cases := []struct {
+ name string
+ fn func(*ProtoNode) error
+ }{
+ {
+ name: "AddRawLink overflow Tsize",
+ fn: func(n *ProtoNode) error {
+ return n.AddRawLink("foo", &ipld.Link{Size: math.MaxUint64, Cid: someCid})
+ },
+ },
+
+ {
+ name: "AddRawLink undefined CID",
+ fn: func(n *ProtoNode) error {
+ return n.AddRawLink("foo", &ipld.Link{Cid: cid.Undef})
+ },
+ },
+
+ {
+ name: "SetLinks overflow Tsize",
+ fn: func(n *ProtoNode) error {
+ return n.SetLinks([]*ipld.Link{{Size: math.MaxUint64, Cid: someCid}})
+ },
+ },
+
+ {
+ name: "SetLinks undefined CID",
+ fn: func(n *ProtoNode) error {
+ return n.SetLinks([]*ipld.Link{{Cid: cid.Undef}})
+ },
+ },
+
+ {
+ name: "UnmarshalJSON overflow Tsize",
+ fn: func(n *ProtoNode) error {
+ return n.UnmarshalJSON([]byte(`{"data":null,"links":[{"Name":"","Size":18446744073709549568,"Cid":{"/":"QmNPWHBrVQiiV8FpyNuEPhB9E2rbvdy9Yx79EY1EJuyf9o"}}]}`))
+ },
+ },
+
+ {
+ name: "UnmarshalJSON undefined CID",
+ fn: func(n *ProtoNode) error {
+ return n.UnmarshalJSON([]byte(`{"data":null,"links":[{"Name":"","Size":100}]}`))
+ },
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ n := NodeWithData([]byte("boop"))
+ err := tc.fn(n)
+ if err == nil {
+ t.Fatal("expected error")
+ }
+ })
+ }
+
+ t.Run("round-trip block with bad Tsize", func(t *testing.T) {
+ badblock, _ := hex.DecodeString("122f0a22122000bb3604d2ecd386227007c548249521fbb9a394e1e26460091d0a692888e7361880f0ffffffffffffff01")
+ n, err := DecodeProtobuf(badblock)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // sanity
+ if len(n.Links()) != 1 {
+ t.Fatal("expected a link")
+ }
+ // sanity
+ if n.Links()[0].Size <= math.MaxInt64 {
+ t.Fatal("expected link Tsize to be oversized")
+ }
+
+ // forced round-trip
+ byts, err := n.EncodeProtobuf(true)
+ if err != nil {
+ t.Fatal(err)
+ }
+ n, err = DecodeProtobuf(byts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(n.Links()) != 1 {
+ t.Fatal("expected a link")
+ }
+ if n.Links()[0].Size != 0 {
+ t.Fatal("expected link Tsize to be truncated on reencode")
+ }
+ })
+}
+
+func TestNode(t *testing.T) {
+
+ n1 := NodeWithData([]byte("beep"))
+ n2 := NodeWithData([]byte("boop"))
+ n3 := NodeWithData([]byte("beep boop"))
+ if err := n3.AddNodeLink("beep-link", n1); err != nil {
+ t.Error(err)
+ }
+ if err := n3.AddNodeLink("boop-link", n2); err != nil {
+ t.Error(err)
+ }
+
+ printn := func(name string, n *ProtoNode) {
+ fmt.Println(">", name)
+ fmt.Println("data:", string(n.Data()))
+
+ fmt.Println("links:")
+ for _, l := range n.Links() {
+ fmt.Println("-", l.Name, l.Size, l.Cid)
+ }
+
+ e, err := n.EncodeProtobuf(false)
+ if err != nil {
+ t.Error(err)
+ } else {
+ fmt.Println("encoded:", e)
+ }
+
+ h := n.Multihash()
+ k := n.Cid().Hash()
+ if k.String() != h.String() {
+ t.Error("Key is not equivalent to multihash")
+ } else {
+ fmt.Println("key: ", k)
+ }
+
+ SubtestNodeStat(t, n)
+ }
+
+ printn("beep", n1)
+ printn("boop", n2)
+ printn("beep boop", n3)
+}
+
+func SubtestNodeStat(t *testing.T, n *ProtoNode) {
+ enc, err := n.EncodeProtobuf(true)
+ if err != nil {
+ t.Error("n.EncodeProtobuf(true) failed")
+ return
+ }
+
+ cumSize, err := n.Size()
+ if err != nil {
+ t.Error("n.Size() failed")
+ return
+ }
+
+ k := n.Cid()
+
+ expected := ipld.NodeStat{
+ NumLinks: len(n.Links()),
+ BlockSize: len(enc),
+ LinksSize: len(enc) - len(n.Data()), // includes framing.
+ DataSize: len(n.Data()),
+ CumulativeSize: int(cumSize),
+ Hash: k.String(),
+ }
+
+ actual, err := n.Stat()
+ if err != nil {
+ t.Error("n.Stat() failed")
+ return
+ }
+
+ if expected != *actual {
+ t.Errorf("n.Stat incorrect.\nexpect: %s\nactual: %s", expected, actual)
+ } else {
+ fmt.Printf("n.Stat correct: %s\n", actual)
+ }
+}
+
+type devZero struct{}
+
+func (devZero) Read(b []byte) (int, error) {
+ for i := range b {
+ b[i] = 0
+ }
+ return len(b), nil
+}
+
+func TestBatchFetch(t *testing.T) {
+ read := io.LimitReader(u.NewTimeSeededRand(), 1024*32)
+ runBatchFetchTest(t, read)
+}
+
+func TestBatchFetchDupBlock(t *testing.T) {
+ read := io.LimitReader(devZero{}, 1024*32)
+ runBatchFetchTest(t, read)
+}
+
+// makeTestDAG creates a simple DAG from the data in a reader.
+// First, a node is created from each 512 bytes of data from the reader
+// (like a the Size chunker would do). Then all nodes are added as children
+// to a root node, which is returned.
+func makeTestDAG(t *testing.T, read io.Reader, ds ipld.DAGService) ipld.Node {
+ p := make([]byte, 512)
+ nodes := []*ProtoNode{}
+
+ for {
+ n, err := io.ReadFull(read, p)
+ if err == io.EOF {
+ break
+ }
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if n != len(p) {
+ t.Fatal("should have read 512 bytes from the reader")
+ }
+
+ protoNode := NodeWithData(p)
+ nodes = append(nodes, protoNode)
+ }
+
+ ctx := context.Background()
+ // Add a root referencing all created nodes
+ root := NodeWithData(nil)
+ for _, n := range nodes {
+ err := root.AddNodeLink(n.Cid().String(), n)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = ds.Add(ctx, n)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ err := ds.Add(ctx, root)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return root
+}
+
+// makeTestDAGReader takes the root node as returned by makeTestDAG and
+// provides a reader that reads all the RawData from that node and its children.
+func makeTestDAGReader(t *testing.T, root ipld.Node, ds ipld.DAGService) io.Reader {
+ ctx := context.Background()
+ buf := new(bytes.Buffer)
+ buf.Write(root.RawData())
+ for _, l := range root.Links() {
+ n, err := ds.Get(ctx, l.Cid)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = buf.Write(n.RawData())
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ return buf
+}
+
+func runBatchFetchTest(t *testing.T, read io.Reader) {
+ ctx := context.Background()
+ var dagservs []ipld.DAGService
+ for _, bsi := range bstest.Mocks(5) {
+ dagservs = append(dagservs, NewDAGService(bsi))
+ }
+
+ root := makeTestDAG(t, read, dagservs[0])
+
+ t.Log("finished setup.")
+
+ dagr := makeTestDAGReader(t, root, dagservs[0])
+
+ expected, err := io.ReadAll(dagr)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = dagservs[0].Add(ctx, root)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ t.Log("Added file to first node.")
+
+ c := root.Cid()
+
+ wg := sync.WaitGroup{}
+ errs := make(chan error)
+
+ for i := 1; i < len(dagservs); i++ {
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+ first, err := dagservs[i].Get(ctx, c)
+ if err != nil {
+ errs <- err
+ }
+ fmt.Println("Got first node back.")
+
+ firstpb, ok := first.(*ProtoNode)
+ if !ok {
+ errs <- ErrNotProtobuf
+ }
+ read := makeTestDAGReader(t, firstpb, dagservs[i])
+ datagot, err := io.ReadAll(read)
+ if err != nil {
+ errs <- err
+ }
+
+ if !bytes.Equal(datagot, expected) {
+ errs <- errors.New("got bad data back")
+ }
+ }(i)
+ }
+
+ go func() {
+ wg.Wait()
+ close(errs)
+ }()
+
+ for err := range errs {
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+}
+
+func TestCantGet(t *testing.T) {
+ ds := dstest.Mock()
+ a := NodeWithData([]byte("A"))
+
+ c := a.Cid()
+ _, err := ds.Get(context.Background(), c)
+ if !strings.Contains(err.Error(), "not found") {
+ t.Fatal("expected err not found, got: ", err)
+ }
+}
+
+func TestFetchGraph(t *testing.T) {
+ var dservs []ipld.DAGService
+ bsis := bstest.Mocks(2)
+ for _, bsi := range bsis {
+ dservs = append(dservs, NewDAGService(bsi))
+ }
+
+ read := io.LimitReader(u.NewTimeSeededRand(), 1024*32)
+ root := makeTestDAG(t, read, dservs[0])
+
+ err := FetchGraph(context.TODO(), root.Cid(), dservs[1])
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // create an offline dagstore and ensure all blocks were fetched
+ bs := bserv.New(bsis[1].Blockstore(), offline.Exchange(bsis[1].Blockstore()))
+
+ offlineDS := NewDAGService(bs)
+
+ err = Walk(context.Background(), offlineDS.GetLinks, root.Cid(), func(_ cid.Cid) bool { return true })
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestFetchGraphWithDepthLimit(t *testing.T) {
+ type testcase struct {
+ depthLim int
+ setLen int
+ }
+
+ tests := []testcase{
+ {1, 4},
+ {0, 1},
+ {-1, 6},
+ {2, 6},
+ {3, 6},
+ }
+
+ testF := func(t *testing.T, tc testcase) {
+ var dservs []ipld.DAGService
+ bsis := bstest.Mocks(2)
+ for _, bsi := range bsis {
+ dservs = append(dservs, NewDAGService(bsi))
+ }
+
+ root := makeDepthTestingGraph(t, dservs[0])
+
+ err := FetchGraphWithDepthLimit(context.TODO(), root.Cid(), tc.depthLim, dservs[1])
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // create an offline dagstore and ensure all blocks were fetched
+ bs := bserv.New(bsis[1].Blockstore(), offline.Exchange(bsis[1].Blockstore()))
+
+ offlineDS := NewDAGService(bs)
+
+ set := make(map[string]int)
+ visitF := func(c cid.Cid, depth int) bool {
+ if tc.depthLim < 0 || depth <= tc.depthLim {
+ set[string(c.Bytes())] = depth
+ return true
+ }
+ return false
+
+ }
+
+ err = WalkDepth(context.Background(), offlineDS.GetLinks, root.Cid(), visitF)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(set) != tc.setLen {
+ t.Fatalf("expected %d nodes but visited %d", tc.setLen, len(set))
+ }
+ }
+
+ for _, tc := range tests {
+ t.Run(fmt.Sprintf("depth limit %d", tc.depthLim), func(t *testing.T) {
+ testF(t, tc)
+ })
+ }
+}
+
+func TestWalk(t *testing.T) {
+ bsi := bstest.Mocks(1)
+ ds := NewDAGService(bsi[0])
+
+ read := io.LimitReader(u.NewTimeSeededRand(), 1024*1024)
+ root := makeTestDAG(t, read, ds)
+
+ set := cid.NewSet()
+
+ err := Walk(context.Background(), ds.GetLinks, root.Cid(), set.Visit)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ traverseAndCheck(t, root, ds, set.Has)
+}
+
+func TestFetchFailure(t *testing.T) {
+ ctx := context.Background()
+
+ ds := dstest.Mock()
+ ds_bad := dstest.Mock()
+
+ top := new(ProtoNode)
+ for i := 0; i < 10; i++ {
+ nd := NodeWithData([]byte{byte('a' + i)})
+ err := ds.Add(ctx, nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = top.AddNodeLink(fmt.Sprintf("AA%d", i), nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ for i := 0; i < 10; i++ {
+ nd := NodeWithData([]byte{'f', 'a' + byte(i)})
+ err := ds_bad.Add(ctx, nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = top.AddNodeLink(fmt.Sprintf("BB%d", i), nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ getters := ipld.GetDAG(ctx, ds, top)
+ for i, getter := range getters {
+ _, err := getter.Get(ctx)
+ if err != nil && i < 10 {
+ t.Fatal(err)
+ }
+ if err == nil && i >= 10 {
+ t.Fatal("should have failed request")
+ }
+ }
+}
+
+func TestUnmarshalFailure(t *testing.T) {
+ badData := []byte("hello world")
+
+ _, err := DecodeProtobuf(badData)
+ if err == nil {
+ t.Fatal("shouldnt succeed to parse this")
+ }
+
+ // now with a bad link
+ pbn := &mdpb.PBNode{Links: []*mdpb.PBLink{{Hash: []byte("not a multihash")}}}
+ badlink, err := pbn.Marshal()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = DecodeProtobuf(badlink)
+ if err == nil {
+ t.Fatal("should have failed to parse node with bad link")
+ }
+
+ n := &ProtoNode{}
+ n.Marshal()
+}
+
+func TestBasicAddGet(t *testing.T) {
+ ctx := context.Background()
+
+ ds := dstest.Mock()
+ nd := new(ProtoNode)
+
+ err := ds.Add(ctx, nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := ds.Get(ctx, nd.Cid())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !nd.Cid().Equals(out.Cid()) {
+ t.Fatal("output didnt match input")
+ }
+}
+
+func TestGetRawNodes(t *testing.T) {
+ ctx := context.Background()
+
+ rn := NewRawNode([]byte("test"))
+
+ ds := dstest.Mock()
+
+ err := ds.Add(ctx, rn)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !rn.Cid().Equals(rn.Cid()) {
+ t.Fatal("output cids didnt match")
+ }
+
+ out, err := ds.Get(ctx, rn.Cid())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !bytes.Equal(out.RawData(), []byte("test")) {
+ t.Fatal("raw block should match input data")
+ }
+
+ if out.Links() != nil {
+ t.Fatal("raw blocks shouldnt have links")
+ }
+
+ if out.Tree("", -1) != nil {
+ t.Fatal("tree should return no paths in a raw block")
+ }
+
+ size, err := out.Size()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if size != 4 {
+ t.Fatal("expected size to be 4")
+ }
+
+ ns, err := out.Stat()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if ns.DataSize != 4 {
+ t.Fatal("expected size to be 4, got: ", ns.DataSize)
+ }
+
+ _, _, err = out.Resolve([]string{"foo"})
+ if err != ErrLinkNotFound {
+ t.Fatal("shouldnt find links under raw blocks")
+ }
+}
+
+func TestProtoNodeResolve(t *testing.T) {
+
+ nd := new(ProtoNode)
+ nd.SetLinks([]*ipld.Link{{Name: "foo", Cid: someCid}})
+
+ lnk, left, err := nd.ResolveLink([]string{"foo", "bar"})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(left) != 1 || left[0] != "bar" {
+ t.Fatal("expected the single path element 'bar' to remain")
+ }
+
+ if lnk.Name != "foo" {
+ t.Fatal("how did we get anything else?")
+ }
+
+ tvals := nd.Tree("", -1)
+ if len(tvals) != 1 || tvals[0] != "foo" {
+ t.Fatal("expected tree to return []{\"foo\"}")
+ }
+}
+
+func TestCidRetention(t *testing.T) {
+ ctx := context.Background()
+
+ nd := new(ProtoNode)
+ nd.SetData([]byte("fooooo"))
+
+ pref := nd.Cid().Prefix()
+ pref.Version = 1
+
+ c2, err := pref.Sum(nd.RawData())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ blk, err := blocks.NewBlockWithCid(nd.RawData(), c2)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ bs := dstest.Bserv()
+ err = bs.AddBlock(ctx, blk)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ds := NewDAGService(bs)
+ out, err := ds.Get(ctx, c2)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !out.Cid().Equals(c2) {
+ t.Fatal("output cid didnt match")
+ }
+}
+
+func TestCidRawDoesnNeedData(t *testing.T) {
+ srv := NewDAGService(dstest.Bserv())
+ nd := NewRawNode([]byte("somedata"))
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ // there is no data for this node in the blockservice
+ // so dag service can't load it
+ links, err := srv.GetLinks(ctx, nd.Cid())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(links) != 0 {
+ t.Fatal("raw node shouldn't have any links")
+ }
+}
+
+func TestRawToJson(t *testing.T) {
+ rawData := []byte{1, 2, 3, 4}
+ nd := NewRawNode(rawData)
+ encoded, err := nd.MarshalJSON()
+ if err != nil {
+ t.Fatal(err)
+ }
+ var res interface{}
+ err = json.Unmarshal(encoded, &res)
+ if err != nil {
+ t.Fatal(err)
+ }
+ resBytes, ok := res.(string)
+ if !ok {
+ t.Fatal("expected to marshal to a string")
+ }
+ if string(rawData) != resBytes {
+ t.Fatal("failed to round-trip bytes")
+ }
+}
+
+func TestGetManyDuplicate(t *testing.T) {
+ ctx := context.Background()
+
+ srv := NewDAGService(dstest.Bserv())
+
+ nd := NodeWithData([]byte("foo"))
+ if err := srv.Add(ctx, nd); err != nil {
+ t.Fatal(err)
+ }
+ nds := srv.GetMany(ctx, []cid.Cid{nd.Cid(), nd.Cid(), nd.Cid()})
+ out, ok := <-nds
+ if !ok {
+ t.Fatal("expecting node foo")
+ }
+ if out.Err != nil {
+ t.Fatal(out.Err)
+ }
+ if !out.Node.Cid().Equals(nd.Cid()) {
+ t.Fatal("got wrong node")
+ }
+ out, ok = <-nds
+ if ok {
+ if out.Err != nil {
+ t.Fatal(out.Err)
+ } else {
+ t.Fatal("expecting no more nodes")
+ }
+ }
+}
+
+func TestEnumerateAsyncFailsNotFound(t *testing.T) {
+ ctx := context.Background()
+
+ a := NodeWithData([]byte("foo1"))
+ b := NodeWithData([]byte("foo2"))
+ c := NodeWithData([]byte("foo3"))
+ d := NodeWithData([]byte("foo4"))
+ e := NodeWithData([]byte("foo5"))
+
+ ds := dstest.Mock()
+ for _, n := range []ipld.Node{a, b, c} {
+ err := ds.Add(ctx, n)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ parent := new(ProtoNode)
+ if err := parent.AddNodeLink("a", a); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := parent.AddNodeLink("b", b); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := parent.AddNodeLink("c", c); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := parent.AddNodeLink("d", d); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := parent.AddNodeLink("e", e); err != nil {
+ t.Fatal(err)
+ }
+
+ err := ds.Add(ctx, parent)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cset := cid.NewSet()
+ err = Walk(ctx, GetLinksDirect(ds), parent.Cid(), cset.Visit)
+ if err == nil {
+ t.Fatal("this should have failed")
+ }
+}
+
+func TestLinkSorting(t *testing.T) {
+ az := "az"
+ aaaa := "aaaa"
+ bbbb := "bbbb"
+ cccc := "cccc"
+
+ azBlk := NewRawNode([]byte(az))
+ aaaaBlk := NewRawNode([]byte(aaaa))
+ bbbbBlk := NewRawNode([]byte(bbbb))
+ ccccBlk := NewRawNode([]byte(cccc))
+ pbn := &mdpb.PBNode{
+ Links: []*mdpb.PBLink{
+ {Hash: bbbbBlk.Cid().Bytes(), Name: &bbbb},
+ {Hash: azBlk.Cid().Bytes(), Name: &az},
+ {Hash: aaaaBlk.Cid().Bytes(), Name: &aaaa},
+ {Hash: ccccBlk.Cid().Bytes(), Name: &cccc},
+ },
+ }
+ byts, err := pbn.Marshal()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ mustLookupNodeString := func(t *testing.T, node prime.Node, name string) prime.Node {
+ subNode, err := node.LookupByString(name)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return subNode
+ }
+
+ mustLookupNodeIndex := func(t *testing.T, node prime.Node, idx int64) prime.Node {
+ subNode, err := node.LookupByIndex(idx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return subNode
+ }
+
+ mustNodeAsString := func(t *testing.T, node prime.Node) string {
+ str, err := node.AsString()
+ if err != nil {
+ t.Fatal(err)
+ }
+ return str
+ }
+
+ verifyUnsortedNode := func(t *testing.T, node *ProtoNode) {
+ links := node.Links()
+ if len(links) != 4 {
+ t.Errorf("wrong number of links, expected 4 but got %d", len(links))
+ }
+ if links[0].Name != bbbb {
+ t.Errorf("expected link 0 to be 'bbbb', got %s", links[0].Name)
+ }
+ if links[1].Name != az {
+ t.Errorf("expected link 0 to be 'az', got %s", links[1].Name)
+ }
+ if links[2].Name != aaaa {
+ t.Errorf("expected link 0 to be 'aaaa', got %s", links[2].Name)
+ }
+ if links[3].Name != cccc {
+ t.Errorf("expected link 0 to be 'cccc', got %s", links[3].Name)
+ }
+
+ // check the go-ipld-prime form
+ linksNode := mustLookupNodeString(t, node, "Links")
+ if linksNode.Length() != 4 {
+ t.Errorf("(Node) wrong number of links, expected 4 but got %d", len(links))
+ }
+ if name := mustNodeAsString(t, mustLookupNodeString(t, mustLookupNodeIndex(t, linksNode, 0), "Name")); name != bbbb {
+ t.Errorf("(Node) expected link 0 to be 'bbbb', got %s", name)
+ }
+ if name := mustNodeAsString(t, mustLookupNodeString(t, mustLookupNodeIndex(t, linksNode, 1), "Name")); name != az {
+ t.Errorf("(Node) expected link 0 to be 'az', got %s", name)
+ }
+ if name := mustNodeAsString(t, mustLookupNodeString(t, mustLookupNodeIndex(t, linksNode, 2), "Name")); name != aaaa {
+ t.Errorf("(Node) expected link 0 to be 'aaaa', got %s", name)
+ }
+ if name := mustNodeAsString(t, mustLookupNodeString(t, mustLookupNodeIndex(t, linksNode, 3), "Name")); name != cccc {
+ t.Errorf("(Node) expected link 0 to be 'cccc', got %s", name)
+ }
+ }
+
+ verifySortedNode := func(t *testing.T, node *ProtoNode) {
+ links := node.Links()
+ if len(links) != 4 {
+ t.Errorf("wrong number of links, expected 4 but got %d", len(links))
+ }
+ if links[0].Name != aaaa {
+ t.Errorf("expected link 0 to be 'aaaa', got %s", links[0].Name)
+ }
+ if links[1].Name != az {
+ t.Errorf("expected link 0 to be 'az', got %s", links[1].Name)
+ }
+ if links[2].Name != bbbb {
+ t.Errorf("expected link 0 to be 'bbbb', got %s", links[2].Name)
+ }
+ if links[3].Name != cccc {
+ t.Errorf("expected link 0 to be 'cccc', got %s", links[3].Name)
+ }
+
+ // check the go-ipld-prime form
+ linksNode := mustLookupNodeString(t, node, "Links")
+ if linksNode.Length() != 4 {
+ t.Errorf("(Node) wrong number of links, expected 4 but got %d", len(links))
+ }
+ if name := mustNodeAsString(t, mustLookupNodeString(t, mustLookupNodeIndex(t, linksNode, 0), "Name")); name != aaaa {
+ t.Errorf("(Node) expected link 0 to be 'aaaa', got %s", name)
+ }
+ if name := mustNodeAsString(t, mustLookupNodeString(t, mustLookupNodeIndex(t, linksNode, 1), "Name")); name != az {
+ t.Errorf("(Node) expected link 0 to be 'az', got %s", name)
+ }
+ if name := mustNodeAsString(t, mustLookupNodeString(t, mustLookupNodeIndex(t, linksNode, 2), "Name")); name != bbbb {
+ t.Errorf("(Node) expected link 0 to be 'bbbb', got %s", name)
+ }
+ if name := mustNodeAsString(t, mustLookupNodeString(t, mustLookupNodeIndex(t, linksNode, 3), "Name")); name != cccc {
+ t.Errorf("(Node) expected link 0 to be 'cccc', got %s", name)
+ }
+ }
+
+ t.Run("decode", func(t *testing.T) {
+ node, err := DecodeProtobuf(byts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ verifyUnsortedNode(t, node)
+ })
+
+ t.Run("RawData() should not mutate, should return original form", func(t *testing.T) {
+ node, err := DecodeProtobuf(byts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ rawData := node.RawData()
+ verifyUnsortedNode(t, node)
+ if !bytes.Equal(rawData, byts) {
+ t.Error("RawData() did not return original bytes")
+ }
+ })
+
+ t.Run("Size() should not mutate", func(t *testing.T) {
+ node, err := DecodeProtobuf(byts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ sz, err := node.Size()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if sz != 182 {
+ t.Errorf("expected size to be 182, got %d", sz)
+ }
+ verifyUnsortedNode(t, node)
+ })
+
+ t.Run("GetPBNode() should not mutate, returned PBNode should be sorted", func(t *testing.T) {
+ node, err := DecodeProtobuf(byts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ rtPBNode := node.GetPBNode()
+ rtByts, err := rtPBNode.Marshal()
+ if err != nil {
+ t.Fatal(err)
+ }
+ verifyUnsortedNode(t, node)
+ rtNode, err := DecodeProtobuf(rtByts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ verifySortedNode(t, rtNode)
+ })
+
+ t.Run("add and remove link should mutate", func(t *testing.T) {
+ node, err := DecodeProtobuf(byts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = node.AddRawLink("foo", &ipld.Link{
+ Size: 10,
+ Cid: someCid,
+ }); err != nil {
+ t.Fatal(err)
+ }
+ if err = node.RemoveNodeLink("foo"); err != nil {
+ t.Fatal(err)
+ }
+ verifySortedNode(t, node)
+ })
+
+ t.Run("update link should not mutate, returned ProtoNode should be sorted", func(t *testing.T) {
+ node, err := DecodeProtobuf(byts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ newNode, err := node.UpdateNodeLink("self", node)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = newNode.RemoveNodeLink("self"); err != nil {
+ t.Fatal(err)
+ }
+ verifySortedNode(t, newNode)
+ verifyUnsortedNode(t, node)
+ })
+
+ t.Run("SetLinks() should mutate", func(t *testing.T) {
+ node, err := DecodeProtobuf(byts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ links := node.Links() // clone
+ node.SetLinks(links)
+ verifySortedNode(t, node)
+ })
+}
+
+func TestProgressIndicator(t *testing.T) {
+ testProgressIndicator(t, 5)
+}
+
+func TestProgressIndicatorNoChildren(t *testing.T) {
+ testProgressIndicator(t, 0)
+}
+
+func testProgressIndicator(t *testing.T, depth int) {
+ ds := dstest.Mock()
+
+ top, numChildren := mkDag(ds, depth)
+
+ v := new(ProgressTracker)
+ ctx := v.DeriveContext(context.Background())
+
+ err := FetchGraph(ctx, top, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if v.Value() != numChildren+1 {
+ t.Errorf("wrong number of children reported in progress indicator, expected %d, got %d",
+ numChildren+1, v.Value())
+ }
+}
+
+func mkDag(ds ipld.DAGService, depth int) (cid.Cid, int) {
+ ctx := context.Background()
+
+ totalChildren := 0
+ f := func() *ProtoNode {
+ p := new(ProtoNode)
+ buf := make([]byte, 16)
+ rand.Read(buf)
+
+ p.SetData(buf)
+ err := ds.Add(ctx, p)
+ if err != nil {
+ panic(err)
+ }
+ return p
+ }
+
+ for i := 0; i < depth; i++ {
+ thisf := f
+ f = func() *ProtoNode {
+ pn := mkNodeWithChildren(thisf, 10)
+ err := ds.Add(ctx, pn)
+ if err != nil {
+ panic(err)
+ }
+ totalChildren += 10
+ return pn
+ }
+ }
+
+ nd := f()
+ err := ds.Add(ctx, nd)
+ if err != nil {
+ panic(err)
+ }
+
+ return nd.Cid(), totalChildren
+}
+
+func mkNodeWithChildren(getChild func() *ProtoNode, width int) *ProtoNode {
+ cur := new(ProtoNode)
+
+ for i := 0; i < width; i++ {
+ c := getChild()
+ if err := cur.AddNodeLink(fmt.Sprint(i), c); err != nil {
+ panic(err)
+ }
+ }
+
+ return cur
+}
diff --git a/ipld/merkledag/node.go b/ipld/merkledag/node.go
new file mode 100644
index 0000000000..24ad8e1654
--- /dev/null
+++ b/ipld/merkledag/node.go
@@ -0,0 +1,575 @@
+package merkledag
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "math"
+ "sort"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ cid "github.com/ipfs/go-cid"
+ format "github.com/ipfs/go-ipld-format"
+ legacy "github.com/ipfs/go-ipld-legacy"
+ logging "github.com/ipfs/go-log/v2"
+ dagpb "github.com/ipld/go-codec-dagpb"
+ ipld "github.com/ipld/go-ipld-prime"
+ mh "github.com/multiformats/go-multihash"
+ mhcore "github.com/multiformats/go-multihash/core"
+)
+
+// Common errors
+var (
+ ErrNotProtobuf = fmt.Errorf("expected protobuf dag node")
+ ErrNotRawNode = fmt.Errorf("expected raw bytes node")
+ ErrLinkNotFound = fmt.Errorf("no link by that name")
+)
+
+var log = logging.Logger("merkledag")
+
+// for testing custom CidBuilders
+var zeros [256]byte
+var zeroCid = mustZeroCid()
+
+type immutableProtoNode struct {
+ encoded []byte
+ dagpb.PBNode
+}
+
+// ProtoNode represents a node in the IPFS Merkle DAG.
+// nodes have opaque data and a set of navigable links.
+// ProtoNode is a go-ipld-legacy.UniversalNode, meaning it is both
+// a go-ipld-prime node and a go-ipld-format node.
+// ProtoNode maintains compatibility with it's original implementation
+// as a go-ipld-format only node, which included some mutability, namely the
+// the ability to add/remove links in place
+//
+// TODO: We should be able to eventually replace this implementation with
+// * go-codec-dagpb for basic DagPB encode/decode to go-ipld-prime
+// * go-unixfsnode ADLs for higher level DAGPB functionality
+// For the time being however, go-unixfsnode is read only and
+// this mutable protonode implementation is needed to support go-unixfs,
+// the only library that implements both read and write for UnixFS v1.
+type ProtoNode struct {
+ links []*format.Link
+ linksDirty bool
+ data []byte
+
+ // cache encoded/marshaled value, kept to make the go-ipld-prime Node interface
+ // work (see prime.go), and to provide a cached []byte encoded form available
+ encoded *immutableProtoNode
+ cached cid.Cid
+
+ // builder specifies cid version and hashing function
+ builder cid.Builder
+}
+
+var v0CidPrefix = cid.Prefix{
+ Codec: cid.DagProtobuf,
+ MhLength: -1,
+ MhType: mh.SHA2_256,
+ Version: 0,
+}
+
+var v1CidPrefix = cid.Prefix{
+ Codec: cid.DagProtobuf,
+ MhLength: -1,
+ MhType: mh.SHA2_256,
+ Version: 1,
+}
+
+// V0CidPrefix returns a prefix for CIDv0
+func V0CidPrefix() cid.Prefix { return v0CidPrefix }
+
+// V1CidPrefix returns a prefix for CIDv1 with the default settings
+func V1CidPrefix() cid.Prefix { return v1CidPrefix }
+
+// PrefixForCidVersion returns the Protobuf prefix for a given CID version
+func PrefixForCidVersion(version int) (cid.Prefix, error) {
+ switch version {
+ case 0:
+ return v0CidPrefix, nil
+ case 1:
+ return v1CidPrefix, nil
+ default:
+ return cid.Prefix{}, fmt.Errorf("unknown CID version: %d", version)
+ }
+}
+
+// CidBuilder returns the CID Builder for this ProtoNode, it is never nil
+func (n *ProtoNode) CidBuilder() cid.Builder {
+ if n.builder == nil {
+ n.builder = v0CidPrefix
+ }
+ return n.builder
+}
+
+// SetCidBuilder sets the CID builder if it is non nil, if nil then it
+// is reset to the default value. An error will be returned if the builder
+// is not usable.
+func (n *ProtoNode) SetCidBuilder(builder cid.Builder) error {
+ if builder == nil {
+ n.builder = v0CidPrefix
+ return nil
+ }
+ switch b := builder.(type) {
+ case cid.Prefix:
+ if err := checkHasher(b.MhType, b.MhLength); err != nil {
+ return err
+ }
+ case *cid.Prefix:
+ if err := checkHasher(b.MhType, b.MhLength); err != nil {
+ return err
+ }
+ default:
+ // We have to test it's a usable hasher by invoking it and checking it
+ // doesn't error. This is only a basic check, there are still ways it may
+ // break
+ if _, err := builder.Sum(zeros[:]); err != nil {
+ return err
+ }
+ }
+ n.builder = builder.WithCodec(cid.DagProtobuf)
+ n.cached = cid.Undef
+ return nil
+}
+
+// check whether the hasher is likely to be a usable one
+func checkHasher(indicator uint64, sizeHint int) error {
+ mhLen := sizeHint
+ if mhLen <= 0 {
+ mhLen = -1
+ }
+ _, err := mhcore.GetVariableHasher(indicator, mhLen)
+ return err
+}
+
+// LinkSlice is a slice of format.Links
+type LinkSlice []*format.Link
+
+func (ls LinkSlice) Len() int { return len(ls) }
+func (ls LinkSlice) Swap(a, b int) { ls[a], ls[b] = ls[b], ls[a] }
+func (ls LinkSlice) Less(a, b int) bool { return ls[a].Name < ls[b].Name }
+
+// NodeWithData builds a new Protonode with the given data.
+func NodeWithData(d []byte) *ProtoNode {
+ return &ProtoNode{data: d}
+}
+
+// AddNodeLink adds a link to another node. The link will be added in
+// sorted order.
+//
+// If sorting has not already been applied to this node (because
+// it was deserialized from a form that did not have sorted links), the links
+// list will be sorted. If a ProtoNode was deserialized from a badly encoded
+// form that did not already have its links sorted, calling AddNodeLink and then
+// RemoveNodeLink for the same link, will not result in an identically encoded
+// form as the links will have been sorted.
+func (n *ProtoNode) AddNodeLink(name string, that format.Node) error {
+ lnk, err := format.MakeLink(that)
+ if err != nil {
+ return err
+ }
+
+ lnk.Name = name
+
+ n.AddRawLink(name, lnk)
+
+ return nil
+}
+
+// AddRawLink adds a copy of a link to this node. The link will be added in
+// sorted order.
+//
+// If sorting has not already been applied to this node (because
+// it was deserialized from a form that did not have sorted links), the links
+// list will be sorted. If a ProtoNode was deserialized from a badly encoded
+// form that did not already have its links sorted, calling AddRawLink and then
+// RemoveNodeLink for the same link, will not result in an identically encoded
+// form as the links will have been sorted.
+func (n *ProtoNode) AddRawLink(name string, l *format.Link) error {
+ lnk := &format.Link{
+ Name: name,
+ Size: l.Size,
+ Cid: l.Cid,
+ }
+ if err := checkLink(lnk); err != nil {
+ return err
+ }
+ n.links = append(n.links, lnk)
+ n.linksDirty = true // needs a sort
+ n.encoded = nil
+ return nil
+}
+
+// RemoveNodeLink removes a link on this node by the given name. If there are
+// no links with this name, ErrLinkNotFound will be returned. If there are more
+// than one link with this name, they will all be removed.
+func (n *ProtoNode) RemoveNodeLink(name string) error {
+ ref := n.links[:0]
+ found := false
+
+ for _, v := range n.links {
+ if v.Name != name {
+ ref = append(ref, v)
+ } else {
+ found = true
+ }
+ }
+
+ if !found {
+ return ErrLinkNotFound
+ }
+
+ n.links = ref
+ // Even though a removal won't change sorting, this node may have come from
+ // a deserialized state with badly sorted links. Now that we are mutating,
+ // we need to ensure the resulting link list is sorted when it gets consumed.
+ n.linksDirty = true
+ n.encoded = nil
+
+ return nil
+}
+
+// GetNodeLink returns a copy of the link with the given name.
+func (n *ProtoNode) GetNodeLink(name string) (*format.Link, error) {
+ for _, l := range n.links {
+ if l.Name == name {
+ return &format.Link{
+ Name: l.Name,
+ Size: l.Size,
+ Cid: l.Cid,
+ }, nil
+ }
+ }
+ return nil, ErrLinkNotFound
+}
+
+// GetLinkedProtoNode returns a copy of the ProtoNode with the given name.
+func (n *ProtoNode) GetLinkedProtoNode(ctx context.Context, ds format.DAGService, name string) (*ProtoNode, error) {
+ nd, err := n.GetLinkedNode(ctx, ds, name)
+ if err != nil {
+ return nil, err
+ }
+
+ pbnd, ok := nd.(*ProtoNode)
+ if !ok {
+ return nil, ErrNotProtobuf
+ }
+
+ return pbnd, nil
+}
+
+// GetLinkedNode returns a copy of the IPLD Node with the given name.
+func (n *ProtoNode) GetLinkedNode(ctx context.Context, ds format.DAGService, name string) (format.Node, error) {
+ lnk, err := n.GetNodeLink(name)
+ if err != nil {
+ return nil, err
+ }
+
+ return lnk.GetNode(ctx, ds)
+}
+
+// Copy returns a copy of the node. The resulting node will have a properly
+// sorted Links list regardless of whether the original came from a badly
+// serialized form that didn't have a sorted list.
+// NOTE: This does not make copies of Node objects in the links.
+func (n *ProtoNode) Copy() format.Node {
+ nnode := new(ProtoNode)
+ if len(n.data) > 0 {
+ nnode.data = make([]byte, len(n.data))
+ copy(nnode.data, n.data)
+ }
+
+ if len(n.links) > 0 {
+ nnode.links = append([]*format.Link(nil), n.links...)
+ // Sort links regardless of linksDirty state, this may have come from a
+ // serialized form that had badly sorted links, in which case linksDirty
+ // will not be true.
+ sort.Stable(LinkSlice(nnode.links))
+ }
+
+ nnode.builder = n.builder
+
+ return nnode
+}
+
+// RawData returns the encoded byte form of this node.
+//
+// Note that this method may return an empty byte slice if there is an error
+// performing the encode. To check whether such an error may have occurred, use
+// node.EncodeProtobuf(false), instead (or prior to calling RawData) and check
+// for its returned error value; the result of EncodeProtobuf is cached so there
+// is minimal overhead when invoking both methods.
+func (n *ProtoNode) RawData() []byte {
+ out, err := n.EncodeProtobuf(false)
+ if err != nil {
+ log.Errorf("failed to encode dag-pb block: %s", err.Error())
+ return nil
+ }
+ return out
+}
+
+// Data returns the data stored by this node.
+func (n *ProtoNode) Data() []byte {
+ return n.data
+}
+
+// SetData stores data in this nodes.
+func (n *ProtoNode) SetData(d []byte) {
+ n.encoded = nil
+ n.cached = cid.Undef
+ n.data = d
+}
+
+// UpdateNodeLink return a copy of the node with the link name set to point to
+// that. The link will be added in sorted order. If a link of the same name
+// existed, it is removed.
+//
+// If sorting has not already been applied to this node (because
+// it was deserialized from a form that did not have sorted links), the links
+// list will be sorted in the returned copy.
+func (n *ProtoNode) UpdateNodeLink(name string, that *ProtoNode) (*ProtoNode, error) {
+ newnode := n.Copy().(*ProtoNode)
+ _ = newnode.RemoveNodeLink(name) // ignore error
+ err := newnode.AddNodeLink(name, that)
+ return newnode, err
+}
+
+// Size returns the total size of the data addressed by node,
+// including the total sizes of references.
+func (n *ProtoNode) Size() (uint64, error) {
+ b, err := n.EncodeProtobuf(false)
+ if err != nil {
+ return 0, err
+ }
+
+ s := uint64(len(b))
+ for _, l := range n.links {
+ s += l.Size
+ }
+ return s, nil
+}
+
+// Stat returns statistics on the node.
+func (n *ProtoNode) Stat() (*format.NodeStat, error) {
+ enc, err := n.EncodeProtobuf(false)
+ if err != nil {
+ return nil, err
+ }
+
+ cumSize, err := n.Size()
+ if err != nil {
+ return nil, err
+ }
+
+ return &format.NodeStat{
+ Hash: n.Cid().String(),
+ NumLinks: len(n.links),
+ BlockSize: len(enc),
+ LinksSize: len(enc) - len(n.data), // includes framing.
+ DataSize: len(n.data),
+ CumulativeSize: int(cumSize),
+ }, nil
+}
+
+// Loggable implements the ipfs/go-log.Loggable interface.
+func (n *ProtoNode) Loggable() map[string]interface{} {
+ return map[string]interface{}{
+ "node": n.String(),
+ }
+}
+
+// UnmarshalJSON reads the node fields from a JSON-encoded byte slice.
+func (n *ProtoNode) UnmarshalJSON(b []byte) error {
+ s := struct {
+ Data []byte `json:"data"`
+ Links []*format.Link `json:"links"`
+ }{}
+
+ err := json.Unmarshal(b, &s)
+ if err != nil {
+ return err
+ }
+
+ n.data = s.Data
+ // Links may not be sorted after deserialization, but we don't change
+ // them until we mutate this node since we're representing the current,
+ // as-serialized state. So n.linksDirty is not set here.
+ n.links = s.Links
+ for _, lnk := range s.Links {
+ if err := checkLink(lnk); err != nil {
+ return err
+ }
+ }
+
+ n.encoded = nil
+ return nil
+}
+
+func checkLink(lnk *format.Link) error {
+ if lnk.Size > math.MaxInt64 {
+ return fmt.Errorf("value of Tsize is too large: %d", lnk.Size)
+ }
+ if !lnk.Cid.Defined() {
+ return errors.New("link must have a value Cid value")
+ }
+ return nil
+}
+
+// MarshalJSON returns a JSON representation of the node.
+func (n *ProtoNode) MarshalJSON() ([]byte, error) {
+ if n.linksDirty {
+ // there was a mutation involving links, make sure we sort
+ sort.Stable(LinkSlice(n.links))
+ n.linksDirty = false
+ n.encoded = nil
+ }
+
+ out := map[string]interface{}{
+ "data": n.data,
+ "links": n.links,
+ }
+
+ return json.Marshal(out)
+}
+
+// Cid returns the node's Cid, calculated according to its prefix
+// and raw data contents.
+//
+// Note that this method may return a CID representing a zero-length byte slice
+// if there is an error performing the encode. To check whether such an error
+// may have occurred, use node.EncodeProtobuf(false), instead (or prior to
+// calling RawData) and check for its returned error value; the result of
+// EncodeProtobuf is cached so there is minimal overhead when invoking both
+// methods.
+func (n *ProtoNode) Cid() cid.Cid {
+ // re-encode if necessary and we'll get a new cached CID
+ if _, err := n.EncodeProtobuf(false); err != nil {
+ log.Errorf("failed to encode dag-pb block: %s", err.Error())
+ // error, return a zero-CID
+ c, err := n.CidBuilder().Sum([]byte{})
+ if err != nil {
+ // CidBuilder was a source of error, return _the_ dag-pb zero CIDv1
+ return zeroCid
+ }
+ return c
+ }
+ return n.cached
+}
+
+// String prints the node's Cid.
+//
+// Note that this method may return a CID representing a zero-length byte slice
+// if there is an error performing the encode. To check whether such an error
+// may have occurred, use node.EncodeProtobuf(false), instead (or prior to
+// calling RawData) and check for its returned error value; the result of
+// EncodeProtobuf is cached so there is minimal overhead when invoking both
+// methods.
+func (n *ProtoNode) String() string {
+ return n.Cid().String()
+}
+
+// Multihash hashes the encoded data of this node.
+//
+// Note that this method may return a multihash representing a zero-length byte
+// slice if there is an error performing the encode. To check whether such an
+// error may have occurred, use node.EncodeProtobuf(false), instead (or prior to
+// calling RawData) and check for its returned error value; the result of
+// EncodeProtobuf is cached so there is minimal overhead when invoking both
+// methods.
+func (n *ProtoNode) Multihash() mh.Multihash {
+ return n.Cid().Hash()
+}
+
+// Links returns a copy of the node's links.
+func (n *ProtoNode) Links() []*format.Link {
+ if n.linksDirty {
+ // there was a mutation involving links, make sure we sort
+ sort.Stable(LinkSlice(n.links))
+ n.linksDirty = false
+ n.encoded = nil
+ }
+ return append([]*format.Link(nil), n.links...)
+}
+
+// SetLinks replaces the node links with a copy of the provided links. Sorting
+// will be applied to the list.
+func (n *ProtoNode) SetLinks(links []*format.Link) error {
+ for _, lnk := range links {
+ if err := checkLink(lnk); err != nil {
+ return err
+ }
+ }
+ n.links = append([]*format.Link(nil), links...)
+ n.linksDirty = true // needs a sort
+ n.encoded = nil
+ return nil
+}
+
+// Resolve is an alias for ResolveLink.
+func (n *ProtoNode) Resolve(path []string) (interface{}, []string, error) {
+ return n.ResolveLink(path)
+}
+
+// ResolveLink consumes the first element of the path and obtains the link
+// corresponding to it from the node. It returns the link
+// and the path without the consumed element.
+func (n *ProtoNode) ResolveLink(path []string) (*format.Link, []string, error) {
+ if len(path) == 0 {
+ return nil, nil, fmt.Errorf("end of path, no more links to resolve")
+ }
+
+ lnk, err := n.GetNodeLink(path[0])
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return lnk, path[1:], nil
+}
+
+// Tree returns the link names of the ProtoNode.
+// ProtoNodes are only ever one path deep, so anything different than an empty
+// string for p results in nothing. The depth parameter is ignored.
+func (n *ProtoNode) Tree(p string, depth int) []string {
+ if p != "" {
+ return nil
+ }
+
+ if n.linksDirty {
+ // there was a mutation involving links, make sure we sort
+ sort.Stable(LinkSlice(n.links))
+ n.linksDirty = false
+ n.encoded = nil
+ }
+
+ out := make([]string, 0, len(n.links))
+ for _, lnk := range n.links {
+ out = append(out, lnk.Name)
+ }
+ return out
+}
+
+func ProtoNodeConverter(b blocks.Block, nd ipld.Node) (legacy.UniversalNode, error) {
+ pbNode, ok := nd.(dagpb.PBNode)
+ if !ok {
+ return nil, ErrNotProtobuf
+ }
+ encoded := &immutableProtoNode{b.RawData(), pbNode}
+ pn := fromImmutableNode(encoded)
+ pn.cached = b.Cid()
+ pn.builder = b.Cid().Prefix()
+ return pn, nil
+}
+
+// TODO: replace with cid.MustParse() when we bump go-cid
+func mustZeroCid() cid.Cid {
+ c, err := cid.Parse("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku")
+ if err != nil {
+ panic(err)
+ }
+ return c
+}
+
+var _ legacy.UniversalNode = &ProtoNode{}
diff --git a/ipld/merkledag/node_test.go b/ipld/merkledag/node_test.go
new file mode 100644
index 0000000000..87fb484e87
--- /dev/null
+++ b/ipld/merkledag/node_test.go
@@ -0,0 +1,194 @@
+package merkledag_test
+
+import (
+ "bytes"
+ "context"
+ "testing"
+
+ . "github.com/ipfs/boxo/ipld/merkledag"
+ mdtest "github.com/ipfs/boxo/ipld/merkledag/test"
+
+ cid "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+var sampleCid cid.Cid
+
+func init() {
+ var err error
+ // make a test CID -- doesn't matter just to add as a link
+ sampleCid, err = cid.Cast([]byte{1, 85, 0, 5, 0, 1, 2, 3, 4})
+ if err != nil {
+ panic(err)
+ }
+}
+
+func TestStableCID(t *testing.T) {
+ nd := &ProtoNode{}
+ nd.SetData([]byte("foobar"))
+ nd.SetLinks([]*ipld.Link{
+ {Name: "a", Cid: sampleCid},
+ {Name: "b", Cid: sampleCid},
+ {Name: "c", Cid: sampleCid},
+ })
+ expected, err := cid.Decode("QmciCHWD9Q47VPX6naY3XsPZGnqVqbedAniGCcaHjBaCri")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !nd.Cid().Equals(expected) {
+ t.Fatalf("Got CID %s, expected CID %s", nd.Cid(), expected)
+ }
+}
+
+func TestRemoveLink(t *testing.T) {
+ nd := &ProtoNode{}
+ nd.SetLinks([]*ipld.Link{
+ {Name: "a", Cid: sampleCid},
+ {Name: "b", Cid: sampleCid},
+ {Name: "a", Cid: sampleCid},
+ {Name: "a", Cid: sampleCid},
+ {Name: "c", Cid: sampleCid},
+ {Name: "a", Cid: sampleCid},
+ })
+
+ err := nd.RemoveNodeLink("a")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(nd.Links()) != 2 {
+ t.Fatal("number of links incorrect")
+ }
+
+ if nd.Links()[0].Name != "b" {
+ t.Fatal("link order wrong")
+ }
+
+ if nd.Links()[1].Name != "c" {
+ t.Fatal("link order wrong")
+ }
+
+ // should fail
+ err = nd.RemoveNodeLink("a")
+ if err != ErrLinkNotFound {
+ t.Fatal("should have failed to remove link")
+ }
+
+ // ensure nothing else got touched
+ if len(nd.Links()) != 2 {
+ t.Fatal("number of links incorrect")
+ }
+
+ if nd.Links()[0].Name != "b" {
+ t.Fatal("link order wrong")
+ }
+
+ if nd.Links()[1].Name != "c" {
+ t.Fatal("link order wrong")
+ }
+}
+
+func TestFindLink(t *testing.T) {
+ ctx := context.Background()
+
+ ds := mdtest.Mock()
+ ndEmpty := new(ProtoNode)
+ err := ds.Add(ctx, ndEmpty)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ kEmpty := ndEmpty.Cid()
+
+ nd := &ProtoNode{}
+ nd.SetLinks([]*ipld.Link{
+ {Name: "a", Cid: kEmpty},
+ {Name: "c", Cid: kEmpty},
+ {Name: "b", Cid: kEmpty},
+ })
+
+ err = ds.Add(ctx, nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ lnk, err := nd.GetNodeLink("b")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if lnk.Name != "b" {
+ t.Fatal("got wrong link back")
+ }
+
+ _, err = nd.GetNodeLink("f")
+ if err != ErrLinkNotFound {
+ t.Fatal("shouldnt have found link")
+ }
+
+ _, err = nd.GetLinkedNode(context.Background(), ds, "b")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ outnd, err := nd.UpdateNodeLink("b", nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ olnk, err := outnd.GetNodeLink("b")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if olnk.Cid.String() == kEmpty.String() {
+ t.Fatal("new link should have different hash")
+ }
+}
+
+func TestNodeCopy(t *testing.T) {
+ nd := &ProtoNode{}
+ nd.SetLinks([]*ipld.Link{
+ {Name: "a", Cid: sampleCid},
+ {Name: "c", Cid: sampleCid},
+ {Name: "b", Cid: sampleCid},
+ })
+
+ nd.SetData([]byte("testing"))
+
+ ond := nd.Copy().(*ProtoNode)
+ ond.SetData(nil)
+
+ if nd.Data() == nil {
+ t.Fatal("should be different objects")
+ }
+}
+
+func TestJsonRoundtrip(t *testing.T) {
+ nd := new(ProtoNode)
+ nd.SetLinks([]*ipld.Link{
+ {Name: "a", Cid: sampleCid},
+ {Name: "c", Cid: sampleCid},
+ {Name: "b", Cid: sampleCid},
+ })
+ nd.SetData([]byte("testing"))
+
+ jb, err := nd.MarshalJSON()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nn := new(ProtoNode)
+ err = nn.UnmarshalJSON(jb)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !bytes.Equal(nn.Data(), nd.Data()) {
+ t.Fatal("data wasnt the same")
+ }
+
+ if !nn.Cid().Equals(nd.Cid()) {
+ t.Fatal("objects differed after marshaling")
+ }
+}
diff --git a/ipld/merkledag/pb/compat_test.go b/ipld/merkledag/pb/compat_test.go
new file mode 100644
index 0000000000..529cee0cad
--- /dev/null
+++ b/ipld/merkledag/pb/compat_test.go
@@ -0,0 +1,299 @@
+package merkledag_pb
+
+// mirrored in JavaScript @ https://github.com/ipld/js-dag-pb/blob/master/test/test-compat.js
+
+import (
+ "encoding/hex"
+ "encoding/json"
+ "testing"
+)
+
+var dataZero []byte = make([]byte, 0)
+var dataSome []byte = []byte{0, 1, 2, 3, 4}
+var cidBytes []byte = []byte{1, 85, 0, 5, 0, 1, 2, 3, 4}
+var zeroName string = ""
+var someName string = "some name"
+var zeroTsize uint64 = 0
+var someTsize uint64 = 1010
+var largeTsize uint64 = 9007199254740991 // JavaScript Number.MAX_SAFE_INTEGER
+
+type testCase struct {
+ name string
+ node *PBNode
+ expectedBytes string
+ expectedForm string
+}
+
+var testCases = []testCase{
+ {
+ name: "empty",
+ node: &PBNode{},
+ expectedBytes: "",
+ expectedForm: "{}",
+ },
+ {
+ name: "Data zero",
+ node: &PBNode{Data: dataZero},
+ expectedBytes: "0a00",
+ expectedForm: `{
+ "Data": ""
+}`,
+ },
+ {
+ name: "Data some",
+ node: &PBNode{Data: dataSome},
+ expectedBytes: "0a050001020304",
+ expectedForm: `{
+ "Data": "0001020304"
+}`,
+ },
+ {
+ name: "Links zero",
+ node: &PBNode{Links: make([]*PBLink, 0)},
+ expectedBytes: "",
+ expectedForm: "{}",
+ },
+ {
+ name: "Data some Links zero",
+ node: &PBNode{Data: dataSome, Links: make([]*PBLink, 0)},
+ expectedBytes: "0a050001020304",
+ expectedForm: `{
+ "Data": "0001020304"
+}`,
+ },
+ {
+ name: "Links empty",
+ node: &PBNode{Links: []*PBLink{{}}},
+ expectedBytes: "1200",
+ expectedForm: `{
+ "Links": [
+ {}
+ ]
+}`,
+ },
+ {
+ name: "Data some Links empty",
+ node: &PBNode{Data: dataSome, Links: []*PBLink{{}}},
+ expectedBytes: "12000a050001020304",
+ expectedForm: `{
+ "Data": "0001020304",
+ "Links": [
+ {}
+ ]
+}`,
+ },
+ {
+ name: "Links Hash zero",
+ node: &PBNode{Links: []*PBLink{{Hash: dataZero}}},
+ expectedBytes: "12020a00",
+ expectedForm: `{
+ "Links": [
+ {
+ "Hash": ""
+ }
+ ]
+}`,
+ },
+ {
+ name: "Links Hash some",
+ node: &PBNode{Links: []*PBLink{{Hash: cidBytes}}},
+ expectedBytes: "120b0a09015500050001020304",
+ expectedForm: `{
+ "Links": [
+ {
+ "Hash": "015500050001020304"
+ }
+ ]
+}`,
+ },
+ {
+ name: "Links Name zero",
+ node: &PBNode{Links: []*PBLink{{Name: &zeroName}}},
+ expectedBytes: "12021200",
+ expectedForm: `{
+ "Links": [
+ {
+ "Name": ""
+ }
+ ]
+}`,
+ },
+ {
+ name: "Links Hash some Name zero",
+ node: &PBNode{Links: []*PBLink{{Hash: cidBytes, Name: &zeroName}}},
+ expectedBytes: "120d0a090155000500010203041200",
+ expectedForm: `{
+ "Links": [
+ {
+ "Hash": "015500050001020304",
+ "Name": ""
+ }
+ ]
+}`,
+ },
+ {
+ name: "Links Name some",
+ node: &PBNode{Links: []*PBLink{{Name: &someName}}},
+ expectedBytes: "120b1209736f6d65206e616d65",
+ expectedForm: `{
+ "Links": [
+ {
+ "Name": "some name"
+ }
+ ]
+}`,
+ },
+ {
+ name: "Links Hash some Name some",
+ node: &PBNode{Links: []*PBLink{{Hash: cidBytes, Name: &someName}}},
+ expectedBytes: "12160a090155000500010203041209736f6d65206e616d65",
+ expectedForm: `{
+ "Links": [
+ {
+ "Hash": "015500050001020304",
+ "Name": "some name"
+ }
+ ]
+}`,
+ },
+ {
+ name: "Links Tsize zero",
+ node: &PBNode{Links: []*PBLink{{Tsize: &zeroTsize}}},
+ expectedBytes: "12021800",
+ expectedForm: `{
+ "Links": [
+ {
+ "Tsize": 0
+ }
+ ]
+}`,
+ },
+ {
+ name: "Links Hash some Tsize zero",
+ node: &PBNode{Links: []*PBLink{{Hash: cidBytes, Tsize: &zeroTsize}}},
+ expectedBytes: "120d0a090155000500010203041800",
+ expectedForm: `{
+ "Links": [
+ {
+ "Hash": "015500050001020304",
+ "Tsize": 0
+ }
+ ]
+}`,
+ },
+ {
+ name: "Links Tsize some",
+ node: &PBNode{Links: []*PBLink{{Tsize: &someTsize}}},
+ expectedBytes: "120318f207",
+ expectedForm: `{
+ "Links": [
+ {
+ "Tsize": 1010
+ }
+ ]
+}`,
+ },
+ {
+ name: "Links Hash some Tsize some",
+ node: &PBNode{Links: []*PBLink{{Hash: cidBytes, Tsize: &largeTsize}}},
+ expectedBytes: "12140a0901550005000102030418ffffffffffffff0f",
+ expectedForm: `{
+ "Links": [
+ {
+ "Hash": "015500050001020304",
+ "Tsize": 9007199254740991
+ }
+ ]
+}`,
+ },
+}
+
+func TestCompat(t *testing.T) {
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ verifyRoundTrip(t, tc)
+ })
+ }
+}
+
+func verifyRoundTrip(t *testing.T, tc testCase) {
+ actualBytes, actualForm, err := nodeRoundTripToString(t, tc.node)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if actualBytes != tc.expectedBytes {
+ t.Logf(
+ "Expected bytes: [%v]\nGot: [%v]\n",
+ tc.expectedBytes,
+ actualBytes)
+ t.Error("Did not match")
+ }
+
+ if actualForm != tc.expectedForm {
+ t.Logf(
+ "Expected form: [%v]\nGot: [%v]\n",
+ tc.expectedForm,
+ actualForm)
+ t.Error("Did not match")
+ }
+}
+
+func nodeRoundTripToString(t *testing.T, n *PBNode) (string, string, error) {
+ bytes, err := n.Marshal()
+ if err != nil {
+ return "", "", err
+ }
+ t.Logf("[%v]\n", hex.EncodeToString(bytes))
+ rt := new(PBNode)
+ if err := rt.Unmarshal(bytes); err != nil {
+ return "", "", err
+ }
+ str, err := json.MarshalIndent(cleanPBNode(t, rt), "", "\t")
+ if err != nil {
+ return "", "", err
+ }
+ return hex.EncodeToString(bytes), string(str), nil
+}
+
+// convert a PBLink into a map for clean JSON marshalling
+func cleanPBLink(t *testing.T, link *PBLink) map[string]interface{} {
+ if link == nil {
+ return nil
+ }
+ // this would be a bad pb decode
+ if link.XXX_unrecognized != nil {
+ t.Fatal("Got unexpected XXX_unrecognized")
+ }
+ nl := make(map[string]interface{})
+ if link.Hash != nil {
+ nl["Hash"] = hex.EncodeToString(link.Hash)
+ }
+ if link.Name != nil {
+ nl["Name"] = link.Name
+ }
+ if link.Tsize != nil {
+ nl["Tsize"] = link.Tsize
+ }
+ return nl
+}
+
+// convert a PBNode into a map for clean JSON marshalling
+func cleanPBNode(t *testing.T, node *PBNode) map[string]interface{} {
+ // this would be a bad pb decode
+ if node.XXX_unrecognized != nil {
+ t.Fatal("Got unexpected XXX_unrecognized")
+ }
+ nn := make(map[string]interface{})
+ if node.Data != nil {
+ nn["Data"] = hex.EncodeToString(node.Data)
+ }
+ if node.Links != nil {
+ links := make([]map[string]interface{}, len(node.Links))
+ for i, l := range node.Links {
+ links[i] = cleanPBLink(t, l)
+ }
+ nn["Links"] = links
+ }
+ return nn
+}
diff --git a/ipld/merkledag/pb/merkledag.pb.go b/ipld/merkledag/pb/merkledag.pb.go
new file mode 100644
index 0000000000..45806ce92d
--- /dev/null
+++ b/ipld/merkledag/pb/merkledag.pb.go
@@ -0,0 +1,1062 @@
+// Code originally generated by protoc-gen-gogo from merkledag.proto,
+// now manually managed
+
+package merkledag_pb
+
+import (
+ bytes "bytes"
+ fmt "fmt"
+ io "io"
+ math "math"
+ math_bits "math/bits"
+ reflect "reflect"
+ strings "strings"
+
+ _ "github.com/gogo/protobuf/gogoproto"
+ proto "github.com/gogo/protobuf/proto"
+)
+
+// DoNotUpgradeFileEverItWillChangeYourHashes warns users about not breaking
+// their file hashes.
+const DoNotUpgradeFileEverItWillChangeYourHashes = `
+This file does not produce canonical protobufs. Unfortunately, if we change it,
+we'll change the hashes of the files we produce.
+
+Do *not regenerate this file.
+`
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
+
+// An IPFS MerkleDAG Link
+type PBLink struct {
+ // multihash of the target object
+ Hash []byte `protobuf:"bytes,1,opt,name=Hash" json:"Hash,omitempty"`
+ // utf string name. should be unique per object
+ Name *string `protobuf:"bytes,2,opt,name=Name" json:"Name,omitempty"`
+ // cumulative size of target object
+ Tsize *uint64 `protobuf:"varint,3,opt,name=Tsize" json:"Tsize,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *PBLink) Reset() { *m = PBLink{} }
+func (*PBLink) ProtoMessage() {}
+func (*PBLink) Descriptor() ([]byte, []int) {
+ return fileDescriptor_10837cc3557cec00, []int{0}
+}
+func (m *PBLink) XXX_Unmarshal(b []byte) error {
+ return m.Unmarshal(b)
+}
+func (m *PBLink) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ if deterministic {
+ return xxx_messageInfo_PBLink.Marshal(b, m, deterministic)
+ } else {
+ b = b[:cap(b)]
+ n, err := m.MarshalToSizedBuffer(b)
+ if err != nil {
+ return nil, err
+ }
+ return b[:n], nil
+ }
+}
+func (m *PBLink) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_PBLink.Merge(m, src)
+}
+func (m *PBLink) XXX_Size() int {
+ return m.Size()
+}
+func (m *PBLink) XXX_DiscardUnknown() {
+ xxx_messageInfo_PBLink.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_PBLink proto.InternalMessageInfo
+
+func (m *PBLink) GetHash() []byte {
+ if m != nil {
+ return m.Hash
+ }
+ return nil
+}
+
+func (m *PBLink) GetName() string {
+ if m != nil && m.Name != nil {
+ return *m.Name
+ }
+ return ""
+}
+
+func (m *PBLink) GetTsize() uint64 {
+ if m != nil && m.Tsize != nil {
+ return *m.Tsize
+ }
+ return 0
+}
+
+// An IPFS MerkleDAG Node
+type PBNode struct {
+ // refs to other objects
+ Links []*PBLink `protobuf:"bytes,2,rep,name=Links" json:"Links,omitempty"`
+ // opaque user data
+ Data []byte `protobuf:"bytes,1,opt,name=Data" json:"Data,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *PBNode) Reset() { *m = PBNode{} }
+func (*PBNode) ProtoMessage() {}
+func (*PBNode) Descriptor() ([]byte, []int) {
+ return fileDescriptor_10837cc3557cec00, []int{1}
+}
+func (m *PBNode) XXX_Unmarshal(b []byte) error {
+ return m.Unmarshal(b)
+}
+func (m *PBNode) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ if deterministic {
+ return xxx_messageInfo_PBNode.Marshal(b, m, deterministic)
+ } else {
+ b = b[:cap(b)]
+ n, err := m.MarshalToSizedBuffer(b)
+ if err != nil {
+ return nil, err
+ }
+ return b[:n], nil
+ }
+}
+func (m *PBNode) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_PBNode.Merge(m, src)
+}
+func (m *PBNode) XXX_Size() int {
+ return m.Size()
+}
+func (m *PBNode) XXX_DiscardUnknown() {
+ xxx_messageInfo_PBNode.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_PBNode proto.InternalMessageInfo
+
+func (m *PBNode) GetLinks() []*PBLink {
+ if m != nil {
+ return m.Links
+ }
+ return nil
+}
+
+func (m *PBNode) GetData() []byte {
+ if m != nil {
+ return m.Data
+ }
+ return nil
+}
+
+func init() {
+ proto.RegisterType((*PBLink)(nil), "merkledag.pb.PBLink")
+ proto.RegisterType((*PBNode)(nil), "merkledag.pb.PBNode")
+}
+
+func init() { proto.RegisterFile("merkledag.proto", fileDescriptor_10837cc3557cec00) }
+
+var fileDescriptor_10837cc3557cec00 = []byte{
+ // 227 bytes of a gzipped FileDescriptorProto
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xcf, 0x4d, 0x2d, 0xca,
+ 0xce, 0x49, 0x4d, 0x49, 0x4c, 0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x41, 0x12, 0x48,
+ 0x92, 0xd2, 0x4d, 0xcf, 0x2c, 0xc9, 0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x4f, 0xcf, 0x4f,
+ 0xcf, 0xd7, 0x07, 0x2b, 0x4a, 0x2a, 0x4d, 0x03, 0xf3, 0xc0, 0x1c, 0x30, 0x0b, 0xa2, 0x59, 0xc9,
+ 0x8d, 0x8b, 0x2d, 0xc0, 0xc9, 0x27, 0x33, 0x2f, 0x5b, 0x48, 0x88, 0x8b, 0xc5, 0x23, 0xb1, 0x38,
+ 0x43, 0x82, 0x51, 0x81, 0x51, 0x83, 0x27, 0x08, 0xcc, 0x06, 0x89, 0xf9, 0x25, 0xe6, 0xa6, 0x4a,
+ 0x30, 0x29, 0x30, 0x6a, 0x70, 0x06, 0x81, 0xd9, 0x42, 0x22, 0x5c, 0xac, 0x21, 0xc5, 0x99, 0x55,
+ 0xa9, 0x12, 0xcc, 0x0a, 0x8c, 0x1a, 0x2c, 0x41, 0x10, 0x8e, 0x92, 0x07, 0xc8, 0x1c, 0xbf, 0xfc,
+ 0x94, 0x54, 0x21, 0x2d, 0x2e, 0x56, 0x90, 0x79, 0xc5, 0x12, 0x4c, 0x0a, 0xcc, 0x1a, 0xdc, 0x46,
+ 0x22, 0x7a, 0xc8, 0xce, 0xd3, 0x83, 0x58, 0x16, 0x04, 0x51, 0x02, 0x32, 0xdf, 0x25, 0xb1, 0x24,
+ 0x11, 0x66, 0x27, 0x88, 0xed, 0xa4, 0x73, 0xe3, 0xa1, 0x1c, 0xc3, 0x83, 0x87, 0x72, 0x8c, 0x1f,
+ 0x1e, 0xca, 0x31, 0xfe, 0x78, 0x28, 0xc7, 0xd8, 0xf0, 0x48, 0x8e, 0x71, 0xc5, 0x23, 0x39, 0xc6,
+ 0x1d, 0x8f, 0xe4, 0x18, 0x0f, 0x3c, 0x92, 0x63, 0x3c, 0xf1, 0x48, 0x8e, 0xf1, 0xc2, 0x23, 0x39,
+ 0xc6, 0x07, 0x8f, 0xe4, 0x18, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0x1d, 0x56, 0xb5, 0x6e, 0x0e,
+ 0x01, 0x00, 0x00,
+}
+
+func (pbLink *PBLink) VerboseEqual(that interface{}) error {
+ if that == nil {
+ if pbLink == nil {
+ return nil
+ }
+ return fmt.Errorf("that == nil && this != nil")
+ }
+
+ that1, ok := that.(*PBLink)
+ if !ok {
+ that2, ok := that.(PBLink)
+ if ok {
+ that1 = &that2
+ } else {
+ return fmt.Errorf("that is not of type *PBLink")
+ }
+ }
+ if that1 == nil {
+ if pbLink == nil {
+ return nil
+ }
+ return fmt.Errorf("that is type *PBLink but is nil && this != nil")
+ } else if pbLink == nil {
+ return fmt.Errorf("that is type *PBLink but is not nil && this == nil")
+ }
+ if !bytes.Equal(pbLink.Hash, that1.Hash) {
+ return fmt.Errorf("this.Hash(%v) is not equal to that.Hash(%v)", pbLink.Hash, that1.Hash)
+ }
+ if pbLink.Name != nil && that1.Name != nil {
+ if *pbLink.Name != *that1.Name {
+ return fmt.Errorf("this.Name(%v) is not equal to that.Name(%v)", *pbLink.Name, *that1.Name)
+ }
+ } else if pbLink.Name != nil {
+ return fmt.Errorf("this.Name == nil && that.Name != nil")
+ } else if that1.Name != nil {
+ return fmt.Errorf("this.Name(%v) is not equal to that.Name(%v)", pbLink.Name, that1.Name)
+ }
+ if pbLink.Tsize != nil && that1.Tsize != nil {
+ if *pbLink.Tsize != *that1.Tsize {
+ return fmt.Errorf("this.Tsize(%v) is not equal to that.Tsize(%v)", *pbLink.Tsize, *that1.Tsize)
+ }
+ } else if pbLink.Tsize != nil {
+ return fmt.Errorf("this.Tsize == nil && that.Tsize != nil")
+ } else if that1.Tsize != nil {
+ return fmt.Errorf("this.Tsize(%v) is not equal to that.Tsize(%v)", pbLink.Tsize, that1.Tsize)
+ }
+ if !bytes.Equal(pbLink.XXX_unrecognized, that1.XXX_unrecognized) {
+ return fmt.Errorf("XXX_unrecognized this(%v) is not equal to that(%v)", pbLink.XXX_unrecognized, that1.XXX_unrecognized)
+ }
+ return nil
+}
+func (pbLink *PBLink) Equal(that interface{}) bool {
+ if that == nil {
+ return pbLink == nil
+ }
+
+ that1, ok := that.(*PBLink)
+ if !ok {
+ that2, ok := that.(PBLink)
+ if ok {
+ that1 = &that2
+ } else {
+ return false
+ }
+ }
+ if that1 == nil {
+ return pbLink == nil
+ } else if pbLink == nil {
+ return false
+ }
+ if !bytes.Equal(pbLink.Hash, that1.Hash) {
+ return false
+ }
+ if pbLink.Name != nil && that1.Name != nil {
+ if *pbLink.Name != *that1.Name {
+ return false
+ }
+ } else if pbLink.Name != nil {
+ return false
+ } else if that1.Name != nil {
+ return false
+ }
+ if pbLink.Tsize != nil && that1.Tsize != nil {
+ if *pbLink.Tsize != *that1.Tsize {
+ return false
+ }
+ } else if pbLink.Tsize != nil {
+ return false
+ } else if that1.Tsize != nil {
+ return false
+ }
+ if !bytes.Equal(pbLink.XXX_unrecognized, that1.XXX_unrecognized) {
+ return false
+ }
+ return true
+}
+func (pbLink *PBNode) VerboseEqual(that interface{}) error {
+ if that == nil {
+ if pbLink == nil {
+ return nil
+ }
+ return fmt.Errorf("that == nil && this != nil")
+ }
+
+ that1, ok := that.(*PBNode)
+ if !ok {
+ that2, ok := that.(PBNode)
+ if ok {
+ that1 = &that2
+ } else {
+ return fmt.Errorf("that is not of type *PBNode")
+ }
+ }
+ if that1 == nil {
+ if pbLink == nil {
+ return nil
+ }
+ return fmt.Errorf("that is type *PBNode but is nil && this != nil")
+ } else if pbLink == nil {
+ return fmt.Errorf("that is type *PBNode but is not nil && this == nil")
+ }
+ if len(pbLink.Links) != len(that1.Links) {
+ return fmt.Errorf("len(this.Links)(%v) is not equal to len(that.Links)(%v)", len(pbLink.Links), len(that1.Links))
+ }
+ for i := range pbLink.Links {
+ if !pbLink.Links[i].Equal(that1.Links[i]) {
+ return fmt.Errorf("this.Links[%v](%v) is not equal to that.Links[%v](%v)", i, pbLink.Links[i], i, that1.Links[i])
+ }
+ }
+ if !bytes.Equal(pbLink.Data, that1.Data) {
+ return fmt.Errorf("this.Data(%v) is not equal to that.Data(%v)", pbLink.Data, that1.Data)
+ }
+ if !bytes.Equal(pbLink.XXX_unrecognized, that1.XXX_unrecognized) {
+ return fmt.Errorf("this.XXX_unrecognized(%v) is not equal to that.XXX_unrecognized(%v)", pbLink.XXX_unrecognized, that1.XXX_unrecognized)
+ }
+ return nil
+}
+
+func (pbNode *PBNode) Equal(that interface{}) bool {
+ if that == nil {
+ return pbNode == nil
+ }
+
+ that1, ok := that.(*PBNode)
+ if !ok {
+ that2, ok := that.(PBNode)
+ if ok {
+ that1 = &that2
+ } else {
+ return false
+ }
+ }
+ if that1 == nil {
+ return pbNode == nil
+ } else if pbNode == nil {
+ return false
+ }
+ if len(pbNode.Links) != len(that1.Links) {
+ return false
+ }
+ for i := range pbNode.Links {
+ if !pbNode.Links[i].Equal(that1.Links[i]) {
+ return false
+ }
+ }
+ if !bytes.Equal(pbNode.Data, that1.Data) {
+ return false
+ }
+ if !bytes.Equal(pbNode.XXX_unrecognized, that1.XXX_unrecognized) {
+ return false
+ }
+ return true
+}
+func (pbLink *PBLink) GoString() string {
+ if pbLink == nil {
+ return "nil"
+ }
+ s := make([]string, 0, 7)
+ s = append(s, "&merkledag_pb.PBLink{")
+ if pbLink.Hash != nil {
+ s = append(s, "Hash: "+valueToGoStringMerkledag(pbLink.Hash, "byte")+",\n")
+ }
+ if pbLink.Name != nil {
+ s = append(s, "Name: "+valueToGoStringMerkledag(pbLink.Name, "string")+",\n")
+ }
+ if pbLink.Tsize != nil {
+ s = append(s, "Tsize: "+valueToGoStringMerkledag(pbLink.Tsize, "uint64")+",\n")
+ }
+ if pbLink.XXX_unrecognized != nil {
+ s = append(s, "XXX_unrecognized:"+fmt.Sprintf("%#v", pbLink.XXX_unrecognized)+",\n")
+ }
+ s = append(s, "}")
+ return strings.Join(s, "")
+}
+func (pbNode *PBNode) GoString() string {
+ if pbNode == nil {
+ return "nil"
+ }
+ s := make([]string, 0, 6)
+ s = append(s, "&merkledag_pb.PBNode{")
+ if pbNode.Links != nil {
+ s = append(s, "Links: "+fmt.Sprintf("%#v", pbNode.Links)+",\n")
+ }
+ if pbNode.Data != nil {
+ s = append(s, "Data: "+valueToGoStringMerkledag(pbNode.Data, "byte")+",\n")
+ }
+ if pbNode.XXX_unrecognized != nil {
+ s = append(s, "XXX_unrecognized:"+fmt.Sprintf("%#v", pbNode.XXX_unrecognized)+",\n")
+ }
+ s = append(s, "}")
+ return strings.Join(s, "")
+}
+func valueToGoStringMerkledag(v interface{}, typ string) string {
+ rv := reflect.ValueOf(v)
+ if rv.IsNil() {
+ return "nil"
+ }
+ pv := reflect.Indirect(rv).Interface()
+ return fmt.Sprintf("func(v %v) *%v { return &v } ( %#v )", typ, typ, pv)
+}
+func (m *PBLink) Marshal() (dAtA []byte, err error) {
+ size := m.Size()
+ dAtA = make([]byte, size)
+ n, err := m.MarshalToSizedBuffer(dAtA[:size])
+ if err != nil {
+ return nil, err
+ }
+ return dAtA[:n], nil
+}
+
+func (m *PBLink) MarshalTo(dAtA []byte) (int, error) {
+ size := m.Size()
+ return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *PBLink) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+ i := len(dAtA)
+ _ = i
+ var l int
+ _ = l
+ if m.XXX_unrecognized != nil {
+ i -= len(m.XXX_unrecognized)
+ copy(dAtA[i:], m.XXX_unrecognized)
+ }
+ if m.Tsize != nil {
+ i = encodeVarintMerkledag(dAtA, i, uint64(*m.Tsize))
+ i--
+ dAtA[i] = 0x18
+ }
+ if m.Name != nil {
+ i -= len(*m.Name)
+ copy(dAtA[i:], *m.Name)
+ i = encodeVarintMerkledag(dAtA, i, uint64(len(*m.Name)))
+ i--
+ dAtA[i] = 0x12
+ }
+ if m.Hash != nil {
+ i -= len(m.Hash)
+ copy(dAtA[i:], m.Hash)
+ i = encodeVarintMerkledag(dAtA, i, uint64(len(m.Hash)))
+ i--
+ dAtA[i] = 0xa
+ }
+ return len(dAtA) - i, nil
+}
+
+func (m *PBNode) Marshal() (dAtA []byte, err error) {
+ size := m.Size()
+ dAtA = make([]byte, size)
+ n, err := m.MarshalToSizedBuffer(dAtA[:size])
+ if err != nil {
+ return nil, err
+ }
+ return dAtA[:n], nil
+}
+
+func (m *PBNode) MarshalTo(dAtA []byte) (int, error) {
+ size := m.Size()
+ return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *PBNode) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+ i := len(dAtA)
+ _ = i
+ var l int
+ _ = l
+ if m.XXX_unrecognized != nil {
+ i -= len(m.XXX_unrecognized)
+ copy(dAtA[i:], m.XXX_unrecognized)
+ }
+ if m.Data != nil {
+ i -= len(m.Data)
+ copy(dAtA[i:], m.Data)
+ i = encodeVarintMerkledag(dAtA, i, uint64(len(m.Data)))
+ i--
+ dAtA[i] = 0xa
+ }
+ if len(m.Links) > 0 {
+ for iNdEx := len(m.Links) - 1; iNdEx >= 0; iNdEx-- {
+ {
+ size, err := m.Links[iNdEx].MarshalToSizedBuffer(dAtA[:i])
+ if err != nil {
+ return 0, err
+ }
+ i -= size
+ i = encodeVarintMerkledag(dAtA, i, uint64(size))
+ }
+ i--
+ dAtA[i] = 0x12
+ }
+ }
+ return len(dAtA) - i, nil
+}
+
+func encodeVarintMerkledag(dAtA []byte, offset int, v uint64) int {
+ offset -= sovMerkledag(v)
+ base := offset
+ for v >= 1<<7 {
+ dAtA[offset] = uint8(v&0x7f | 0x80)
+ v >>= 7
+ offset++
+ }
+ dAtA[offset] = uint8(v)
+ return base
+}
+func NewPopulatedPBLink(r randyMerkledag, easy bool) *PBLink {
+ this := &PBLink{}
+ if r.Intn(5) != 0 {
+ v1 := r.Intn(100)
+ this.Hash = make([]byte, v1)
+ for i := 0; i < v1; i++ {
+ this.Hash[i] = byte(r.Intn(256))
+ }
+ }
+ if r.Intn(5) != 0 {
+ v2 := string(randStringMerkledag(r))
+ this.Name = &v2
+ }
+ if r.Intn(5) != 0 {
+ v3 := uint64(uint64(r.Uint32()))
+ this.Tsize = &v3
+ }
+ if !easy && r.Intn(10) != 0 {
+ this.XXX_unrecognized = randUnrecognizedMerkledag(r, 4)
+ }
+ return this
+}
+
+func NewPopulatedPBNode(r randyMerkledag, easy bool) *PBNode {
+ this := &PBNode{}
+ if r.Intn(5) != 0 {
+ v4 := r.Intn(100)
+ this.Data = make([]byte, v4)
+ for i := 0; i < v4; i++ {
+ this.Data[i] = byte(r.Intn(256))
+ }
+ }
+ if r.Intn(5) != 0 {
+ v5 := r.Intn(5)
+ this.Links = make([]*PBLink, v5)
+ for i := 0; i < v5; i++ {
+ this.Links[i] = NewPopulatedPBLink(r, easy)
+ }
+ }
+ if !easy && r.Intn(10) != 0 {
+ this.XXX_unrecognized = randUnrecognizedMerkledag(r, 3)
+ }
+ return this
+}
+
+type randyMerkledag interface {
+ Float32() float32
+ Float64() float64
+ Int63() int64
+ Int31() int32
+ Uint32() uint32
+ Intn(n int) int
+}
+
+func randUTF8RuneMerkledag(r randyMerkledag) rune {
+ ru := r.Intn(62)
+ if ru < 10 {
+ return rune(ru + 48)
+ } else if ru < 36 {
+ return rune(ru + 55)
+ }
+ return rune(ru + 61)
+}
+func randStringMerkledag(r randyMerkledag) string {
+ v6 := r.Intn(100)
+ tmps := make([]rune, v6)
+ for i := 0; i < v6; i++ {
+ tmps[i] = randUTF8RuneMerkledag(r)
+ }
+ return string(tmps)
+}
+func randUnrecognizedMerkledag(r randyMerkledag, maxFieldNumber int) (dAtA []byte) {
+ l := r.Intn(5)
+ for i := 0; i < l; i++ {
+ wire := r.Intn(4)
+ if wire == 3 {
+ wire = 5
+ }
+ fieldNumber := maxFieldNumber + r.Intn(100)
+ dAtA = randFieldMerkledag(dAtA, r, fieldNumber, wire)
+ }
+ return dAtA
+}
+func randFieldMerkledag(dAtA []byte, r randyMerkledag, fieldNumber int, wire int) []byte {
+ key := uint32(fieldNumber)<<3 | uint32(wire)
+ switch wire {
+ case 0:
+ dAtA = encodeVarintPopulateMerkledag(dAtA, uint64(key))
+ v7 := r.Int63()
+ if r.Intn(2) == 0 {
+ v7 *= -1
+ }
+ dAtA = encodeVarintPopulateMerkledag(dAtA, uint64(v7))
+ case 1:
+ dAtA = encodeVarintPopulateMerkledag(dAtA, uint64(key))
+ dAtA = append(dAtA, byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)))
+ case 2:
+ dAtA = encodeVarintPopulateMerkledag(dAtA, uint64(key))
+ ll := r.Intn(100)
+ dAtA = encodeVarintPopulateMerkledag(dAtA, uint64(ll))
+ for j := 0; j < ll; j++ {
+ dAtA = append(dAtA, byte(r.Intn(256)))
+ }
+ default:
+ dAtA = encodeVarintPopulateMerkledag(dAtA, uint64(key))
+ dAtA = append(dAtA, byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)))
+ }
+ return dAtA
+}
+func encodeVarintPopulateMerkledag(dAtA []byte, v uint64) []byte {
+ for v >= 1<<7 {
+ dAtA = append(dAtA, uint8(uint64(v)&0x7f|0x80))
+ v >>= 7
+ }
+ dAtA = append(dAtA, uint8(v))
+ return dAtA
+}
+func (m *PBLink) Size() (n int) {
+ if m == nil {
+ return 0
+ }
+ var l int
+ _ = l
+ if m.Hash != nil {
+ l = len(m.Hash)
+ n += 1 + l + sovMerkledag(uint64(l))
+ }
+ if m.Name != nil {
+ l = len(*m.Name)
+ n += 1 + l + sovMerkledag(uint64(l))
+ }
+ if m.Tsize != nil {
+ n += 1 + sovMerkledag(uint64(*m.Tsize))
+ }
+ if m.XXX_unrecognized != nil {
+ n += len(m.XXX_unrecognized)
+ }
+ return n
+}
+
+func (m *PBNode) Size() (n int) {
+ if m == nil {
+ return 0
+ }
+ var l int
+ _ = l
+ if m.Data != nil {
+ l = len(m.Data)
+ n += 1 + l + sovMerkledag(uint64(l))
+ }
+ if len(m.Links) > 0 {
+ for _, e := range m.Links {
+ l = e.Size()
+ n += 1 + l + sovMerkledag(uint64(l))
+ }
+ }
+ if m.XXX_unrecognized != nil {
+ n += len(m.XXX_unrecognized)
+ }
+ return n
+}
+
+func sovMerkledag(x uint64) (n int) {
+ return (math_bits.Len64(x|1) + 6) / 7
+}
+func (pbLink *PBLink) String() string {
+ if pbLink == nil {
+ return "nil"
+ }
+ s := strings.Join([]string{`&PBLink{`,
+ `Hash:` + valueToStringMerkledag(pbLink.Hash) + `,`,
+ `Name:` + valueToStringMerkledag(pbLink.Name) + `,`,
+ `Tsize:` + valueToStringMerkledag(pbLink.Tsize) + `,`,
+ `XXX_unrecognized:` + fmt.Sprintf("%v", pbLink.XXX_unrecognized) + `,`,
+ `}`,
+ }, "")
+ return s
+}
+func (pbNode *PBNode) String() string {
+ if pbNode == nil {
+ return "nil"
+ }
+ repeatedStringForLinks := "[]*PBLink{"
+ for _, f := range pbNode.Links {
+ repeatedStringForLinks += strings.Replace(f.String(), "PBLink", "PBLink", 1) + ","
+ }
+ repeatedStringForLinks += "}"
+ s := strings.Join([]string{`&PBNode{`,
+ `Data:` + valueToStringMerkledag(pbNode.Data) + `,`,
+ `Links:` + repeatedStringForLinks + `,`,
+ `XXX_unrecognized:` + fmt.Sprintf("%v", pbNode.XXX_unrecognized) + `,`,
+ `}`,
+ }, "")
+ return s
+}
+func valueToStringMerkledag(v interface{}) string {
+ rv := reflect.ValueOf(v)
+ if rv.IsNil() {
+ return "nil"
+ }
+ pv := reflect.Indirect(rv).Interface()
+ return fmt.Sprintf("*%v", pv)
+}
+func (m *PBLink) Unmarshal(dAtA []byte) error {
+ l := len(dAtA)
+ iNdEx := 0
+ for iNdEx < l {
+ preIndex := iNdEx
+ var wire uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowMerkledag
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ wire |= uint64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ fieldNum := int32(wire >> 3)
+ wireType := int(wire & 0x7)
+ if wireType == 4 {
+ return fmt.Errorf("proto: PBLink: wiretype end group for non-group")
+ }
+ if fieldNum <= 0 {
+ return fmt.Errorf("proto: PBLink: illegal tag %d (wire type %d)", fieldNum, wire)
+ }
+ switch fieldNum {
+ case 1:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Hash", wireType)
+ }
+ var byteLen int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowMerkledag
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ byteLen |= int(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if byteLen < 0 {
+ return ErrInvalidLengthMerkledag
+ }
+ postIndex := iNdEx + byteLen
+ if postIndex < 0 {
+ return ErrInvalidLengthMerkledag
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.Hash = append(m.Hash[:0], dAtA[iNdEx:postIndex]...)
+ if m.Hash == nil {
+ m.Hash = []byte{}
+ }
+ iNdEx = postIndex
+ case 2:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
+ }
+ var stringLen uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowMerkledag
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ stringLen |= uint64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ intStringLen := int(stringLen)
+ if intStringLen < 0 {
+ return ErrInvalidLengthMerkledag
+ }
+ postIndex := iNdEx + intStringLen
+ if postIndex < 0 {
+ return ErrInvalidLengthMerkledag
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ s := string(dAtA[iNdEx:postIndex])
+ m.Name = &s
+ iNdEx = postIndex
+ case 3:
+ if wireType != 0 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Tsize", wireType)
+ }
+ var v uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowMerkledag
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ v |= uint64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ m.Tsize = &v
+ default:
+ iNdEx = preIndex
+ skippy, err := skipMerkledag(dAtA[iNdEx:])
+ if err != nil {
+ return err
+ }
+ if skippy < 0 {
+ return ErrInvalidLengthMerkledag
+ }
+ if (iNdEx + skippy) < 0 {
+ return ErrInvalidLengthMerkledag
+ }
+ if (iNdEx + skippy) > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
+ iNdEx += skippy
+ }
+ }
+
+ if iNdEx > l {
+ return io.ErrUnexpectedEOF
+ }
+ return nil
+}
+func (m *PBNode) Unmarshal(dAtA []byte) error {
+ l := len(dAtA)
+ iNdEx := 0
+ for iNdEx < l {
+ preIndex := iNdEx
+ var wire uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowMerkledag
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ wire |= uint64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ fieldNum := int32(wire >> 3)
+ wireType := int(wire & 0x7)
+ if wireType == 4 {
+ return fmt.Errorf("proto: PBNode: wiretype end group for non-group")
+ }
+ if fieldNum <= 0 {
+ return fmt.Errorf("proto: PBNode: illegal tag %d (wire type %d)", fieldNum, wire)
+ }
+ switch fieldNum {
+ case 1:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Data", wireType)
+ }
+ var byteLen int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowMerkledag
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ byteLen |= int(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if byteLen < 0 {
+ return ErrInvalidLengthMerkledag
+ }
+ postIndex := iNdEx + byteLen
+ if postIndex < 0 {
+ return ErrInvalidLengthMerkledag
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.Data = append(m.Data[:0], dAtA[iNdEx:postIndex]...)
+ if m.Data == nil {
+ m.Data = []byte{}
+ }
+ iNdEx = postIndex
+ case 2:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Links", wireType)
+ }
+ var msglen int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowMerkledag
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ msglen |= int(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if msglen < 0 {
+ return ErrInvalidLengthMerkledag
+ }
+ postIndex := iNdEx + msglen
+ if postIndex < 0 {
+ return ErrInvalidLengthMerkledag
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.Links = append(m.Links, &PBLink{})
+ if err := m.Links[len(m.Links)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+ return err
+ }
+ iNdEx = postIndex
+ default:
+ iNdEx = preIndex
+ skippy, err := skipMerkledag(dAtA[iNdEx:])
+ if err != nil {
+ return err
+ }
+ if skippy < 0 {
+ return ErrInvalidLengthMerkledag
+ }
+ if (iNdEx + skippy) < 0 {
+ return ErrInvalidLengthMerkledag
+ }
+ if (iNdEx + skippy) > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
+ iNdEx += skippy
+ }
+ }
+
+ if iNdEx > l {
+ return io.ErrUnexpectedEOF
+ }
+ return nil
+}
+func skipMerkledag(dAtA []byte) (n int, err error) {
+ l := len(dAtA)
+ iNdEx := 0
+ depth := 0
+ for iNdEx < l {
+ var wire uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return 0, ErrIntOverflowMerkledag
+ }
+ if iNdEx >= l {
+ return 0, io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ wire |= (uint64(b) & 0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ wireType := int(wire & 0x7)
+ switch wireType {
+ case 0:
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return 0, ErrIntOverflowMerkledag
+ }
+ if iNdEx >= l {
+ return 0, io.ErrUnexpectedEOF
+ }
+ iNdEx++
+ if dAtA[iNdEx-1] < 0x80 {
+ break
+ }
+ }
+ case 1:
+ iNdEx += 8
+ case 2:
+ var length int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return 0, ErrIntOverflowMerkledag
+ }
+ if iNdEx >= l {
+ return 0, io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ length |= (int(b) & 0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if length < 0 {
+ return 0, ErrInvalidLengthMerkledag
+ }
+ iNdEx += length
+ if iNdEx < 0 {
+ return 0, ErrInvalidLengthMerkledag
+ }
+ case 3:
+ depth++
+ case 4:
+ if depth == 0 {
+ return 0, ErrUnexpectedEndOfGroupMerkledag
+ }
+ depth--
+ case 5:
+ iNdEx += 4
+ default:
+ return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
+ }
+ if depth == 0 {
+ return iNdEx, nil
+ }
+ }
+ return 0, io.ErrUnexpectedEOF
+}
+
+var (
+ ErrInvalidLengthMerkledag = fmt.Errorf("proto: negative length found during unmarshaling")
+ ErrIntOverflowMerkledag = fmt.Errorf("proto: integer overflow")
+ ErrUnexpectedEndOfGroupMerkledag = fmt.Errorf("proto: unexpected end of group")
+)
diff --git a/ipld/merkledag/pb/merkledag.proto b/ipld/merkledag/pb/merkledag.proto
new file mode 100644
index 0000000000..ec540e681e
--- /dev/null
+++ b/ipld/merkledag/pb/merkledag.proto
@@ -0,0 +1,41 @@
+syntax = "proto2";
+
+package merkledag.pb;
+
+import "github.com/gogo/protobuf/gogoproto/gogo.proto";
+
+option (gogoproto.gostring_all) = true;
+option (gogoproto.equal_all) = true;
+option (gogoproto.verbose_equal_all) = true;
+option (gogoproto.goproto_stringer_all) = false;
+option (gogoproto.stringer_all) = true;
+option (gogoproto.populate_all) = true;
+option (gogoproto.testgen_all) = true;
+option (gogoproto.benchgen_all) = true;
+option (gogoproto.marshaler_all) = true;
+option (gogoproto.sizer_all) = true;
+option (gogoproto.unmarshaler_all) = true;
+
+
+// An IPFS MerkleDAG Link
+message PBLink {
+
+ // multihash of the target object
+ optional bytes Hash = 1;
+
+ // utf string name. should be unique per object
+ optional string Name = 2;
+
+ // cumulative size of target object
+ optional uint64 Tsize = 3;
+}
+
+// An IPFS MerkleDAG Node
+message PBNode {
+
+ // refs to other objects
+ repeated PBLink Links = 2;
+
+ // opaque user data
+ optional bytes Data = 1;
+}
diff --git a/ipld/merkledag/pb/merkledagpb_test.go b/ipld/merkledag/pb/merkledagpb_test.go
new file mode 100644
index 0000000000..f72b306da8
--- /dev/null
+++ b/ipld/merkledag/pb/merkledagpb_test.go
@@ -0,0 +1,486 @@
+// Code generated by protoc-gen-gogo. DO NOT EDIT.
+// source: merkledag.proto
+
+package merkledag_pb
+
+import (
+ fmt "fmt"
+ go_parser "go/parser"
+ math "math"
+ math_rand "math/rand"
+ testing "testing"
+ time "time"
+
+ _ "github.com/gogo/protobuf/gogoproto"
+ github_com_gogo_protobuf_jsonpb "github.com/gogo/protobuf/jsonpb"
+ github_com_gogo_protobuf_proto "github.com/gogo/protobuf/proto"
+ proto "github.com/gogo/protobuf/proto"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+func TestPBLinkProto(t *testing.T) {
+ seed := time.Now().UnixNano()
+ popr := math_rand.New(math_rand.NewSource(seed))
+ p := NewPopulatedPBLink(popr, false)
+ dAtA, err := github_com_gogo_protobuf_proto.Marshal(p)
+ if err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ msg := &PBLink{}
+ if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ littlefuzz := make([]byte, len(dAtA))
+ copy(littlefuzz, dAtA)
+ for i := range dAtA {
+ dAtA[i] = byte(popr.Intn(256))
+ }
+ if err := p.VerboseEqual(msg); err != nil {
+ t.Fatalf("seed = %d, %#v !VerboseProto %#v, since %v", seed, msg, p, err)
+ }
+ if !p.Equal(msg) {
+ t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p)
+ }
+ if len(littlefuzz) > 0 {
+ fuzzamount := 100
+ for i := 0; i < fuzzamount; i++ {
+ littlefuzz[popr.Intn(len(littlefuzz))] = byte(popr.Intn(256))
+ littlefuzz = append(littlefuzz, byte(popr.Intn(256)))
+ }
+ // shouldn't panic
+ _ = github_com_gogo_protobuf_proto.Unmarshal(littlefuzz, msg)
+ }
+}
+
+func TestPBLinkMarshalTo(t *testing.T) {
+ seed := time.Now().UnixNano()
+ popr := math_rand.New(math_rand.NewSource(seed))
+ p := NewPopulatedPBLink(popr, false)
+ size := p.Size()
+ dAtA := make([]byte, size)
+ for i := range dAtA {
+ dAtA[i] = byte(popr.Intn(256))
+ }
+ _, err := p.MarshalTo(dAtA)
+ if err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ msg := &PBLink{}
+ if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ for i := range dAtA {
+ dAtA[i] = byte(popr.Intn(256))
+ }
+ if err := p.VerboseEqual(msg); err != nil {
+ t.Fatalf("seed = %d, %#v !VerboseProto %#v, since %v", seed, msg, p, err)
+ }
+ if !p.Equal(msg) {
+ t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p)
+ }
+}
+
+func BenchmarkPBLinkProtoMarshal(b *testing.B) {
+ popr := math_rand.New(math_rand.NewSource(616))
+ total := 0
+ pops := make([]*PBLink, 10000)
+ for i := 0; i < 10000; i++ {
+ pops[i] = NewPopulatedPBLink(popr, false)
+ }
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ dAtA, err := github_com_gogo_protobuf_proto.Marshal(pops[i%10000])
+ if err != nil {
+ panic(err)
+ }
+ total += len(dAtA)
+ }
+ b.SetBytes(int64(total / b.N))
+}
+
+func BenchmarkPBLinkProtoUnmarshal(b *testing.B) {
+ popr := math_rand.New(math_rand.NewSource(616))
+ total := 0
+ datas := make([][]byte, 10000)
+ for i := 0; i < 10000; i++ {
+ dAtA, err := github_com_gogo_protobuf_proto.Marshal(NewPopulatedPBLink(popr, false))
+ if err != nil {
+ panic(err)
+ }
+ datas[i] = dAtA
+ }
+ msg := &PBLink{}
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ total += len(datas[i%10000])
+ if err := github_com_gogo_protobuf_proto.Unmarshal(datas[i%10000], msg); err != nil {
+ panic(err)
+ }
+ }
+ b.SetBytes(int64(total / b.N))
+}
+
+func TestPBNodeProto(t *testing.T) {
+ seed := time.Now().UnixNano()
+ popr := math_rand.New(math_rand.NewSource(seed))
+ p := NewPopulatedPBNode(popr, false)
+ dAtA, err := github_com_gogo_protobuf_proto.Marshal(p)
+ if err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ msg := &PBNode{}
+ if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ littlefuzz := make([]byte, len(dAtA))
+ copy(littlefuzz, dAtA)
+ for i := range dAtA {
+ dAtA[i] = byte(popr.Intn(256))
+ }
+ if err := p.VerboseEqual(msg); err != nil {
+ t.Fatalf("seed = %d, %#v !VerboseProto %#v, since %v", seed, msg, p, err)
+ }
+ if !p.Equal(msg) {
+ t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p)
+ }
+ if len(littlefuzz) > 0 {
+ fuzzamount := 100
+ for i := 0; i < fuzzamount; i++ {
+ littlefuzz[popr.Intn(len(littlefuzz))] = byte(popr.Intn(256))
+ littlefuzz = append(littlefuzz, byte(popr.Intn(256)))
+ }
+ // shouldn't panic
+ _ = github_com_gogo_protobuf_proto.Unmarshal(littlefuzz, msg)
+ }
+}
+
+func TestPBNodeMarshalTo(t *testing.T) {
+ seed := time.Now().UnixNano()
+ popr := math_rand.New(math_rand.NewSource(seed))
+ p := NewPopulatedPBNode(popr, false)
+ size := p.Size()
+ dAtA := make([]byte, size)
+ for i := range dAtA {
+ dAtA[i] = byte(popr.Intn(256))
+ }
+ _, err := p.MarshalTo(dAtA)
+ if err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ msg := &PBNode{}
+ if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ for i := range dAtA {
+ dAtA[i] = byte(popr.Intn(256))
+ }
+ if err := p.VerboseEqual(msg); err != nil {
+ t.Fatalf("seed = %d, %#v !VerboseProto %#v, since %v", seed, msg, p, err)
+ }
+ if !p.Equal(msg) {
+ t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p)
+ }
+}
+
+func BenchmarkPBNodeProtoMarshal(b *testing.B) {
+ popr := math_rand.New(math_rand.NewSource(616))
+ total := 0
+ pops := make([]*PBNode, 10000)
+ for i := 0; i < 10000; i++ {
+ pops[i] = NewPopulatedPBNode(popr, false)
+ }
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ dAtA, err := github_com_gogo_protobuf_proto.Marshal(pops[i%10000])
+ if err != nil {
+ panic(err)
+ }
+ total += len(dAtA)
+ }
+ b.SetBytes(int64(total / b.N))
+}
+
+func BenchmarkPBNodeProtoUnmarshal(b *testing.B) {
+ popr := math_rand.New(math_rand.NewSource(616))
+ total := 0
+ datas := make([][]byte, 10000)
+ for i := 0; i < 10000; i++ {
+ dAtA, err := github_com_gogo_protobuf_proto.Marshal(NewPopulatedPBNode(popr, false))
+ if err != nil {
+ panic(err)
+ }
+ datas[i] = dAtA
+ }
+ msg := &PBNode{}
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ total += len(datas[i%10000])
+ if err := github_com_gogo_protobuf_proto.Unmarshal(datas[i%10000], msg); err != nil {
+ panic(err)
+ }
+ }
+ b.SetBytes(int64(total / b.N))
+}
+
+func TestPBLinkJSON(t *testing.T) {
+ seed := time.Now().UnixNano()
+ popr := math_rand.New(math_rand.NewSource(seed))
+ p := NewPopulatedPBLink(popr, true)
+ marshaler := github_com_gogo_protobuf_jsonpb.Marshaler{}
+ jsondata, err := marshaler.MarshalToString(p)
+ if err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ msg := &PBLink{}
+ err = github_com_gogo_protobuf_jsonpb.UnmarshalString(jsondata, msg)
+ if err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ if err := p.VerboseEqual(msg); err != nil {
+ t.Fatalf("seed = %d, %#v !VerboseProto %#v, since %v", seed, msg, p, err)
+ }
+ if !p.Equal(msg) {
+ t.Fatalf("seed = %d, %#v !Json Equal %#v", seed, msg, p)
+ }
+}
+func TestPBNodeJSON(t *testing.T) {
+ seed := time.Now().UnixNano()
+ popr := math_rand.New(math_rand.NewSource(seed))
+ p := NewPopulatedPBNode(popr, true)
+ marshaler := github_com_gogo_protobuf_jsonpb.Marshaler{}
+ jsondata, err := marshaler.MarshalToString(p)
+ if err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ msg := &PBNode{}
+ err = github_com_gogo_protobuf_jsonpb.UnmarshalString(jsondata, msg)
+ if err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ if err := p.VerboseEqual(msg); err != nil {
+ t.Fatalf("seed = %d, %#v !VerboseProto %#v, since %v", seed, msg, p, err)
+ }
+ if !p.Equal(msg) {
+ t.Fatalf("seed = %d, %#v !Json Equal %#v", seed, msg, p)
+ }
+}
+func TestPBLinkProtoText(t *testing.T) {
+ seed := time.Now().UnixNano()
+ popr := math_rand.New(math_rand.NewSource(seed))
+ p := NewPopulatedPBLink(popr, true)
+ dAtA := github_com_gogo_protobuf_proto.MarshalTextString(p)
+ msg := &PBLink{}
+ if err := github_com_gogo_protobuf_proto.UnmarshalText(dAtA, msg); err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ if err := p.VerboseEqual(msg); err != nil {
+ t.Fatalf("seed = %d, %#v !VerboseProto %#v, since %v", seed, msg, p, err)
+ }
+ if !p.Equal(msg) {
+ t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p)
+ }
+}
+
+func TestPBLinkProtoCompactText(t *testing.T) {
+ seed := time.Now().UnixNano()
+ popr := math_rand.New(math_rand.NewSource(seed))
+ p := NewPopulatedPBLink(popr, true)
+ dAtA := github_com_gogo_protobuf_proto.CompactTextString(p)
+ msg := &PBLink{}
+ if err := github_com_gogo_protobuf_proto.UnmarshalText(dAtA, msg); err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ if err := p.VerboseEqual(msg); err != nil {
+ t.Fatalf("seed = %d, %#v !VerboseProto %#v, since %v", seed, msg, p, err)
+ }
+ if !p.Equal(msg) {
+ t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p)
+ }
+}
+
+func TestPBNodeProtoText(t *testing.T) {
+ seed := time.Now().UnixNano()
+ popr := math_rand.New(math_rand.NewSource(seed))
+ p := NewPopulatedPBNode(popr, true)
+ dAtA := github_com_gogo_protobuf_proto.MarshalTextString(p)
+ msg := &PBNode{}
+ if err := github_com_gogo_protobuf_proto.UnmarshalText(dAtA, msg); err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ if err := p.VerboseEqual(msg); err != nil {
+ t.Fatalf("seed = %d, %#v !VerboseProto %#v, since %v", seed, msg, p, err)
+ }
+ if !p.Equal(msg) {
+ t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p)
+ }
+}
+
+func TestPBNodeProtoCompactText(t *testing.T) {
+ seed := time.Now().UnixNano()
+ popr := math_rand.New(math_rand.NewSource(seed))
+ p := NewPopulatedPBNode(popr, true)
+ dAtA := github_com_gogo_protobuf_proto.CompactTextString(p)
+ msg := &PBNode{}
+ if err := github_com_gogo_protobuf_proto.UnmarshalText(dAtA, msg); err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ if err := p.VerboseEqual(msg); err != nil {
+ t.Fatalf("seed = %d, %#v !VerboseProto %#v, since %v", seed, msg, p, err)
+ }
+ if !p.Equal(msg) {
+ t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p)
+ }
+}
+
+func TestPBLinkVerboseEqual(t *testing.T) {
+ popr := math_rand.New(math_rand.NewSource(time.Now().UnixNano()))
+ p := NewPopulatedPBLink(popr, false)
+ dAtA, err := github_com_gogo_protobuf_proto.Marshal(p)
+ if err != nil {
+ panic(err)
+ }
+ msg := &PBLink{}
+ if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil {
+ panic(err)
+ }
+ if err := p.VerboseEqual(msg); err != nil {
+ t.Fatalf("%#v !VerboseEqual %#v, since %v", msg, p, err)
+ }
+}
+func TestPBNodeVerboseEqual(t *testing.T) {
+ popr := math_rand.New(math_rand.NewSource(time.Now().UnixNano()))
+ p := NewPopulatedPBNode(popr, false)
+ dAtA, err := github_com_gogo_protobuf_proto.Marshal(p)
+ if err != nil {
+ panic(err)
+ }
+ msg := &PBNode{}
+ if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil {
+ panic(err)
+ }
+ if err := p.VerboseEqual(msg); err != nil {
+ t.Fatalf("%#v !VerboseEqual %#v, since %v", msg, p, err)
+ }
+}
+func TestPBLinkGoString(t *testing.T) {
+ popr := math_rand.New(math_rand.NewSource(time.Now().UnixNano()))
+ p := NewPopulatedPBLink(popr, false)
+ s1 := p.GoString()
+ s2 := fmt.Sprintf("%#v", p)
+ if s1 != s2 {
+ t.Fatalf("GoString want %v got %v", s1, s2)
+ }
+ _, err := go_parser.ParseExpr(s1)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+func TestPBNodeGoString(t *testing.T) {
+ popr := math_rand.New(math_rand.NewSource(time.Now().UnixNano()))
+ p := NewPopulatedPBNode(popr, false)
+ s1 := p.GoString()
+ s2 := fmt.Sprintf("%#v", p)
+ if s1 != s2 {
+ t.Fatalf("GoString want %v got %v", s1, s2)
+ }
+ _, err := go_parser.ParseExpr(s1)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+func TestPBLinkSize(t *testing.T) {
+ seed := time.Now().UnixNano()
+ popr := math_rand.New(math_rand.NewSource(seed))
+ p := NewPopulatedPBLink(popr, true)
+ size2 := github_com_gogo_protobuf_proto.Size(p)
+ dAtA, err := github_com_gogo_protobuf_proto.Marshal(p)
+ if err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ size := p.Size()
+ if len(dAtA) != size {
+ t.Errorf("seed = %d, size %v != marshalled size %v", seed, size, len(dAtA))
+ }
+ if size2 != size {
+ t.Errorf("seed = %d, size %v != before marshal proto.Size %v", seed, size, size2)
+ }
+ size3 := github_com_gogo_protobuf_proto.Size(p)
+ if size3 != size {
+ t.Errorf("seed = %d, size %v != after marshal proto.Size %v", seed, size, size3)
+ }
+}
+
+func BenchmarkPBLinkSize(b *testing.B) {
+ popr := math_rand.New(math_rand.NewSource(616))
+ total := 0
+ pops := make([]*PBLink, 1000)
+ for i := 0; i < 1000; i++ {
+ pops[i] = NewPopulatedPBLink(popr, false)
+ }
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ total += pops[i%1000].Size()
+ }
+ b.SetBytes(int64(total / b.N))
+}
+
+func TestPBNodeSize(t *testing.T) {
+ seed := time.Now().UnixNano()
+ popr := math_rand.New(math_rand.NewSource(seed))
+ p := NewPopulatedPBNode(popr, true)
+ size2 := github_com_gogo_protobuf_proto.Size(p)
+ dAtA, err := github_com_gogo_protobuf_proto.Marshal(p)
+ if err != nil {
+ t.Fatalf("seed = %d, err = %v", seed, err)
+ }
+ size := p.Size()
+ if len(dAtA) != size {
+ t.Errorf("seed = %d, size %v != marshalled size %v", seed, size, len(dAtA))
+ }
+ if size2 != size {
+ t.Errorf("seed = %d, size %v != before marshal proto.Size %v", seed, size, size2)
+ }
+ size3 := github_com_gogo_protobuf_proto.Size(p)
+ if size3 != size {
+ t.Errorf("seed = %d, size %v != after marshal proto.Size %v", seed, size, size3)
+ }
+}
+
+func BenchmarkPBNodeSize(b *testing.B) {
+ popr := math_rand.New(math_rand.NewSource(616))
+ total := 0
+ pops := make([]*PBNode, 1000)
+ for i := 0; i < 1000; i++ {
+ pops[i] = NewPopulatedPBNode(popr, false)
+ }
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ total += pops[i%1000].Size()
+ }
+ b.SetBytes(int64(total / b.N))
+}
+
+func TestPBLinkStringer(t *testing.T) {
+ popr := math_rand.New(math_rand.NewSource(time.Now().UnixNano()))
+ p := NewPopulatedPBLink(popr, false)
+ s1 := p.String()
+ s2 := fmt.Sprintf("%v", p)
+ if s1 != s2 {
+ t.Fatalf("String want %v got %v", s1, s2)
+ }
+}
+func TestPBNodeStringer(t *testing.T) {
+ popr := math_rand.New(math_rand.NewSource(time.Now().UnixNano()))
+ p := NewPopulatedPBNode(popr, false)
+ s1 := p.String()
+ s2 := fmt.Sprintf("%v", p)
+ if s1 != s2 {
+ t.Fatalf("String want %v got %v", s1, s2)
+ }
+}
+
+//These tests are generated by github.com/gogo/protobuf/plugin/testgen
diff --git a/ipld/merkledag/pb/stability_test.go b/ipld/merkledag/pb/stability_test.go
new file mode 100644
index 0000000000..5da833713e
--- /dev/null
+++ b/ipld/merkledag/pb/stability_test.go
@@ -0,0 +1,24 @@
+package merkledag_pb
+
+import (
+ bytes "bytes"
+ "testing"
+)
+
+func TestStability(t *testing.T) {
+ correct := []byte("\x12\x87\x01\n;\x81\x869\xacH\xa4Ư\xa2\xf1X\x1a\x8b\x95%\xe2\x0f\xdah\x92\u007f+/\xf86\xf75x\xdb\x0f\xa5L)\xf7\xfd\x92\x8d\x92\xcaC\xf1\x93\xde\xe4\u007fY\x15I\xf5\x97\xa8\x11\xc8\xfag\xab\x03\x1e\xbd\x12B|CMw`mHq{>?|vd{0F7>8m[C`HSg3UcXmGs-qp-z6{Kc.tGX->H07\x18\xeaئ\xd0\b\x129\x121LZ3,V9jnmk^veYEV71EMLt9;6]}bnkU2e7GXmqisoCPV0C+ni\x18Ա\xfe\x8b\f\x12u\n'\xf2#\xc1\xc0nQ\xf9\xb5\x19\x80\xcd\xf8\x06k1\xf6#\x84\x1c\xb6\xbf\xeaY\x9b\xd8O\x84\x04\xdbKq\xe4\xae\xf2\xd6\xe9*\x16B\x12D[gVeg4=t}EGSu82+dmgvQ+Tr>_sLUJ|iZ[P2y2T67ilvEikK}\\iru?IF?mVS[Mv9KG8+\x18\x92\xa0\xf9\xa1\n\n\x11?̎\v\x06ѣ\x80nH\x12\x00\xa7\xd2w͝")
+ n := new(PBNode)
+ err := n.Unmarshal(correct)
+ if err != nil {
+ t.Fatal(err)
+ }
+ d, err := n.Marshal()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(d, correct) {
+ t.Logf("%q", d)
+ t.Fatal("protobuf not stable")
+ }
+
+}
diff --git a/ipld/merkledag/pb/upgrade_check.go b/ipld/merkledag/pb/upgrade_check.go
new file mode 100644
index 0000000000..e5a6473c13
--- /dev/null
+++ b/ipld/merkledag/pb/upgrade_check.go
@@ -0,0 +1,5 @@
+package merkledag_pb
+
+// Make sure the user doesn't upgrade this package!
+// This will fail to build if the user does.
+const _ = DoNotUpgradeFileEverItWillChangeYourHashes
diff --git a/ipld/merkledag/prime.go b/ipld/merkledag/prime.go
new file mode 100644
index 0000000000..5f9bbc691e
--- /dev/null
+++ b/ipld/merkledag/prime.go
@@ -0,0 +1,206 @@
+package merkledag
+
+import (
+ dagpb "github.com/ipld/go-codec-dagpb"
+ "github.com/ipld/go-ipld-prime"
+)
+
+// Protonode was originally implemented as a go-ipld-format node, and included
+// functionality that does not fit well into the model for go-ipld-prime, namely
+// the ability ot modify the node in place.
+
+// In order to support the go-ipld-prime interface, all of these prime methods
+// serialize and rebuild the go-ipld-prime node as needed, so that it remains up
+// to date with mutations made via the add/remove link methods
+
+// Kind returns a value from the Kind enum describing what the
+// essential serializable kind of this node is (map, list, integer, etc).
+// Most other handling of a node requires first switching upon the kind.
+func (n *ProtoNode) Kind() ipld.Kind {
+ _, _ = n.EncodeProtobuf(false)
+ return n.encoded.Kind()
+}
+
+// LookupByString looks up a child object in this node and returns it.
+// The returned Node may be any of the Kind:
+// a primitive (string, int64, etc), a map, a list, or a link.
+//
+// If the Kind of this Node is not Kind_Map, a nil node and an error
+// will be returned.
+//
+// If the key does not exist, a nil node and an error will be returned.
+func (n *ProtoNode) LookupByString(key string) (ipld.Node, error) {
+ _, err := n.EncodeProtobuf(false)
+ if err != nil {
+ return nil, err
+ }
+ return n.encoded.LookupByString(key)
+}
+
+// LookupByNode is the equivalent of LookupByString, but takes a reified Node
+// as a parameter instead of a plain string.
+// This mechanism is useful if working with typed maps (if the key types
+// have constraints, and you already have a reified `schema.TypedNode` value,
+// using that value can save parsing and validation costs);
+// and may simply be convenient if you already have a Node value in hand.
+//
+// (When writing generic functions over Node, a good rule of thumb is:
+// when handling a map, check for `schema.TypedNode`, and in this case prefer
+// the LookupByNode(Node) method; otherwise, favor LookupByString; typically
+// implementations will have their fastest paths thusly.)
+func (n *ProtoNode) LookupByNode(key ipld.Node) (ipld.Node, error) {
+ _, err := n.EncodeProtobuf(false)
+ if err != nil {
+ return nil, err
+ }
+ return n.encoded.LookupByNode(key)
+}
+
+// LookupByIndex is the equivalent of LookupByString but for indexing into a list.
+// As with LookupByString, the returned Node may be any of the Kind:
+// a primitive (string, int64, etc), a map, a list, or a link.
+//
+// If the Kind of this Node is not Kind_List, a nil node and an error
+// will be returned.
+//
+// If idx is out of range, a nil node and an error will be returned.
+func (n *ProtoNode) LookupByIndex(idx int64) (ipld.Node, error) {
+ _, err := n.EncodeProtobuf(false)
+ if err != nil {
+ return nil, err
+ }
+ return n.encoded.LookupByIndex(idx)
+}
+
+// LookupBySegment is will act as either LookupByString or LookupByIndex,
+// whichever is contextually appropriate.
+//
+// Using LookupBySegment may imply an "atoi" conversion if used on a list node,
+// or an "itoa" conversion if used on a map node. If an "itoa" conversion
+// takes place, it may error, and this method may return that error.
+func (n *ProtoNode) LookupBySegment(seg ipld.PathSegment) (ipld.Node, error) {
+ _, err := n.EncodeProtobuf(false)
+ if err != nil {
+ return nil, err
+ }
+ return n.encoded.LookupBySegment(seg)
+}
+
+// Note that when using codegenerated types, there may be a fifth variant
+// of lookup method on maps: `Get($GeneratedTypeKey) $GeneratedTypeValue`!
+// MapIterator returns an iterator which yields key-value pairs
+// traversing the node.
+// If the node kind is anything other than a map, nil will be returned.
+//
+// The iterator will yield every entry in the map; that is, it
+// can be expected that itr.Next will be called node.Length times
+// before itr.Done becomes true.
+func (n *ProtoNode) MapIterator() ipld.MapIterator {
+ _, _ = n.EncodeProtobuf(false)
+ return n.encoded.MapIterator()
+}
+
+// ListIterator returns an iterator which yields key-value pairs
+// traversing the node.
+// If the node kind is anything other than a list, nil will be returned.
+//
+// The iterator will yield every entry in the list; that is, it
+// can be expected that itr.Next will be called node.Length times
+// before itr.Done becomes true.
+func (n *ProtoNode) ListIterator() ipld.ListIterator {
+ _, _ = n.EncodeProtobuf(false)
+ return n.encoded.ListIterator()
+}
+
+// Length returns the length of a list, or the number of entries in a map,
+// or -1 if the node is not of list nor map kind.
+func (n *ProtoNode) Length() int64 {
+ _, _ = n.EncodeProtobuf(false)
+ return n.encoded.Length()
+}
+
+// Absent nodes are returned when traversing a struct field that is
+// defined by a schema but unset in the data. (Absent nodes are not
+// possible otherwise; you'll only see them from `schema.TypedNode`.)
+// The absent flag is necessary so iterating over structs can
+// unambiguously make the distinction between values that are
+// present-and-null versus values that are absent.
+//
+// Absent nodes respond to `Kind()` as `ipld.Kind_Null`,
+// for lack of any better descriptive value; you should therefore
+// always check IsAbsent rather than just a switch on kind
+// when it may be important to handle absent values distinctly.
+func (n *ProtoNode) IsAbsent() bool {
+ _, _ = n.EncodeProtobuf(false)
+ return n.encoded.IsAbsent()
+}
+
+func (n *ProtoNode) IsNull() bool {
+ _, _ = n.EncodeProtobuf(false)
+ return n.encoded.IsNull()
+}
+
+func (n *ProtoNode) AsBool() (bool, error) {
+ _, err := n.EncodeProtobuf(false)
+ if err != nil {
+ return false, err
+ }
+ return n.encoded.AsBool()
+}
+
+func (n *ProtoNode) AsInt() (int64, error) {
+ _, err := n.EncodeProtobuf(false)
+ if err != nil {
+ return 0, err
+ }
+ return n.encoded.AsInt()
+}
+
+func (n *ProtoNode) AsFloat() (float64, error) {
+ _, err := n.EncodeProtobuf(false)
+ if err != nil {
+ return 0, err
+ }
+ return n.encoded.AsFloat()
+}
+
+func (n *ProtoNode) AsString() (string, error) {
+ _, err := n.EncodeProtobuf(false)
+ if err != nil {
+ return "", err
+ }
+ return n.encoded.AsString()
+}
+
+func (n *ProtoNode) AsBytes() ([]byte, error) {
+ _, err := n.EncodeProtobuf(false)
+ if err != nil {
+ return nil, err
+ }
+ return n.encoded.AsBytes()
+}
+
+func (n *ProtoNode) AsLink() (ipld.Link, error) {
+ _, err := n.EncodeProtobuf(false)
+ if err != nil {
+ return nil, err
+ }
+ return n.encoded.AsLink()
+}
+
+// Prototype returns a NodePrototype which can describe some properties of this node's implementation,
+// and also be used to get a NodeBuilder,
+// which can be use to create new nodes with the same implementation as this one.
+//
+// For typed nodes, the NodePrototype will also implement schema.Type.
+//
+// For Advanced Data Layouts, the NodePrototype will encapsulate any additional
+// parameters and configuration of the ADL, and will also (usually)
+// implement NodePrototypeSupportingAmend.
+//
+// Calling this method should not cause an allocation.
+func (n *ProtoNode) Prototype() ipld.NodePrototype {
+ return dagpb.Type.PBNode
+}
+
+var _ ipld.Node = &ProtoNode{}
diff --git a/ipld/merkledag/raw.go b/ipld/merkledag/raw.go
new file mode 100644
index 0000000000..68e2473e17
--- /dev/null
+++ b/ipld/merkledag/raw.go
@@ -0,0 +1,126 @@
+package merkledag
+
+import (
+ "encoding/json"
+ "fmt"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ u "github.com/ipfs/boxo/util"
+ legacy "github.com/ipfs/go-ipld-legacy"
+ ipld "github.com/ipld/go-ipld-prime"
+ basicnode "github.com/ipld/go-ipld-prime/node/basic"
+
+ cid "github.com/ipfs/go-cid"
+ format "github.com/ipfs/go-ipld-format"
+)
+
+// RawNode represents a node which only contains data.
+type RawNode struct {
+ blocks.Block
+
+ // Always a node/basic Bytes.
+ // We can't reference a specific type, as it's not exposed there.
+ // If we find that the interface indirection really matters,
+ // then we could possibly use dagpb.Bytes.
+ ipld.Node
+}
+
+var _ legacy.UniversalNode = &RawNode{}
+
+// NewRawNode creates a RawNode using the default sha2-256 hash function.
+func NewRawNode(data []byte) *RawNode {
+ h := u.Hash(data)
+ c := cid.NewCidV1(cid.Raw, h)
+ blk, _ := blocks.NewBlockWithCid(data, c)
+ return &RawNode{blk, basicnode.NewBytes(data)}
+}
+
+// DecodeRawBlock is a block decoder for raw IPLD nodes conforming to `node.DecodeBlockFunc`.
+func DecodeRawBlock(block blocks.Block) (format.Node, error) {
+ if block.Cid().Type() != cid.Raw {
+ return nil, fmt.Errorf("raw nodes cannot be decoded from non-raw blocks: %d", block.Cid().Type())
+ }
+ // Once you "share" a block, it should be immutable. Therefore, we can just use this block as-is.
+ return &RawNode{block, basicnode.NewBytes(block.RawData())}, nil
+}
+
+var _ format.DecodeBlockFunc = DecodeRawBlock
+
+// NewRawNodeWPrefix creates a RawNode using the provided cid builder
+func NewRawNodeWPrefix(data []byte, builder cid.Builder) (*RawNode, error) {
+ builder = builder.WithCodec(cid.Raw)
+ c, err := builder.Sum(data)
+ if err != nil {
+ return nil, err
+ }
+ blk, err := blocks.NewBlockWithCid(data, c)
+ if err != nil {
+ return nil, err
+ }
+ // Once you "share" a block, it should be immutable. Therefore, we can just use this block as-is.
+ return &RawNode{blk, basicnode.NewBytes(data)}, nil
+}
+
+// Links returns nil.
+func (rn *RawNode) Links() []*format.Link {
+ return nil
+}
+
+// ResolveLink returns an error.
+func (rn *RawNode) ResolveLink(path []string) (*format.Link, []string, error) {
+ return nil, nil, ErrLinkNotFound
+}
+
+// Resolve returns an error.
+func (rn *RawNode) Resolve(path []string) (interface{}, []string, error) {
+ return nil, nil, ErrLinkNotFound
+}
+
+// Tree returns nil.
+func (rn *RawNode) Tree(p string, depth int) []string {
+ return nil
+}
+
+// Copy performs a deep copy of this node and returns it as an format.Node
+func (rn *RawNode) Copy() format.Node {
+ copybuf := make([]byte, len(rn.RawData()))
+ copy(copybuf, rn.RawData())
+ nblk, err := blocks.NewBlockWithCid(rn.RawData(), rn.Cid())
+ if err != nil {
+ // programmer error
+ panic("failure attempting to clone raw block: " + err.Error())
+ }
+ // Once you "share" a block, it should be immutable. Therefore, we can just use this block as-is.
+ return &RawNode{nblk, basicnode.NewBytes(nblk.RawData())}
+}
+
+// Size returns the size of this node
+func (rn *RawNode) Size() (uint64, error) {
+ return uint64(len(rn.RawData())), nil
+}
+
+// Stat returns some Stats about this node.
+func (rn *RawNode) Stat() (*format.NodeStat, error) {
+ return &format.NodeStat{
+ Hash: rn.Cid().String(),
+ NumLinks: 0,
+ BlockSize: len(rn.RawData()),
+ LinksSize: 0,
+ CumulativeSize: len(rn.RawData()),
+ DataSize: len(rn.RawData()),
+ }, nil
+}
+
+// MarshalJSON is required for our "ipfs dag" commands.
+func (rn *RawNode) MarshalJSON() ([]byte, error) {
+ return json.Marshal(string(rn.RawData()))
+}
+
+func RawNodeConverter(b blocks.Block, nd ipld.Node) (legacy.UniversalNode, error) {
+ if nd.Kind() != ipld.Kind_Bytes {
+ return nil, ErrNotRawNode
+ }
+ return &RawNode{b, nd}, nil
+}
+
+var _ legacy.UniversalNode = (*RawNode)(nil)
diff --git a/ipld/merkledag/readonly.go b/ipld/merkledag/readonly.go
new file mode 100644
index 0000000000..36242fbeb1
--- /dev/null
+++ b/ipld/merkledag/readonly.go
@@ -0,0 +1,20 @@
+package merkledag
+
+import (
+ "fmt"
+
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+// ErrReadOnly is used when a read-only datastructure is written to.
+var ErrReadOnly = fmt.Errorf("cannot write to readonly DAGService")
+
+// NewReadOnlyDagService takes a NodeGetter, and returns a full DAGService
+// implementation that returns ErrReadOnly when its 'write' methods are
+// invoked.
+func NewReadOnlyDagService(ng ipld.NodeGetter) ipld.DAGService {
+ return &ComboService{
+ Read: ng,
+ Write: &ErrorService{ErrReadOnly},
+ }
+}
diff --git a/ipld/merkledag/readonly_test.go b/ipld/merkledag/readonly_test.go
new file mode 100644
index 0000000000..0d00eebfb5
--- /dev/null
+++ b/ipld/merkledag/readonly_test.go
@@ -0,0 +1,64 @@
+package merkledag_test
+
+import (
+ "context"
+ "testing"
+
+ . "github.com/ipfs/boxo/ipld/merkledag"
+ dstest "github.com/ipfs/boxo/ipld/merkledag/test"
+
+ cid "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+func TestReadonlyProperties(t *testing.T) {
+ ds := dstest.Mock()
+ ro := NewReadOnlyDagService(ds)
+
+ ctx := context.Background()
+ nds := []ipld.Node{
+ NewRawNode([]byte("foo1")),
+ NewRawNode([]byte("foo2")),
+ NewRawNode([]byte("foo3")),
+ NewRawNode([]byte("foo4")),
+ }
+ cids := []cid.Cid{
+ nds[0].Cid(),
+ nds[1].Cid(),
+ nds[2].Cid(),
+ nds[3].Cid(),
+ }
+
+ // add to the actual underlying datastore
+ if err := ds.Add(ctx, nds[2]); err != nil {
+ t.Fatal(err)
+ }
+ if err := ds.Add(ctx, nds[3]); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := ro.Add(ctx, nds[0]); err != ErrReadOnly {
+ t.Fatal("expected ErrReadOnly")
+ }
+ if err := ro.Add(ctx, nds[2]); err != ErrReadOnly {
+ t.Fatal("expected ErrReadOnly")
+ }
+
+ if err := ro.AddMany(ctx, nds[0:1]); err != ErrReadOnly {
+ t.Fatal("expected ErrReadOnly")
+ }
+
+ if err := ro.Remove(ctx, cids[3]); err != ErrReadOnly {
+ t.Fatal("expected ErrReadOnly")
+ }
+ if err := ro.RemoveMany(ctx, cids[1:2]); err != ErrReadOnly {
+ t.Fatal("expected ErrReadOnly")
+ }
+
+ if _, err := ro.Get(ctx, cids[0]); !ipld.IsNotFound(err) {
+ t.Fatal("expected ErrNotFound")
+ }
+ if _, err := ro.Get(ctx, cids[3]); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/ipld/merkledag/rwservice.go b/ipld/merkledag/rwservice.go
new file mode 100644
index 0000000000..a916350a63
--- /dev/null
+++ b/ipld/merkledag/rwservice.go
@@ -0,0 +1,47 @@
+package merkledag
+
+import (
+ "context"
+
+ cid "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+// ComboService implements ipld.DAGService, using 'Read' for all fetch methods,
+// and 'Write' for all methods that add new objects.
+type ComboService struct {
+ Read ipld.NodeGetter
+ Write ipld.DAGService
+}
+
+var _ ipld.DAGService = (*ComboService)(nil)
+
+// Add writes a new node using the Write DAGService.
+func (cs *ComboService) Add(ctx context.Context, nd ipld.Node) error {
+ return cs.Write.Add(ctx, nd)
+}
+
+// AddMany adds nodes using the Write DAGService.
+func (cs *ComboService) AddMany(ctx context.Context, nds []ipld.Node) error {
+ return cs.Write.AddMany(ctx, nds)
+}
+
+// Get fetches a node using the Read DAGService.
+func (cs *ComboService) Get(ctx context.Context, c cid.Cid) (ipld.Node, error) {
+ return cs.Read.Get(ctx, c)
+}
+
+// GetMany fetches nodes using the Read DAGService.
+func (cs *ComboService) GetMany(ctx context.Context, cids []cid.Cid) <-chan *ipld.NodeOption {
+ return cs.Read.GetMany(ctx, cids)
+}
+
+// Remove deletes a node using the Write DAGService.
+func (cs *ComboService) Remove(ctx context.Context, c cid.Cid) error {
+ return cs.Write.Remove(ctx, c)
+}
+
+// RemoveMany deletes nodes using the Write DAGService.
+func (cs *ComboService) RemoveMany(ctx context.Context, cids []cid.Cid) error {
+ return cs.Write.RemoveMany(ctx, cids)
+}
diff --git a/ipld/merkledag/session.go b/ipld/merkledag/session.go
new file mode 100644
index 0000000000..c7bbff169c
--- /dev/null
+++ b/ipld/merkledag/session.go
@@ -0,0 +1,21 @@
+package merkledag
+
+import (
+ "context"
+
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+// SessionMaker is an object that can generate a new fetching session.
+type SessionMaker interface {
+ Session(context.Context) ipld.NodeGetter
+}
+
+// NewSession returns a session backed NodeGetter if the given NodeGetter
+// implements SessionMaker.
+func NewSession(ctx context.Context, g ipld.NodeGetter) ipld.NodeGetter {
+ if sm, ok := g.(SessionMaker); ok {
+ return sm.Session(ctx)
+ }
+ return g
+}
diff --git a/ipld/merkledag/test/utils.go b/ipld/merkledag/test/utils.go
new file mode 100644
index 0000000000..302d194d6d
--- /dev/null
+++ b/ipld/merkledag/test/utils.go
@@ -0,0 +1,23 @@
+package mdutils
+
+import (
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+
+ bsrv "github.com/ipfs/boxo/blockservice"
+ blockstore "github.com/ipfs/boxo/blockstore"
+ offline "github.com/ipfs/boxo/exchange/offline"
+ ds "github.com/ipfs/go-datastore"
+ dssync "github.com/ipfs/go-datastore/sync"
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+// Mock returns a new thread-safe, mock DAGService.
+func Mock() ipld.DAGService {
+ return dag.NewDAGService(Bserv())
+}
+
+// Bserv returns a new, thread-safe, mock BlockService.
+func Bserv() bsrv.BlockService {
+ bstore := blockstore.NewBlockstore(dssync.MutexWrap(ds.NewMapDatastore()))
+ return bsrv.New(bstore, offline.Exchange(bstore))
+}
diff --git a/ipld/merkledag/traverse/traverse.go b/ipld/merkledag/traverse/traverse.go
new file mode 100644
index 0000000000..dbc426fa92
--- /dev/null
+++ b/ipld/merkledag/traverse/traverse.go
@@ -0,0 +1,222 @@
+// Package traverse provides merkledag traversal functions
+package traverse
+
+import (
+ "context"
+ "errors"
+
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+// Order is an identifier for traversal algorithm orders
+type Order int
+
+// These constants define different traversing methods
+const (
+ // DFSPre defines depth-first pre-order
+ DFSPre Order = iota
+ // DFSPost defines depth-first post-order
+ DFSPost
+ // BFS defines breadth-first order
+ BFS
+)
+
+// Options specifies a series of traversal options
+type Options struct {
+ DAG ipld.NodeGetter // the dagservice to fetch nodes
+ Order Order // what order to traverse in
+ Func Func // the function to perform at each step
+ ErrFunc ErrFunc // see ErrFunc. Optional
+
+ SkipDuplicates bool // whether to skip duplicate nodes
+}
+
+// State is a current traversal state
+type State struct {
+ Node ipld.Node
+ Depth int
+}
+
+type traversal struct {
+ opts Options
+ seen map[string]struct{}
+}
+
+func (t *traversal) shouldSkip(n ipld.Node) (bool, error) {
+ if t.opts.SkipDuplicates {
+ k := n.Cid()
+ if _, found := t.seen[k.KeyString()]; found {
+ return true, nil
+ }
+ t.seen[k.KeyString()] = struct{}{}
+ }
+
+ return false, nil
+}
+
+func (t *traversal) callFunc(next State) error {
+ return t.opts.Func(next)
+}
+
+// getNode returns the node for link. If it return an error,
+// stop processing. if it returns a nil node, just skip it.
+//
+// the error handling is a little complicated.
+func (t *traversal) getNode(link *ipld.Link) (ipld.Node, error) {
+
+ getNode := func(l *ipld.Link) (ipld.Node, error) {
+ next, err := l.GetNode(context.TODO(), t.opts.DAG)
+ if err != nil {
+ return nil, err
+ }
+
+ skip, err := t.shouldSkip(next)
+ if skip {
+ next = nil
+ }
+ return next, err
+ }
+
+ next, err := getNode(link)
+ if err != nil && t.opts.ErrFunc != nil { // attempt recovery.
+ err = t.opts.ErrFunc(err)
+ next = nil // skip regardless
+ }
+ return next, err
+}
+
+// Func is the type of the function called for each dag.Node visited by Traverse.
+// The traversal argument contains the current traversal state.
+// If an error is returned, processing stops.
+type Func func(current State) error
+
+// ErrFunc is provided to handle problems when walking to the Node. Traverse
+// will call ErrFunc with the error encountered. ErrFunc can decide how to
+// handle that error, and return an error back to Traversal with how to proceed:
+// - nil - skip the Node and its children, but continue processing
+// - all other errors halt processing immediately.
+//
+// If ErrFunc is nil, Traversal will stop, as if:
+//
+// opts.ErrFunc = func(err error) { return err }
+type ErrFunc func(err error) error
+
+// Traverse initiates a DAG traversal with the given options starting at
+// the given root.
+func Traverse(root ipld.Node, o Options) error {
+ t := traversal{
+ opts: o,
+ seen: map[string]struct{}{},
+ }
+
+ state := State{
+ Node: root,
+ Depth: 0,
+ }
+
+ switch o.Order {
+ default:
+ return dfsPreTraverse(state, &t)
+ case DFSPre:
+ return dfsPreTraverse(state, &t)
+ case DFSPost:
+ return dfsPostTraverse(state, &t)
+ case BFS:
+ return bfsTraverse(state, &t)
+ }
+}
+
+type dfsFunc func(state State, t *traversal) error
+
+func dfsPreTraverse(state State, t *traversal) error {
+ if err := t.callFunc(state); err != nil {
+ return err
+ }
+ return dfsDescend(dfsPreTraverse, state, t)
+}
+
+func dfsPostTraverse(state State, t *traversal) error {
+ if err := dfsDescend(dfsPostTraverse, state, t); err != nil {
+ return err
+ }
+ return t.callFunc(state)
+}
+
+func dfsDescend(df dfsFunc, curr State, t *traversal) error {
+ for _, l := range curr.Node.Links() {
+ node, err := t.getNode(l)
+ if err != nil {
+ return err
+ }
+ if node == nil { // skip
+ continue
+ }
+
+ next := State{
+ Node: node,
+ Depth: curr.Depth + 1,
+ }
+ if err := df(next, t); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func bfsTraverse(root State, t *traversal) error {
+
+ if skip, err := t.shouldSkip(root.Node); skip || err != nil {
+ return err
+ }
+
+ var q queue
+ q.enq(root)
+ for q.len() > 0 {
+ curr := q.deq()
+ if curr.Node == nil {
+ return errors.New("failed to dequeue though queue not empty")
+ }
+
+ // call user's func
+ if err := t.callFunc(curr); err != nil {
+ return err
+ }
+
+ for _, l := range curr.Node.Links() {
+ node, err := t.getNode(l)
+ if err != nil {
+ return err
+ }
+ if node == nil { // skip
+ continue
+ }
+
+ q.enq(State{
+ Node: node,
+ Depth: curr.Depth + 1,
+ })
+ }
+ }
+ return nil
+}
+
+type queue struct {
+ s []State
+}
+
+func (q *queue) enq(n State) {
+ q.s = append(q.s, n)
+}
+
+func (q *queue) deq() State {
+ if len(q.s) < 1 {
+ return State{}
+ }
+ n := q.s[0]
+ q.s = q.s[1:]
+ return n
+}
+
+func (q *queue) len() int {
+ return len(q.s)
+}
diff --git a/ipld/merkledag/traverse/traverse_test.go b/ipld/merkledag/traverse/traverse_test.go
new file mode 100644
index 0000000000..c19f1b0545
--- /dev/null
+++ b/ipld/merkledag/traverse/traverse_test.go
@@ -0,0 +1,418 @@
+package traverse
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "testing"
+
+ mdag "github.com/ipfs/boxo/ipld/merkledag"
+ mdagtest "github.com/ipfs/boxo/ipld/merkledag/test"
+
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+func TestDFSPreNoSkip(t *testing.T) {
+ ds := mdagtest.Mock()
+ opts := Options{Order: DFSPre, DAG: ds}
+
+ testWalkOutputs(t, newFan(t, ds), opts, []byte(`
+0 /a
+1 /a/aa
+1 /a/ab
+1 /a/ac
+1 /a/ad
+`))
+
+ testWalkOutputs(t, newLinkedList(t, ds), opts, []byte(`
+0 /a
+1 /a/aa
+2 /a/aa/aaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+`))
+
+ testWalkOutputs(t, newBinaryTree(t, ds), opts, []byte(`
+0 /a
+1 /a/aa
+2 /a/aa/aaa
+2 /a/aa/aab
+1 /a/ab
+2 /a/ab/aba
+2 /a/ab/abb
+`))
+
+ testWalkOutputs(t, newBinaryDAG(t, ds), opts, []byte(`
+0 /a
+1 /a/aa
+2 /a/aa/aaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+2 /a/aa/aaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+1 /a/aa
+2 /a/aa/aaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+2 /a/aa/aaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+`))
+}
+
+func TestDFSPreSkip(t *testing.T) {
+ ds := mdagtest.Mock()
+ opts := Options{Order: DFSPre, SkipDuplicates: true, DAG: ds}
+
+ testWalkOutputs(t, newFan(t, ds), opts, []byte(`
+0 /a
+1 /a/aa
+1 /a/ab
+1 /a/ac
+1 /a/ad
+`))
+
+ testWalkOutputs(t, newLinkedList(t, ds), opts, []byte(`
+0 /a
+1 /a/aa
+2 /a/aa/aaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+`))
+
+ testWalkOutputs(t, newBinaryTree(t, ds), opts, []byte(`
+0 /a
+1 /a/aa
+2 /a/aa/aaa
+2 /a/aa/aab
+1 /a/ab
+2 /a/ab/aba
+2 /a/ab/abb
+`))
+
+ testWalkOutputs(t, newBinaryDAG(t, ds), opts, []byte(`
+0 /a
+1 /a/aa
+2 /a/aa/aaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+`))
+}
+
+func TestDFSPostNoSkip(t *testing.T) {
+ ds := mdagtest.Mock()
+ opts := Options{Order: DFSPost, DAG: ds}
+
+ testWalkOutputs(t, newFan(t, ds), opts, []byte(`
+1 /a/aa
+1 /a/ab
+1 /a/ac
+1 /a/ad
+0 /a
+`))
+
+ testWalkOutputs(t, newLinkedList(t, ds), opts, []byte(`
+4 /a/aa/aaa/aaaa/aaaaa
+3 /a/aa/aaa/aaaa
+2 /a/aa/aaa
+1 /a/aa
+0 /a
+`))
+
+ testWalkOutputs(t, newBinaryTree(t, ds), opts, []byte(`
+2 /a/aa/aaa
+2 /a/aa/aab
+1 /a/aa
+2 /a/ab/aba
+2 /a/ab/abb
+1 /a/ab
+0 /a
+`))
+
+ testWalkOutputs(t, newBinaryDAG(t, ds), opts, []byte(`
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+3 /a/aa/aaa/aaaa
+2 /a/aa/aaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+3 /a/aa/aaa/aaaa
+2 /a/aa/aaa
+1 /a/aa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+3 /a/aa/aaa/aaaa
+2 /a/aa/aaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+3 /a/aa/aaa/aaaa
+2 /a/aa/aaa
+1 /a/aa
+0 /a
+`))
+}
+
+func TestDFSPostSkip(t *testing.T) {
+ ds := mdagtest.Mock()
+ opts := Options{Order: DFSPost, SkipDuplicates: true, DAG: ds}
+
+ testWalkOutputs(t, newFan(t, ds), opts, []byte(`
+1 /a/aa
+1 /a/ab
+1 /a/ac
+1 /a/ad
+0 /a
+`))
+
+ testWalkOutputs(t, newLinkedList(t, ds), opts, []byte(`
+4 /a/aa/aaa/aaaa/aaaaa
+3 /a/aa/aaa/aaaa
+2 /a/aa/aaa
+1 /a/aa
+0 /a
+`))
+
+ testWalkOutputs(t, newBinaryTree(t, ds), opts, []byte(`
+2 /a/aa/aaa
+2 /a/aa/aab
+1 /a/aa
+2 /a/ab/aba
+2 /a/ab/abb
+1 /a/ab
+0 /a
+`))
+
+ testWalkOutputs(t, newBinaryDAG(t, ds), opts, []byte(`
+4 /a/aa/aaa/aaaa/aaaaa
+3 /a/aa/aaa/aaaa
+2 /a/aa/aaa
+1 /a/aa
+0 /a
+`))
+}
+
+func TestBFSNoSkip(t *testing.T) {
+ ds := mdagtest.Mock()
+ opts := Options{Order: BFS, DAG: ds}
+
+ testWalkOutputs(t, newFan(t, ds), opts, []byte(`
+0 /a
+1 /a/aa
+1 /a/ab
+1 /a/ac
+1 /a/ad
+`))
+
+ testWalkOutputs(t, newLinkedList(t, ds), opts, []byte(`
+0 /a
+1 /a/aa
+2 /a/aa/aaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+`))
+
+ testWalkOutputs(t, newBinaryTree(t, ds), opts, []byte(`
+0 /a
+1 /a/aa
+1 /a/ab
+2 /a/aa/aaa
+2 /a/aa/aab
+2 /a/ab/aba
+2 /a/ab/abb
+`))
+
+ testWalkOutputs(t, newBinaryDAG(t, ds), opts, []byte(`
+0 /a
+1 /a/aa
+1 /a/aa
+2 /a/aa/aaa
+2 /a/aa/aaa
+2 /a/aa/aaa
+2 /a/aa/aaa
+3 /a/aa/aaa/aaaa
+3 /a/aa/aaa/aaaa
+3 /a/aa/aaa/aaaa
+3 /a/aa/aaa/aaaa
+3 /a/aa/aaa/aaaa
+3 /a/aa/aaa/aaaa
+3 /a/aa/aaa/aaaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+4 /a/aa/aaa/aaaa/aaaaa
+`))
+}
+
+func TestBFSSkip(t *testing.T) {
+ ds := mdagtest.Mock()
+ opts := Options{Order: BFS, SkipDuplicates: true, DAG: ds}
+
+ testWalkOutputs(t, newFan(t, ds), opts, []byte(`
+0 /a
+1 /a/aa
+1 /a/ab
+1 /a/ac
+1 /a/ad
+`))
+
+ testWalkOutputs(t, newLinkedList(t, ds), opts, []byte(`
+0 /a
+1 /a/aa
+2 /a/aa/aaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+`))
+
+ testWalkOutputs(t, newBinaryTree(t, ds), opts, []byte(`
+0 /a
+1 /a/aa
+1 /a/ab
+2 /a/aa/aaa
+2 /a/aa/aab
+2 /a/ab/aba
+2 /a/ab/abb
+`))
+
+ testWalkOutputs(t, newBinaryDAG(t, ds), opts, []byte(`
+0 /a
+1 /a/aa
+2 /a/aa/aaa
+3 /a/aa/aaa/aaaa
+4 /a/aa/aaa/aaaa/aaaaa
+`))
+}
+
+func testWalkOutputs(t *testing.T, root ipld.Node, opts Options, expect []byte) {
+ expect = bytes.TrimLeft(expect, "\n")
+
+ buf := new(bytes.Buffer)
+ walk := func(current State) error {
+ s := fmt.Sprintf("%d %s\n", current.Depth, current.Node.(*mdag.ProtoNode).Data())
+ t.Logf("walk: %s", s)
+ buf.Write([]byte(s))
+ return nil
+ }
+
+ opts.Func = walk
+ if err := Traverse(root, opts); err != nil {
+ t.Error(err)
+ return
+ }
+
+ actual := buf.Bytes()
+ if !bytes.Equal(actual, expect) {
+ t.Error("error: outputs differ")
+ t.Logf("expect:\n%s", expect)
+ t.Logf("actual:\n%s", actual)
+ } else {
+ t.Logf("expect matches actual:\n%s", expect)
+ }
+}
+
+func newFan(t *testing.T, ds ipld.DAGService) ipld.Node {
+ a := mdag.NodeWithData([]byte("/a"))
+ addLink(t, ds, a, child(t, ds, a, "aa"))
+ addLink(t, ds, a, child(t, ds, a, "ab"))
+ addLink(t, ds, a, child(t, ds, a, "ac"))
+ addLink(t, ds, a, child(t, ds, a, "ad"))
+ return a
+}
+
+func newLinkedList(t *testing.T, ds ipld.DAGService) ipld.Node {
+ a := mdag.NodeWithData([]byte("/a"))
+ aa := child(t, ds, a, "aa")
+ aaa := child(t, ds, aa, "aaa")
+ aaaa := child(t, ds, aaa, "aaaa")
+ aaaaa := child(t, ds, aaaa, "aaaaa")
+ addLink(t, ds, aaaa, aaaaa)
+ addLink(t, ds, aaa, aaaa)
+ addLink(t, ds, aa, aaa)
+ addLink(t, ds, a, aa)
+ return a
+}
+
+func newBinaryTree(t *testing.T, ds ipld.DAGService) ipld.Node {
+ a := mdag.NodeWithData([]byte("/a"))
+ aa := child(t, ds, a, "aa")
+ ab := child(t, ds, a, "ab")
+ addLink(t, ds, aa, child(t, ds, aa, "aaa"))
+ addLink(t, ds, aa, child(t, ds, aa, "aab"))
+ addLink(t, ds, ab, child(t, ds, ab, "aba"))
+ addLink(t, ds, ab, child(t, ds, ab, "abb"))
+ addLink(t, ds, a, aa)
+ addLink(t, ds, a, ab)
+ return a
+}
+
+func newBinaryDAG(t *testing.T, ds ipld.DAGService) ipld.Node {
+ a := mdag.NodeWithData([]byte("/a"))
+ aa := child(t, ds, a, "aa")
+ aaa := child(t, ds, aa, "aaa")
+ aaaa := child(t, ds, aaa, "aaaa")
+ aaaaa := child(t, ds, aaaa, "aaaaa")
+ addLink(t, ds, aaaa, aaaaa)
+ addLink(t, ds, aaaa, aaaaa)
+ addLink(t, ds, aaa, aaaa)
+ addLink(t, ds, aaa, aaaa)
+ addLink(t, ds, aa, aaa)
+ addLink(t, ds, aa, aaa)
+ addLink(t, ds, a, aa)
+ addLink(t, ds, a, aa)
+ return a
+}
+
+func addLink(t *testing.T, ds ipld.DAGService, a, b ipld.Node) {
+ to := string(a.(*mdag.ProtoNode).Data()) + "2" + string(b.(*mdag.ProtoNode).Data())
+ if err := ds.Add(context.Background(), b); err != nil {
+ t.Error(err)
+ }
+ if err := a.(*mdag.ProtoNode).AddNodeLink(to, b.(*mdag.ProtoNode)); err != nil {
+ t.Error(err)
+ }
+}
+
+func child(t *testing.T, ds ipld.DAGService, a ipld.Node, name string) ipld.Node {
+ return mdag.NodeWithData([]byte(string(a.(*mdag.ProtoNode).Data()) + "/" + name))
+}
diff --git a/ipns/README.md b/ipns/README.md
new file mode 100644
index 0000000000..0afa0be00b
--- /dev/null
+++ b/ipns/README.md
@@ -0,0 +1,31 @@
+## Usage
+
+To create a new IPNS record:
+
+```go
+import (
+ "time"
+
+ ipns "github.com/ipfs/boxo/ipns"
+ crypto "github.com/libp2p/go-libp2p/core/crypto"
+)
+
+// Generate a private key to sign the IPNS record with. Most of the time,
+// however, you'll want to retrieve an already-existing key from IPFS using the
+// go-ipfs/core/coreapi CoreAPI.KeyAPI() interface.
+privateKey, publicKey, err := crypto.GenerateKeyPair(crypto.RSA, 2048)
+if err != nil {
+ panic(err)
+}
+
+// Create an IPNS record that expires in one hour and points to the IPFS address
+// /ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5
+ipnsRecord, err := ipns.Create(privateKey, []byte("/ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5"), 0, time.Now().Add(1*time.Hour))
+if err != nil {
+ panic(err)
+}
+```
+
+Once you have the record, you’ll need to use IPFS to *publish* it.
+
+There are several other major operations you can do with `go-ipns`. Check out the [API docs](https://pkg.go.dev/github.com/ipfs/boxo/ipns) or look at the tests in this repo for examples.
diff --git a/ipns/errors.go b/ipns/errors.go
new file mode 100644
index 0000000000..d78aafffa0
--- /dev/null
+++ b/ipns/errors.go
@@ -0,0 +1,44 @@
+package ipns
+
+import (
+ "errors"
+)
+
+// ErrExpiredRecord should be returned when an ipns record is
+// invalid due to being too old
+var ErrExpiredRecord = errors.New("expired record")
+
+// ErrUnrecognizedValidity is returned when an IpnsRecord has an
+// unknown validity type.
+var ErrUnrecognizedValidity = errors.New("unrecognized validity type")
+
+// ErrInvalidPath should be returned when an ipns record path
+// is not in a valid format
+var ErrInvalidPath = errors.New("record path invalid")
+
+// ErrSignature should be returned when an ipns record fails
+// signature verification
+var ErrSignature = errors.New("record signature verification failed")
+
+// ErrKeyFormat should be returned when an ipns record key is
+// incorrectly formatted (not a peer ID)
+var ErrKeyFormat = errors.New("record key could not be parsed into peer ID")
+
+// ErrPublicKeyNotFound should be returned when the public key
+// corresponding to the ipns record path cannot be retrieved
+// from the peer store
+var ErrPublicKeyNotFound = errors.New("public key not found in peer store")
+
+// ErrPublicKeyMismatch should be returned when the public key embedded in the
+// record doesn't match the expected public key.
+var ErrPublicKeyMismatch = errors.New("public key in record did not match expected pubkey")
+
+// ErrBadRecord should be returned when an ipns record cannot be unmarshalled
+var ErrBadRecord = errors.New("record could not be unmarshalled")
+
+// 10 KiB limit defined in https://github.com/ipfs/specs/pull/319
+const MaxRecordSize int = 10 << (10 * 1)
+
+// ErrRecordSize should be returned when an ipns record is
+// invalid due to being too big
+var ErrRecordSize = errors.New("record exceeds allowed size limit")
diff --git a/ipns/examples/embed.go b/ipns/examples/embed.go
new file mode 100644
index 0000000000..1f33d514d5
--- /dev/null
+++ b/ipns/examples/embed.go
@@ -0,0 +1,27 @@
+package examples
+
+import (
+ "time"
+
+ pb "github.com/ipfs/boxo/ipns/pb"
+
+ "github.com/ipfs/boxo/ipns"
+ "github.com/libp2p/go-libp2p/core/crypto"
+)
+
+// CreateEntryWithEmbed shows how you can create an IPNS entry
+// and embed it with a public key. For ed25519 keys this is not needed
+// so attempting to embed with an ed25519 key, will not actually embed the key
+func CreateEntryWithEmbed(ipfsPath string, publicKey crypto.PubKey, privateKey crypto.PrivKey) (*pb.IpnsEntry, error) {
+ ipfsPathByte := []byte(ipfsPath)
+ eol := time.Now().Add(time.Hour * 48)
+ entry, err := ipns.Create(privateKey, ipfsPathByte, 1, eol, 0)
+ if err != nil {
+ return nil, err
+ }
+ err = ipns.EmbedPublicKey(publicKey, entry)
+ if err != nil {
+ return nil, err
+ }
+ return entry, nil
+}
diff --git a/ipns/examples/examples_test.go b/ipns/examples/examples_test.go
new file mode 100644
index 0000000000..0fe182d9f3
--- /dev/null
+++ b/ipns/examples/examples_test.go
@@ -0,0 +1,60 @@
+package examples_test
+
+import (
+ "testing"
+
+ "github.com/ipfs/boxo/ipns/examples"
+ "github.com/libp2p/go-libp2p/core/crypto"
+)
+
+var testPath = "/ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5"
+
+func TestKeyGeneration(t *testing.T) {
+ _, err := generateRSAKey()
+ if err != nil {
+ t.Error(err)
+ }
+
+ _, err = generateEDKey()
+ if err != nil {
+ t.Error(err)
+ }
+}
+
+func TestEmbeddedEntryCreation(t *testing.T) {
+ rk, err := generateRSAKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ek, err := generateEDKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = examples.CreateEntryWithEmbed(testPath, rk.GetPublic(), rk)
+ if err != nil {
+ t.Error(err)
+ }
+
+ _, err = examples.CreateEntryWithEmbed(testPath, ek.GetPublic(), ek)
+ if err != nil {
+ t.Error(err)
+ }
+
+}
+func generateRSAKey() (crypto.PrivKey, error) {
+ k, err := examples.GenerateRSAKeyPair(2048)
+ if err != nil {
+ return nil, err
+ }
+ return k, nil
+}
+
+func generateEDKey() (crypto.PrivKey, error) {
+ // ED25519 uses 256bit keys, and ignore the bit param
+ k, err := examples.GenerateEDKeyPair()
+ if err != nil {
+ return nil, err
+ }
+ return k, nil
+}
diff --git a/ipns/examples/key.go b/ipns/examples/key.go
new file mode 100644
index 0000000000..94f219b8d2
--- /dev/null
+++ b/ipns/examples/key.go
@@ -0,0 +1,24 @@
+package examples
+
+import (
+ "github.com/libp2p/go-libp2p/core/crypto"
+)
+
+// GenerateRSAKeyPair is used to generate an RSA key pair
+func GenerateRSAKeyPair(bits int) (crypto.PrivKey, error) {
+ priv, _, err := crypto.GenerateKeyPair(crypto.RSA, bits)
+ if err != nil {
+ return nil, err
+ }
+ return priv, nil
+}
+
+// GenerateEDKeyPair is used to generate an ED25519 keypair
+func GenerateEDKeyPair() (crypto.PrivKey, error) {
+ // ED25519 ignores the bit param and uses 256bit keys
+ priv, _, err := crypto.GenerateKeyPair(crypto.Ed25519, 256)
+ if err != nil {
+ return nil, err
+ }
+ return priv, nil
+}
diff --git a/ipns/ipns.go b/ipns/ipns.go
new file mode 100644
index 0000000000..87ddadb034
--- /dev/null
+++ b/ipns/ipns.go
@@ -0,0 +1,398 @@
+package ipns
+
+import (
+ "bytes"
+ "fmt"
+ "sort"
+ "time"
+
+ "github.com/multiformats/go-multicodec"
+ "github.com/pkg/errors"
+
+ "github.com/ipld/go-ipld-prime"
+ _ "github.com/ipld/go-ipld-prime/codec/dagcbor" // used to import the DagCbor encoder/decoder
+ ipldcodec "github.com/ipld/go-ipld-prime/multicodec"
+ basicnode "github.com/ipld/go-ipld-prime/node/basic"
+
+ "github.com/gogo/protobuf/proto"
+
+ pb "github.com/ipfs/boxo/ipns/pb"
+
+ u "github.com/ipfs/boxo/util"
+ ic "github.com/libp2p/go-libp2p/core/crypto"
+ "github.com/libp2p/go-libp2p/core/peer"
+)
+
+const (
+ validity = "Validity"
+ validityType = "ValidityType"
+ value = "Value"
+ sequence = "Sequence"
+ ttl = "TTL"
+)
+
+// Create creates a new IPNS entry and signs it with the given private key.
+//
+// This function does not embed the public key. If you want to do that, use
+// `EmbedPublicKey`.
+func Create(sk ic.PrivKey, val []byte, seq uint64, eol time.Time, ttl time.Duration) (*pb.IpnsEntry, error) {
+ entry := new(pb.IpnsEntry)
+
+ entry.Value = val
+ typ := pb.IpnsEntry_EOL
+ entry.ValidityType = &typ
+ entry.Sequence = &seq
+ entry.Validity = []byte(u.FormatRFC3339(eol))
+
+ ttlNs := uint64(ttl.Nanoseconds())
+ entry.Ttl = proto.Uint64(ttlNs)
+
+ cborData, err := createCborDataForIpnsEntry(entry)
+ if err != nil {
+ return nil, err
+ }
+ entry.Data = cborData
+
+ // For now we still create V1 signatures. These are deprecated, and not
+ // used during verification anymore (Validate func requires SignatureV2),
+ // but setting it here allows legacy nodes (e.g., go-ipfs < v0.9.0) to
+ // still resolve IPNS published by modern nodes.
+ sig1, err := sk.Sign(ipnsEntryDataForSigV1(entry))
+ if err != nil {
+ return nil, errors.Wrap(err, "could not compute signature data")
+ }
+ entry.SignatureV1 = sig1
+
+ sig2Data, err := ipnsEntryDataForSigV2(entry)
+ if err != nil {
+ return nil, err
+ }
+ sig2, err := sk.Sign(sig2Data)
+ if err != nil {
+ return nil, err
+ }
+ entry.SignatureV2 = sig2
+
+ return entry, nil
+}
+
+func createCborDataForIpnsEntry(e *pb.IpnsEntry) ([]byte, error) {
+ m := make(map[string]ipld.Node)
+ var keys []string
+ m[value] = basicnode.NewBytes(e.GetValue())
+ keys = append(keys, value)
+
+ m[validity] = basicnode.NewBytes(e.GetValidity())
+ keys = append(keys, validity)
+
+ m[validityType] = basicnode.NewInt(int64(e.GetValidityType()))
+ keys = append(keys, validityType)
+
+ m[sequence] = basicnode.NewInt(int64(e.GetSequence()))
+ keys = append(keys, sequence)
+
+ m[ttl] = basicnode.NewInt(int64(e.GetTtl()))
+ keys = append(keys, ttl)
+
+ sort.Sort(cborMapKeyString_RFC7049(keys))
+
+ newNd := basicnode.Prototype__Map{}.NewBuilder()
+ ma, err := newNd.BeginMap(int64(len(keys)))
+ if err != nil {
+ return nil, err
+ }
+
+ for _, k := range keys {
+ if err := ma.AssembleKey().AssignString(k); err != nil {
+ return nil, err
+ }
+ if err := ma.AssembleValue().AssignNode(m[k]); err != nil {
+ return nil, err
+ }
+ }
+
+ if err := ma.Finish(); err != nil {
+ return nil, err
+ }
+
+ nd := newNd.Build()
+
+ enc, err := ipldcodec.LookupEncoder(uint64(multicodec.DagCbor))
+ if err != nil {
+ return nil, err
+ }
+
+ buf := new(bytes.Buffer)
+ if err := enc(nd, buf); err != nil {
+ return nil, err
+ }
+
+ return buf.Bytes(), nil
+}
+
+// Validates validates the given IPNS entry against the given public key.
+func Validate(pk ic.PubKey, entry *pb.IpnsEntry) error {
+ // Make sure max size is respected
+ if entry.Size() > MaxRecordSize {
+ return ErrRecordSize
+ }
+
+ // Check the ipns record signature with the public key
+ if entry.GetSignatureV2() == nil {
+ // always error if no valid signature could be found
+ return ErrSignature
+ }
+
+ sig2Data, err := ipnsEntryDataForSigV2(entry)
+ if err != nil {
+ return fmt.Errorf("could not compute signature data: %w", err)
+ }
+ if ok, err := pk.Verify(sig2Data, entry.GetSignatureV2()); err != nil || !ok {
+ return ErrSignature
+ }
+
+ // TODO: If we switch from pb.IpnsEntry to a more generic IpnsRecord type then perhaps we should only check
+ // this if there is no v1 signature. In the meanwhile this helps avoid some potential rough edges around people
+ // checking the entry fields instead of doing CBOR decoding everywhere.
+ // See https://github.com/ipfs/boxo/ipns/pull/42 for next steps here
+ if err := validateCborDataMatchesPbData(entry); err != nil {
+ return err
+ }
+
+ eol, err := GetEOL(entry)
+ if err != nil {
+ return err
+ }
+ if time.Now().After(eol) {
+ return ErrExpiredRecord
+ }
+ return nil
+}
+
+// TODO: Most of this function could probably be replaced with codegen
+func validateCborDataMatchesPbData(entry *pb.IpnsEntry) error {
+ if len(entry.GetData()) == 0 {
+ return fmt.Errorf("record data is missing")
+ }
+
+ dec, err := ipldcodec.LookupDecoder(uint64(multicodec.DagCbor))
+ if err != nil {
+ return err
+ }
+
+ ndbuilder := basicnode.Prototype__Map{}.NewBuilder()
+ if err := dec(ndbuilder, bytes.NewReader(entry.GetData())); err != nil {
+ return err
+ }
+
+ fullNd := ndbuilder.Build()
+ nd, err := fullNd.LookupByString(value)
+ if err != nil {
+ return err
+ }
+ ndBytes, err := nd.AsBytes()
+ if err != nil {
+ return err
+ }
+ if !bytes.Equal(entry.GetValue(), ndBytes) {
+ return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", value)
+ }
+
+ nd, err = fullNd.LookupByString(validity)
+ if err != nil {
+ return err
+ }
+ ndBytes, err = nd.AsBytes()
+ if err != nil {
+ return err
+ }
+ if !bytes.Equal(entry.GetValidity(), ndBytes) {
+ return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", validity)
+ }
+
+ nd, err = fullNd.LookupByString(validityType)
+ if err != nil {
+ return err
+ }
+ ndInt, err := nd.AsInt()
+ if err != nil {
+ return err
+ }
+ if int64(entry.GetValidityType()) != ndInt {
+ return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", validityType)
+ }
+
+ nd, err = fullNd.LookupByString(sequence)
+ if err != nil {
+ return err
+ }
+ ndInt, err = nd.AsInt()
+ if err != nil {
+ return err
+ }
+
+ if entry.GetSequence() != uint64(ndInt) {
+ return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", sequence)
+ }
+
+ nd, err = fullNd.LookupByString("TTL")
+ if err != nil {
+ return err
+ }
+ ndInt, err = nd.AsInt()
+ if err != nil {
+ return err
+ }
+ if entry.GetTtl() != uint64(ndInt) {
+ return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", ttl)
+ }
+
+ return nil
+}
+
+// GetEOL returns the EOL of this IPNS entry
+//
+// This function returns ErrUnrecognizedValidity if the validity type of the
+// record isn't EOL. Otherwise, it returns an error if it can't parse the EOL.
+func GetEOL(entry *pb.IpnsEntry) (time.Time, error) {
+ if entry.GetValidityType() != pb.IpnsEntry_EOL {
+ return time.Time{}, ErrUnrecognizedValidity
+ }
+ return u.ParseRFC3339(string(entry.GetValidity()))
+}
+
+// EmbedPublicKey embeds the given public key in the given ipns entry. While not
+// strictly required, some nodes (e.g., DHT servers) may reject IPNS entries
+// that don't embed their public keys as they may not be able to validate them
+// efficiently.
+func EmbedPublicKey(pk ic.PubKey, entry *pb.IpnsEntry) error {
+ // Try extracting the public key from the ID. If we can, *don't* embed
+ // it.
+ id, err := peer.IDFromPublicKey(pk)
+ if err != nil {
+ return err
+ }
+ if _, err := id.ExtractPublicKey(); err != peer.ErrNoPublicKey {
+ // Either a *real* error or nil.
+ return err
+ }
+
+ // We failed to extract the public key from the peer ID, embed it in the
+ // record.
+ pkBytes, err := ic.MarshalPublicKey(pk)
+ if err != nil {
+ return err
+ }
+ entry.PubKey = pkBytes
+ return nil
+}
+
+// ExtractPublicKey extracts a public key matching `pid` from the IPNS record,
+// if possible.
+//
+// This function returns (nil, nil) when no public key can be extracted and
+// nothing is malformed.
+func ExtractPublicKey(pid peer.ID, entry *pb.IpnsEntry) (ic.PubKey, error) {
+ if entry.PubKey != nil {
+ pk, err := ic.UnmarshalPublicKey(entry.PubKey)
+ if err != nil {
+ return nil, fmt.Errorf("unmarshaling pubkey in record: %s", err)
+ }
+
+ expPid, err := peer.IDFromPublicKey(pk)
+ if err != nil {
+ return nil, fmt.Errorf("could not regenerate peerID from pubkey: %s", err)
+ }
+
+ if pid != expPid {
+ return nil, ErrPublicKeyMismatch
+ }
+ return pk, nil
+ }
+
+ return pid.ExtractPublicKey()
+}
+
+// Compare compares two IPNS entries. It returns:
+//
+// * -1 if a is older than b
+// * 0 if a and b cannot be ordered (this doesn't mean that they are equal)
+// * +1 if a is newer than b
+//
+// It returns an error when either a or b are malformed.
+//
+// NOTE: It *does not* validate the records, the caller is responsible for calling
+// `Validate` first.
+//
+// NOTE: If a and b cannot be ordered by this function, you can determine their
+// order by comparing their serialized byte representations (using
+// `bytes.Compare`). You must do this if you are implementing a libp2p record
+// validator (or you can just use the one provided for you by this package).
+func Compare(a, b *pb.IpnsEntry) (int, error) {
+ aHasV2Sig := a.GetSignatureV2() != nil
+ bHasV2Sig := b.GetSignatureV2() != nil
+
+ // Having a newer signature version is better than an older signature version
+ if aHasV2Sig && !bHasV2Sig {
+ return 1, nil
+ } else if !aHasV2Sig && bHasV2Sig {
+ return -1, nil
+ }
+
+ as := a.GetSequence()
+ bs := b.GetSequence()
+
+ if as > bs {
+ return 1, nil
+ } else if as < bs {
+ return -1, nil
+ }
+
+ at, err := u.ParseRFC3339(string(a.GetValidity()))
+ if err != nil {
+ return 0, err
+ }
+
+ bt, err := u.ParseRFC3339(string(b.GetValidity()))
+ if err != nil {
+ return 0, err
+ }
+
+ if at.After(bt) {
+ return 1, nil
+ } else if bt.After(at) {
+ return -1, nil
+ }
+
+ return 0, nil
+}
+
+func ipnsEntryDataForSigV1(e *pb.IpnsEntry) []byte {
+ return bytes.Join([][]byte{
+ e.Value,
+ e.Validity,
+ []byte(fmt.Sprint(e.GetValidityType())),
+ },
+ []byte{})
+}
+
+func ipnsEntryDataForSigV2(e *pb.IpnsEntry) ([]byte, error) {
+ dataForSig := []byte("ipns-signature:")
+ dataForSig = append(dataForSig, e.Data...)
+
+ return dataForSig, nil
+}
+
+type cborMapKeyString_RFC7049 []string
+
+func (x cborMapKeyString_RFC7049) Len() int { return len(x) }
+func (x cborMapKeyString_RFC7049) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
+func (x cborMapKeyString_RFC7049) Less(i, j int) bool {
+ li, lj := len(x[i]), len(x[j])
+ if li == lj {
+ return x[i] < x[j]
+ }
+ return li < lj
+}
+
+var _ sort.Interface = (cborMapKeyString_RFC7049)(nil)
diff --git a/ipns/ipns_test.go b/ipns/ipns_test.go
new file mode 100644
index 0000000000..e6d521ce4f
--- /dev/null
+++ b/ipns/ipns_test.go
@@ -0,0 +1,63 @@
+package ipns
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ u "github.com/ipfs/boxo/util"
+ ci "github.com/libp2p/go-libp2p/core/crypto"
+ "github.com/libp2p/go-libp2p/core/peer"
+)
+
+func TestEmbedPublicKey(t *testing.T) {
+
+ sr := u.NewTimeSeededRand()
+ priv, pub, err := ci.GenerateKeyPairWithReader(ci.RSA, 2048, sr)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ pid, err := peer.IDFromPublicKey(pub)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ e, err := Create(priv, []byte("/a/b"), 0, time.Now().Add(1*time.Hour), 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := EmbedPublicKey(pub, e); err != nil {
+ t.Fatal(err)
+ }
+ embeddedPk, err := ci.UnmarshalPublicKey(e.PubKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ embeddedPid, err := peer.IDFromPublicKey(embeddedPk)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if embeddedPid != pid {
+ t.Fatalf("pid mismatch: %s != %s", pid, embeddedPid)
+ }
+}
+
+func ExampleCreate() {
+ // Generate a private key to sign the IPNS record with. Most of the time,
+ // however, you'll want to retrieve an already-existing key from IPFS using
+ // go-ipfs/core/coreapi CoreAPI.KeyAPI() interface.
+ privateKey, _, err := ci.GenerateKeyPair(ci.RSA, 2048)
+ if err != nil {
+ panic(err)
+ }
+
+ // Create an IPNS record that expires in one hour and points to the IPFS address
+ // /ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5
+ ipnsRecord, err := Create(privateKey, []byte("/ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5"), 0, time.Now().Add(1*time.Hour), 0)
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(ipnsRecord)
+}
diff --git a/ipns/pb/Makefile b/ipns/pb/Makefile
new file mode 100644
index 0000000000..eb14b5768a
--- /dev/null
+++ b/ipns/pb/Makefile
@@ -0,0 +1,11 @@
+PB = $(wildcard *.proto)
+GO = $(PB:.proto=.pb.go)
+
+all: $(GO)
+
+%.pb.go: %.proto
+ protoc --proto_path=$(GOPATH)/src:. --gogofast_out=. $<
+
+clean:
+ rm -f *.pb.go
+ rm -f *.go
diff --git a/ipns/pb/ipns.pb.go b/ipns/pb/ipns.pb.go
new file mode 100644
index 0000000000..1e24888527
--- /dev/null
+++ b/ipns/pb/ipns.pb.go
@@ -0,0 +1,992 @@
+// Code generated by protoc-gen-gogo. DO NOT EDIT.
+// source: ipns.proto
+
+package ipns_pb
+
+import (
+ fmt "fmt"
+ proto "github.com/gogo/protobuf/proto"
+ io "io"
+ math "math"
+ math_bits "math/bits"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
+
+type IpnsEntry_ValidityType int32
+
+const (
+ // setting an EOL says "this record is valid until..."
+ IpnsEntry_EOL IpnsEntry_ValidityType = 0
+)
+
+var IpnsEntry_ValidityType_name = map[int32]string{
+ 0: "EOL",
+}
+
+var IpnsEntry_ValidityType_value = map[string]int32{
+ "EOL": 0,
+}
+
+func (x IpnsEntry_ValidityType) Enum() *IpnsEntry_ValidityType {
+ p := new(IpnsEntry_ValidityType)
+ *p = x
+ return p
+}
+
+func (x IpnsEntry_ValidityType) String() string {
+ return proto.EnumName(IpnsEntry_ValidityType_name, int32(x))
+}
+
+func (x *IpnsEntry_ValidityType) UnmarshalJSON(data []byte) error {
+ value, err := proto.UnmarshalJSONEnum(IpnsEntry_ValidityType_value, data, "IpnsEntry_ValidityType")
+ if err != nil {
+ return err
+ }
+ *x = IpnsEntry_ValidityType(value)
+ return nil
+}
+
+func (IpnsEntry_ValidityType) EnumDescriptor() ([]byte, []int) {
+ return fileDescriptor_4d5b16fb32bfe8ea, []int{0, 0}
+}
+
+type IpnsEntry struct {
+ Value []byte `protobuf:"bytes,1,opt,name=value" json:"value,omitempty"`
+ SignatureV1 []byte `protobuf:"bytes,2,opt,name=signatureV1" json:"signatureV1,omitempty"`
+ ValidityType *IpnsEntry_ValidityType `protobuf:"varint,3,opt,name=validityType,enum=ipns.v1.pb.IpnsEntry_ValidityType" json:"validityType,omitempty"`
+ Validity []byte `protobuf:"bytes,4,opt,name=validity" json:"validity,omitempty"`
+ Sequence *uint64 `protobuf:"varint,5,opt,name=sequence" json:"sequence,omitempty"`
+ Ttl *uint64 `protobuf:"varint,6,opt,name=ttl" json:"ttl,omitempty"`
+ // 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
+ // peerID, making this field unnecessary.
+ PubKey []byte `protobuf:"bytes,7,opt,name=pubKey" json:"pubKey,omitempty"`
+ SignatureV2 []byte `protobuf:"bytes,8,opt,name=signatureV2" json:"signatureV2,omitempty"`
+ Data []byte `protobuf:"bytes,9,opt,name=data" json:"data,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *IpnsEntry) Reset() { *m = IpnsEntry{} }
+func (m *IpnsEntry) String() string { return proto.CompactTextString(m) }
+func (*IpnsEntry) ProtoMessage() {}
+func (*IpnsEntry) Descriptor() ([]byte, []int) {
+ return fileDescriptor_4d5b16fb32bfe8ea, []int{0}
+}
+func (m *IpnsEntry) XXX_Unmarshal(b []byte) error {
+ return m.Unmarshal(b)
+}
+func (m *IpnsEntry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ if deterministic {
+ return xxx_messageInfo_IpnsEntry.Marshal(b, m, deterministic)
+ } else {
+ b = b[:cap(b)]
+ n, err := m.MarshalToSizedBuffer(b)
+ if err != nil {
+ return nil, err
+ }
+ return b[:n], nil
+ }
+}
+func (m *IpnsEntry) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_IpnsEntry.Merge(m, src)
+}
+func (m *IpnsEntry) XXX_Size() int {
+ return m.Size()
+}
+func (m *IpnsEntry) XXX_DiscardUnknown() {
+ xxx_messageInfo_IpnsEntry.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_IpnsEntry proto.InternalMessageInfo
+
+func (m *IpnsEntry) GetValue() []byte {
+ if m != nil {
+ return m.Value
+ }
+ return nil
+}
+
+func (m *IpnsEntry) GetSignatureV1() []byte {
+ if m != nil {
+ return m.SignatureV1
+ }
+ return nil
+}
+
+func (m *IpnsEntry) GetValidityType() IpnsEntry_ValidityType {
+ if m != nil && m.ValidityType != nil {
+ return *m.ValidityType
+ }
+ return IpnsEntry_EOL
+}
+
+func (m *IpnsEntry) GetValidity() []byte {
+ if m != nil {
+ return m.Validity
+ }
+ return nil
+}
+
+func (m *IpnsEntry) GetSequence() uint64 {
+ if m != nil && m.Sequence != nil {
+ return *m.Sequence
+ }
+ return 0
+}
+
+func (m *IpnsEntry) GetTtl() uint64 {
+ if m != nil && m.Ttl != nil {
+ return *m.Ttl
+ }
+ return 0
+}
+
+func (m *IpnsEntry) GetPubKey() []byte {
+ if m != nil {
+ return m.PubKey
+ }
+ return nil
+}
+
+func (m *IpnsEntry) GetSignatureV2() []byte {
+ if m != nil {
+ return m.SignatureV2
+ }
+ return nil
+}
+
+func (m *IpnsEntry) GetData() []byte {
+ if m != nil {
+ return m.Data
+ }
+ return nil
+}
+
+type IpnsSignatureV2Checker struct {
+ PubKey []byte `protobuf:"bytes,7,opt,name=pubKey" json:"pubKey,omitempty"`
+ SignatureV2 []byte `protobuf:"bytes,8,opt,name=signatureV2" json:"signatureV2,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *IpnsSignatureV2Checker) Reset() { *m = IpnsSignatureV2Checker{} }
+func (m *IpnsSignatureV2Checker) String() string { return proto.CompactTextString(m) }
+func (*IpnsSignatureV2Checker) ProtoMessage() {}
+func (*IpnsSignatureV2Checker) Descriptor() ([]byte, []int) {
+ return fileDescriptor_4d5b16fb32bfe8ea, []int{1}
+}
+func (m *IpnsSignatureV2Checker) XXX_Unmarshal(b []byte) error {
+ return m.Unmarshal(b)
+}
+func (m *IpnsSignatureV2Checker) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ if deterministic {
+ return xxx_messageInfo_IpnsSignatureV2Checker.Marshal(b, m, deterministic)
+ } else {
+ b = b[:cap(b)]
+ n, err := m.MarshalToSizedBuffer(b)
+ if err != nil {
+ return nil, err
+ }
+ return b[:n], nil
+ }
+}
+func (m *IpnsSignatureV2Checker) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_IpnsSignatureV2Checker.Merge(m, src)
+}
+func (m *IpnsSignatureV2Checker) XXX_Size() int {
+ return m.Size()
+}
+func (m *IpnsSignatureV2Checker) XXX_DiscardUnknown() {
+ xxx_messageInfo_IpnsSignatureV2Checker.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_IpnsSignatureV2Checker proto.InternalMessageInfo
+
+func (m *IpnsSignatureV2Checker) GetPubKey() []byte {
+ if m != nil {
+ return m.PubKey
+ }
+ return nil
+}
+
+func (m *IpnsSignatureV2Checker) GetSignatureV2() []byte {
+ if m != nil {
+ return m.SignatureV2
+ }
+ return nil
+}
+
+func init() {
+ proto.RegisterEnum("ipns.v1.pb.IpnsEntry_ValidityType", IpnsEntry_ValidityType_name, IpnsEntry_ValidityType_value)
+ proto.RegisterType((*IpnsEntry)(nil), "ipns.v1.pb.IpnsEntry")
+ proto.RegisterType((*IpnsSignatureV2Checker)(nil), "ipns.v1.pb.IpnsSignatureV2Checker")
+}
+
+func init() { proto.RegisterFile("ipns.proto", fileDescriptor_4d5b16fb32bfe8ea) }
+
+var fileDescriptor_4d5b16fb32bfe8ea = []byte{
+ // 272 bytes of a gzipped FileDescriptorProto
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xca, 0x2c, 0xc8, 0x2b,
+ 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x82, 0xb0, 0xcb, 0x0c, 0xf5, 0x0a, 0x92, 0x94, 0xf6,
+ 0x30, 0x71, 0x71, 0x7a, 0x16, 0xe4, 0x15, 0xbb, 0xe6, 0x95, 0x14, 0x55, 0x0a, 0x89, 0x70, 0xb1,
+ 0x96, 0x25, 0xe6, 0x94, 0xa6, 0x4a, 0x30, 0x2a, 0x30, 0x6a, 0xf0, 0x04, 0x41, 0x38, 0x42, 0x0a,
+ 0x5c, 0xdc, 0xc5, 0x99, 0xe9, 0x79, 0x89, 0x25, 0xa5, 0x45, 0xa9, 0x61, 0x86, 0x12, 0x4c, 0x60,
+ 0x39, 0x64, 0x21, 0x21, 0x37, 0x2e, 0x9e, 0xb2, 0xc4, 0x9c, 0xcc, 0x94, 0xcc, 0x92, 0xca, 0x90,
+ 0xca, 0x82, 0x54, 0x09, 0x66, 0x05, 0x46, 0x0d, 0x3e, 0x23, 0x25, 0x3d, 0x84, 0x45, 0x7a, 0x70,
+ 0x4b, 0xf4, 0xc2, 0x90, 0x54, 0x06, 0xa1, 0xe8, 0x13, 0x92, 0xe2, 0xe2, 0x80, 0xf1, 0x25, 0x58,
+ 0xc0, 0xd6, 0xc0, 0xf9, 0x20, 0xb9, 0xe2, 0xd4, 0xc2, 0xd2, 0xd4, 0xbc, 0xe4, 0x54, 0x09, 0x56,
+ 0x05, 0x46, 0x0d, 0x96, 0x20, 0x38, 0x5f, 0x48, 0x80, 0x8b, 0xb9, 0xa4, 0x24, 0x47, 0x82, 0x0d,
+ 0x2c, 0x0c, 0x62, 0x0a, 0x89, 0x71, 0xb1, 0x15, 0x94, 0x26, 0x79, 0xa7, 0x56, 0x4a, 0xb0, 0x83,
+ 0xcd, 0x81, 0xf2, 0x50, 0xfd, 0x62, 0x24, 0xc1, 0x81, 0xee, 0x17, 0x23, 0x21, 0x21, 0x2e, 0x96,
+ 0x94, 0xc4, 0x92, 0x44, 0x09, 0x4e, 0xb0, 0x14, 0x98, 0xad, 0x24, 0xce, 0xc5, 0x83, 0xec, 0x6a,
+ 0x21, 0x76, 0x2e, 0x66, 0x57, 0x7f, 0x1f, 0x01, 0x06, 0xa5, 0x20, 0x2e, 0x31, 0x90, 0xc7, 0x82,
+ 0x11, 0xfa, 0x9d, 0x33, 0x52, 0x93, 0xb3, 0x53, 0x8b, 0xc8, 0x77, 0x80, 0x93, 0xe8, 0x89, 0x47,
+ 0x72, 0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x18, 0xc5, 0x0e, 0x0a, 0xc3, 0xf8,
+ 0x82, 0x24, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbd, 0x45, 0xdd, 0x1a, 0xc2, 0x01, 0x00, 0x00,
+}
+
+func (m *IpnsEntry) Marshal() (dAtA []byte, err error) {
+ size := m.Size()
+ dAtA = make([]byte, size)
+ n, err := m.MarshalToSizedBuffer(dAtA[:size])
+ if err != nil {
+ return nil, err
+ }
+ return dAtA[:n], nil
+}
+
+func (m *IpnsEntry) MarshalTo(dAtA []byte) (int, error) {
+ size := m.Size()
+ return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *IpnsEntry) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+ i := len(dAtA)
+ _ = i
+ var l int
+ _ = l
+ if m.XXX_unrecognized != nil {
+ i -= len(m.XXX_unrecognized)
+ copy(dAtA[i:], m.XXX_unrecognized)
+ }
+ if m.Data != nil {
+ i -= len(m.Data)
+ copy(dAtA[i:], m.Data)
+ i = encodeVarintIpns(dAtA, i, uint64(len(m.Data)))
+ i--
+ dAtA[i] = 0x4a
+ }
+ if m.SignatureV2 != nil {
+ i -= len(m.SignatureV2)
+ copy(dAtA[i:], m.SignatureV2)
+ i = encodeVarintIpns(dAtA, i, uint64(len(m.SignatureV2)))
+ i--
+ dAtA[i] = 0x42
+ }
+ if m.PubKey != nil {
+ i -= len(m.PubKey)
+ copy(dAtA[i:], m.PubKey)
+ i = encodeVarintIpns(dAtA, i, uint64(len(m.PubKey)))
+ i--
+ dAtA[i] = 0x3a
+ }
+ if m.Ttl != nil {
+ i = encodeVarintIpns(dAtA, i, uint64(*m.Ttl))
+ i--
+ dAtA[i] = 0x30
+ }
+ if m.Sequence != nil {
+ i = encodeVarintIpns(dAtA, i, uint64(*m.Sequence))
+ i--
+ dAtA[i] = 0x28
+ }
+ if m.Validity != nil {
+ i -= len(m.Validity)
+ copy(dAtA[i:], m.Validity)
+ i = encodeVarintIpns(dAtA, i, uint64(len(m.Validity)))
+ i--
+ dAtA[i] = 0x22
+ }
+ if m.ValidityType != nil {
+ i = encodeVarintIpns(dAtA, i, uint64(*m.ValidityType))
+ i--
+ dAtA[i] = 0x18
+ }
+ if m.SignatureV1 != nil {
+ i -= len(m.SignatureV1)
+ copy(dAtA[i:], m.SignatureV1)
+ i = encodeVarintIpns(dAtA, i, uint64(len(m.SignatureV1)))
+ i--
+ dAtA[i] = 0x12
+ }
+ if m.Value != nil {
+ i -= len(m.Value)
+ copy(dAtA[i:], m.Value)
+ i = encodeVarintIpns(dAtA, i, uint64(len(m.Value)))
+ i--
+ dAtA[i] = 0xa
+ }
+ return len(dAtA) - i, nil
+}
+
+func (m *IpnsSignatureV2Checker) Marshal() (dAtA []byte, err error) {
+ size := m.Size()
+ dAtA = make([]byte, size)
+ n, err := m.MarshalToSizedBuffer(dAtA[:size])
+ if err != nil {
+ return nil, err
+ }
+ return dAtA[:n], nil
+}
+
+func (m *IpnsSignatureV2Checker) MarshalTo(dAtA []byte) (int, error) {
+ size := m.Size()
+ return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *IpnsSignatureV2Checker) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+ i := len(dAtA)
+ _ = i
+ var l int
+ _ = l
+ if m.XXX_unrecognized != nil {
+ i -= len(m.XXX_unrecognized)
+ copy(dAtA[i:], m.XXX_unrecognized)
+ }
+ if m.SignatureV2 != nil {
+ i -= len(m.SignatureV2)
+ copy(dAtA[i:], m.SignatureV2)
+ i = encodeVarintIpns(dAtA, i, uint64(len(m.SignatureV2)))
+ i--
+ dAtA[i] = 0x42
+ }
+ if m.PubKey != nil {
+ i -= len(m.PubKey)
+ copy(dAtA[i:], m.PubKey)
+ i = encodeVarintIpns(dAtA, i, uint64(len(m.PubKey)))
+ i--
+ dAtA[i] = 0x3a
+ }
+ return len(dAtA) - i, nil
+}
+
+func encodeVarintIpns(dAtA []byte, offset int, v uint64) int {
+ offset -= sovIpns(v)
+ base := offset
+ for v >= 1<<7 {
+ dAtA[offset] = uint8(v&0x7f | 0x80)
+ v >>= 7
+ offset++
+ }
+ dAtA[offset] = uint8(v)
+ return base
+}
+func (m *IpnsEntry) Size() (n int) {
+ if m == nil {
+ return 0
+ }
+ var l int
+ _ = l
+ if m.Value != nil {
+ l = len(m.Value)
+ n += 1 + l + sovIpns(uint64(l))
+ }
+ if m.SignatureV1 != nil {
+ l = len(m.SignatureV1)
+ n += 1 + l + sovIpns(uint64(l))
+ }
+ if m.ValidityType != nil {
+ n += 1 + sovIpns(uint64(*m.ValidityType))
+ }
+ if m.Validity != nil {
+ l = len(m.Validity)
+ n += 1 + l + sovIpns(uint64(l))
+ }
+ if m.Sequence != nil {
+ n += 1 + sovIpns(uint64(*m.Sequence))
+ }
+ if m.Ttl != nil {
+ n += 1 + sovIpns(uint64(*m.Ttl))
+ }
+ if m.PubKey != nil {
+ l = len(m.PubKey)
+ n += 1 + l + sovIpns(uint64(l))
+ }
+ if m.SignatureV2 != nil {
+ l = len(m.SignatureV2)
+ n += 1 + l + sovIpns(uint64(l))
+ }
+ if m.Data != nil {
+ l = len(m.Data)
+ n += 1 + l + sovIpns(uint64(l))
+ }
+ if m.XXX_unrecognized != nil {
+ n += len(m.XXX_unrecognized)
+ }
+ return n
+}
+
+func (m *IpnsSignatureV2Checker) Size() (n int) {
+ if m == nil {
+ return 0
+ }
+ var l int
+ _ = l
+ if m.PubKey != nil {
+ l = len(m.PubKey)
+ n += 1 + l + sovIpns(uint64(l))
+ }
+ if m.SignatureV2 != nil {
+ l = len(m.SignatureV2)
+ n += 1 + l + sovIpns(uint64(l))
+ }
+ if m.XXX_unrecognized != nil {
+ n += len(m.XXX_unrecognized)
+ }
+ return n
+}
+
+func sovIpns(x uint64) (n int) {
+ return (math_bits.Len64(x|1) + 6) / 7
+}
+func sozIpns(x uint64) (n int) {
+ return sovIpns(uint64((x << 1) ^ uint64((int64(x) >> 63))))
+}
+func (m *IpnsEntry) Unmarshal(dAtA []byte) error {
+ l := len(dAtA)
+ iNdEx := 0
+ for iNdEx < l {
+ preIndex := iNdEx
+ var wire uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowIpns
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ wire |= uint64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ fieldNum := int32(wire >> 3)
+ wireType := int(wire & 0x7)
+ if wireType == 4 {
+ return fmt.Errorf("proto: IpnsEntry: wiretype end group for non-group")
+ }
+ if fieldNum <= 0 {
+ return fmt.Errorf("proto: IpnsEntry: illegal tag %d (wire type %d)", fieldNum, wire)
+ }
+ switch fieldNum {
+ case 1:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType)
+ }
+ var byteLen int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowIpns
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ byteLen |= int(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if byteLen < 0 {
+ return ErrInvalidLengthIpns
+ }
+ postIndex := iNdEx + byteLen
+ if postIndex < 0 {
+ return ErrInvalidLengthIpns
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.Value = append(m.Value[:0], dAtA[iNdEx:postIndex]...)
+ if m.Value == nil {
+ m.Value = []byte{}
+ }
+ iNdEx = postIndex
+ case 2:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field SignatureV1", wireType)
+ }
+ var byteLen int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowIpns
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ byteLen |= int(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if byteLen < 0 {
+ return ErrInvalidLengthIpns
+ }
+ postIndex := iNdEx + byteLen
+ if postIndex < 0 {
+ return ErrInvalidLengthIpns
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.SignatureV1 = append(m.SignatureV1[:0], dAtA[iNdEx:postIndex]...)
+ if m.SignatureV1 == nil {
+ m.SignatureV1 = []byte{}
+ }
+ iNdEx = postIndex
+ case 3:
+ if wireType != 0 {
+ return fmt.Errorf("proto: wrong wireType = %d for field ValidityType", wireType)
+ }
+ var v IpnsEntry_ValidityType
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowIpns
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ v |= IpnsEntry_ValidityType(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ m.ValidityType = &v
+ case 4:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Validity", wireType)
+ }
+ var byteLen int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowIpns
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ byteLen |= int(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if byteLen < 0 {
+ return ErrInvalidLengthIpns
+ }
+ postIndex := iNdEx + byteLen
+ if postIndex < 0 {
+ return ErrInvalidLengthIpns
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.Validity = append(m.Validity[:0], dAtA[iNdEx:postIndex]...)
+ if m.Validity == nil {
+ m.Validity = []byte{}
+ }
+ iNdEx = postIndex
+ case 5:
+ if wireType != 0 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Sequence", wireType)
+ }
+ var v uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowIpns
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ v |= uint64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ m.Sequence = &v
+ case 6:
+ if wireType != 0 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Ttl", wireType)
+ }
+ var v uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowIpns
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ v |= uint64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ m.Ttl = &v
+ case 7:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field PubKey", wireType)
+ }
+ var byteLen int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowIpns
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ byteLen |= int(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if byteLen < 0 {
+ return ErrInvalidLengthIpns
+ }
+ postIndex := iNdEx + byteLen
+ if postIndex < 0 {
+ return ErrInvalidLengthIpns
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.PubKey = append(m.PubKey[:0], dAtA[iNdEx:postIndex]...)
+ if m.PubKey == nil {
+ m.PubKey = []byte{}
+ }
+ iNdEx = postIndex
+ case 8:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field SignatureV2", wireType)
+ }
+ var byteLen int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowIpns
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ byteLen |= int(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if byteLen < 0 {
+ return ErrInvalidLengthIpns
+ }
+ postIndex := iNdEx + byteLen
+ if postIndex < 0 {
+ return ErrInvalidLengthIpns
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.SignatureV2 = append(m.SignatureV2[:0], dAtA[iNdEx:postIndex]...)
+ if m.SignatureV2 == nil {
+ m.SignatureV2 = []byte{}
+ }
+ iNdEx = postIndex
+ case 9:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Data", wireType)
+ }
+ var byteLen int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowIpns
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ byteLen |= int(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if byteLen < 0 {
+ return ErrInvalidLengthIpns
+ }
+ postIndex := iNdEx + byteLen
+ if postIndex < 0 {
+ return ErrInvalidLengthIpns
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.Data = append(m.Data[:0], dAtA[iNdEx:postIndex]...)
+ if m.Data == nil {
+ m.Data = []byte{}
+ }
+ iNdEx = postIndex
+ default:
+ iNdEx = preIndex
+ skippy, err := skipIpns(dAtA[iNdEx:])
+ if err != nil {
+ return err
+ }
+ if skippy < 0 {
+ return ErrInvalidLengthIpns
+ }
+ if (iNdEx + skippy) < 0 {
+ return ErrInvalidLengthIpns
+ }
+ if (iNdEx + skippy) > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
+ iNdEx += skippy
+ }
+ }
+
+ if iNdEx > l {
+ return io.ErrUnexpectedEOF
+ }
+ return nil
+}
+func (m *IpnsSignatureV2Checker) Unmarshal(dAtA []byte) error {
+ l := len(dAtA)
+ iNdEx := 0
+ for iNdEx < l {
+ preIndex := iNdEx
+ var wire uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowIpns
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ wire |= uint64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ fieldNum := int32(wire >> 3)
+ wireType := int(wire & 0x7)
+ if wireType == 4 {
+ return fmt.Errorf("proto: IpnsSignatureV2Checker: wiretype end group for non-group")
+ }
+ if fieldNum <= 0 {
+ return fmt.Errorf("proto: IpnsSignatureV2Checker: illegal tag %d (wire type %d)", fieldNum, wire)
+ }
+ switch fieldNum {
+ case 7:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field PubKey", wireType)
+ }
+ var byteLen int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowIpns
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ byteLen |= int(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if byteLen < 0 {
+ return ErrInvalidLengthIpns
+ }
+ postIndex := iNdEx + byteLen
+ if postIndex < 0 {
+ return ErrInvalidLengthIpns
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.PubKey = append(m.PubKey[:0], dAtA[iNdEx:postIndex]...)
+ if m.PubKey == nil {
+ m.PubKey = []byte{}
+ }
+ iNdEx = postIndex
+ case 8:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field SignatureV2", wireType)
+ }
+ var byteLen int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowIpns
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ byteLen |= int(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if byteLen < 0 {
+ return ErrInvalidLengthIpns
+ }
+ postIndex := iNdEx + byteLen
+ if postIndex < 0 {
+ return ErrInvalidLengthIpns
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.SignatureV2 = append(m.SignatureV2[:0], dAtA[iNdEx:postIndex]...)
+ if m.SignatureV2 == nil {
+ m.SignatureV2 = []byte{}
+ }
+ iNdEx = postIndex
+ default:
+ iNdEx = preIndex
+ skippy, err := skipIpns(dAtA[iNdEx:])
+ if err != nil {
+ return err
+ }
+ if skippy < 0 {
+ return ErrInvalidLengthIpns
+ }
+ if (iNdEx + skippy) < 0 {
+ return ErrInvalidLengthIpns
+ }
+ if (iNdEx + skippy) > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
+ iNdEx += skippy
+ }
+ }
+
+ if iNdEx > l {
+ return io.ErrUnexpectedEOF
+ }
+ return nil
+}
+func skipIpns(dAtA []byte) (n int, err error) {
+ l := len(dAtA)
+ iNdEx := 0
+ depth := 0
+ for iNdEx < l {
+ var wire uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return 0, ErrIntOverflowIpns
+ }
+ if iNdEx >= l {
+ return 0, io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ wire |= (uint64(b) & 0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ wireType := int(wire & 0x7)
+ switch wireType {
+ case 0:
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return 0, ErrIntOverflowIpns
+ }
+ if iNdEx >= l {
+ return 0, io.ErrUnexpectedEOF
+ }
+ iNdEx++
+ if dAtA[iNdEx-1] < 0x80 {
+ break
+ }
+ }
+ case 1:
+ iNdEx += 8
+ case 2:
+ var length int
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return 0, ErrIntOverflowIpns
+ }
+ if iNdEx >= l {
+ return 0, io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ length |= (int(b) & 0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ if length < 0 {
+ return 0, ErrInvalidLengthIpns
+ }
+ iNdEx += length
+ case 3:
+ depth++
+ case 4:
+ if depth == 0 {
+ return 0, ErrUnexpectedEndOfGroupIpns
+ }
+ depth--
+ case 5:
+ iNdEx += 4
+ default:
+ return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
+ }
+ if iNdEx < 0 {
+ return 0, ErrInvalidLengthIpns
+ }
+ if depth == 0 {
+ return iNdEx, nil
+ }
+ }
+ return 0, io.ErrUnexpectedEOF
+}
+
+var (
+ ErrInvalidLengthIpns = fmt.Errorf("proto: negative length found during unmarshaling")
+ ErrIntOverflowIpns = fmt.Errorf("proto: integer overflow")
+ ErrUnexpectedEndOfGroupIpns = fmt.Errorf("proto: unexpected end of group")
+)
diff --git a/ipns/pb/ipns.proto b/ipns/pb/ipns.proto
new file mode 100644
index 0000000000..bd89a34ec4
--- /dev/null
+++ b/ipns/pb/ipns.proto
@@ -0,0 +1,36 @@
+syntax = "proto2";
+
+package ipns.v1.pb;
+
+option go_package = "ipns_pb";
+
+message IpnsEntry {
+ enum ValidityType {
+ // setting an EOL says "this record is valid until..."
+ EOL = 0;
+ }
+ optional bytes value = 1;
+ optional bytes signatureV1 = 2;
+
+ optional ValidityType validityType = 3;
+ optional bytes validity = 4;
+
+ optional uint64 sequence = 5;
+
+ 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
+ // peerID, making this field unnecessary.
+ optional bytes pubKey = 7;
+
+ optional bytes signatureV2 = 8;
+
+ optional bytes data = 9;
+}
+
+message IpnsSignatureV2Checker {
+ optional bytes pubKey = 7;
+ optional bytes signatureV2 = 8;
+}
diff --git a/ipns/record.go b/ipns/record.go
new file mode 100644
index 0000000000..b479dab90e
--- /dev/null
+++ b/ipns/record.go
@@ -0,0 +1,126 @@
+package ipns
+
+import (
+ "bytes"
+ "errors"
+
+ pb "github.com/ipfs/boxo/ipns/pb"
+
+ "github.com/gogo/protobuf/proto"
+ logging "github.com/ipfs/go-log/v2"
+ record "github.com/libp2p/go-libp2p-record"
+ ic "github.com/libp2p/go-libp2p/core/crypto"
+ "github.com/libp2p/go-libp2p/core/peer"
+ pstore "github.com/libp2p/go-libp2p/core/peerstore"
+)
+
+var log = logging.Logger("ipns")
+
+var _ record.Validator = Validator{}
+
+// RecordKey returns the libp2p record key for a given peer ID.
+func RecordKey(pid peer.ID) string {
+ return "/ipns/" + string(pid)
+}
+
+// Validator is an IPNS record validator that satisfies the libp2p record
+// validator interface.
+type Validator struct {
+ // KeyBook, if non-nil, will be used to lookup keys for validating IPNS
+ // records.
+ KeyBook pstore.KeyBook
+}
+
+// Validate validates an IPNS record.
+func (v Validator) Validate(key string, value []byte) error {
+ ns, pidString, err := record.SplitKey(key)
+ if err != nil || ns != "ipns" {
+ return ErrInvalidPath
+ }
+
+ // Parse the value into an IpnsEntry
+ entry := new(pb.IpnsEntry)
+ err = proto.Unmarshal(value, entry)
+ if err != nil {
+ return ErrBadRecord
+ }
+
+ // Get the public key defined by the ipns path
+ pid, err := peer.IDFromBytes([]byte(pidString))
+ if err != nil {
+ log.Debugf("failed to parse ipns record key %s into peer ID", pidString)
+ return ErrKeyFormat
+ }
+
+ pubk, err := v.getPublicKey(pid, entry)
+ if err != nil {
+ return err
+ }
+
+ return Validate(pubk, entry)
+}
+
+func (v Validator) getPublicKey(pid peer.ID, entry *pb.IpnsEntry) (ic.PubKey, error) {
+ switch pk, err := ExtractPublicKey(pid, entry); err {
+ case peer.ErrNoPublicKey:
+ case nil:
+ return pk, nil
+ default:
+ return nil, err
+ }
+
+ if v.KeyBook == nil {
+ log.Debugf("public key with hash %s not found in IPNS record and no peer store provided", pid)
+ return nil, ErrPublicKeyNotFound
+ }
+
+ pubk := v.KeyBook.PubKey(pid)
+ if pubk == nil {
+ log.Debugf("public key with hash %s not found in peer store", pid)
+ return nil, ErrPublicKeyNotFound
+ }
+ return pubk, nil
+}
+
+// Select selects the best record by checking which has the highest sequence
+// number and latest EOL.
+//
+// This function returns an error if any of the records fail to parse. Validate
+// your records first!
+func (v Validator) Select(k string, vals [][]byte) (int, error) {
+ var recs []*pb.IpnsEntry
+ for _, v := range vals {
+ e := new(pb.IpnsEntry)
+ if err := proto.Unmarshal(v, e); err != nil {
+ return -1, err
+ }
+ recs = append(recs, e)
+ }
+
+ return selectRecord(recs, vals)
+}
+
+func selectRecord(recs []*pb.IpnsEntry, vals [][]byte) (int, error) {
+ switch len(recs) {
+ case 0:
+ return -1, errors.New("no usable records in given set")
+ case 1:
+ return 0, nil
+ }
+
+ var i int
+ for j := 1; j < len(recs); j++ {
+ cmp, err := Compare(recs[i], recs[j])
+ if err != nil {
+ return -1, err
+ }
+ if cmp == 0 {
+ cmp = bytes.Compare(vals[i], vals[j])
+ }
+ if cmp < 0 {
+ i = j
+ }
+ }
+
+ return i, nil
+}
diff --git a/ipns/select_test.go b/ipns/select_test.go
new file mode 100644
index 0000000000..5b435f62d8
--- /dev/null
+++ b/ipns/select_test.go
@@ -0,0 +1,126 @@
+package ipns
+
+import (
+ "fmt"
+ "math/rand"
+ "testing"
+ "time"
+
+ pb "github.com/ipfs/boxo/ipns/pb"
+
+ "github.com/gogo/protobuf/proto"
+ u "github.com/ipfs/boxo/util"
+ ci "github.com/libp2p/go-libp2p/core/crypto"
+)
+
+func shuffle(a []*pb.IpnsEntry) {
+ for n := 0; n < 5; n++ {
+ for i := range a {
+ j := rand.Intn(len(a))
+ a[i], a[j] = a[j], a[i]
+ }
+ }
+}
+
+func AssertSelected(r *pb.IpnsEntry, from ...*pb.IpnsEntry) error {
+ shuffle(from)
+ var vals [][]byte
+ for _, r := range from {
+ data, err := proto.Marshal(r)
+ if err != nil {
+ return err
+ }
+ vals = append(vals, data)
+ }
+
+ i, err := selectRecord(from, vals)
+ if err != nil {
+ return err
+ }
+
+ if from[i] != r {
+ return fmt.Errorf("selected incorrect record %d", i)
+ }
+
+ return nil
+}
+
+func TestOrdering(t *testing.T) {
+ // select timestamp so selection is deterministic
+ ts := time.Unix(1000000, 0)
+
+ // generate a key for signing the records
+ r := u.NewSeededRand(15) // generate deterministic keypair
+ priv, _, err := ci.GenerateKeyPairWithReader(ci.RSA, 2048, r)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ e1, err := Create(priv, []byte("foo"), 1, ts.Add(time.Hour), 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ e2, err := Create(priv, []byte("bar"), 2, ts.Add(time.Hour), 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ e3, err := Create(priv, []byte("baz"), 3, ts.Add(time.Hour), 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ e4, err := Create(priv, []byte("cat"), 3, ts.Add(time.Hour*2), 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ e5, err := Create(priv, []byte("dog"), 4, ts.Add(time.Hour*3), 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ e6, err := Create(priv, []byte("fish"), 4, ts.Add(time.Hour*3), 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // e1 is the only record, i hope it gets this right
+ err = AssertSelected(e1, e1)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // e2 has the highest sequence number
+ err = AssertSelected(e2, e1, e2)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // e3 has the highest sequence number
+ err = AssertSelected(e3, e1, e2, e3)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // e4 has a higher timeout
+ err = AssertSelected(e4, e1, e2, e3, e4)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // e5 has the highest sequence number
+ err = AssertSelected(e5, e1, e2, e3, e4, e5)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // e6 should be selected as its signauture will win in the comparison
+ err = AssertSelected(e6, e1, e2, e3, e4, e5, e6)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _ = []interface{}{e1, e2, e3, e4, e5, e6}
+}
diff --git a/ipns/validate_test.go b/ipns/validate_test.go
new file mode 100644
index 0000000000..e75d0aee7e
--- /dev/null
+++ b/ipns/validate_test.go
@@ -0,0 +1,415 @@
+package ipns
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "math/rand"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/gogo/protobuf/proto"
+ pb "github.com/ipfs/boxo/ipns/pb"
+ u "github.com/ipfs/boxo/util"
+ ipldcodec "github.com/ipld/go-ipld-prime/multicodec"
+ basicnode "github.com/ipld/go-ipld-prime/node/basic"
+ "github.com/libp2p/go-libp2p/core/crypto"
+ "github.com/libp2p/go-libp2p/core/peer"
+ pstore "github.com/libp2p/go-libp2p/core/peerstore"
+ "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem"
+ "github.com/multiformats/go-multicodec"
+)
+
+func testValidatorCase(t *testing.T, priv crypto.PrivKey, kbook pstore.KeyBook, key string, val []byte, eol time.Time, exp error) {
+ t.Helper()
+
+ match := func(t *testing.T, err error) {
+ t.Helper()
+ if err != exp {
+ params := fmt.Sprintf("key: %s\neol: %s\n", key, eol)
+ if exp == nil {
+ t.Fatalf("Unexpected error %s for params %s", err, params)
+ } else if err == nil {
+ t.Fatalf("Expected error %s but there was no error for params %s", exp, params)
+ } else {
+ t.Fatalf("Expected error %s but got %s for params %s", exp, err, params)
+ }
+ }
+ }
+
+ testValidatorCaseMatchFunc(t, priv, kbook, key, val, eol, match)
+}
+
+func testValidatorCaseMatchFunc(t *testing.T, priv crypto.PrivKey, kbook pstore.KeyBook, key string, val []byte, eol time.Time, matchf func(*testing.T, error)) {
+ t.Helper()
+ validator := Validator{kbook}
+
+ data := val
+ if data == nil {
+ p := []byte("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG")
+ entry, err := Create(priv, p, 1, eol, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ data, err = proto.Marshal(entry)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ matchf(t, validator.Validate(key, data))
+}
+
+func TestValidator(t *testing.T) {
+ ts := time.Now()
+
+ priv, id, _ := genKeys(t)
+ priv2, id2, _ := genKeys(t)
+ kbook, err := pstoremem.NewPeerstore()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := kbook.AddPubKey(id, priv.GetPublic()); err != nil {
+ t.Fatal(err)
+ }
+ emptyKbook, err := pstoremem.NewPeerstore()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ testValidatorCase(t, priv, kbook, "/ipns/"+string(id), nil, ts.Add(time.Hour), nil)
+ testValidatorCase(t, priv, kbook, "/ipns/"+string(id), nil, ts.Add(time.Hour*-1), ErrExpiredRecord)
+ testValidatorCase(t, priv, kbook, "/ipns/"+string(id), []byte("bad data"), ts.Add(time.Hour), ErrBadRecord)
+ testValidatorCase(t, priv, kbook, "/ipns/"+"bad key", nil, ts.Add(time.Hour), ErrKeyFormat)
+ testValidatorCase(t, priv, emptyKbook, "/ipns/"+string(id), nil, ts.Add(time.Hour), ErrPublicKeyNotFound)
+ testValidatorCase(t, priv2, kbook, "/ipns/"+string(id2), nil, ts.Add(time.Hour), ErrPublicKeyNotFound)
+ testValidatorCase(t, priv2, kbook, "/ipns/"+string(id), nil, ts.Add(time.Hour), ErrSignature)
+ testValidatorCase(t, priv, kbook, "//"+string(id), nil, ts.Add(time.Hour), ErrInvalidPath)
+ testValidatorCase(t, priv, kbook, "/wrong/"+string(id), nil, ts.Add(time.Hour), ErrInvalidPath)
+}
+
+func mustMarshal(t *testing.T, entry *pb.IpnsEntry) []byte {
+ t.Helper()
+ data, err := proto.Marshal(entry)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return data
+}
+
+func TestEmbeddedPubKeyValidate(t *testing.T) {
+ goodeol := time.Now().Add(time.Hour)
+ kbook, err := pstoremem.NewPeerstore()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ pth := []byte("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG")
+
+ priv, _, ipnsk := genKeys(t)
+
+ entry, err := Create(priv, pth, 1, goodeol, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ testValidatorCase(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, ErrPublicKeyNotFound)
+
+ pubkb, err := crypto.MarshalPublicKey(priv.GetPublic())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ entry.PubKey = pubkb
+ testValidatorCase(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, nil)
+
+ entry.PubKey = []byte("probably not a public key")
+ testValidatorCaseMatchFunc(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, func(t *testing.T, err error) {
+ if !strings.Contains(err.Error(), "unmarshaling pubkey in record:") {
+ t.Fatal("expected pubkey unmarshaling to fail")
+ }
+ })
+
+ opriv, _, _ := genKeys(t)
+ wrongkeydata, err := crypto.MarshalPublicKey(opriv.GetPublic())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ entry.PubKey = wrongkeydata
+ testValidatorCase(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, ErrPublicKeyMismatch)
+}
+
+func TestPeerIDPubKeyValidate(t *testing.T) {
+ t.Skip("disabled until libp2p/go-libp2p-crypto#51 is fixed")
+
+ goodeol := time.Now().Add(time.Hour)
+ kbook, err := pstoremem.NewPeerstore()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ pth := []byte("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG")
+
+ sk, pk, err := crypto.GenerateEd25519Key(rand.New(rand.NewSource(42)))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ pid, err := peer.IDFromPublicKey(pk)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ipnsk := "/ipns/" + string(pid)
+
+ entry, err := Create(sk, pth, 1, goodeol, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ dataNoKey, err := proto.Marshal(entry)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ testValidatorCase(t, sk, kbook, ipnsk, dataNoKey, goodeol, nil)
+}
+
+func TestOnlySignatureV2Validate(t *testing.T) {
+ goodeol := time.Now().Add(time.Hour)
+
+ sk, pk, err := crypto.GenerateEd25519Key(rand.New(rand.NewSource(42)))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ path1 := []byte("/path/1")
+ entry, err := Create(sk, path1, 1, goodeol, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Validate(pk, entry); err != nil {
+ t.Fatal(err)
+ }
+
+ entry.SignatureV2 = nil
+ if err := Validate(pk, entry); !errors.Is(err, ErrSignature) {
+ t.Fatal(err)
+ }
+}
+
+func TestSignatureV1Ignored(t *testing.T) {
+ goodeol := time.Now().Add(time.Hour)
+
+ sk, pk, err := crypto.GenerateEd25519Key(rand.New(rand.NewSource(42)))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ pid, err := peer.IDFromPublicKey(pk)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ipnsk := "/ipns/" + string(pid)
+
+ path1 := []byte("/path/1")
+ entry1, err := Create(sk, path1, 1, goodeol, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ path2 := []byte("/path/2")
+ entry2, err := Create(sk, path2, 2, goodeol, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Validate(pk, entry1); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Validate(pk, entry2); err != nil {
+ t.Fatal(err)
+ }
+
+ v := Validator{}
+ best, err := v.Select(ipnsk, [][]byte{mustMarshal(t, entry1), mustMarshal(t, entry2)})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if best != 1 {
+ t.Fatal("entry2 should be better than entry1")
+ }
+
+ // Having only the v1 signature should be invalid
+ entry2.SignatureV2 = nil
+ if err := Validate(pk, entry2); !errors.Is(err, ErrSignature) {
+ t.Fatal(err)
+ }
+
+ // Record with v2 signature should always be preferred
+ best, err = v.Select(ipnsk, [][]byte{mustMarshal(t, entry1), mustMarshal(t, entry2)})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if best != 0 {
+ t.Fatal("entry1 should be better than entry2")
+ }
+
+ // Having a missing v1 signature is acceptable as long as there is a valid v2 signature
+ entry1.SignatureV1 = nil
+ if err := Validate(pk, entry1); err != nil {
+ t.Fatal(err)
+ }
+
+ // Having an invalid v1 signature is acceptable as long as there is a valid v2 signature
+ entry1.SignatureV1 = []byte("garbage")
+ if err := Validate(pk, entry1); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestMaxSizeValidate(t *testing.T) {
+ goodeol := time.Now().Add(time.Hour)
+
+ sk, pk, err := crypto.GenerateEd25519Key(rand.New(rand.NewSource(42)))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Create record over the max size (value+other fields)
+ value := make([]byte, MaxRecordSize)
+ entry, err := Create(sk, value, 1, goodeol, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Must fail with ErrRecordSize
+ if err := Validate(pk, entry); !errors.Is(err, ErrRecordSize) {
+ t.Fatal(err)
+ }
+}
+
+func TestCborDataCanonicalization(t *testing.T) {
+ goodeol := time.Now().Add(time.Hour)
+
+ sk, pk, err := crypto.GenerateEd25519Key(rand.New(rand.NewSource(42)))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ path := append([]byte("/path/1"), 0x00)
+ seqnum := uint64(1)
+ entry, err := Create(sk, path, seqnum, goodeol, time.Hour)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Validate(pk, entry); err != nil {
+ t.Fatal(err)
+ }
+
+ dec, err := ipldcodec.LookupDecoder(uint64(multicodec.DagCbor))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ndbuilder := basicnode.Prototype__Map{}.NewBuilder()
+ if err := dec(ndbuilder, bytes.NewReader(entry.GetData())); err != nil {
+ t.Fatal(err)
+ }
+
+ nd := ndbuilder.Build()
+ iter := nd.MapIterator()
+ var fields []string
+ for !iter.Done() {
+ k, v, err := iter.Next()
+ if err != nil {
+ t.Fatal(err)
+ }
+ kstr, err := k.AsString()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ switch kstr {
+ case value:
+ b, err := v.AsBytes()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(path, b) {
+ t.Fatal("value did not match")
+ }
+ case sequence:
+ s, err := v.AsInt()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if uint64(s) != seqnum {
+ t.Fatal("sequence numbers did not match")
+ }
+ case validity:
+ val, err := v.AsBytes()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(val, []byte(u.FormatRFC3339(goodeol))) {
+ t.Fatal("validity did not match")
+ }
+ case validityType:
+ vt, err := v.AsInt()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if uint64(vt) != 0 {
+ t.Fatal("validity types did not match")
+ }
+ case ttl:
+ ttlVal, err := v.AsInt()
+ if err != nil {
+ t.Fatal(err)
+ }
+ // TODO: test non-zero TTL
+ if uint64(ttlVal) != uint64(time.Hour.Nanoseconds()) {
+ t.Fatal("TTLs did not match")
+ }
+ }
+
+ fields = append(fields, kstr)
+ }
+
+ // Check for map sort order (i.e. by length then by value)
+ expectedOrder := []string{"TTL", "Value", "Sequence", "Validity", "ValidityType"}
+ if len(fields) != len(expectedOrder) {
+ t.Fatal("wrong number of fields")
+ }
+
+ for i, f := range fields {
+ expected := expectedOrder[i]
+ if f != expected {
+ t.Fatalf("expected %s, got %s", expected, f)
+ }
+ }
+}
+
+func genKeys(t *testing.T) (crypto.PrivKey, peer.ID, string) {
+ sr := u.NewTimeSeededRand()
+ priv, _, err := crypto.GenerateKeyPairWithReader(crypto.RSA, 2048, sr)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Create entry with expiry in one hour
+ pid, err := peer.IDFromPrivateKey(priv)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ipnsKey := RecordKey(pid)
+
+ return priv, pid, ipnsKey
+}
diff --git a/keystore/keystore.go b/keystore/keystore.go
new file mode 100644
index 0000000000..fc6793a1ee
--- /dev/null
+++ b/keystore/keystore.go
@@ -0,0 +1,188 @@
+package keystore
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "encoding/base32"
+
+ logging "github.com/ipfs/go-log"
+ ci "github.com/libp2p/go-libp2p/core/crypto"
+)
+
+var log = logging.Logger("keystore")
+
+var codec = base32.StdEncoding.WithPadding(base32.NoPadding)
+
+// Keystore provides a key management interface
+type Keystore interface {
+ // Has returns whether or not a key exists in the Keystore
+ Has(string) (bool, error)
+ // Put stores a key in the Keystore, if a key with the same name already exists, returns ErrKeyExists
+ Put(string, ci.PrivKey) error
+ // Get retrieves a key from the Keystore if it exists, and returns ErrNoSuchKey
+ // otherwise.
+ Get(string) (ci.PrivKey, error)
+ // Delete removes a key from the Keystore
+ Delete(string) error
+ // List returns a list of key identifier
+ List() ([]string, error)
+}
+
+// ErrNoSuchKey is an error message returned when no key of a given name was found.
+var ErrNoSuchKey = fmt.Errorf("no key by the given name was found")
+
+// ErrKeyExists is an error message returned when a key already exists
+var ErrKeyExists = fmt.Errorf("key by that name already exists, refusing to overwrite")
+
+const keyFilenamePrefix = "key_"
+
+// FSKeystore is a keystore backed by files in a given directory stored on disk.
+type FSKeystore struct {
+ dir string
+}
+
+// NewFSKeystore returns a new filesystem-backed keystore.
+func NewFSKeystore(dir string) (*FSKeystore, error) {
+ err := os.Mkdir(dir, 0700)
+ switch {
+ case os.IsExist(err):
+ case err == nil:
+ default:
+ return nil, err
+ }
+ return &FSKeystore{dir}, nil
+}
+
+// Has returns whether or not a key exists in the Keystore
+func (ks *FSKeystore) Has(name string) (bool, error) {
+ name, err := encode(name)
+ if err != nil {
+ return false, err
+ }
+
+ kp := filepath.Join(ks.dir, name)
+
+ _, err = os.Stat(kp)
+
+ if os.IsNotExist(err) {
+ return false, nil
+ }
+ return err == nil, err
+}
+
+// Put stores a key in the Keystore, if a key with the same name already exists, returns ErrKeyExists
+func (ks *FSKeystore) Put(name string, k ci.PrivKey) error {
+ name, err := encode(name)
+ if err != nil {
+ return err
+ }
+
+ b, err := ci.MarshalPrivateKey(k)
+ if err != nil {
+ return err
+ }
+
+ kp := filepath.Join(ks.dir, name)
+
+ fi, err := os.OpenFile(kp, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0400)
+ if err != nil {
+ if os.IsExist(err) {
+ err = ErrKeyExists
+ }
+ return err
+ }
+ defer fi.Close()
+
+ _, err = fi.Write(b)
+
+ return err
+}
+
+// Get retrieves a key from the Keystore if it exists, and returns ErrNoSuchKey
+// otherwise.
+func (ks *FSKeystore) Get(name string) (ci.PrivKey, error) {
+ name, err := encode(name)
+ if err != nil {
+ return nil, err
+ }
+
+ kp := filepath.Join(ks.dir, name)
+
+ data, err := os.ReadFile(kp)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil, ErrNoSuchKey
+ }
+ return nil, err
+ }
+
+ return ci.UnmarshalPrivateKey(data)
+}
+
+// Delete removes a key from the Keystore
+func (ks *FSKeystore) Delete(name string) error {
+ name, err := encode(name)
+ if err != nil {
+ return err
+ }
+
+ kp := filepath.Join(ks.dir, name)
+
+ return os.Remove(kp)
+}
+
+// List return a list of key identifier
+func (ks *FSKeystore) List() ([]string, error) {
+ dir, err := os.Open(ks.dir)
+ if err != nil {
+ return nil, err
+ }
+
+ dirs, err := dir.Readdirnames(0)
+ if err != nil {
+ return nil, err
+ }
+
+ list := make([]string, 0, len(dirs))
+
+ for _, name := range dirs {
+ decodedName, err := decode(name)
+ if err == nil {
+ list = append(list, decodedName)
+ } else {
+ log.Errorf("Ignoring keyfile with invalid encoded filename: %s", name)
+ }
+ }
+
+ return list, nil
+}
+
+func encode(name string) (string, error) {
+ if name == "" {
+ return "", fmt.Errorf("key name must be at least one character")
+ }
+
+ encodedName := codec.EncodeToString([]byte(name))
+ log.Debugf("Encoded key name: %s to: %s", name, encodedName)
+
+ return keyFilenamePrefix + strings.ToLower(encodedName), nil
+}
+
+func decode(name string) (string, error) {
+ if !strings.HasPrefix(name, keyFilenamePrefix) {
+ return "", fmt.Errorf("key's filename has unexpected format")
+ }
+
+ nameWithoutPrefix := strings.ToUpper(name[len(keyFilenamePrefix):])
+ decodedName, err := codec.DecodeString(nameWithoutPrefix)
+ if err != nil {
+ return "", err
+ }
+
+ log.Debugf("Decoded key name: %s to: %s", name, decodedName)
+
+ return string(decodedName), nil
+}
diff --git a/keystore/keystore_test.go b/keystore/keystore_test.go
new file mode 100644
index 0000000000..9a44062173
--- /dev/null
+++ b/keystore/keystore_test.go
@@ -0,0 +1,277 @@
+package keystore
+
+import (
+ "fmt"
+ "math/rand"
+ "os"
+ "path/filepath"
+ "sort"
+ "testing"
+
+ ci "github.com/libp2p/go-libp2p/core/crypto"
+)
+
+type rr struct{}
+
+func (rr rr) Read(b []byte) (int, error) {
+ return rand.Read(b)
+}
+
+func privKeyOrFatal(t *testing.T) ci.PrivKey {
+ priv, _, err := ci.GenerateEd25519Key(rr{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ return priv
+}
+
+func TestKeystoreBasics(t *testing.T) {
+ tdir, err := os.MkdirTemp("", "keystore-test")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ks, err := NewFSKeystore(tdir)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ l, err := ks.List()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(l) != 0 {
+ t.Fatal("expected no keys")
+ }
+
+ k1 := privKeyOrFatal(t)
+ k2 := privKeyOrFatal(t)
+ k3 := privKeyOrFatal(t)
+ k4 := privKeyOrFatal(t)
+
+ err = ks.Put("foo", k1)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = ks.Put("bar", k2)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ l, err = ks.List()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sort.Strings(l)
+ if l[0] != "bar" || l[1] != "foo" {
+ t.Fatal("wrong entries listed")
+ }
+
+ if err := assertDirContents(tdir, []string{"foo", "bar"}); err != nil {
+ t.Fatal(err)
+ }
+
+ err = ks.Put("foo", k3)
+ if err == nil {
+ t.Fatal("should not be able to overwrite key")
+ }
+
+ if err := assertDirContents(tdir, []string{"foo", "bar"}); err != nil {
+ t.Fatal(err)
+ }
+
+ exist, err := ks.Has("foo")
+ if !exist {
+ t.Fatal("should know it has a key named foo")
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ exist, err = ks.Has("nonexistingkey")
+ if exist {
+ t.Fatal("should know it doesn't have a key named nonexistingkey")
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := ks.Delete("bar"); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := assertDirContents(tdir, []string{"foo"}); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := ks.Put("beep", k3); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := ks.Put("boop", k4); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := assertDirContents(tdir, []string{"foo", "beep", "boop"}); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := assertGetKey(ks, "foo", k1); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := assertGetKey(ks, "beep", k3); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := assertGetKey(ks, "boop", k4); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := ks.Put("..///foo/", k1); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := ks.Put("", k1); err == nil {
+ t.Fatal("shouldn't be able to put a key with no name")
+ }
+
+ if err := ks.Put(".foo", k1); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestInvalidKeyFiles(t *testing.T) {
+ tdir, err := os.MkdirTemp("", "keystore-test")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer os.RemoveAll(tdir)
+
+ ks, err := NewFSKeystore(tdir)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ key := privKeyOrFatal(t)
+
+ bytes, err := key.Raw()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ encodedName, err := encode("valid")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = os.WriteFile(filepath.Join(ks.dir, encodedName), bytes, 0644)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = os.WriteFile(filepath.Join(ks.dir, "z.invalid"), bytes, 0644)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ l, err := ks.List()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sort.Strings(l)
+ if len(l) != 1 {
+ t.Fatal("wrong entry count")
+ }
+
+ if l[0] != "valid" {
+ t.Fatal("wrong entries listed")
+ }
+
+ exist, err := ks.Has("valid")
+ if !exist {
+ t.Fatal("should know it has a key named valid")
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestNonExistingKey(t *testing.T) {
+ tdir, err := os.MkdirTemp("", "keystore-test")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ks, err := NewFSKeystore(tdir)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ k, err := ks.Get("does-it-exist")
+ if err != ErrNoSuchKey {
+ t.Fatalf("expected: %s, got %s", ErrNoSuchKey, err)
+ }
+ if k != nil {
+ t.Fatalf("Get on nonexistant key should give nil")
+ }
+}
+
+func TestMakeKeystoreNoDir(t *testing.T) {
+ _, err := NewFSKeystore("/this/is/not/a/real/dir")
+ if err == nil {
+ t.Fatal("shouldnt be able to make a keystore in a nonexistant directory")
+ }
+}
+
+func assertGetKey(ks Keystore, name string, exp ci.PrivKey) error {
+ outK, err := ks.Get(name)
+ if err != nil {
+ return err
+ }
+
+ if !outK.Equals(exp) {
+ return fmt.Errorf("key we got out didn't match expectation")
+ }
+
+ return nil
+}
+
+func assertDirContents(dir string, exp []string) error {
+ finfos, err := os.ReadDir(dir)
+ if err != nil {
+ return err
+ }
+
+ if len(finfos) != len(exp) {
+ return fmt.Errorf("expected %d directory entries", len(exp))
+ }
+
+ var names []string
+ for _, fi := range finfos {
+ decodedName, err := decode(fi.Name())
+ if err != nil {
+ return err
+ }
+ names = append(names, decodedName)
+ }
+
+ sort.Strings(names)
+ sort.Strings(exp)
+ if len(names) != len(exp) {
+ return fmt.Errorf("directory had wrong number of entries in it")
+ }
+
+ for i, v := range names {
+ if v != exp[i] {
+ return fmt.Errorf("had wrong entry in directory")
+ }
+ }
+ return nil
+}
diff --git a/keystore/memkeystore.go b/keystore/memkeystore.go
new file mode 100644
index 0000000000..0ea62f4e1a
--- /dev/null
+++ b/keystore/memkeystore.go
@@ -0,0 +1,64 @@
+package keystore
+
+import (
+ "errors"
+
+ ci "github.com/libp2p/go-libp2p/core/crypto"
+)
+
+// MemKeystore is an in memory keystore implementation that is not persisted to
+// any backing storage.
+type MemKeystore struct {
+ keys map[string]ci.PrivKey
+}
+
+// NewMemKeystore creates a MemKeystore.
+func NewMemKeystore() *MemKeystore {
+ return &MemKeystore{make(map[string]ci.PrivKey)}
+}
+
+// Has return whether or not a key exists in the Keystore
+func (mk *MemKeystore) Has(name string) (bool, error) {
+ _, ok := mk.keys[name]
+ return ok, nil
+}
+
+// Put store a key in the Keystore
+func (mk *MemKeystore) Put(name string, k ci.PrivKey) error {
+ if name == "" {
+ return errors.New("key name must be at least one character")
+ }
+
+ _, ok := mk.keys[name]
+ if ok {
+ return ErrKeyExists
+ }
+
+ mk.keys[name] = k
+ return nil
+}
+
+// Get retrieve a key from the Keystore
+func (mk *MemKeystore) Get(name string) (ci.PrivKey, error) {
+ k, ok := mk.keys[name]
+ if !ok {
+ return nil, ErrNoSuchKey
+ }
+
+ return k, nil
+}
+
+// Delete remove a key from the Keystore
+func (mk *MemKeystore) Delete(name string) error {
+ delete(mk.keys, name)
+ return nil
+}
+
+// List return a list of key identifier
+func (mk *MemKeystore) List() ([]string, error) {
+ out := make([]string, 0, len(mk.keys))
+ for k := range mk.keys {
+ out = append(out, k)
+ }
+ return out, nil
+}
diff --git a/keystore/memkeystore_test.go b/keystore/memkeystore_test.go
new file mode 100644
index 0000000000..907cbbd0e7
--- /dev/null
+++ b/keystore/memkeystore_test.go
@@ -0,0 +1,99 @@
+package keystore
+
+import (
+ "sort"
+ "testing"
+)
+
+func TestMemKeyStoreBasics(t *testing.T) {
+ ks := NewMemKeystore()
+
+ l, err := ks.List()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(l) != 0 {
+ t.Fatal("expected no keys")
+ }
+
+ k1 := privKeyOrFatal(t)
+ k2 := privKeyOrFatal(t)
+ k3 := privKeyOrFatal(t)
+ k4 := privKeyOrFatal(t)
+
+ err = ks.Put("foo", k1)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = ks.Put("bar", k2)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ l, err = ks.List()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sort.Strings(l)
+ if l[0] != "bar" || l[1] != "foo" {
+ t.Fatal("wrong entries listed")
+ }
+
+ err = ks.Put("foo", k3)
+ if err == nil {
+ t.Fatal("should not be able to overwrite key")
+ }
+
+ exist, err := ks.Has("foo")
+ if !exist {
+ t.Fatal("should know it has a key named foo")
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ exist, err = ks.Has("nonexistingkey")
+ if exist {
+ t.Fatal("should know it doesn't have a key named nonexistingkey")
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := ks.Delete("bar"); err != nil {
+ t.Fatal(err)
+ }
+ if err := ks.Put("beep", k3); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := ks.Put("boop", k4); err != nil {
+ t.Fatal(err)
+ }
+ if err := assertGetKey(ks, "foo", k1); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := assertGetKey(ks, "beep", k3); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := assertGetKey(ks, "boop", k4); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := ks.Put("..///foo/", k1); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := ks.Put("", k1); err == nil {
+ t.Fatal("shouldn't be able to put a key with no name")
+ }
+
+ if err := ks.Put(".foo", k1); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/logo.svg b/logo.svg
new file mode 100644
index 0000000000..735f3ab805
--- /dev/null
+++ b/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/mfs/dir.go b/mfs/dir.go
new file mode 100644
index 0000000000..f107925325
--- /dev/null
+++ b/mfs/dir.go
@@ -0,0 +1,427 @@
+package mfs
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "path"
+ "sync"
+ "time"
+
+ ft "github.com/ipfs/boxo/unixfs"
+ uio "github.com/ipfs/boxo/unixfs/io"
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+
+ cid "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+var ErrNotYetImplemented = errors.New("not yet implemented")
+var ErrInvalidChild = errors.New("invalid child node")
+var ErrDirExists = errors.New("directory already has entry by that name")
+
+// TODO: There's too much functionality associated with this structure,
+// let's organize it (and if possible extract part of it elsewhere)
+// and document the main features of `Directory` here.
+type Directory struct {
+ inode
+
+ // Internal cache with added entries to the directory, its cotents
+ // are synched with the underlying `unixfsDir` node in `sync()`.
+ entriesCache map[string]FSNode
+
+ lock sync.Mutex
+ // TODO: What content is being protected here exactly? The entire directory?
+
+ ctx context.Context
+
+ // UnixFS directory implementation used for creating,
+ // reading and editing directories.
+ unixfsDir uio.Directory
+
+ modTime time.Time
+}
+
+// NewDirectory constructs a new MFS directory.
+//
+// You probably don't want to call this directly. Instead, construct a new root
+// using NewRoot.
+func NewDirectory(ctx context.Context, name string, node ipld.Node, parent parent, dserv ipld.DAGService) (*Directory, error) {
+ db, err := uio.NewDirectoryFromNode(dserv, node)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Directory{
+ inode: inode{
+ name: name,
+ parent: parent,
+ dagService: dserv,
+ },
+ ctx: ctx,
+ unixfsDir: db,
+ entriesCache: make(map[string]FSNode),
+ modTime: time.Now(),
+ }, nil
+}
+
+// GetCidBuilder gets the CID builder of the root node
+func (d *Directory) GetCidBuilder() cid.Builder {
+ return d.unixfsDir.GetCidBuilder()
+}
+
+// SetCidBuilder sets the CID builder
+func (d *Directory) SetCidBuilder(b cid.Builder) {
+ d.unixfsDir.SetCidBuilder(b)
+}
+
+// This method implements the `parent` interface. It first does the local
+// update of the child entry in the underlying UnixFS directory and saves
+// the newly created directory node with the updated entry in the DAG
+// service. Then it propagates the update upwards (through this same
+// interface) repeating the whole process in the parent.
+func (d *Directory) updateChildEntry(c child) error {
+ newDirNode, err := d.localUpdate(c)
+ if err != nil {
+ return err
+ }
+
+ // Continue to propagate the update process upwards
+ // (all the way up to the root).
+ return d.parent.updateChildEntry(child{d.name, newDirNode})
+}
+
+// This method implements the part of `updateChildEntry` that needs
+// to be locked around: in charge of updating the UnixFS layer and
+// generating the new node reflecting the update. It also stores the
+// new node in the DAG layer.
+func (d *Directory) localUpdate(c child) (*dag.ProtoNode, error) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+
+ err := d.updateChild(c)
+ if err != nil {
+ return nil, err
+ }
+ // TODO: Clearly define how are we propagating changes to lower layers
+ // like UnixFS.
+
+ nd, err := d.unixfsDir.GetNode()
+ if err != nil {
+ return nil, err
+ }
+
+ pbnd, ok := nd.(*dag.ProtoNode)
+ if !ok {
+ return nil, dag.ErrNotProtobuf
+ }
+
+ err = d.dagService.Add(d.ctx, nd)
+ if err != nil {
+ return nil, err
+ }
+
+ return pbnd.Copy().(*dag.ProtoNode), nil
+ // TODO: Why do we need a copy?
+}
+
+// Update child entry in the underlying UnixFS directory.
+func (d *Directory) updateChild(c child) error {
+ err := d.unixfsDir.AddChild(d.ctx, c.Name, c.Node)
+ if err != nil {
+ return err
+ }
+
+ d.modTime = time.Now()
+
+ return nil
+}
+
+func (d *Directory) Type() NodeType {
+ return TDir
+}
+
+// childNode returns a FSNode under this directory by the given name if it exists.
+// it does *not* check the cached dirs and files
+func (d *Directory) childNode(name string) (FSNode, error) {
+ nd, err := d.childFromDag(name)
+ if err != nil {
+ return nil, err
+ }
+
+ return d.cacheNode(name, nd)
+}
+
+// cacheNode caches a node into d.childDirs or d.files and returns the FSNode.
+func (d *Directory) cacheNode(name string, nd ipld.Node) (FSNode, error) {
+ switch nd := nd.(type) {
+ case *dag.ProtoNode:
+ fsn, err := ft.FSNodeFromBytes(nd.Data())
+ if err != nil {
+ return nil, err
+ }
+
+ switch fsn.Type() {
+ case ft.TDirectory, ft.THAMTShard:
+ ndir, err := NewDirectory(d.ctx, name, nd, d, d.dagService)
+ if err != nil {
+ return nil, err
+ }
+
+ d.entriesCache[name] = ndir
+ return ndir, nil
+ case ft.TFile, ft.TRaw, ft.TSymlink:
+ nfi, err := NewFile(name, nd, d, d.dagService)
+ if err != nil {
+ return nil, err
+ }
+ d.entriesCache[name] = nfi
+ return nfi, nil
+ case ft.TMetadata:
+ return nil, ErrNotYetImplemented
+ default:
+ return nil, ErrInvalidChild
+ }
+ case *dag.RawNode:
+ nfi, err := NewFile(name, nd, d, d.dagService)
+ if err != nil {
+ return nil, err
+ }
+ d.entriesCache[name] = nfi
+ return nfi, nil
+ default:
+ return nil, fmt.Errorf("unrecognized node type in cache node")
+ }
+}
+
+// Child returns the child of this directory by the given name
+func (d *Directory) Child(name string) (FSNode, error) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ return d.childUnsync(name)
+}
+
+func (d *Directory) Uncache(name string) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ delete(d.entriesCache, name)
+}
+
+// childFromDag searches through this directories dag node for a child link
+// with the given name
+func (d *Directory) childFromDag(name string) (ipld.Node, error) {
+ return d.unixfsDir.Find(d.ctx, name)
+}
+
+// childUnsync returns the child under this directory by the given name
+// without locking, useful for operations which already hold a lock
+func (d *Directory) childUnsync(name string) (FSNode, error) {
+ entry, ok := d.entriesCache[name]
+ if ok {
+ return entry, nil
+ }
+
+ return d.childNode(name)
+}
+
+type NodeListing struct {
+ Name string
+ Type int
+ Size int64
+ Hash string
+}
+
+func (d *Directory) ListNames(ctx context.Context) ([]string, error) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+
+ var out []string
+ err := d.unixfsDir.ForEachLink(ctx, func(l *ipld.Link) error {
+ out = append(out, l.Name)
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return out, nil
+}
+
+func (d *Directory) List(ctx context.Context) ([]NodeListing, error) {
+ var out []NodeListing
+ err := d.ForEachEntry(ctx, func(nl NodeListing) error {
+ out = append(out, nl)
+ return nil
+ })
+ return out, err
+}
+
+func (d *Directory) ForEachEntry(ctx context.Context, f func(NodeListing) error) error {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+ return d.unixfsDir.ForEachLink(ctx, func(l *ipld.Link) error {
+ c, err := d.childUnsync(l.Name)
+ if err != nil {
+ return err
+ }
+
+ nd, err := c.GetNode()
+ if err != nil {
+ return err
+ }
+
+ child := NodeListing{
+ Name: l.Name,
+ Type: int(c.Type()),
+ Hash: nd.Cid().String(),
+ }
+
+ if c, ok := c.(*File); ok {
+ size, err := c.Size()
+ if err != nil {
+ return err
+ }
+ child.Size = size
+ }
+
+ return f(child)
+ })
+}
+
+func (d *Directory) Mkdir(name string) (*Directory, error) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+
+ fsn, err := d.childUnsync(name)
+ if err == nil {
+ switch fsn := fsn.(type) {
+ case *Directory:
+ return fsn, os.ErrExist
+ case *File:
+ return nil, os.ErrExist
+ default:
+ return nil, fmt.Errorf("unrecognized type: %#v", fsn)
+ }
+ }
+
+ ndir := ft.EmptyDirNode()
+ ndir.SetCidBuilder(d.GetCidBuilder())
+
+ err = d.dagService.Add(d.ctx, ndir)
+ if err != nil {
+ return nil, err
+ }
+
+ err = d.unixfsDir.AddChild(d.ctx, name, ndir)
+ if err != nil {
+ return nil, err
+ }
+
+ dirobj, err := NewDirectory(d.ctx, name, ndir, d, d.dagService)
+ if err != nil {
+ return nil, err
+ }
+
+ d.entriesCache[name] = dirobj
+ return dirobj, nil
+}
+
+func (d *Directory) Unlink(name string) error {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+
+ delete(d.entriesCache, name)
+
+ return d.unixfsDir.RemoveChild(d.ctx, name)
+}
+
+func (d *Directory) Flush() error {
+ nd, err := d.GetNode()
+ if err != nil {
+ return err
+ }
+
+ return d.parent.updateChildEntry(child{d.name, nd})
+}
+
+// AddChild adds the node 'nd' under this directory giving it the name 'name'
+func (d *Directory) AddChild(name string, nd ipld.Node) error {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+
+ _, err := d.childUnsync(name)
+ if err == nil {
+ return ErrDirExists
+ }
+
+ err = d.dagService.Add(d.ctx, nd)
+ if err != nil {
+ return err
+ }
+
+ err = d.unixfsDir.AddChild(d.ctx, name, nd)
+ if err != nil {
+ return err
+ }
+
+ d.modTime = time.Now()
+ return nil
+}
+
+func (d *Directory) sync() error {
+ for name, entry := range d.entriesCache {
+ nd, err := entry.GetNode()
+ if err != nil {
+ return err
+ }
+
+ err = d.updateChild(child{name, nd})
+ if err != nil {
+ return err
+ }
+ }
+
+ // TODO: Should we clean the cache here?
+
+ return nil
+}
+
+func (d *Directory) Path() string {
+ cur := d
+ var out string
+ for cur != nil {
+ switch parent := cur.parent.(type) {
+ case *Directory:
+ out = path.Join(cur.name, out)
+ cur = parent
+ case *Root:
+ return "/" + out
+ default:
+ panic("directory parent neither a directory nor a root")
+ }
+ }
+ return out
+}
+
+func (d *Directory) GetNode() (ipld.Node, error) {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+
+ err := d.sync()
+ if err != nil {
+ return nil, err
+ }
+
+ nd, err := d.unixfsDir.GetNode()
+ if err != nil {
+ return nil, err
+ }
+
+ err = d.dagService.Add(d.ctx, nd)
+ if err != nil {
+ return nil, err
+ }
+
+ return nd.Copy(), err
+}
diff --git a/mfs/fd.go b/mfs/fd.go
new file mode 100644
index 0000000000..7f9897b1cb
--- /dev/null
+++ b/mfs/fd.go
@@ -0,0 +1,197 @@
+package mfs
+
+import (
+ "fmt"
+ "io"
+
+ mod "github.com/ipfs/boxo/unixfs/mod"
+
+ context "context"
+
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+type state uint8
+
+const (
+ stateCreated state = iota
+ stateFlushed
+ stateDirty
+ stateClosed
+)
+
+// One `File` can have many `FileDescriptor`s associated to it
+// (only one if it's RW, many if they are RO, see `File.desclock`).
+// A `FileDescriptor` contains the "view" of the file (through an
+// instance of a `DagModifier`), that's why it (and not the `File`)
+// has the responsibility to `Flush` (which crystallizes that view
+// in the `File`'s `Node`).
+type FileDescriptor interface {
+ io.Reader
+ CtxReadFull(context.Context, []byte) (int, error)
+
+ io.Writer
+ io.WriterAt
+
+ io.Closer
+ io.Seeker
+
+ Truncate(int64) error
+ Size() (int64, error)
+ Flush() error
+}
+
+type fileDescriptor struct {
+ inode *File
+ mod *mod.DagModifier
+ flags Flags
+
+ state state
+}
+
+func (fi *fileDescriptor) checkWrite() error {
+ if fi.state == stateClosed {
+ return ErrClosed
+ }
+ if !fi.flags.Write {
+ return fmt.Errorf("file is read-only")
+ }
+ return nil
+}
+
+func (fi *fileDescriptor) checkRead() error {
+ if fi.state == stateClosed {
+ return ErrClosed
+ }
+ if !fi.flags.Read {
+ return fmt.Errorf("file is write-only")
+ }
+ return nil
+}
+
+// Size returns the size of the file referred to by this descriptor
+func (fi *fileDescriptor) Size() (int64, error) {
+ return fi.mod.Size()
+}
+
+// Truncate truncates the file to size
+func (fi *fileDescriptor) Truncate(size int64) error {
+ if err := fi.checkWrite(); err != nil {
+ return fmt.Errorf("truncate failed: %s", err)
+ }
+ fi.state = stateDirty
+ return fi.mod.Truncate(size)
+}
+
+// Write writes the given data to the file at its current offset
+func (fi *fileDescriptor) Write(b []byte) (int, error) {
+ if err := fi.checkWrite(); err != nil {
+ return 0, fmt.Errorf("write failed: %s", err)
+ }
+ fi.state = stateDirty
+ return fi.mod.Write(b)
+}
+
+// Read reads into the given buffer from the current offset
+func (fi *fileDescriptor) Read(b []byte) (int, error) {
+ if err := fi.checkRead(); err != nil {
+ return 0, fmt.Errorf("read failed: %s", err)
+ }
+ return fi.mod.Read(b)
+}
+
+// Read reads into the given buffer from the current offset
+func (fi *fileDescriptor) CtxReadFull(ctx context.Context, b []byte) (int, error) {
+ if err := fi.checkRead(); err != nil {
+ return 0, fmt.Errorf("read failed: %s", err)
+ }
+ return fi.mod.CtxReadFull(ctx, b)
+}
+
+// Close flushes, then propogates the modified dag node up the directory structure
+// and signals a republish to occur
+func (fi *fileDescriptor) Close() error {
+ if fi.state == stateClosed {
+ return ErrClosed
+ }
+ if fi.flags.Write {
+ defer fi.inode.desclock.Unlock()
+ } else if fi.flags.Read {
+ defer fi.inode.desclock.RUnlock()
+ }
+ err := fi.flushUp(fi.flags.Sync)
+ fi.state = stateClosed
+ return err
+}
+
+// Flush generates a new version of the node of the underlying
+// UnixFS directory (adding it to the DAG service) and updates
+// the entry in the parent directory (setting `fullSync` to
+// propagate the update all the way to the root).
+func (fi *fileDescriptor) Flush() error {
+ return fi.flushUp(true)
+}
+
+// flushUp syncs the file and adds it to the dagservice
+// it *must* be called with the File's lock taken
+// If `fullSync` is set the changes are propagated upwards
+// (the `Up` part of `flushUp`).
+func (fi *fileDescriptor) flushUp(fullSync bool) error {
+ var nd ipld.Node
+ switch fi.state {
+ case stateCreated, stateDirty:
+ var err error
+ nd, err = fi.mod.GetNode()
+ if err != nil {
+ return err
+ }
+ err = fi.inode.dagService.Add(context.TODO(), nd)
+ if err != nil {
+ return err
+ }
+
+ // TODO: Very similar logic to the update process in
+ // `Directory`, the logic should be unified, both structures
+ // (`File` and `Directory`) are backed by a IPLD node with
+ // a UnixFS format that is the actual target of the update
+ // (regenerating it and adding it to the DAG service).
+ fi.inode.nodeLock.Lock()
+ // Always update the file descriptor's inode with the created/modified node.
+ fi.inode.node = nd
+ // Save the members to be used for subsequent calls
+ parent := fi.inode.parent
+ name := fi.inode.name
+ fi.inode.nodeLock.Unlock()
+
+ // Bubble up the update's to the parent, only if fullSync is set to true.
+ if fullSync {
+ if err := parent.updateChildEntry(child{name, nd}); err != nil {
+ return err
+ }
+ }
+
+ fi.state = stateFlushed
+ return nil
+ case stateFlushed:
+ return nil
+ default:
+ panic("invalid state")
+ }
+}
+
+// Seek implements io.Seeker
+func (fi *fileDescriptor) Seek(offset int64, whence int) (int64, error) {
+ if fi.state == stateClosed {
+ return 0, fmt.Errorf("seek failed: %s", ErrClosed)
+ }
+ return fi.mod.Seek(offset, whence)
+}
+
+// Write At writes the given bytes at the offset 'at'
+func (fi *fileDescriptor) WriteAt(b []byte, at int64) (int, error) {
+ if err := fi.checkWrite(); err != nil {
+ return 0, fmt.Errorf("write-at failed: %s", err)
+ }
+ fi.state = stateDirty
+ return fi.mod.WriteAt(b, at)
+}
diff --git a/mfs/file.go b/mfs/file.go
new file mode 100644
index 0000000000..24203e6a00
--- /dev/null
+++ b/mfs/file.go
@@ -0,0 +1,179 @@
+package mfs
+
+import (
+ "context"
+ "fmt"
+ "sync"
+
+ ft "github.com/ipfs/boxo/unixfs"
+ mod "github.com/ipfs/boxo/unixfs/mod"
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+
+ chunker "github.com/ipfs/boxo/chunker"
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+// File represents a file in the MFS, its logic its mainly targeted
+// to coordinating (potentially many) `FileDescriptor`s pointing to
+// it.
+type File struct {
+ inode
+
+ // Lock to coordinate the `FileDescriptor`s associated to this file.
+ desclock sync.RWMutex
+
+ // This isn't any node, it's the root node that represents the
+ // entire DAG of nodes that comprise the file.
+ // TODO: Rename, there should be an explicit term for these root nodes
+ // of a particular sub-DAG that abstract an upper layer's entity.
+ node ipld.Node
+
+ // Lock around the `node` that represents this file, necessary because
+ // there may be many `FileDescriptor`s operating on this `File`.
+ nodeLock sync.RWMutex
+
+ RawLeaves bool
+}
+
+// NewFile returns a NewFile object with the given parameters. If the
+// Cid version is non-zero RawLeaves will be enabled.
+func NewFile(name string, node ipld.Node, parent parent, dserv ipld.DAGService) (*File, error) {
+ fi := &File{
+ inode: inode{
+ name: name,
+ parent: parent,
+ dagService: dserv,
+ },
+ node: node,
+ }
+ if node.Cid().Prefix().Version > 0 {
+ fi.RawLeaves = true
+ }
+ return fi, nil
+}
+
+func (fi *File) Open(flags Flags) (_ FileDescriptor, _retErr error) {
+ if flags.Write {
+ fi.desclock.Lock()
+ defer func() {
+ if _retErr != nil {
+ fi.desclock.Unlock()
+ }
+ }()
+ } else if flags.Read {
+ fi.desclock.RLock()
+ defer func() {
+ if _retErr != nil {
+ fi.desclock.RUnlock()
+ }
+ }()
+ } else {
+ return nil, fmt.Errorf("file opened for neither reading nor writing")
+ }
+
+ fi.nodeLock.RLock()
+ node := fi.node
+ fi.nodeLock.RUnlock()
+
+ // TODO: Move this `switch` logic outside (maybe even
+ // to another package, this seems like a job of UnixFS),
+ // `NewDagModifier` uses the IPLD node, we're not
+ // extracting anything just doing a safety check.
+ switch node := node.(type) {
+ case *dag.ProtoNode:
+ fsn, err := ft.FSNodeFromBytes(node.Data())
+ if err != nil {
+ return nil, err
+ }
+
+ switch fsn.Type() {
+ default:
+ return nil, fmt.Errorf("unsupported fsnode type for 'file'")
+ case ft.TSymlink:
+ return nil, fmt.Errorf("symlinks not yet supported")
+ case ft.TFile, ft.TRaw:
+ // OK case
+ }
+ case *dag.RawNode:
+ // Ok as well.
+ }
+
+ dmod, err := mod.NewDagModifier(context.TODO(), node, fi.dagService, chunker.DefaultSplitter)
+ // TODO: Remove the use of the `chunker` package here, add a new `NewDagModifier` in
+ // `go-unixfs` with the `DefaultSplitter` already included.
+ if err != nil {
+ return nil, err
+ }
+ dmod.RawLeaves = fi.RawLeaves
+
+ return &fileDescriptor{
+ inode: fi,
+ flags: flags,
+ mod: dmod,
+ state: stateCreated,
+ }, nil
+}
+
+// Size returns the size of this file
+// TODO: Should we be providing this API?
+// TODO: There's already a `FileDescriptor.Size()` that
+// through the `DagModifier`'s `fileSize` function is doing
+// pretty much the same thing as here, we should at least call
+// that function and wrap the `ErrNotUnixfs` with an MFS text.
+func (fi *File) Size() (int64, error) {
+ fi.nodeLock.RLock()
+ defer fi.nodeLock.RUnlock()
+ switch nd := fi.node.(type) {
+ case *dag.ProtoNode:
+ fsn, err := ft.FSNodeFromBytes(nd.Data())
+ if err != nil {
+ return 0, err
+ }
+ return int64(fsn.FileSize()), nil
+ case *dag.RawNode:
+ return int64(len(nd.RawData())), nil
+ default:
+ return 0, fmt.Errorf("unrecognized node type in mfs/file.Size()")
+ }
+}
+
+// GetNode returns the dag node associated with this file
+// TODO: Use this method and do not access the `nodeLock` directly anywhere else.
+func (fi *File) GetNode() (ipld.Node, error) {
+ fi.nodeLock.RLock()
+ defer fi.nodeLock.RUnlock()
+ return fi.node, nil
+}
+
+// TODO: Tight coupling with the `FileDescriptor`, at the
+// very least this should be an independent function that
+// takes a `File` argument and automates the open/flush/close
+// operations.
+// TODO: Why do we need to flush a file that isn't opened?
+// (the `OpenWriteOnly` seems to implicitly be targeting a
+// closed file, a file we forgot to flush? can we close
+// a file without flushing?)
+func (fi *File) Flush() error {
+ // open the file in fullsync mode
+ fd, err := fi.Open(Flags{Write: true, Sync: true})
+ if err != nil {
+ return err
+ }
+
+ defer fd.Close()
+
+ return fd.Flush()
+}
+
+func (fi *File) Sync() error {
+ // just being able to take the writelock means the descriptor is synced
+ // TODO: Why?
+ fi.desclock.Lock()
+ defer fi.desclock.Unlock() // Defer works around "empty critical section (SA2001)"
+ return nil
+}
+
+// Type returns the type FSNode this is
+func (fi *File) Type() NodeType {
+ return TFile
+}
diff --git a/mfs/inode.go b/mfs/inode.go
new file mode 100644
index 0000000000..50bed0b38e
--- /dev/null
+++ b/mfs/inode.go
@@ -0,0 +1,21 @@
+package mfs
+
+import (
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+// inode abstracts the common characteristics of the MFS `File`
+// and `Directory`. All of its attributes are initialized at
+// creation.
+type inode struct {
+ // name of this `inode` in the MFS path (the same value
+ // is also stored as the name of the DAG link).
+ name string
+
+ // parent directory of this `inode` (which may be the `Root`).
+ parent parent
+
+ // dagService used to store modifications made to the contents
+ // of the file or directory the `inode` belongs to.
+ dagService ipld.DAGService
+}
diff --git a/mfs/mfs_test.go b/mfs/mfs_test.go
new file mode 100644
index 0000000000..e99e73eaac
--- /dev/null
+++ b/mfs/mfs_test.go
@@ -0,0 +1,1421 @@
+package mfs
+
+import (
+ "bytes"
+ "context"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "math/rand"
+ "os"
+ "sort"
+ "sync"
+ "testing"
+ "time"
+
+ path "github.com/ipfs/boxo/path"
+
+ bserv "github.com/ipfs/boxo/blockservice"
+ ft "github.com/ipfs/boxo/unixfs"
+ importer "github.com/ipfs/boxo/unixfs/importer"
+ uio "github.com/ipfs/boxo/unixfs/io"
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+
+ bstore "github.com/ipfs/boxo/blockstore"
+ chunker "github.com/ipfs/boxo/chunker"
+ offline "github.com/ipfs/boxo/exchange/offline"
+ u "github.com/ipfs/boxo/util"
+ cid "github.com/ipfs/go-cid"
+ ds "github.com/ipfs/go-datastore"
+ dssync "github.com/ipfs/go-datastore/sync"
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+func emptyDirNode() *dag.ProtoNode {
+ return dag.NodeWithData(ft.FolderPBData())
+}
+
+func getDagserv(t *testing.T) ipld.DAGService {
+ db := dssync.MutexWrap(ds.NewMapDatastore())
+ bs := bstore.NewBlockstore(db)
+ blockserv := bserv.New(bs, offline.Exchange(bs))
+ return dag.NewDAGService(blockserv)
+}
+
+func getRandFile(t *testing.T, ds ipld.DAGService, size int64) ipld.Node {
+ r := io.LimitReader(u.NewTimeSeededRand(), size)
+ return fileNodeFromReader(t, ds, r)
+}
+
+func fileNodeFromReader(t *testing.T, ds ipld.DAGService, r io.Reader) ipld.Node {
+ nd, err := importer.BuildDagFromReader(ds, chunker.DefaultSplitter(r))
+ if err != nil {
+ t.Fatal(err)
+ }
+ return nd
+}
+
+func mkdirP(t *testing.T, root *Directory, pth string) *Directory {
+ dirs := path.SplitList(pth)
+ cur := root
+ for _, d := range dirs {
+ n, err := cur.Mkdir(d)
+ if err != nil && err != os.ErrExist {
+ t.Fatal(err)
+ }
+ if err == os.ErrExist {
+ fsn, err := cur.Child(d)
+ if err != nil {
+ t.Fatal(err)
+ }
+ switch fsn := fsn.(type) {
+ case *Directory:
+ n = fsn
+ case *File:
+ t.Fatal("tried to make a directory where a file already exists")
+ }
+ }
+
+ cur = n
+ }
+ return cur
+}
+
+func assertDirNotAtPath(root *Directory, pth string) error {
+ _, err := DirLookup(root, pth)
+ if err == nil {
+ return fmt.Errorf("%s exists in %s", pth, root.name)
+ }
+ return nil
+}
+
+func assertDirAtPath(root *Directory, pth string, children []string) error {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ fsn, err := DirLookup(root, pth)
+ if err != nil {
+ return err
+ }
+
+ dir, ok := fsn.(*Directory)
+ if !ok {
+ return fmt.Errorf("%s was not a directory", pth)
+ }
+
+ listing, err := dir.List(ctx)
+ if err != nil {
+ return err
+ }
+
+ var names []string
+ for _, d := range listing {
+ names = append(names, d.Name)
+ }
+
+ sort.Strings(children)
+ sort.Strings(names)
+ if !compStrArrs(children, names) {
+ return errors.New("directories children did not match")
+ }
+
+ return nil
+}
+
+func compStrArrs(a, b []string) bool {
+ if len(a) != len(b) {
+ return false
+ }
+
+ for i := 0; i < len(a); i++ {
+ if a[i] != b[i] {
+ return false
+ }
+ }
+
+ return true
+}
+
+func assertFileAtPath(ds ipld.DAGService, root *Directory, expn ipld.Node, pth string) error {
+ exp, ok := expn.(*dag.ProtoNode)
+ if !ok {
+ return dag.ErrNotProtobuf
+ }
+
+ parts := path.SplitList(pth)
+ cur := root
+ for i, d := range parts[:len(parts)-1] {
+ next, err := cur.Child(d)
+ if err != nil {
+ return fmt.Errorf("looking for %s failed: %s", pth, err)
+ }
+
+ nextDir, ok := next.(*Directory)
+ if !ok {
+ return fmt.Errorf("%s points to a non-directory", parts[:i+1])
+ }
+
+ cur = nextDir
+ }
+
+ last := parts[len(parts)-1]
+ finaln, err := cur.Child(last)
+ if err != nil {
+ return err
+ }
+
+ file, ok := finaln.(*File)
+ if !ok {
+ return fmt.Errorf("%s was not a file", pth)
+ }
+
+ rfd, err := file.Open(Flags{Read: true})
+ if err != nil {
+ return err
+ }
+
+ out, err := io.ReadAll(rfd)
+ if err != nil {
+ return err
+ }
+
+ expbytes, err := catNode(ds, exp)
+ if err != nil {
+ return err
+ }
+
+ if !bytes.Equal(out, expbytes) {
+ return fmt.Errorf("incorrect data at path")
+ }
+ return nil
+}
+
+func catNode(ds ipld.DAGService, nd *dag.ProtoNode) ([]byte, error) {
+ r, err := uio.NewDagReader(context.TODO(), nd, ds)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+
+ return io.ReadAll(r)
+}
+
+func setupRoot(ctx context.Context, t *testing.T) (ipld.DAGService, *Root) {
+ ds := getDagserv(t)
+
+ root := emptyDirNode()
+ rt, err := NewRoot(ctx, ds, root, func(ctx context.Context, c cid.Cid) error {
+ fmt.Println("PUBLISHED: ", c)
+ return nil
+ })
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ return ds, rt
+}
+
+func TestBasic(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ ds, rt := setupRoot(ctx, t)
+
+ rootdir := rt.GetDirectory()
+
+ // test making a basic dir
+ _, err := rootdir.Mkdir("a")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ path := "a/b/c/d/e/f/g"
+ d := mkdirP(t, rootdir, path)
+
+ fi := getRandFile(t, ds, 1000)
+
+ // test inserting that file
+ err = d.AddChild("afile", fi)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = assertFileAtPath(ds, rootdir, fi, "a/b/c/d/e/f/g/afile")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestMkdir(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ _, rt := setupRoot(ctx, t)
+
+ rootdir := rt.GetDirectory()
+
+ dirsToMake := []string{"a", "B", "foo", "bar", "cats", "fish"}
+ sort.Strings(dirsToMake) // sort for easy comparing later
+
+ for _, d := range dirsToMake {
+ _, err := rootdir.Mkdir(d)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ err := assertDirAtPath(rootdir, "/", dirsToMake)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, d := range dirsToMake {
+ mkdirP(t, rootdir, "a/"+d)
+ }
+
+ err = assertDirAtPath(rootdir, "/a", dirsToMake)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // mkdir over existing dir should fail
+ _, err = rootdir.Mkdir("a")
+ if err == nil {
+ t.Fatal("should have failed!")
+ }
+}
+
+func TestDirectoryLoadFromDag(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ ds, rt := setupRoot(ctx, t)
+
+ rootdir := rt.GetDirectory()
+
+ nd := getRandFile(t, ds, 1000)
+ err := ds.Add(ctx, nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fihash := nd.Cid()
+
+ dir := emptyDirNode()
+ err = ds.Add(ctx, dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ dirhash := dir.Cid()
+
+ top := emptyDirNode()
+ top.SetLinks([]*ipld.Link{
+ {
+ Name: "a",
+ Cid: fihash,
+ },
+ {
+ Name: "b",
+ Cid: dirhash,
+ },
+ })
+
+ err = rootdir.AddChild("foo", top)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // get this dir
+ topi, err := rootdir.Child("foo")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ topd := topi.(*Directory)
+
+ path := topd.Path()
+ if path != "/foo" {
+ t.Fatalf("Expected path '/foo', got '%s'", path)
+ }
+
+ // mkdir over existing but unloaded child file should fail
+ _, err = topd.Mkdir("a")
+ if err == nil {
+ t.Fatal("expected to fail!")
+ }
+
+ // mkdir over existing but unloaded child dir should fail
+ _, err = topd.Mkdir("b")
+ if err == nil {
+ t.Fatal("expected to fail!")
+ }
+
+ // adding a child over an existing path fails
+ err = topd.AddChild("b", nd)
+ if err == nil {
+ t.Fatal("expected to fail!")
+ }
+
+ err = assertFileAtPath(ds, rootdir, nd, "foo/a")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = assertDirAtPath(rootdir, "foo/b", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = rootdir.Unlink("foo")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = assertDirAtPath(rootdir, "", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestMvFile(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ dagService, rt := setupRoot(ctx, t)
+ rootDir := rt.GetDirectory()
+
+ fi := getRandFile(t, dagService, 1000)
+
+ err := rootDir.AddChild("afile", fi)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = Mv(rt, "/afile", "/bfile")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = assertFileAtPath(dagService, rootDir, fi, "bfile")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestMvFileToSubdir(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ dagService, rt := setupRoot(ctx, t)
+ rootDir := rt.GetDirectory()
+
+ _ = mkdirP(t, rootDir, "test1")
+
+ fi := getRandFile(t, dagService, 1000)
+
+ err := rootDir.AddChild("afile", fi)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = Mv(rt, "/afile", "/test1")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = assertFileAtPath(dagService, rootDir, fi, "test1/afile")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestMvFileToSubdirWithRename(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ dagService, rt := setupRoot(ctx, t)
+ rootDir := rt.GetDirectory()
+
+ _ = mkdirP(t, rootDir, "test1")
+
+ fi := getRandFile(t, dagService, 1000)
+
+ err := rootDir.AddChild("afile", fi)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = Mv(rt, "/afile", "/test1/bfile")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = assertFileAtPath(dagService, rootDir, fi, "test1/bfile")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestMvDir(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ dagService, rt := setupRoot(ctx, t)
+ rootDir := rt.GetDirectory()
+
+ _ = mkdirP(t, rootDir, "test1")
+ d2 := mkdirP(t, rootDir, "test2")
+
+ fi := getRandFile(t, dagService, 1000)
+
+ err := d2.AddChild("afile", fi)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = Mv(rt, "/test2", "/test1")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = assertDirNotAtPath(rootDir, "test2")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = assertFileAtPath(dagService, rootDir, fi, "test1/test2/afile")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestMfsFile(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ ds, rt := setupRoot(ctx, t)
+
+ rootdir := rt.GetDirectory()
+
+ fisize := 1000
+ nd := getRandFile(t, ds, 1000)
+
+ err := rootdir.AddChild("file", nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fsn, err := rootdir.Child("file")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fi := fsn.(*File)
+
+ if fi.Type() != TFile {
+ t.Fatal("some is seriously wrong here")
+ }
+
+ wfd, err := fi.Open(Flags{Read: true, Write: true, Sync: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // assert size is as expected
+ size, err := fi.Size()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if size != int64(fisize) {
+ t.Fatal("size isnt correct")
+ }
+
+ // write to beginning of file
+ b := []byte("THIS IS A TEST")
+ n, err := wfd.Write(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if n != len(b) {
+ t.Fatal("didnt write correct number of bytes")
+ }
+
+ // make sure size hasnt changed
+ size, err = wfd.Size()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if size != int64(fisize) {
+ t.Fatal("size isnt correct")
+ }
+
+ // seek back to beginning
+ ns, err := wfd.Seek(0, io.SeekStart)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if ns != 0 {
+ t.Fatal("didnt seek to beginning")
+ }
+
+ // read back bytes we wrote
+ buf := make([]byte, len(b))
+ n, err = wfd.Read(buf)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if n != len(buf) {
+ t.Fatal("didnt read enough")
+ }
+
+ if !bytes.Equal(buf, b) {
+ t.Fatal("data read was different than data written")
+ }
+
+ // truncate file to ten bytes
+ err = wfd.Truncate(10)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ size, err = wfd.Size()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if size != 10 {
+ t.Fatal("size was incorrect: ", size)
+ }
+
+ // 'writeAt' to extend it
+ data := []byte("this is a test foo foo foo")
+ nwa, err := wfd.WriteAt(data, 5)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if nwa != len(data) {
+ t.Fatal(err)
+ }
+
+ // assert size once more
+ size, err = wfd.Size()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if size != int64(5+len(data)) {
+ t.Fatal("size was incorrect")
+ }
+
+ // close it out!
+ err = wfd.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // make sure we can get node. TODO: verify it later
+ _, err = fi.GetNode()
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestMfsDirListNames(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ ds, rt := setupRoot(ctx, t)
+
+ rootdir := rt.GetDirectory()
+
+ rand.Seed(time.Now().UTC().UnixNano())
+
+ total := rand.Intn(10) + 1
+ fNames := make([]string, 0, total)
+
+ for i := 0; i < total; i++ {
+ fn := randomName()
+ fNames = append(fNames, fn)
+ nd := getRandFile(t, ds, rand.Int63n(1000)+1)
+ err := rootdir.AddChild(fn, nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ list, err := rootdir.ListNames(ctx)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, lName := range list {
+ found := false
+ for _, fName := range fNames {
+ if lName == fName {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Fatal(lName + " not found in directory listing")
+ }
+ }
+}
+
+func randomWalk(d *Directory, n int) (*Directory, error) {
+ for i := 0; i < n; i++ {
+ dirents, err := d.List(context.Background())
+ if err != nil {
+ return nil, err
+ }
+
+ var childdirs []NodeListing
+ for _, child := range dirents {
+ if child.Type == int(TDir) {
+ childdirs = append(childdirs, child)
+ }
+ }
+ if len(childdirs) == 0 {
+ return d, nil
+ }
+
+ next := childdirs[rand.Intn(len(childdirs))].Name
+
+ nextD, err := d.Child(next)
+ if err != nil {
+ return nil, err
+ }
+
+ d = nextD.(*Directory)
+ }
+ return d, nil
+}
+
+func randomName() string {
+ set := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_"
+ length := rand.Intn(10) + 2
+ var out string
+ for i := 0; i < length; i++ {
+ j := rand.Intn(len(set))
+ out += set[j : j+1]
+ }
+ return out
+}
+
+func actorMakeFile(d *Directory) error {
+ d, err := randomWalk(d, rand.Intn(7))
+ if err != nil {
+ return err
+ }
+
+ name := randomName()
+ f, err := NewFile(name, dag.NodeWithData(ft.FilePBData(nil, 0)), d, d.dagService)
+ if err != nil {
+ return err
+ }
+
+ wfd, err := f.Open(Flags{Write: true, Sync: true})
+ if err != nil {
+ return err
+ }
+
+ rread := rand.New(rand.NewSource(time.Now().UnixNano()))
+ r := io.LimitReader(rread, int64(77*rand.Intn(123)+1))
+ _, err = io.Copy(wfd, r)
+ if err != nil {
+ return err
+ }
+
+ return wfd.Close()
+}
+
+func actorMkdir(d *Directory) error {
+ d, err := randomWalk(d, rand.Intn(7))
+ if err != nil {
+ return err
+ }
+
+ _, err = d.Mkdir(randomName())
+
+ return err
+}
+
+func randomFile(d *Directory) (*File, error) {
+ d, err := randomWalk(d, rand.Intn(6))
+ if err != nil {
+ return nil, err
+ }
+
+ ents, err := d.List(context.Background())
+ if err != nil {
+ return nil, err
+ }
+
+ var files []string
+ for _, e := range ents {
+ if e.Type == int(TFile) {
+ files = append(files, e.Name)
+ }
+ }
+
+ if len(files) == 0 {
+ return nil, nil
+ }
+
+ fname := files[rand.Intn(len(files))]
+ fsn, err := d.Child(fname)
+ if err != nil {
+ return nil, err
+ }
+
+ fi, ok := fsn.(*File)
+ if !ok {
+ return nil, errors.New("file wasn't a file, race?")
+ }
+
+ return fi, nil
+}
+
+func actorWriteFile(d *Directory) error {
+ fi, err := randomFile(d)
+ if err != nil {
+ return err
+ }
+ if fi == nil {
+ return nil
+ }
+
+ size := rand.Intn(1024) + 1
+ buf := make([]byte, size)
+ rand.Read(buf)
+
+ s, err := fi.Size()
+ if err != nil {
+ return err
+ }
+
+ wfd, err := fi.Open(Flags{Write: true, Sync: true})
+ if err != nil {
+ return err
+ }
+
+ offset := rand.Int63n(s)
+
+ n, err := wfd.WriteAt(buf, offset)
+ if err != nil {
+ return err
+ }
+ if n != size {
+ return fmt.Errorf("didnt write enough")
+ }
+
+ return wfd.Close()
+}
+
+func actorReadFile(d *Directory) error {
+ fi, err := randomFile(d)
+ if err != nil {
+ return err
+ }
+ if fi == nil {
+ return nil
+ }
+
+ _, err = fi.Size()
+ if err != nil {
+ return err
+ }
+
+ rfd, err := fi.Open(Flags{Read: true})
+ if err != nil {
+ return err
+ }
+
+ _, err = io.ReadAll(rfd)
+ if err != nil {
+ return err
+ }
+
+ return rfd.Close()
+}
+
+func testActor(rt *Root, iterations int, errs chan error) {
+ d := rt.GetDirectory()
+ for i := 0; i < iterations; i++ {
+ switch rand.Intn(5) {
+ case 0:
+ if err := actorMkdir(d); err != nil {
+ errs <- err
+ return
+ }
+ case 1, 2:
+ if err := actorMakeFile(d); err != nil {
+ errs <- err
+ return
+ }
+ case 3:
+ if err := actorWriteFile(d); err != nil {
+ errs <- err
+ return
+ }
+ case 4:
+ if err := actorReadFile(d); err != nil {
+ errs <- err
+ return
+ }
+ }
+ }
+ errs <- nil
+}
+
+func TestMfsStress(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ _, rt := setupRoot(ctx, t)
+
+ numroutines := 10
+
+ errs := make(chan error)
+ for i := 0; i < numroutines; i++ {
+ go testActor(rt, 50, errs)
+ }
+
+ for i := 0; i < numroutines; i++ {
+ err := <-errs
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+}
+
+func TestMfsHugeDir(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ _, rt := setupRoot(ctx, t)
+
+ for i := 0; i < 10000; i++ {
+ err := Mkdir(rt, fmt.Sprintf("/dir%d", i), MkdirOpts{Mkparents: false, Flush: false})
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+}
+
+func TestMkdirP(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ _, rt := setupRoot(ctx, t)
+
+ err := Mkdir(rt, "/a/b/c/d/e/f", MkdirOpts{Mkparents: true, Flush: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestConcurrentWriteAndFlush(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ ds, rt := setupRoot(ctx, t)
+
+ d := mkdirP(t, rt.GetDirectory(), "foo/bar/baz")
+ fn := fileNodeFromReader(t, ds, bytes.NewBuffer(nil))
+ err := d.AddChild("file", fn)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nloops := 500
+
+ wg := new(sync.WaitGroup)
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for i := 0; i < nloops; i++ {
+ err := writeFile(rt, "/foo/bar/baz/file", func(_ []byte) []byte { return []byte("STUFF") })
+ if err != nil {
+ t.Error("file write failed: ", err)
+ return
+ }
+ }
+ }()
+
+ for i := 0; i < nloops; i++ {
+ _, err := rt.GetDirectory().GetNode()
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ wg.Wait()
+}
+
+func TestFlushing(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ _, rt := setupRoot(ctx, t)
+
+ dir := rt.GetDirectory()
+ c := mkdirP(t, dir, "a/b/c")
+ d := mkdirP(t, dir, "a/b/d")
+ e := mkdirP(t, dir, "a/b/e")
+
+ data := []byte("this is a test\n")
+ nd1 := dag.NodeWithData(ft.FilePBData(data, uint64(len(data))))
+
+ if err := c.AddChild("TEST", nd1); err != nil {
+ t.Fatal(err)
+ }
+ if err := d.AddChild("TEST", nd1); err != nil {
+ t.Fatal(err)
+ }
+ if err := e.AddChild("TEST", nd1); err != nil {
+ t.Fatal(err)
+ }
+ if err := dir.AddChild("FILE", nd1); err != nil {
+ t.Fatal(err)
+ }
+
+ nd, err := FlushPath(ctx, rt, "/a/b/c/TEST")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if nd.Cid().String() != "QmYi7wrRFKVCcTB56A6Pep2j31Q5mHfmmu21RzHXu25RVR" {
+ t.Fatalf("unexpected node from FlushPath: %s", nd.Cid())
+ }
+
+ if _, err := FlushPath(ctx, rt, "/a/b/d/TEST"); err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err := FlushPath(ctx, rt, "/a/b/e/TEST"); err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err := FlushPath(ctx, rt, "/FILE"); err != nil {
+ t.Fatal(err)
+ }
+
+ rnd, err := dir.GetNode()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ pbrnd, ok := rnd.(*dag.ProtoNode)
+ if !ok {
+ t.Fatal(dag.ErrNotProtobuf)
+ }
+
+ fsnode, err := ft.FSNodeFromBytes(pbrnd.Data())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if fsnode.Type() != ft.TDirectory {
+ t.Fatal("root wasnt a directory")
+ }
+
+ rnk := rnd.Cid()
+ exp := "QmWMVyhTuyxUrXX3ynz171jq76yY3PktfY9Bxiph7b9ikr"
+ if rnk.String() != exp {
+ t.Fatalf("dag looks wrong, expected %s, but got %s", exp, rnk.String())
+ }
+}
+
+func readFile(rt *Root, path string, offset int64, buf []byte) error {
+ n, err := Lookup(rt, path)
+ if err != nil {
+ return err
+ }
+
+ fi, ok := n.(*File)
+ if !ok {
+ return fmt.Errorf("%s was not a file", path)
+ }
+
+ fd, err := fi.Open(Flags{Read: true})
+ if err != nil {
+ return err
+ }
+
+ _, err = fd.Seek(offset, io.SeekStart)
+ if err != nil {
+ return err
+ }
+
+ nread, err := fd.Read(buf)
+ if err != nil {
+ return err
+ }
+ if nread != len(buf) {
+ return fmt.Errorf("didn't read enough")
+ }
+
+ return fd.Close()
+}
+
+func TestConcurrentReads(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ ds, rt := setupRoot(ctx, t)
+
+ rootdir := rt.GetDirectory()
+
+ path := "a/b/c"
+ d := mkdirP(t, rootdir, path)
+
+ buf := make([]byte, 2048)
+ rand.Read(buf)
+
+ fi := fileNodeFromReader(t, ds, bytes.NewReader(buf))
+ err := d.AddChild("afile", fi)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var wg sync.WaitGroup
+ nloops := 100
+ for i := 0; i < 10; i++ {
+ wg.Add(1)
+ go func(me int) {
+ defer wg.Done()
+ mybuf := make([]byte, len(buf))
+ for j := 0; j < nloops; j++ {
+ offset := rand.Intn(len(buf))
+ length := rand.Intn(len(buf) - offset)
+
+ err := readFile(rt, "/a/b/c/afile", int64(offset), mybuf[:length])
+ if err != nil {
+ t.Error("readfile failed: ", err)
+ return
+ }
+
+ if !bytes.Equal(mybuf[:length], buf[offset:offset+length]) {
+ t.Error("incorrect read!")
+ }
+ }
+ }(i)
+ }
+ wg.Wait()
+}
+
+func writeFile(rt *Root, path string, transform func([]byte) []byte) error {
+ n, err := Lookup(rt, path)
+ if err != nil {
+ return err
+ }
+
+ fi, ok := n.(*File)
+ if !ok {
+ return fmt.Errorf("expected to receive a file, but didnt get one")
+ }
+
+ fd, err := fi.Open(Flags{Read: true, Write: true, Sync: true})
+ if err != nil {
+ return err
+ }
+ defer fd.Close()
+
+ data, err := io.ReadAll(fd)
+ if err != nil {
+ return err
+ }
+ data = transform(data)
+
+ _, err = fd.Seek(0, io.SeekStart)
+ if err != nil {
+ return err
+ }
+ err = fd.Truncate(0)
+ if err != nil {
+ return err
+ }
+
+ nw, err := fd.Write(data)
+ if err != nil {
+ return err
+ }
+
+ if nw != len(data) {
+ return fmt.Errorf("wrote incorrect amount: %d != 10", nw)
+ }
+
+ return nil
+}
+
+func TestConcurrentWrites(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ ds, rt := setupRoot(ctx, t)
+
+ rootdir := rt.GetDirectory()
+
+ path := "a/b/c"
+ d := mkdirP(t, rootdir, path)
+
+ fi := fileNodeFromReader(t, ds, bytes.NewReader(make([]byte, 0)))
+ err := d.AddChild("afile", fi)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var wg sync.WaitGroup
+ nloops := 100
+ errs := make(chan error, 1000)
+ for i := 0; i < 10; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ var lastSeen uint64
+ for j := 0; j < nloops; j++ {
+ err := writeFile(rt, "a/b/c/afile", func(buf []byte) []byte {
+ if len(buf) == 0 {
+ if lastSeen > 0 {
+ errs <- fmt.Errorf("file corrupted, last seen: %d", lastSeen)
+ return buf
+ }
+ buf = make([]byte, 8)
+ } else if len(buf) != 8 {
+ errs <- fmt.Errorf("buf not the right size")
+ return buf
+ }
+
+ num := binary.LittleEndian.Uint64(buf)
+ if num < lastSeen {
+ errs <- fmt.Errorf("count decreased: was %d, is %d", lastSeen, num)
+ return buf
+ } else {
+ t.Logf("count correct: was %d, is %d", lastSeen, num)
+ }
+ num++
+ binary.LittleEndian.PutUint64(buf, num)
+ lastSeen = num
+ return buf
+ })
+ if err != nil {
+ errs <- fmt.Errorf("writefile failed: %v", err)
+ return
+ }
+ }
+ }()
+ }
+ wg.Wait()
+ close(errs)
+ for e := range errs {
+ t.Fatal(e)
+ }
+ buf := make([]byte, 8)
+ if err := readFile(rt, "a/b/c/afile", 0, buf); err != nil {
+ t.Fatal(err)
+ }
+ actual := binary.LittleEndian.Uint64(buf)
+ expected := uint64(10 * nloops)
+ if actual != expected {
+ t.Fatalf("iteration mismatch: expect %d, got %d", expected, actual)
+ }
+}
+
+func TestFileDescriptors(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ ds, rt := setupRoot(ctx, t)
+ dir := rt.GetDirectory()
+
+ nd := dag.NodeWithData(ft.FilePBData(nil, 0))
+ fi, err := NewFile("test", nd, dir, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // test read only
+ rfd1, err := fi.Open(Flags{Read: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = rfd1.Truncate(0)
+ if err == nil {
+ t.Fatal("shouldnt be able to truncate readonly fd")
+ }
+
+ _, err = rfd1.Write([]byte{})
+ if err == nil {
+ t.Fatal("shouldnt be able to write to readonly fd")
+ }
+
+ _, err = rfd1.Read([]byte{})
+ if err != nil {
+ t.Fatalf("expected to be able to read from file: %s", err)
+ }
+
+ done := make(chan struct{})
+ go func() {
+ defer close(done)
+ // can open second readonly file descriptor
+ rfd2, err := fi.Open(Flags{Read: true})
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ rfd2.Close()
+ }()
+
+ select {
+ case <-time.After(time.Second):
+ t.Fatal("open second file descriptor failed")
+ case <-done:
+ }
+
+ if t.Failed() {
+ return
+ }
+
+ // test not being able to open for write until reader are closed
+ done = make(chan struct{})
+ go func() {
+ defer close(done)
+ wfd1, err := fi.Open(Flags{Write: true, Sync: true})
+ if err != nil {
+ t.Error(err)
+ }
+
+ wfd1.Close()
+ }()
+
+ select {
+ case <-time.After(time.Millisecond * 200):
+ case <-done:
+ if t.Failed() {
+ return
+ }
+
+ t.Fatal("shouldnt have been able to open file for writing")
+ }
+
+ err = rfd1.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ select {
+ case <-time.After(time.Second):
+ t.Fatal("should have been able to open write fd after closing read fd")
+ case <-done:
+ }
+
+ wfd, err := fi.Open(Flags{Write: true, Sync: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = wfd.Read([]byte{})
+ if err == nil {
+ t.Fatal("shouldnt have been able to read from write only filedescriptor")
+ }
+
+ _, err = wfd.Write([]byte{})
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestTruncateAtSize(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ ds, rt := setupRoot(ctx, t)
+
+ dir := rt.GetDirectory()
+
+ nd := dag.NodeWithData(ft.FilePBData(nil, 0))
+ fi, err := NewFile("test", nd, dir, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fd, err := fi.Open(Flags{Read: true, Write: true, Sync: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer fd.Close()
+ _, err = fd.Write([]byte("test"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ fd.Truncate(4)
+}
+
+func TestTruncateAndWrite(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ ds, rt := setupRoot(ctx, t)
+
+ dir := rt.GetDirectory()
+
+ nd := dag.NodeWithData(ft.FilePBData(nil, 0))
+ fi, err := NewFile("test", nd, dir, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fd, err := fi.Open(Flags{Read: true, Write: true, Sync: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer fd.Close()
+ for i := 0; i < 200; i++ {
+ err = fd.Truncate(0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ l, err := fd.Write([]byte("test"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if l != len("test") {
+ t.Fatal("incorrect write length")
+ }
+
+ _, err = fd.Seek(0, io.SeekStart)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ data, err := io.ReadAll(fd)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(data) != "test" {
+ t.Fatalf("read error at read %d, read: %v", i, data)
+ }
+ }
+}
+
+func TestFSNodeType(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ ds, rt := setupRoot(ctx, t)
+
+ // check for IsDir
+ nd := dag.NodeWithData(ft.FolderPBData())
+ di, err := NewDirectory(ctx, "test", nd, rt.GetDirectory(), ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ret := IsDir(di)
+ if !ret {
+ t.Fatal("FSNode type should be dir, but not")
+ }
+
+ // check for IsFile
+ fnd := dag.NodeWithData(ft.FilePBData(nil, 0))
+ fi, err := NewFile("test", fnd, rt.GetDirectory(), ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ret = IsFile(fi)
+ if !ret {
+ t.Fatal("FSNode type should be file, but not")
+ }
+}
diff --git a/mfs/ops.go b/mfs/ops.go
new file mode 100644
index 0000000000..78156dd52d
--- /dev/null
+++ b/mfs/ops.go
@@ -0,0 +1,246 @@
+package mfs
+
+import (
+ "context"
+ "fmt"
+ "os"
+ gopath "path"
+ "strings"
+
+ path "github.com/ipfs/boxo/path"
+
+ cid "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+// TODO: Evaluate moving all this operations to as `Root`
+// methods, since all of them use it as its first argument
+// and there is no clear documentation that explains this
+// separation.
+
+// Mv moves the file or directory at 'src' to 'dst'
+// TODO: Document what the strings 'src' and 'dst' represent.
+func Mv(r *Root, src, dst string) error {
+ srcDirName, srcFname := gopath.Split(src)
+
+ var dstDirName string
+ var dstFname string
+ if dst[len(dst)-1] == '/' {
+ dstDirName = dst
+ dstFname = srcFname
+ } else {
+ dstDirName, dstFname = gopath.Split(dst)
+ }
+
+ // get parent directories of both src and dest first
+ dstDir, err := lookupDir(r, dstDirName)
+ if err != nil {
+ return err
+ }
+
+ srcDir, err := lookupDir(r, srcDirName)
+ if err != nil {
+ return err
+ }
+
+ srcObj, err := srcDir.Child(srcFname)
+ if err != nil {
+ return err
+ }
+
+ nd, err := srcObj.GetNode()
+ if err != nil {
+ return err
+ }
+
+ fsn, err := dstDir.Child(dstFname)
+ if err == nil {
+ switch n := fsn.(type) {
+ case *File:
+ _ = dstDir.Unlink(dstFname)
+ case *Directory:
+ dstDir = n
+ dstFname = srcFname
+ default:
+ return fmt.Errorf("unexpected type at path: %s", dst)
+ }
+ } else if err != os.ErrNotExist {
+ return err
+ }
+
+ err = dstDir.AddChild(dstFname, nd)
+ if err != nil {
+ return err
+ }
+
+ if srcDir.name == dstDir.name && srcFname == dstFname {
+ return nil
+ }
+
+ return srcDir.Unlink(srcFname)
+}
+
+func lookupDir(r *Root, path string) (*Directory, error) {
+ di, err := Lookup(r, path)
+ if err != nil {
+ return nil, err
+ }
+
+ d, ok := di.(*Directory)
+ if !ok {
+ return nil, fmt.Errorf("%s is not a directory", path)
+ }
+
+ return d, nil
+}
+
+// PutNode inserts 'nd' at 'path' in the given mfs
+// TODO: Rename or clearly document that this is not about nodes but actually
+// MFS files/directories (that in the underlying representation can be
+// considered as just nodes).
+// TODO: Document why are we handling IPLD nodes in the first place when we
+// are actually referring to files/directories (that is, it can't be any
+// node, it has to have a specific format).
+// TODO: Can this function add directories or just files? What would be the
+// difference between adding a directory with this method and creating it
+// with `Mkdir`.
+func PutNode(r *Root, path string, nd ipld.Node) error {
+ dirp, filename := gopath.Split(path)
+ if filename == "" {
+ return fmt.Errorf("cannot create file with empty name")
+ }
+
+ pdir, err := lookupDir(r, dirp)
+ if err != nil {
+ return err
+ }
+
+ return pdir.AddChild(filename, nd)
+}
+
+// MkdirOpts is used by Mkdir
+type MkdirOpts struct {
+ Mkparents bool
+ Flush bool
+ CidBuilder cid.Builder
+}
+
+// Mkdir creates a directory at 'path' under the directory 'd', creating
+// intermediary directories as needed if 'mkparents' is set to true
+func Mkdir(r *Root, pth string, opts MkdirOpts) error {
+ if pth == "" {
+ return fmt.Errorf("no path given to Mkdir")
+ }
+ parts := path.SplitList(pth)
+ if parts[0] == "" {
+ parts = parts[1:]
+ }
+
+ // allow 'mkdir /a/b/c/' to create c
+ if parts[len(parts)-1] == "" {
+ parts = parts[:len(parts)-1]
+ }
+
+ if len(parts) == 0 {
+ // this will only happen on 'mkdir /'
+ if opts.Mkparents {
+ return nil
+ }
+ return fmt.Errorf("cannot create directory '/': Already exists")
+ }
+
+ cur := r.GetDirectory()
+ for i, d := range parts[:len(parts)-1] {
+ fsn, err := cur.Child(d)
+ if err == os.ErrNotExist && opts.Mkparents {
+ mkd, err := cur.Mkdir(d)
+ if err != nil {
+ return err
+ }
+ if opts.CidBuilder != nil {
+ mkd.SetCidBuilder(opts.CidBuilder)
+ }
+ fsn = mkd
+ } else if err != nil {
+ return err
+ }
+
+ next, ok := fsn.(*Directory)
+ if !ok {
+ return fmt.Errorf("%s was not a directory", path.Join(parts[:i]))
+ }
+ cur = next
+ }
+
+ final, err := cur.Mkdir(parts[len(parts)-1])
+ if err != nil {
+ if !opts.Mkparents || err != os.ErrExist || final == nil {
+ return err
+ }
+ }
+ if opts.CidBuilder != nil {
+ final.SetCidBuilder(opts.CidBuilder)
+ }
+
+ if opts.Flush {
+ err := final.Flush()
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// Lookup extracts the root directory and performs a lookup under it.
+// TODO: Now that the root is always a directory, can this function
+// be collapsed with `DirLookup`? Or at least be made a method of `Root`?
+func Lookup(r *Root, path string) (FSNode, error) {
+ dir := r.GetDirectory()
+
+ return DirLookup(dir, path)
+}
+
+// DirLookup will look up a file or directory at the given path
+// under the directory 'd'
+func DirLookup(d *Directory, pth string) (FSNode, error) {
+ pth = strings.Trim(pth, "/")
+ parts := path.SplitList(pth)
+ if len(parts) == 1 && parts[0] == "" {
+ return d, nil
+ }
+
+ var cur FSNode
+ cur = d
+ for i, p := range parts {
+ chdir, ok := cur.(*Directory)
+ if !ok {
+ return nil, fmt.Errorf("cannot access %s: Not a directory", path.Join(parts[:i+1]))
+ }
+
+ child, err := chdir.Child(p)
+ if err != nil {
+ return nil, err
+ }
+
+ cur = child
+ }
+ return cur, nil
+}
+
+// TODO: Document this function and link its functionality
+// with the republisher.
+func FlushPath(ctx context.Context, rt *Root, pth string) (ipld.Node, error) {
+ nd, err := Lookup(rt, pth)
+ if err != nil {
+ return nil, err
+ }
+
+ err = nd.Flush()
+ if err != nil {
+ return nil, err
+ }
+
+ rt.repub.WaitPub(ctx)
+ return nd.GetNode()
+}
diff --git a/mfs/options.go b/mfs/options.go
new file mode 100644
index 0000000000..6bdcd71006
--- /dev/null
+++ b/mfs/options.go
@@ -0,0 +1,7 @@
+package mfs
+
+type Flags struct {
+ Read bool
+ Write bool
+ Sync bool
+}
diff --git a/mfs/repub.go b/mfs/repub.go
new file mode 100644
index 0000000000..463810414c
--- /dev/null
+++ b/mfs/repub.go
@@ -0,0 +1,197 @@
+package mfs
+
+import (
+ "context"
+ "time"
+
+ cid "github.com/ipfs/go-cid"
+)
+
+// PubFunc is the user-defined function that determines exactly what
+// logic entails "publishing" a `Cid` value.
+type PubFunc func(context.Context, cid.Cid) error
+
+// Republisher manages when to publish a given entry.
+type Republisher struct {
+ TimeoutLong time.Duration
+ TimeoutShort time.Duration
+ RetryTimeout time.Duration
+ pubfunc PubFunc
+
+ update chan cid.Cid
+ immediatePublish chan chan struct{}
+
+ ctx context.Context
+ cancel func()
+}
+
+// NewRepublisher creates a new Republisher object to republish the given root
+// using the given short and long time intervals.
+func NewRepublisher(ctx context.Context, pf PubFunc, tshort, tlong time.Duration) *Republisher {
+ ctx, cancel := context.WithCancel(ctx)
+ return &Republisher{
+ TimeoutShort: tshort,
+ TimeoutLong: tlong,
+ RetryTimeout: tlong,
+ update: make(chan cid.Cid, 1),
+ pubfunc: pf,
+ immediatePublish: make(chan chan struct{}),
+ ctx: ctx,
+ cancel: cancel,
+ }
+}
+
+// WaitPub waits for the current value to be published (or returns early
+// if it already has).
+func (rp *Republisher) WaitPub(ctx context.Context) error {
+ wait := make(chan struct{})
+ select {
+ case rp.immediatePublish <- wait:
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ select {
+ case <-wait:
+ return nil
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+}
+
+func (rp *Republisher) Close() error {
+ // TODO(steb): Wait for `Run` to stop
+ err := rp.WaitPub(rp.ctx)
+ rp.cancel()
+ return err
+}
+
+// Update the current value. The value will be published after a delay but each
+// consecutive call to Update may extend this delay up to TimeoutLong.
+func (rp *Republisher) Update(c cid.Cid) {
+ select {
+ case <-rp.update:
+ select {
+ case rp.update <- c:
+ default:
+ // Don't try again. If we hit this case, there's a
+ // concurrent publish and we can safely let that
+ // concurrent publish win.
+ }
+ case rp.update <- c:
+ }
+}
+
+// Run contains the core logic of the `Republisher`. It calls the user-defined
+// `pubfunc` function whenever the `Cid` value is updated to a *new* value. The
+// complexity comes from the fact that `pubfunc` may be slow so we need to batch
+// updates.
+//
+// Algorithm:
+// 1. When we receive the first update after publishing, we set a `longer` timer.
+// 2. When we receive any update, we reset the `quick` timer.
+// 3. If either the `quick` timeout or the `longer` timeout elapses,
+// we call `publish` with the latest updated value.
+//
+// The `longer` timer ensures that we delay publishing by at most
+// `TimeoutLong`. The `quick` timer allows us to publish sooner if
+// it looks like there are no more updates coming down the pipe.
+//
+// Note: If a publish fails, we retry repeatedly every TimeoutRetry.
+func (rp *Republisher) Run(lastPublished cid.Cid) {
+ quick := time.NewTimer(0)
+ if !quick.Stop() {
+ <-quick.C
+ }
+ longer := time.NewTimer(0)
+ if !longer.Stop() {
+ <-longer.C
+ }
+
+ var toPublish cid.Cid
+ for rp.ctx.Err() == nil {
+ var waiter chan struct{}
+
+ select {
+ case <-rp.ctx.Done():
+ return
+ case newValue := <-rp.update:
+ // Skip already published values.
+ if lastPublished.Equals(newValue) {
+ // Break to the end of the switch to cleanup any
+ // timers.
+ toPublish = cid.Undef
+ break
+ }
+
+ // If we aren't already waiting to publish something,
+ // reset the long timeout.
+ if !toPublish.Defined() {
+ longer.Reset(rp.TimeoutLong)
+ }
+
+ // Always reset the short timeout.
+ quick.Reset(rp.TimeoutShort)
+
+ // Finally, set the new value to publish.
+ toPublish = newValue
+ continue
+ case waiter = <-rp.immediatePublish:
+ // Make sure to grab the *latest* value to publish.
+ select {
+ case toPublish = <-rp.update:
+ default:
+ }
+
+ // Avoid publishing duplicate values
+ if lastPublished.Equals(toPublish) {
+ toPublish = cid.Undef
+ }
+ case <-quick.C:
+ case <-longer.C:
+ }
+
+ // Cleanup, publish, and close waiters.
+
+ // 1. Stop any timers. Don't use the `if !t.Stop() { ... }`
+ // idiom as these timers may not be running.
+
+ quick.Stop()
+ select {
+ case <-quick.C:
+ default:
+ }
+
+ longer.Stop()
+ select {
+ case <-longer.C:
+ default:
+ }
+
+ // 2. If we have a value to publish, publish it now.
+ if toPublish.Defined() {
+ for {
+ err := rp.pubfunc(rp.ctx, toPublish)
+ if err == nil {
+ break
+ }
+ // Keep retrying until we succeed or we abort.
+ // TODO(steb): We could try pulling new values
+ // off `update` but that's not critical (and
+ // complicates this code a bit). We'll pull off
+ // a new value on the next loop through.
+ select {
+ case <-time.After(rp.RetryTimeout):
+ case <-rp.ctx.Done():
+ return
+ }
+ }
+ lastPublished = toPublish
+ toPublish = cid.Undef
+ }
+
+ // 3. Trigger anything waiting in `WaitPub`.
+ if waiter != nil {
+ close(waiter)
+ }
+ }
+}
diff --git a/mfs/repub_test.go b/mfs/repub_test.go
new file mode 100644
index 0000000000..6be5624ab1
--- /dev/null
+++ b/mfs/repub_test.go
@@ -0,0 +1,75 @@
+package mfs
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ cid "github.com/ipfs/go-cid"
+ ci "github.com/libp2p/go-libp2p-testing/ci"
+)
+
+func TestRepublisher(t *testing.T) {
+ if ci.IsRunning() {
+ t.Skip("dont run timing tests in CI")
+ }
+
+ ctx := context.TODO()
+
+ pub := make(chan struct{})
+
+ pf := func(ctx context.Context, c cid.Cid) error {
+ pub <- struct{}{}
+ return nil
+ }
+
+ testCid1, _ := cid.Parse("QmeomffUNfmQy76CQGy9NdmqEnnHU9soCexBnGU3ezPHVH")
+ testCid2, _ := cid.Parse("QmeomffUNfmQy76CQGy9NdmqEnnHU9soCexBnGU3ezPHVX")
+
+ tshort := time.Millisecond * 50
+ tlong := time.Second / 2
+
+ rp := NewRepublisher(ctx, pf, tshort, tlong)
+ go rp.Run(cid.Undef)
+
+ rp.Update(testCid1)
+
+ // should hit short timeout
+ select {
+ case <-time.After(tshort * 2):
+ t.Fatal("publish didnt happen in time")
+ case <-pub:
+ }
+
+ cctx, cancel := context.WithCancel(context.Background())
+
+ go func() {
+ for {
+ rp.Update(testCid2)
+ time.Sleep(time.Millisecond * 10)
+ select {
+ case <-cctx.Done():
+ return
+ default:
+ }
+ }
+ }()
+
+ select {
+ case <-pub:
+ t.Fatal("shouldnt have received publish yet!")
+ case <-time.After((tlong * 9) / 10):
+ }
+ select {
+ case <-pub:
+ case <-time.After(tlong / 2):
+ t.Fatal("waited too long for pub!")
+ }
+
+ cancel()
+
+ err := rp.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/mfs/root.go b/mfs/root.go
new file mode 100644
index 0000000000..96706a14f9
--- /dev/null
+++ b/mfs/root.go
@@ -0,0 +1,218 @@
+// package mfs implements an in memory model of a mutable IPFS filesystem.
+// TODO: Develop on this line (and move it to `doc.go`).
+
+package mfs
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ ft "github.com/ipfs/boxo/unixfs"
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+
+ ipld "github.com/ipfs/go-ipld-format"
+ logging "github.com/ipfs/go-log"
+)
+
+// TODO: Remove if not used.
+var ErrNotExist = errors.New("no such rootfs")
+var ErrClosed = errors.New("file closed")
+
+var log = logging.Logger("mfs")
+
+// TODO: Remove if not used.
+var ErrIsDirectory = errors.New("error: is a directory")
+
+// The information that an MFS `Directory` has about its children
+// when updating one of its entries: when a child mutates it signals
+// its parent directory to update its entry (under `Name`) with the
+// new content (in `Node`).
+type child struct {
+ Name string
+ Node ipld.Node
+}
+
+// This interface represents the basic property of MFS directories of updating
+// children entries with modified content. Implemented by both the MFS
+// `Directory` and `Root` (which is basically a `Directory` with republishing
+// support).
+//
+// TODO: What is `fullsync`? (unnamed `bool` argument)
+// TODO: There are two types of persistence/flush that need to be
+// distinguished here, one at the DAG level (when I store the modified
+// nodes in the DAG service) and one in the UnixFS/MFS level (when I modify
+// the entry/link of the directory that pointed to the modified node).
+type parent interface {
+ // Method called by a child to its parent to signal to update the content
+ // pointed to in the entry by that child's name. The child sends its own
+ // information in the `child` structure. As modifying a directory entry
+ // entails modifying its contents the parent will also call *its* parent's
+ // `updateChildEntry` to update the entry pointing to the new directory,
+ // this mechanism is in turn repeated until reaching the `Root`.
+ updateChildEntry(c child) error
+}
+
+type NodeType int
+
+const (
+ TFile NodeType = iota
+ TDir
+)
+
+// FSNode abstracts the `Directory` and `File` structures, it represents
+// any child node in the MFS (i.e., all the nodes besides the `Root`). It
+// is the counterpart of the `parent` interface which represents any
+// parent node in the MFS (`Root` and `Directory`).
+// (Not to be confused with the `unixfs.FSNode`.)
+type FSNode interface {
+ GetNode() (ipld.Node, error)
+
+ Flush() error
+ Type() NodeType
+}
+
+// IsDir checks whether the FSNode is dir type
+func IsDir(fsn FSNode) bool {
+ return fsn.Type() == TDir
+}
+
+// IsFile checks whether the FSNode is file type
+func IsFile(fsn FSNode) bool {
+ return fsn.Type() == TFile
+}
+
+// Root represents the root of a filesystem tree.
+type Root struct {
+
+ // Root directory of the MFS layout.
+ dir *Directory
+
+ repub *Republisher
+}
+
+// NewRoot creates a new Root and starts up a republisher routine for it.
+func NewRoot(parent context.Context, ds ipld.DAGService, node *dag.ProtoNode, pf PubFunc) (*Root, error) {
+
+ var repub *Republisher
+ if pf != nil {
+ repub = NewRepublisher(parent, pf, time.Millisecond*300, time.Second*3)
+
+ // No need to take the lock here since we just created
+ // the `Republisher` and no one has access to it yet.
+
+ go repub.Run(node.Cid())
+ }
+
+ root := &Root{
+ repub: repub,
+ }
+
+ fsn, err := ft.FSNodeFromBytes(node.Data())
+ if err != nil {
+ log.Error("IPNS pointer was not unixfs node")
+ // TODO: IPNS pointer?
+ return nil, err
+ }
+
+ switch fsn.Type() {
+ case ft.TDirectory, ft.THAMTShard:
+ newDir, err := NewDirectory(parent, node.String(), node, root, ds)
+ if err != nil {
+ return nil, err
+ }
+
+ root.dir = newDir
+ case ft.TFile, ft.TMetadata, ft.TRaw:
+ return nil, fmt.Errorf("root can't be a file (unixfs type: %s)", fsn.Type())
+ // TODO: This special error reporting case doesn't seem worth it, we either
+ // have a UnixFS directory or we don't.
+ default:
+ return nil, fmt.Errorf("unrecognized unixfs type: %s", fsn.Type())
+ }
+ return root, nil
+}
+
+// GetDirectory returns the root directory.
+func (kr *Root) GetDirectory() *Directory {
+ return kr.dir
+}
+
+// Flush signals that an update has occurred since the last publish,
+// and updates the Root republisher.
+// TODO: We are definitely abusing the "flush" terminology here.
+func (kr *Root) Flush() error {
+ nd, err := kr.GetDirectory().GetNode()
+ if err != nil {
+ return err
+ }
+
+ if kr.repub != nil {
+ kr.repub.Update(nd.Cid())
+ }
+ return nil
+}
+
+// FlushMemFree flushes the root directory and then uncaches all of its links.
+// This has the effect of clearing out potentially stale references and allows
+// them to be garbage collected.
+// CAUTION: Take care not to ever call this while holding a reference to any
+// child directories. Those directories will be bad references and using them
+// may have unintended racy side effects.
+// A better implemented mfs system (one that does smarter internal caching and
+// refcounting) shouldnt need this method.
+// TODO: Review the motivation behind this method once the cache system is
+// refactored.
+func (kr *Root) FlushMemFree(ctx context.Context) error {
+ dir := kr.GetDirectory()
+
+ if err := dir.Flush(); err != nil {
+ return err
+ }
+
+ dir.lock.Lock()
+ defer dir.lock.Unlock()
+
+ for name := range dir.entriesCache {
+ delete(dir.entriesCache, name)
+ }
+ // TODO: Can't we just create new maps?
+
+ return nil
+}
+
+// updateChildEntry implements the `parent` interface, and signals
+// to the publisher that there are changes ready to be published.
+// This is the only thing that separates a `Root` from a `Directory`.
+// TODO: Evaluate merging both.
+// TODO: The `sync` argument isn't used here (we've already reached
+// the top), document it and maybe make it an anonymous variable (if
+// that's possible).
+func (kr *Root) updateChildEntry(c child) error {
+ err := kr.GetDirectory().dagService.Add(context.TODO(), c.Node)
+ if err != nil {
+ return err
+ }
+ // TODO: Why are we not using the inner directory lock nor
+ // applying the same procedure as `Directory.updateChildEntry`?
+
+ if kr.repub != nil {
+ kr.repub.Update(c.Node.Cid())
+ }
+ return nil
+}
+
+func (kr *Root) Close() error {
+ nd, err := kr.GetDirectory().GetNode()
+ if err != nil {
+ return err
+ }
+
+ if kr.repub != nil {
+ kr.repub.Update(nd.Cid())
+ return kr.repub.Close()
+ }
+
+ return nil
+}
diff --git a/namesys/base.go b/namesys/base.go
new file mode 100644
index 0000000000..06b24bedce
--- /dev/null
+++ b/namesys/base.go
@@ -0,0 +1,126 @@
+package namesys
+
+import (
+ "context"
+ "strings"
+ "time"
+
+ opts "github.com/ipfs/boxo/coreiface/options/namesys"
+ path "github.com/ipfs/boxo/path"
+)
+
+type onceResult struct {
+ value path.Path
+ ttl time.Duration
+ err error
+}
+
+type resolver interface {
+ resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult
+}
+
+// resolve is a helper for implementing Resolver.ResolveN using resolveOnce.
+func resolve(ctx context.Context, r resolver, name string, options opts.ResolveOpts) (path.Path, error) {
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ err := ErrResolveFailed
+ var p path.Path
+
+ resCh := resolveAsync(ctx, r, name, options)
+
+ for res := range resCh {
+ p, err = res.Path, res.Err
+ if err != nil {
+ break
+ }
+ }
+
+ return p, err
+}
+
+func resolveAsync(ctx context.Context, r resolver, name string, options opts.ResolveOpts) <-chan Result {
+ ctx, span := StartSpan(ctx, "ResolveAsync")
+ defer span.End()
+
+ resCh := r.resolveOnceAsync(ctx, name, options)
+ depth := options.Depth
+ outCh := make(chan Result, 1)
+
+ go func() {
+ defer close(outCh)
+ ctx, span := StartSpan(ctx, "ResolveAsync.Worker")
+ defer span.End()
+
+ var subCh <-chan Result
+ var cancelSub context.CancelFunc
+ defer func() {
+ if cancelSub != nil {
+ cancelSub()
+ }
+ }()
+
+ for {
+ select {
+ case res, ok := <-resCh:
+ if !ok {
+ resCh = nil
+ break
+ }
+
+ if res.err != nil {
+ emitResult(ctx, outCh, Result{Err: res.err})
+ return
+ }
+ log.Debugf("resolved %s to %s", name, res.value.String())
+ if !strings.HasPrefix(res.value.String(), ipnsPrefix) {
+ emitResult(ctx, outCh, Result{Path: res.value})
+ break
+ }
+
+ if depth == 1 {
+ emitResult(ctx, outCh, Result{Path: res.value, Err: ErrResolveRecursion})
+ break
+ }
+
+ subopts := options
+ if subopts.Depth > 1 {
+ subopts.Depth--
+ }
+
+ var subCtx context.Context
+ if cancelSub != nil {
+ // Cancel previous recursive resolve since it won't be used anyways
+ cancelSub()
+ }
+ subCtx, cancelSub = context.WithCancel(ctx)
+ _ = cancelSub
+
+ p := strings.TrimPrefix(res.value.String(), ipnsPrefix)
+ subCh = resolveAsync(subCtx, r, p, subopts)
+ case res, ok := <-subCh:
+ if !ok {
+ subCh = nil
+ break
+ }
+
+ // We don't bother returning here in case of context timeout as there is
+ // no good reason to do that, and we may still be able to emit a result
+ emitResult(ctx, outCh, res)
+ case <-ctx.Done():
+ return
+ }
+ if resCh == nil && subCh == nil {
+ return
+ }
+ }
+ }()
+ return outCh
+}
+
+func emitResult(ctx context.Context, outCh chan<- Result, r Result) {
+ select {
+ case outCh <- r:
+ case <-ctx.Done():
+ }
+}
diff --git a/namesys/cache.go b/namesys/cache.go
new file mode 100644
index 0000000000..8b7f50794c
--- /dev/null
+++ b/namesys/cache.go
@@ -0,0 +1,62 @@
+package namesys
+
+import (
+ "time"
+
+ path "github.com/ipfs/boxo/path"
+)
+
+func (ns *mpns) cacheGet(name string) (path.Path, bool) {
+ // existence of optional mapping defined via IPFS_NS_MAP is checked first
+ if ns.staticMap != nil {
+ val, ok := ns.staticMap[name]
+ if ok {
+ return val, true
+ }
+ }
+
+ if ns.cache == nil {
+ return "", false
+ }
+
+ ientry, ok := ns.cache.Get(name)
+ if !ok {
+ return "", false
+ }
+
+ entry, ok := ientry.(cacheEntry)
+ if !ok {
+ // should never happen, purely for sanity
+ log.Panicf("unexpected type %T in cache for %q.", ientry, name)
+ }
+
+ if time.Now().Before(entry.eol) {
+ return entry.val, true
+ }
+
+ ns.cache.Remove(name)
+
+ return "", false
+}
+
+func (ns *mpns) cacheSet(name string, val path.Path, ttl time.Duration) {
+ if ns.cache == nil || ttl <= 0 {
+ return
+ }
+ ns.cache.Add(name, cacheEntry{
+ val: val,
+ eol: time.Now().Add(ttl),
+ })
+}
+
+func (ns *mpns) cacheInvalidate(name string) {
+ if ns.cache == nil {
+ return
+ }
+ ns.cache.Remove(name)
+}
+
+type cacheEntry struct {
+ val path.Path
+ eol time.Time
+}
diff --git a/namesys/dns.go b/namesys/dns.go
new file mode 100644
index 0000000000..6f846fcdac
--- /dev/null
+++ b/namesys/dns.go
@@ -0,0 +1,195 @@
+package namesys
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net"
+ gpath "path"
+ "strings"
+
+ opts "github.com/ipfs/boxo/coreiface/options/namesys"
+ path "github.com/ipfs/boxo/path"
+ dns "github.com/miekg/dns"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
+)
+
+// LookupTXTFunc is a function that lookups TXT record values.
+type LookupTXTFunc func(ctx context.Context, name string) (txt []string, err error)
+
+// DNSResolver implements a Resolver on DNS domains
+type DNSResolver struct {
+ lookupTXT LookupTXTFunc
+ // TODO: maybe some sort of caching?
+ // cache would need a timeout
+}
+
+// NewDNSResolver constructs a name resolver using DNS TXT records.
+func NewDNSResolver(lookup LookupTXTFunc) *DNSResolver {
+ return &DNSResolver{lookupTXT: lookup}
+}
+
+// Resolve implements Resolver.
+func (r *DNSResolver) Resolve(ctx context.Context, name string, options ...opts.ResolveOpt) (path.Path, error) {
+ ctx, span := StartSpan(ctx, "DNSResolver.Resolve")
+ defer span.End()
+
+ return resolve(ctx, r, name, opts.ProcessOpts(options))
+}
+
+// ResolveAsync implements Resolver.
+func (r *DNSResolver) ResolveAsync(ctx context.Context, name string, options ...opts.ResolveOpt) <-chan Result {
+ ctx, span := StartSpan(ctx, "DNSResolver.ResolveAsync")
+ defer span.End()
+
+ return resolveAsync(ctx, r, name, opts.ProcessOpts(options))
+}
+
+type lookupRes struct {
+ path path.Path
+ error error
+}
+
+// resolveOnce implements resolver.
+// TXT records for a given domain name should contain a b58
+// encoded multihash.
+func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult {
+ ctx, span := StartSpan(ctx, "DNSResolver.ResolveOnceAsync")
+ defer span.End()
+
+ var fqdn string
+ out := make(chan onceResult, 1)
+ segments := strings.SplitN(name, "/", 2)
+ domain := segments[0]
+
+ if _, ok := dns.IsDomainName(domain); !ok {
+ out <- onceResult{err: fmt.Errorf("not a valid domain name: %s", domain)}
+ close(out)
+ return out
+ }
+ log.Debugf("DNSResolver resolving %s", domain)
+
+ if strings.HasSuffix(domain, ".") {
+ fqdn = domain
+ } else {
+ fqdn = domain + "."
+ }
+
+ rootChan := make(chan lookupRes, 1)
+ go workDomain(ctx, r, fqdn, rootChan)
+
+ subChan := make(chan lookupRes, 1)
+ go workDomain(ctx, r, "_dnslink."+fqdn, subChan)
+
+ appendPath := func(p path.Path) (path.Path, error) {
+ if len(segments) > 1 {
+ return path.FromSegments("", strings.TrimRight(p.String(), "/"), segments[1])
+ }
+ return p, nil
+ }
+
+ go func() {
+ defer close(out)
+ ctx, span := StartSpan(ctx, "DNSResolver.ResolveOnceAsync.Worker")
+ defer span.End()
+
+ var rootResErr, subResErr error
+ for {
+ select {
+ case subRes, ok := <-subChan:
+ if !ok {
+ subChan = nil
+ break
+ }
+ if subRes.error == nil {
+ p, err := appendPath(subRes.path)
+ emitOnceResult(ctx, out, onceResult{value: p, err: err})
+ // Return without waiting for rootRes, since this result
+ // (for "_dnslink."+fqdn) takes precedence
+ return
+ }
+ subResErr = subRes.error
+ case rootRes, ok := <-rootChan:
+ if !ok {
+ rootChan = nil
+ break
+ }
+ if rootRes.error == nil {
+ p, err := appendPath(rootRes.path)
+ emitOnceResult(ctx, out, onceResult{value: p, err: err})
+ // Do not return here. Wait for subRes so that it is
+ // output last if good, thereby giving subRes precedence.
+ } else {
+ rootResErr = rootRes.error
+ }
+ case <-ctx.Done():
+ return
+ }
+ if subChan == nil && rootChan == nil {
+ // If here, then both lookups are done
+ //
+ // If both lookups failed due to no TXT records with a
+ // dnslink, then output a more specific error message
+ if rootResErr == ErrResolveFailed && subResErr == ErrResolveFailed {
+ // Wrap error so that it can be tested if it is a ErrResolveFailed
+ err := fmt.Errorf("%w: _dnslink subdomain at %q is missing a TXT record (https://docs.ipfs.tech/concepts/dnslink/)", ErrResolveFailed, gpath.Base(name))
+ emitOnceResult(ctx, out, onceResult{err: err})
+ }
+ return
+ }
+ }
+ }()
+
+ return out
+}
+
+func workDomain(ctx context.Context, r *DNSResolver, name string, res chan lookupRes) {
+ ctx, span := StartSpan(ctx, "DNSResolver.WorkDomain", trace.WithAttributes(attribute.String("Name", name)))
+ defer span.End()
+
+ defer close(res)
+
+ txt, err := r.lookupTXT(ctx, name)
+ if err != nil {
+ if dnsErr, ok := err.(*net.DNSError); ok {
+ // If no TXT records found, return same error as when no text
+ // records contain dnslink. Otherwise, return the actual error.
+ if dnsErr.IsNotFound {
+ err = ErrResolveFailed
+ }
+ }
+ // Could not look up any text records for name
+ res <- lookupRes{"", err}
+ return
+ }
+
+ for _, t := range txt {
+ p, err := parseEntry(t)
+ if err == nil {
+ res <- lookupRes{p, nil}
+ return
+ }
+ }
+
+ // There were no TXT records with a dnslink
+ res <- lookupRes{"", ErrResolveFailed}
+}
+
+func parseEntry(txt string) (path.Path, error) {
+ p, err := path.ParseCidToPath(txt) // bare IPFS multihashes
+ if err == nil {
+ return p, nil
+ }
+
+ return tryParseDNSLink(txt)
+}
+
+func tryParseDNSLink(txt string) (path.Path, error) {
+ parts := strings.SplitN(txt, "=", 2)
+ if len(parts) == 2 && parts[0] == "dnslink" {
+ return path.ParsePath(parts[1])
+ }
+
+ return "", errors.New("not a valid dnslink entry")
+}
diff --git a/namesys/dns_test.go b/namesys/dns_test.go
new file mode 100644
index 0000000000..a31a53582b
--- /dev/null
+++ b/namesys/dns_test.go
@@ -0,0 +1,184 @@
+package namesys
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ opts "github.com/ipfs/boxo/coreiface/options/namesys"
+)
+
+type mockDNS struct {
+ entries map[string][]string
+}
+
+func (m *mockDNS) lookupTXT(ctx context.Context, name string) (txt []string, err error) {
+ txt, ok := m.entries[name]
+ if !ok {
+ return nil, fmt.Errorf("no TXT entry for %s", name)
+ }
+ return txt, nil
+}
+
+func TestDnsEntryParsing(t *testing.T) {
+ goodEntries := []string{
+ "QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD",
+ "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD",
+ "dnslink=/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD",
+ "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/foo",
+ "dnslink=/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/bar",
+ "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/foo/bar/baz",
+ "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/foo/bar/baz/",
+ "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD",
+ }
+
+ badEntries := []string{
+ "QmYhE8xgFCjGcz6PHgnvJz5NOTCORRECT",
+ "quux=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD",
+ "dnslink=",
+ "dnslink=/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/foo",
+ "dnslink=ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/bar",
+ }
+
+ for _, e := range goodEntries {
+ _, err := parseEntry(e)
+ if err != nil {
+ t.Log("expected entry to parse correctly!")
+ t.Log(e)
+ t.Fatal(err)
+ }
+ }
+
+ for _, e := range badEntries {
+ _, err := parseEntry(e)
+ if err == nil {
+ t.Log("expected entry parse to fail!")
+ t.Fatal(err)
+ }
+ }
+}
+
+func newMockDNS() *mockDNS {
+ return &mockDNS{
+ entries: map[string][]string{
+ "multihash.example.com.": {
+ "dnslink=QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD",
+ },
+ "ipfs.example.com.": {
+ "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD",
+ },
+ "_dnslink.dipfs.example.com.": {
+ "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD",
+ },
+ "dns1.example.com.": {
+ "dnslink=/ipns/ipfs.example.com",
+ },
+ "dns2.example.com.": {
+ "dnslink=/ipns/dns1.example.com",
+ },
+ "multi.example.com.": {
+ "some stuff",
+ "dnslink=/ipns/dns1.example.com",
+ "masked dnslink=/ipns/example.invalid",
+ },
+ "equals.example.com.": {
+ "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/=equals",
+ },
+ "loop1.example.com.": {
+ "dnslink=/ipns/loop2.example.com",
+ },
+ "loop2.example.com.": {
+ "dnslink=/ipns/loop1.example.com",
+ },
+ "_dnslink.dloop1.example.com.": {
+ "dnslink=/ipns/loop2.example.com",
+ },
+ "_dnslink.dloop2.example.com.": {
+ "dnslink=/ipns/loop1.example.com",
+ },
+ "bad.example.com.": {
+ "dnslink=",
+ },
+ "withsegment.example.com.": {
+ "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment",
+ },
+ "withrecsegment.example.com.": {
+ "dnslink=/ipns/withsegment.example.com/subsub",
+ },
+ "withtrailing.example.com.": {
+ "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/",
+ },
+ "withtrailingrec.example.com.": {
+ "dnslink=/ipns/withtrailing.example.com/segment/",
+ },
+ "double.example.com.": {
+ "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD",
+ },
+ "_dnslink.double.example.com.": {
+ "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD",
+ },
+ "double.conflict.com.": {
+ "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD",
+ },
+ "_dnslink.conflict.example.com.": {
+ "dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjE",
+ },
+ "fqdn.example.com.": {
+ "dnslink=/ipfs/QmYvMB9yrsSf7RKBghkfwmHJkzJhW2ZgVwq3LxBXXPasFr",
+ },
+ "en.wikipedia-on-ipfs.org.": {
+ "dnslink=/ipfs/bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze",
+ },
+ "custom.non-icann.tldextravaganza.": {
+ "dnslink=/ipfs/bafybeieto6mcuvqlechv4iadoqvnffondeiwxc2bcfcewhvpsd2odvbmvm",
+ },
+ "singlednslabelshouldbeok.": {
+ "dnslink=/ipfs/bafybeih4a6ylafdki6ailjrdvmr7o4fbbeceeeuty4v3qyyouiz5koqlpi",
+ },
+ "www.wealdtech.eth.": {
+ "dnslink=/ipns/ipfs.example.com",
+ },
+ },
+ }
+}
+
+func TestDNSResolution(t *testing.T) {
+ mock := newMockDNS()
+ r := &DNSResolver{lookupTXT: mock.lookupTXT}
+ testResolution(t, r, "multihash.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
+ testResolution(t, r, "ipfs.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
+ testResolution(t, r, "dipfs.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
+ testResolution(t, r, "dns1.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
+ testResolution(t, r, "dns1.example.com", 1, "/ipns/ipfs.example.com", ErrResolveRecursion)
+ testResolution(t, r, "dns2.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
+ testResolution(t, r, "dns2.example.com", 1, "/ipns/dns1.example.com", ErrResolveRecursion)
+ testResolution(t, r, "dns2.example.com", 2, "/ipns/ipfs.example.com", ErrResolveRecursion)
+ testResolution(t, r, "multi.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
+ testResolution(t, r, "multi.example.com", 1, "/ipns/dns1.example.com", ErrResolveRecursion)
+ testResolution(t, r, "multi.example.com", 2, "/ipns/ipfs.example.com", ErrResolveRecursion)
+ testResolution(t, r, "equals.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/=equals", nil)
+ testResolution(t, r, "loop1.example.com", 1, "/ipns/loop2.example.com", ErrResolveRecursion)
+ testResolution(t, r, "loop1.example.com", 2, "/ipns/loop1.example.com", ErrResolveRecursion)
+ testResolution(t, r, "loop1.example.com", 3, "/ipns/loop2.example.com", ErrResolveRecursion)
+ testResolution(t, r, "loop1.example.com", opts.DefaultDepthLimit, "/ipns/loop1.example.com", ErrResolveRecursion)
+ testResolution(t, r, "dloop1.example.com", 1, "/ipns/loop2.example.com", ErrResolveRecursion)
+ testResolution(t, r, "dloop1.example.com", 2, "/ipns/loop1.example.com", ErrResolveRecursion)
+ testResolution(t, r, "dloop1.example.com", 3, "/ipns/loop2.example.com", ErrResolveRecursion)
+ testResolution(t, r, "dloop1.example.com", opts.DefaultDepthLimit, "/ipns/loop1.example.com", ErrResolveRecursion)
+ testResolution(t, r, "bad.example.com", opts.DefaultDepthLimit, "", ErrResolveFailed)
+ testResolution(t, r, "withsegment.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment", nil)
+ testResolution(t, r, "withrecsegment.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment/subsub", nil)
+ testResolution(t, r, "withsegment.example.com/test1", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment/test1", nil)
+ testResolution(t, r, "withrecsegment.example.com/test2", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment/subsub/test2", nil)
+ testResolution(t, r, "withrecsegment.example.com/test3/", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment/subsub/test3/", nil)
+ testResolution(t, r, "withtrailingrec.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment/", nil)
+ testResolution(t, r, "double.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
+ testResolution(t, r, "conflict.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjE", nil)
+ testResolution(t, r, "fqdn.example.com.", opts.DefaultDepthLimit, "/ipfs/QmYvMB9yrsSf7RKBghkfwmHJkzJhW2ZgVwq3LxBXXPasFr", nil)
+ testResolution(t, r, "en.wikipedia-on-ipfs.org", 2, "/ipfs/bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze", nil)
+ testResolution(t, r, "custom.non-icann.tldextravaganza.", 2, "/ipfs/bafybeieto6mcuvqlechv4iadoqvnffondeiwxc2bcfcewhvpsd2odvbmvm", nil)
+ testResolution(t, r, "singlednslabelshouldbeok", 2, "/ipfs/bafybeih4a6ylafdki6ailjrdvmr7o4fbbeceeeuty4v3qyyouiz5koqlpi", nil)
+ testResolution(t, r, "www.wealdtech.eth", 1, "/ipns/ipfs.example.com", ErrResolveRecursion)
+ testResolution(t, r, "www.wealdtech.eth", 2, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
+ testResolution(t, r, "www.wealdtech.eth", 2, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
+}
diff --git a/namesys/interface.go b/namesys/interface.go
new file mode 100644
index 0000000000..655be179db
--- /dev/null
+++ b/namesys/interface.go
@@ -0,0 +1,100 @@
+/*
+Package namesys implements resolvers and publishers for the IPFS
+naming system (IPNS).
+
+The core of IPFS is an immutable, content-addressable Merkle graph.
+That works well for many use cases, but doesn't allow you to answer
+questions like "what is Alice's current homepage?". The mutable name
+system allows Alice to publish information like:
+
+ The current homepage for alice.example.com is
+ /ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj
+
+or:
+
+ The current homepage for node
+ QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
+ is
+ /ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj
+
+The mutable name system also allows users to resolve those references
+to find the immutable IPFS object currently referenced by a given
+mutable name.
+
+For command-line bindings to this functionality, see:
+
+ ipfs name
+ ipfs dns
+ ipfs resolve
+*/
+package namesys
+
+import (
+ "errors"
+
+ "context"
+
+ opts "github.com/ipfs/boxo/coreiface/options/namesys"
+ "github.com/ipfs/boxo/path"
+ ci "github.com/libp2p/go-libp2p/core/crypto"
+)
+
+// ErrResolveFailed signals an error when attempting to resolve.
+var ErrResolveFailed = errors.New("could not resolve name")
+
+// ErrResolveRecursion signals a recursion-depth limit.
+var ErrResolveRecursion = errors.New(
+ "could not resolve name (recursion limit exceeded)")
+
+// ErrPublishFailed signals an error when attempting to publish.
+var ErrPublishFailed = errors.New("could not publish name")
+
+// NameSystem represents a cohesive name publishing and resolving system.
+//
+// Publishing a name is the process of establishing a mapping, a key-value
+// pair, according to naming rules and databases.
+//
+// Resolving a name is the process of looking up the value associated with the
+// key (name).
+type NameSystem interface {
+ Resolver
+ Publisher
+}
+
+// Result is the return type for Resolver.ResolveAsync.
+type Result struct {
+ Path path.Path
+ Err error
+}
+
+// Resolver is an object capable of resolving names.
+type Resolver interface {
+
+ // Resolve performs a recursive lookup, returning the dereferenced
+ // path. For example, if ipfs.io has a DNS TXT record pointing to
+ // /ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
+ // and there is a DHT IPNS entry for
+ // QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
+ // -> /ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj
+ // then
+ // Resolve(ctx, "/ipns/ipfs.io")
+ // will resolve both names, returning
+ // /ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj
+ //
+ // There is a default depth-limit to avoid infinite recursion. Most
+ // users will be fine with this default limit, but if you need to
+ // adjust the limit you can specify it as an option.
+ Resolve(ctx context.Context, name string, options ...opts.ResolveOpt) (value path.Path, err error)
+
+ // ResolveAsync performs recursive name lookup, like Resolve, but it returns
+ // entries as they are discovered in the DHT. Each returned result is guaranteed
+ // to be "better" (which usually means newer) than the previous one.
+ ResolveAsync(ctx context.Context, name string, options ...opts.ResolveOpt) <-chan Result
+}
+
+// Publisher is an object capable of publishing particular names.
+type Publisher interface {
+ // Publish establishes a name-value mapping.
+ // TODO make this not PrivKey specific.
+ Publish(ctx context.Context, name ci.PrivKey, value path.Path, options ...opts.PublishOption) error
+}
diff --git a/namesys/ipns_resolver_validation_test.go b/namesys/ipns_resolver_validation_test.go
new file mode 100644
index 0000000000..9799e5ba37
--- /dev/null
+++ b/namesys/ipns_resolver_validation_test.go
@@ -0,0 +1,209 @@
+package namesys
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ opts "github.com/ipfs/boxo/coreiface/options/namesys"
+ "github.com/ipfs/boxo/ipns"
+ ipns_pb "github.com/ipfs/boxo/ipns/pb"
+ "github.com/ipfs/boxo/path"
+ mockrouting "github.com/ipfs/boxo/routing/mock"
+ "github.com/ipfs/boxo/routing/offline"
+ ds "github.com/ipfs/go-datastore"
+ dssync "github.com/ipfs/go-datastore/sync"
+ record "github.com/libp2p/go-libp2p-record"
+ testutil "github.com/libp2p/go-libp2p-testing/net"
+ ci "github.com/libp2p/go-libp2p/core/crypto"
+ "github.com/libp2p/go-libp2p/core/peer"
+ pstore "github.com/libp2p/go-libp2p/core/peerstore"
+ "github.com/libp2p/go-libp2p/core/routing"
+ "github.com/libp2p/go-libp2p/core/test"
+ "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem"
+)
+
+func TestResolverValidation(t *testing.T) {
+ t.Run("RSA",
+ func(t *testing.T) {
+ testResolverValidation(t, ci.RSA)
+ })
+ t.Run("Ed25519",
+ func(t *testing.T) {
+ testResolverValidation(t, ci.Ed25519)
+ })
+ t.Run("ECDSA",
+ func(t *testing.T) {
+ testResolverValidation(t, ci.ECDSA)
+ })
+ t.Run("Secp256k1",
+ func(t *testing.T) {
+ testResolverValidation(t, ci.Secp256k1)
+ })
+}
+
+func testResolverValidation(t *testing.T, keyType int) {
+ ctx := context.Background()
+ rid := testutil.RandIdentityOrFatal(t)
+ dstore := dssync.MutexWrap(ds.NewMapDatastore())
+ peerstore, err := pstoremem.NewPeerstore()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ vstore := newMockValueStore(rid, dstore, peerstore)
+ resolver := NewIpnsResolver(vstore)
+
+ nvVstore := offline.NewOfflineRouter(dstore, mockrouting.MockValidator{})
+
+ // Create entry with expiry in one hour
+ priv, id, _, ipnsDHTPath := genKeys(t, keyType)
+ ts := time.Now()
+ p := []byte("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG")
+ entry, err := createIPNSRecordWithEmbeddedPublicKey(priv, p, 1, ts.Add(time.Hour), 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Publish entry
+ err = PublishEntry(ctx, vstore, ipnsDHTPath, entry)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Resolve entry
+ resp, err := resolve(ctx, resolver, id.Pretty(), opts.DefaultResolveOpts())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if resp != path.Path(p) {
+ t.Fatalf("Mismatch between published path %s and resolved path %s", p, resp)
+ }
+ // Create expired entry
+ expiredEntry, err := createIPNSRecordWithEmbeddedPublicKey(priv, p, 1, ts.Add(-1*time.Hour), 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Publish entry
+ err = PublishEntry(ctx, nvVstore, ipnsDHTPath, expiredEntry)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Record should fail validation because entry is expired
+ _, err = resolve(ctx, resolver, id.Pretty(), opts.DefaultResolveOpts())
+ if err == nil {
+ t.Fatal("ValidateIpnsRecord should have returned error")
+ }
+
+ // Create IPNS record path with a different private key
+ priv2, id2, _, ipnsDHTPath2 := genKeys(t, keyType)
+
+ // Publish entry
+ err = PublishEntry(ctx, nvVstore, ipnsDHTPath2, entry)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Record should fail validation because public key defined by
+ // ipns path doesn't match record signature
+ _, err = resolve(ctx, resolver, id2.Pretty(), opts.DefaultResolveOpts())
+ if err == nil {
+ t.Fatal("ValidateIpnsRecord should have failed signature verification")
+ }
+
+ // Try embedding the incorrect private key inside the entry
+ if err := ipns.EmbedPublicKey(priv2.GetPublic(), entry); err != nil {
+ t.Fatal(err)
+ }
+
+ // Publish entry
+ err = PublishEntry(ctx, nvVstore, ipnsDHTPath2, entry)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Record should fail validation because public key defined by
+ // ipns path doesn't match record signature
+ _, err = resolve(ctx, resolver, id2.Pretty(), opts.DefaultResolveOpts())
+ if err == nil {
+ t.Fatal("ValidateIpnsRecord should have failed signature verification")
+ }
+}
+
+func genKeys(t *testing.T, keyType int) (ci.PrivKey, peer.ID, string, string) {
+ bits := 0
+ if keyType == ci.RSA {
+ bits = 2048
+ }
+
+ sk, pk, err := test.RandTestKeyPair(keyType, bits)
+ if err != nil {
+ t.Fatal(err)
+ }
+ id, err := peer.IDFromPublicKey(pk)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return sk, id, PkKeyForID(id), ipns.RecordKey(id)
+}
+
+func createIPNSRecordWithEmbeddedPublicKey(sk ci.PrivKey, val []byte, seq uint64, eol time.Time, ttl time.Duration) (*ipns_pb.IpnsEntry, error) {
+ entry, err := ipns.Create(sk, val, seq, eol, ttl)
+ if err != nil {
+ return nil, err
+ }
+ if err := ipns.EmbedPublicKey(sk.GetPublic(), entry); err != nil {
+ return nil, err
+ }
+
+ return entry, nil
+}
+
+type mockValueStore struct {
+ r routing.ValueStore
+ kbook pstore.KeyBook
+}
+
+func newMockValueStore(id testutil.Identity, dstore ds.Datastore, kbook pstore.KeyBook) *mockValueStore {
+ return &mockValueStore{
+ r: offline.NewOfflineRouter(dstore, record.NamespacedValidator{
+ "ipns": ipns.Validator{KeyBook: kbook},
+ "pk": record.PublicKeyValidator{},
+ }),
+ kbook: kbook,
+ }
+}
+
+func (m *mockValueStore) GetValue(ctx context.Context, k string, opts ...routing.Option) ([]byte, error) {
+ return m.r.GetValue(ctx, k, opts...)
+}
+
+func (m *mockValueStore) SearchValue(ctx context.Context, k string, opts ...routing.Option) (<-chan []byte, error) {
+ return m.r.SearchValue(ctx, k, opts...)
+}
+
+func (m *mockValueStore) GetPublicKey(ctx context.Context, p peer.ID) (ci.PubKey, error) {
+ pk := m.kbook.PubKey(p)
+ if pk != nil {
+ return pk, nil
+ }
+
+ pkkey := routing.KeyForPublicKey(p)
+ val, err := m.GetValue(ctx, pkkey)
+ if err != nil {
+ return nil, err
+ }
+
+ pk, err = ci.UnmarshalPublicKey(val)
+ if err != nil {
+ return nil, err
+ }
+
+ return pk, m.kbook.AddPubKey(p, pk)
+}
+
+func (m *mockValueStore) PutValue(ctx context.Context, k string, d []byte, opts ...routing.Option) error {
+ return m.r.PutValue(ctx, k, d, opts...)
+}
diff --git a/namesys/namesys.go b/namesys/namesys.go
new file mode 100644
index 0000000000..e341b63d72
--- /dev/null
+++ b/namesys/namesys.go
@@ -0,0 +1,338 @@
+// Package namesys defines Resolver and Publisher interfaces for IPNS paths,
+// that is, IPFS paths in the form of /ipns/. A "resolved"
+// IPNS path becomes an /ipfs/ path.
+//
+// Traditionally, these paths would be in the form of /ipns/peer_id, which
+// references an IPNS record in a distributed ValueStore (usually the IPFS
+// DHT).
+//
+// Additionally, the /ipns/ namespace can also be used with domain names that
+// use DNSLink (/ipns/, https://docs.ipfs.io/concepts/dnslink/)
+//
+// The package provides implementations for all three resolvers.
+package namesys
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "strings"
+ "time"
+
+ lru "github.com/hashicorp/golang-lru"
+ iface "github.com/ipfs/boxo/coreiface"
+ opts "github.com/ipfs/boxo/coreiface/options/namesys"
+ "github.com/ipfs/boxo/path"
+ "github.com/ipfs/go-cid"
+ ds "github.com/ipfs/go-datastore"
+ dssync "github.com/ipfs/go-datastore/sync"
+ ci "github.com/libp2p/go-libp2p/core/crypto"
+ "github.com/libp2p/go-libp2p/core/peer"
+ "github.com/libp2p/go-libp2p/core/routing"
+ "github.com/miekg/dns"
+ madns "github.com/multiformats/go-multiaddr-dns"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
+)
+
+// mpns (a multi-protocol NameSystem) implements generic IPFS naming.
+//
+// Uses several Resolvers:
+// (a) IPFS routing naming: SFS-like PKI names.
+// (b) dns domains: resolves using links in DNS TXT records
+//
+// It can only publish to: (a) IPFS routing naming.
+type mpns struct {
+ ds ds.Datastore
+
+ dnsResolver, ipnsResolver resolver
+ ipnsPublisher Publisher
+
+ staticMap map[string]path.Path
+ cache *lru.Cache
+}
+
+type Option func(*mpns) error
+
+// WithCache is an option that instructs the name system to use a (LRU) cache of the given size.
+func WithCache(size int) Option {
+ return func(ns *mpns) error {
+ if size <= 0 {
+ return fmt.Errorf("invalid cache size %d; must be > 0", size)
+ }
+
+ cache, err := lru.New(size)
+ if err != nil {
+ return err
+ }
+
+ ns.cache = cache
+ return nil
+ }
+}
+
+// WithDNSResolver is an option that supplies a custom DNS resolver to use instead of the system
+// default.
+func WithDNSResolver(rslv madns.BasicResolver) Option {
+ return func(ns *mpns) error {
+ ns.dnsResolver = NewDNSResolver(rslv.LookupTXT)
+ return nil
+ }
+}
+
+// WithDatastore is an option that supplies a datastore to use instead of an in-memory map datastore. The datastore is used to store published IPNS records and make them available for querying.
+func WithDatastore(ds ds.Datastore) Option {
+ return func(ns *mpns) error {
+ ns.ds = ds
+ return nil
+ }
+}
+
+func loadStaticMap(list string) (map[string]path.Path, error) {
+ staticMap := make(map[string]path.Path)
+ for _, pair := range strings.Split(list, ",") {
+ mapping := strings.SplitN(pair, ":", 2)
+ key := mapping[0]
+ value := path.FromString(mapping[1])
+
+ ipnsKey, err := peer.Decode(key)
+ if err == nil {
+ key = iface.FormatKeyID(ipnsKey)
+ }
+
+ staticMap[key] = value
+ }
+ return staticMap, nil
+}
+
+// NewNameSystem will construct the IPFS naming system based on Routing
+func NewNameSystem(r routing.ValueStore, opts ...Option) (NameSystem, error) {
+ var staticMap map[string]path.Path
+
+ // Prewarm namesys cache with static records for deterministic tests and debugging.
+ // Useful for testing things like DNSLink without real DNS lookup.
+ // Example:
+ // IPFS_NS_MAP="dnslink-test.example.com:/ipfs/bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am"
+ if list := os.Getenv("IPFS_NS_MAP"); list != "" {
+ var err error
+ staticMap, err = loadStaticMap(list)
+
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ ns := &mpns{
+ staticMap: staticMap,
+ }
+
+ for _, opt := range opts {
+ err := opt(ns)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if ns.ds == nil {
+ ns.ds = dssync.MutexWrap(ds.NewMapDatastore())
+ }
+
+ if ns.dnsResolver == nil {
+ ns.dnsResolver = NewDNSResolver(madns.DefaultResolver.LookupTXT)
+ }
+
+ ns.ipnsResolver = NewIpnsResolver(r)
+ ns.ipnsPublisher = NewIpnsPublisher(r, ns.ds)
+
+ return ns, nil
+}
+
+// DefaultResolverCacheTTL defines max ttl of a record placed in namesys cache.
+const DefaultResolverCacheTTL = time.Minute
+
+// Resolve implements Resolver.
+func (ns *mpns) Resolve(ctx context.Context, name string, options ...opts.ResolveOpt) (path.Path, error) {
+ ctx, span := StartSpan(ctx, "MPNS.Resolve", trace.WithAttributes(attribute.String("Name", name)))
+ defer span.End()
+
+ if strings.HasPrefix(name, "/ipfs/") {
+ return path.ParsePath(name)
+ }
+
+ if !strings.HasPrefix(name, "/") {
+ return path.ParsePath("/ipfs/" + name)
+ }
+
+ return resolve(ctx, ns, name, opts.ProcessOpts(options))
+}
+
+func (ns *mpns) ResolveAsync(ctx context.Context, name string, options ...opts.ResolveOpt) <-chan Result {
+ ctx, span := StartSpan(ctx, "MPNS.ResolveAsync", trace.WithAttributes(attribute.String("Name", name)))
+ defer span.End()
+
+ if strings.HasPrefix(name, "/ipfs/") {
+ p, err := path.ParsePath(name)
+ res := make(chan Result, 1)
+ res <- Result{p, err}
+ close(res)
+ return res
+ }
+
+ if !strings.HasPrefix(name, "/") {
+ p, err := path.ParsePath("/ipfs/" + name)
+ res := make(chan Result, 1)
+ res <- Result{p, err}
+ close(res)
+ return res
+ }
+
+ return resolveAsync(ctx, ns, name, opts.ProcessOpts(options))
+}
+
+// resolveOnce implements resolver.
+func (ns *mpns) resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult {
+ ctx, span := StartSpan(ctx, "MPNS.ResolveOnceAsync")
+ defer span.End()
+
+ out := make(chan onceResult, 1)
+
+ if !strings.HasPrefix(name, ipnsPrefix) {
+ name = ipnsPrefix + name
+ }
+ segments := strings.SplitN(name, "/", 4)
+ if len(segments) < 3 || segments[0] != "" {
+ log.Debugf("invalid name syntax for %s", name)
+ out <- onceResult{err: ErrResolveFailed}
+ close(out)
+ return out
+ }
+
+ key := segments[2]
+
+ // Resolver selection:
+ // 1. if it is a PeerID/CID/multihash resolve through "ipns".
+ // 2. if it is a domain name, resolve through "dns"
+
+ var res resolver
+ ipnsKey, err := peer.Decode(key)
+
+ // CIDs in IPNS are expected to have libp2p-key multicodec
+ // We ease the transition by returning a more meaningful error with a valid CID
+ if err != nil {
+ ipnsCid, cidErr := cid.Decode(key)
+ if cidErr == nil && ipnsCid.Version() == 1 && ipnsCid.Type() != cid.Libp2pKey {
+ fixedCid := cid.NewCidV1(cid.Libp2pKey, ipnsCid.Hash()).String()
+ codecErr := fmt.Errorf("peer ID represented as CIDv1 require libp2p-key multicodec: retry with /ipns/%s", fixedCid)
+ log.Debugf("RoutingResolver: could not convert public key hash %q to peer ID: %s\n", key, codecErr)
+ out <- onceResult{err: codecErr}
+ close(out)
+ return out
+ }
+ }
+
+ cacheKey := key
+ if err == nil {
+ cacheKey = iface.FormatKeyID(ipnsKey)
+ }
+
+ if p, ok := ns.cacheGet(cacheKey); ok {
+ var err error
+ if len(segments) > 3 {
+ p, err = path.FromSegments("", strings.TrimRight(p.String(), "/"), segments[3])
+ }
+ span.SetAttributes(attribute.Bool("CacheHit", true))
+ span.RecordError(err)
+
+ out <- onceResult{value: p, err: err}
+ close(out)
+ return out
+ }
+ span.SetAttributes(attribute.Bool("CacheHit", false))
+
+ if err == nil {
+ res = ns.ipnsResolver
+ } else if _, ok := dns.IsDomainName(key); ok {
+ res = ns.dnsResolver
+ } else {
+ out <- onceResult{err: fmt.Errorf("invalid IPNS root: %q", key)}
+ close(out)
+ return out
+ }
+
+ resCh := res.resolveOnceAsync(ctx, key, options)
+ var best onceResult
+ go func() {
+ defer close(out)
+ for {
+ select {
+ case res, ok := <-resCh:
+ if !ok {
+ if best != (onceResult{}) {
+ ns.cacheSet(cacheKey, best.value, best.ttl)
+ }
+ return
+ }
+ if res.err == nil {
+ best = res
+ }
+ p := res.value
+ err := res.err
+ ttl := res.ttl
+
+ // Attach rest of the path
+ if len(segments) > 3 {
+ p, err = path.FromSegments("", strings.TrimRight(p.String(), "/"), segments[3])
+ }
+
+ emitOnceResult(ctx, out, onceResult{value: p, ttl: ttl, err: err})
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+
+ return out
+}
+
+func emitOnceResult(ctx context.Context, outCh chan<- onceResult, r onceResult) {
+ select {
+ case outCh <- r:
+ case <-ctx.Done():
+ }
+}
+
+// Publish implements Publisher
+func (ns *mpns) Publish(ctx context.Context, name ci.PrivKey, value path.Path, options ...opts.PublishOption) error {
+ ctx, span := StartSpan(ctx, "MPNS.Publish")
+ defer span.End()
+
+ // This is a bit hacky. We do this because the EOL is based on the current
+ // time, but also needed in the end of the function. Therefore, we parse
+ // the options immediately and add an option PublishWithEOL with the EOL
+ // calculated in this moment.
+ publishOpts := opts.ProcessPublishOptions(options)
+ options = append(options, opts.PublishWithEOL(publishOpts.EOL))
+
+ id, err := peer.IDFromPrivateKey(name)
+ if err != nil {
+ span.RecordError(err)
+ return err
+ }
+ span.SetAttributes(attribute.String("ID", id.String()))
+ if err := ns.ipnsPublisher.Publish(ctx, name, value, options...); err != nil {
+ // Invalidate the cache. Publishing may _partially_ succeed but
+ // still return an error.
+ ns.cacheInvalidate(string(id))
+ span.RecordError(err)
+ return err
+ }
+ ttl := DefaultResolverCacheTTL
+ if publishOpts.TTL >= 0 {
+ ttl = publishOpts.TTL
+ }
+ if ttEOL := time.Until(publishOpts.EOL); ttEOL < ttl {
+ ttl = ttEOL
+ }
+ ns.cacheSet(string(id), value, ttl)
+ return nil
+}
diff --git a/namesys/namesys_test.go b/namesys/namesys_test.go
new file mode 100644
index 0000000000..a3f9df01aa
--- /dev/null
+++ b/namesys/namesys_test.go
@@ -0,0 +1,210 @@
+package namesys
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "testing"
+ "time"
+
+ opts "github.com/ipfs/boxo/coreiface/options/namesys"
+ "github.com/ipfs/boxo/ipns"
+ "github.com/ipfs/boxo/path"
+ offroute "github.com/ipfs/boxo/routing/offline"
+ ds "github.com/ipfs/go-datastore"
+ dssync "github.com/ipfs/go-datastore/sync"
+ record "github.com/libp2p/go-libp2p-record"
+ ci "github.com/libp2p/go-libp2p/core/crypto"
+ "github.com/libp2p/go-libp2p/core/peer"
+ "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem"
+)
+
+type mockResolver struct {
+ entries map[string]string
+}
+
+func testResolution(t *testing.T, resolver Resolver, name string, depth uint, expected string, expError error) {
+ t.Helper()
+ p, err := resolver.Resolve(context.Background(), name, opts.Depth(depth))
+ if !errors.Is(err, expError) {
+ t.Fatal(fmt.Errorf(
+ "expected %s with a depth of %d to have a '%s' error, but got '%s'",
+ name, depth, expError, err))
+ }
+ if p.String() != expected {
+ t.Fatal(fmt.Errorf(
+ "%s with depth %d resolved to %s != %s",
+ name, depth, p.String(), expected))
+ }
+}
+
+func (r *mockResolver) resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult {
+ p, err := path.ParsePath(r.entries[name])
+ out := make(chan onceResult, 1)
+ out <- onceResult{value: p, err: err}
+ close(out)
+ return out
+}
+
+func mockResolverOne() *mockResolver {
+ return &mockResolver{
+ entries: map[string]string{
+ "QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy": "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj",
+ "QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n": "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy",
+ "QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD": "/ipns/ipfs.io",
+ "QmQ4QZh8nrsczdUEwTyfBope4THUhqxqc1fx6qYhhzZQei": "/ipfs/QmP3ouCnU8NNLsW6261pAx2pNLV2E4dQoisB1sgda12Act",
+ "12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5": "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", // ed25519+identity multihash
+ "bafzbeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm": "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", // cidv1 in base32 with libp2p-key multicodec
+ },
+ }
+}
+
+func mockResolverTwo() *mockResolver {
+ return &mockResolver{
+ entries: map[string]string{
+ "ipfs.io": "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n",
+ },
+ }
+}
+
+func TestNamesysResolution(t *testing.T) {
+ r := &mpns{
+ ipnsResolver: mockResolverOne(),
+ dnsResolver: mockResolverTwo(),
+ }
+
+ testResolution(t, r, "Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", opts.DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil)
+ testResolution(t, r, "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", opts.DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil)
+ testResolution(t, r, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", opts.DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil)
+ testResolution(t, r, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", 1, "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", ErrResolveRecursion)
+ testResolution(t, r, "/ipns/ipfs.io", opts.DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil)
+ testResolution(t, r, "/ipns/ipfs.io", 1, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", ErrResolveRecursion)
+ testResolution(t, r, "/ipns/ipfs.io", 2, "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", ErrResolveRecursion)
+ testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", opts.DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil)
+ testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 1, "/ipns/ipfs.io", ErrResolveRecursion)
+ testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 2, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", ErrResolveRecursion)
+ testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 3, "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", ErrResolveRecursion)
+ testResolution(t, r, "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", 1, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", ErrResolveRecursion)
+ testResolution(t, r, "/ipns/bafzbeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", 1, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", ErrResolveRecursion)
+}
+
+func TestNamesysResolutionWithCache(t *testing.T) {
+ nsMap := "dnslink-test.example.com:/ipfs/bafyaaeykceeaeeqlgiydemzngazc2mrtbimaw,12D3KooWQbpsnyzdBcxw6GUMbijV8WgXE4L8EtfnbcQWLfyxBKho:/ipfs/bafyaagakcyeaeeqqgiydemzngazc2mrtfvuxa3ttbimba,k51qzi5uqu5dkwkqm42v9j9kqcam2jiuvloi16g72i4i4amoo2m8u3ol3mqu6s:/ipfs/bafyaahikdmeaeeqvgiydemzngazc2mrtfvuxa3ttfvsgk5lybimbk"
+
+ staticMap, err := loadStaticMap(nsMap)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ r := &mpns{
+ ipnsResolver: mockResolverOne(),
+ dnsResolver: mockResolverTwo(),
+ staticMap: staticMap,
+ }
+
+ testResolution(t, r, "/ipns/dnslink-test.example.com", opts.DefaultDepthLimit, "/ipfs/bafyaaeykceeaeeqlgiydemzngazc2mrtbimaw", nil)
+
+ testResolution(t, r, "/ipns/bafzaajaiaejcbw5i6oyqsktsn36r2vxgl2jzosyao46rybqztxt4rx4tfa3hpogg", opts.DefaultDepthLimit, "/ipfs/bafyaagakcyeaeeqqgiydemzngazc2mrtfvuxa3ttbimba", nil)
+ testResolution(t, r, "/ipns/k51qzi5uqu5dlnojhwrggtpty9c0cp5hvnkdozowth4eqb726jvoros8k9niyu", opts.DefaultDepthLimit, "/ipfs/bafyaagakcyeaeeqqgiydemzngazc2mrtfvuxa3ttbimba", nil)
+ testResolution(t, r, "/ipns/12D3KooWQbpsnyzdBcxw6GUMbijV8WgXE4L8EtfnbcQWLfyxBKho", opts.DefaultDepthLimit, "/ipfs/bafyaagakcyeaeeqqgiydemzngazc2mrtfvuxa3ttbimba", nil)
+
+ testResolution(t, r, "/ipns/bafzaajaiaejcbpltl72da5f3y7ojrtsa7hsfn5bbnkjbkwyesziqqtdry6vjilku", opts.DefaultDepthLimit, "/ipfs/bafyaahikdmeaeeqvgiydemzngazc2mrtfvuxa3ttfvsgk5lybimbk", nil)
+ testResolution(t, r, "/ipns/k51qzi5uqu5dkwkqm42v9j9kqcam2jiuvloi16g72i4i4amoo2m8u3ol3mqu6s", opts.DefaultDepthLimit, "/ipfs/bafyaahikdmeaeeqvgiydemzngazc2mrtfvuxa3ttfvsgk5lybimbk", nil)
+ testResolution(t, r, "/ipns/12D3KooWNZuG8phqhoNK9KWcUhwfzA3biDKNCUNVWEaJgigr6Acj", opts.DefaultDepthLimit, "/ipfs/bafyaahikdmeaeeqvgiydemzngazc2mrtfvuxa3ttfvsgk5lybimbk", nil)
+
+}
+
+func TestPublishWithCache0(t *testing.T) {
+ dst := dssync.MutexWrap(ds.NewMapDatastore())
+ priv, _, err := ci.GenerateKeyPair(ci.RSA, 2048)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ps, err := pstoremem.NewPeerstore()
+ if err != nil {
+ t.Fatal(err)
+ }
+ pid, err := peer.IDFromPrivateKey(priv)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = ps.AddPrivKey(pid, priv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ routing := offroute.NewOfflineRouter(dst, record.NamespacedValidator{
+ "ipns": ipns.Validator{KeyBook: ps},
+ "pk": record.PublicKeyValidator{},
+ })
+
+ nsys, err := NewNameSystem(routing, WithDatastore(dst))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // CID is arbitrary.
+ p, err := path.ParsePath("QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn")
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = nsys.Publish(context.Background(), priv, p)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestPublishWithTTL(t *testing.T) {
+ dst := dssync.MutexWrap(ds.NewMapDatastore())
+ priv, _, err := ci.GenerateKeyPair(ci.RSA, 2048)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ps, err := pstoremem.NewPeerstore()
+ if err != nil {
+ t.Fatal(err)
+ }
+ pid, err := peer.IDFromPrivateKey(priv)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = ps.AddPrivKey(pid, priv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ routing := offroute.NewOfflineRouter(dst, record.NamespacedValidator{
+ "ipns": ipns.Validator{KeyBook: ps},
+ "pk": record.PublicKeyValidator{},
+ })
+
+ nsys, err := NewNameSystem(routing, WithDatastore(dst), WithCache(128))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // CID is arbitrary.
+ p, err := path.ParsePath("QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ttl := 1 * time.Second
+ eol := time.Now().Add(2 * time.Second)
+
+ err = nsys.Publish(context.Background(), priv, p, opts.PublishWithEOL(eol), opts.PublishWithTTL(ttl))
+ if err != nil {
+ t.Fatal(err)
+ }
+ ientry, ok := nsys.(*mpns).cache.Get(string(pid))
+ if !ok {
+ t.Fatal("cache get failed")
+ }
+ entry, ok := ientry.(cacheEntry)
+ if !ok {
+ t.Fatal("bad cache item returned")
+ }
+ if entry.eol.Sub(eol) > 10*time.Millisecond {
+ t.Fatalf("bad cache ttl: expected %s, got %s", eol, entry.eol)
+ }
+}
diff --git a/namesys/publisher.go b/namesys/publisher.go
new file mode 100644
index 0000000000..24a0b8e4d1
--- /dev/null
+++ b/namesys/publisher.go
@@ -0,0 +1,285 @@
+package namesys
+
+import (
+ "context"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/gogo/protobuf/proto"
+ opts "github.com/ipfs/boxo/coreiface/options/namesys"
+ "github.com/ipfs/boxo/ipns"
+ pb "github.com/ipfs/boxo/ipns/pb"
+ "github.com/ipfs/boxo/path"
+ ds "github.com/ipfs/go-datastore"
+ dsquery "github.com/ipfs/go-datastore/query"
+ "github.com/libp2p/go-libp2p/core/crypto"
+ "github.com/libp2p/go-libp2p/core/peer"
+ "github.com/libp2p/go-libp2p/core/routing"
+ "github.com/whyrusleeping/base32"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
+)
+
+const ipnsPrefix = "/ipns/"
+
+// IpnsPublisher is capable of publishing and resolving names to the IPFS
+// routing system.
+type IpnsPublisher struct {
+ routing routing.ValueStore
+ ds ds.Datastore
+
+ // Used to ensure we assign IPNS records *sequential* sequence numbers.
+ mu sync.Mutex
+}
+
+// NewIpnsPublisher constructs a publisher for the IPFS Routing name system.
+func NewIpnsPublisher(route routing.ValueStore, ds ds.Datastore) *IpnsPublisher {
+ if ds == nil {
+ panic("nil datastore")
+ }
+ return &IpnsPublisher{routing: route, ds: ds}
+}
+
+// Publish implements Publisher. Accepts a keypair and a value,
+// and publishes it out to the routing system
+func (p *IpnsPublisher) Publish(ctx context.Context, k crypto.PrivKey, value path.Path, options ...opts.PublishOption) error {
+ log.Debugf("Publish %s", value)
+
+ ctx, span := StartSpan(ctx, "IpnsPublisher.Publish", trace.WithAttributes(attribute.String("Value", value.String())))
+ defer span.End()
+
+ record, err := p.updateRecord(ctx, k, value, options...)
+ if err != nil {
+ return err
+ }
+
+ return PutRecordToRouting(ctx, p.routing, k.GetPublic(), record)
+}
+
+// IpnsDsKey returns a datastore key given an IPNS identifier (peer
+// ID). Defines the storage key for IPNS records in the local datastore.
+func IpnsDsKey(id peer.ID) ds.Key {
+ return ds.NewKey("/ipns/" + base32.RawStdEncoding.EncodeToString([]byte(id)))
+}
+
+// ListPublished returns the latest IPNS records published by this node and
+// their expiration times.
+//
+// This method will not search the routing system for records published by other
+// nodes.
+func (p *IpnsPublisher) ListPublished(ctx context.Context) (map[peer.ID]*pb.IpnsEntry, error) {
+ query, err := p.ds.Query(ctx, dsquery.Query{
+ Prefix: ipnsPrefix,
+ })
+ if err != nil {
+ return nil, err
+ }
+ defer query.Close()
+
+ records := make(map[peer.ID]*pb.IpnsEntry)
+ for {
+ select {
+ case result, ok := <-query.Next():
+ if !ok {
+ return records, nil
+ }
+ if result.Error != nil {
+ return nil, result.Error
+ }
+ e := new(pb.IpnsEntry)
+ if err := proto.Unmarshal(result.Value, e); err != nil {
+ // Might as well return what we can.
+ log.Error("found an invalid IPNS entry:", err)
+ continue
+ }
+ if !strings.HasPrefix(result.Key, ipnsPrefix) {
+ log.Errorf("datastore query for keys with prefix %s returned a key: %s", ipnsPrefix, result.Key)
+ continue
+ }
+ k := result.Key[len(ipnsPrefix):]
+ pid, err := base32.RawStdEncoding.DecodeString(k)
+ if err != nil {
+ log.Errorf("ipns ds key invalid: %s", result.Key)
+ continue
+ }
+ records[peer.ID(pid)] = e
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+ }
+}
+
+// GetPublished returns the record this node has published corresponding to the
+// given peer ID.
+//
+// If `checkRouting` is true and we have no existing record, this method will
+// check the routing system for any existing records.
+func (p *IpnsPublisher) GetPublished(ctx context.Context, id peer.ID, checkRouting bool) (*pb.IpnsEntry, error) {
+ ctx, cancel := context.WithTimeout(ctx, time.Second*30)
+ defer cancel()
+
+ value, err := p.ds.Get(ctx, IpnsDsKey(id))
+ switch err {
+ case nil:
+ case ds.ErrNotFound:
+ if !checkRouting {
+ return nil, nil
+ }
+ ipnskey := ipns.RecordKey(id)
+ value, err = p.routing.GetValue(ctx, ipnskey)
+ if err != nil {
+ // Not found or other network issue. Can't really do
+ // anything about this case.
+ if err != routing.ErrNotFound {
+ log.Debugf("error when determining the last published IPNS record for %s: %s", id, err)
+ }
+
+ return nil, nil
+ }
+ default:
+ return nil, err
+ }
+ e := new(pb.IpnsEntry)
+ if err := proto.Unmarshal(value, e); err != nil {
+ return nil, err
+ }
+ return e, nil
+}
+
+func (p *IpnsPublisher) updateRecord(ctx context.Context, k crypto.PrivKey, value path.Path, options ...opts.PublishOption) (*pb.IpnsEntry, error) {
+ id, err := peer.IDFromPrivateKey(k)
+ if err != nil {
+ return nil, err
+ }
+
+ p.mu.Lock()
+ defer p.mu.Unlock()
+
+ // get previous records sequence number
+ rec, err := p.GetPublished(ctx, id, true)
+ if err != nil {
+ return nil, err
+ }
+
+ seqno := rec.GetSequence() // returns 0 if rec is nil
+ if rec != nil && value != path.Path(rec.GetValue()) {
+ // Don't bother incrementing the sequence number unless the
+ // value changes.
+ seqno++
+ }
+
+ opts := opts.ProcessPublishOptions(options)
+
+ // Create record
+ entry, err := ipns.Create(k, []byte(value), seqno, opts.EOL, opts.TTL)
+ if err != nil {
+ return nil, err
+ }
+
+ data, err := proto.Marshal(entry)
+ if err != nil {
+ return nil, err
+ }
+
+ // Put the new record.
+ key := IpnsDsKey(id)
+ if err := p.ds.Put(ctx, key, data); err != nil {
+ return nil, err
+ }
+ if err := p.ds.Sync(ctx, key); err != nil {
+ return nil, err
+ }
+ return entry, nil
+}
+
+// PutRecordToRouting publishes the given entry using the provided ValueStore,
+// keyed on the ID associated with the provided public key. The public key is
+// also made available to the routing system so that entries can be verified.
+func PutRecordToRouting(ctx context.Context, r routing.ValueStore, k crypto.PubKey, entry *pb.IpnsEntry) error {
+ ctx, span := StartSpan(ctx, "PutRecordToRouting")
+ defer span.End()
+
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ errs := make(chan error, 2) // At most two errors (IPNS, and public key)
+
+ if err := ipns.EmbedPublicKey(k, entry); err != nil {
+ return err
+ }
+
+ id, err := peer.IDFromPublicKey(k)
+ if err != nil {
+ return err
+ }
+
+ go func() {
+ errs <- PublishEntry(ctx, r, ipns.RecordKey(id), entry)
+ }()
+
+ // Publish the public key if a public key cannot be extracted from the ID
+ // TODO: once v0.4.16 is widespread enough, we can stop doing this
+ // and at that point we can even deprecate the /pk/ namespace in the dht
+ //
+ // NOTE: This check actually checks if the public key has been embedded
+ // in the IPNS entry. This check is sufficient because we embed the
+ // public key in the IPNS entry if it can't be extracted from the ID.
+ if entry.PubKey != nil {
+ go func() {
+ errs <- PublishPublicKey(ctx, r, PkKeyForID(id), k)
+ }()
+
+ if err := waitOnErrChan(ctx, errs); err != nil {
+ return err
+ }
+ }
+
+ return waitOnErrChan(ctx, errs)
+}
+
+func waitOnErrChan(ctx context.Context, errs chan error) error {
+ select {
+ case err := <-errs:
+ return err
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+}
+
+// PublishPublicKey stores the given public key in the ValueStore with the
+// given key.
+func PublishPublicKey(ctx context.Context, r routing.ValueStore, k string, pubk crypto.PubKey) error {
+ ctx, span := StartSpan(ctx, "PublishPublicKey", trace.WithAttributes(attribute.String("Key", k)))
+ defer span.End()
+
+ log.Debugf("Storing pubkey at: %s", k)
+ pkbytes, err := crypto.MarshalPublicKey(pubk)
+ if err != nil {
+ return err
+ }
+
+ // Store associated public key
+ return r.PutValue(ctx, k, pkbytes)
+}
+
+// PublishEntry stores the given IpnsEntry in the ValueStore with the given
+// ipnskey.
+func PublishEntry(ctx context.Context, r routing.ValueStore, ipnskey string, rec *pb.IpnsEntry) error {
+ ctx, span := StartSpan(ctx, "PublishEntry", trace.WithAttributes(attribute.String("IPNSKey", ipnskey)))
+ defer span.End()
+
+ data, err := proto.Marshal(rec)
+ if err != nil {
+ return err
+ }
+
+ log.Debugf("Storing ipns entry at: %x", ipnskey)
+ // Store ipns entry at "/ipns/"+h(pubkey)
+ return r.PutValue(ctx, ipnskey, data)
+}
+
+// PkKeyForID returns the public key routing key for the given peer ID.
+func PkKeyForID(id peer.ID) string {
+ return "/pk/" + string(id)
+}
diff --git a/namesys/publisher_test.go b/namesys/publisher_test.go
new file mode 100644
index 0000000000..b40593c760
--- /dev/null
+++ b/namesys/publisher_test.go
@@ -0,0 +1,156 @@
+package namesys
+
+import (
+ "context"
+ "crypto/rand"
+ "testing"
+ "time"
+
+ "github.com/ipfs/boxo/path"
+
+ dshelp "github.com/ipfs/boxo/datastore/dshelp"
+ "github.com/ipfs/boxo/ipns"
+ mockrouting "github.com/ipfs/boxo/routing/mock"
+ ds "github.com/ipfs/go-datastore"
+ dssync "github.com/ipfs/go-datastore/sync"
+ testutil "github.com/libp2p/go-libp2p-testing/net"
+ ci "github.com/libp2p/go-libp2p/core/crypto"
+ "github.com/libp2p/go-libp2p/core/peer"
+ ma "github.com/multiformats/go-multiaddr"
+)
+
+type identity struct {
+ testutil.PeerNetParams
+}
+
+func (p *identity) ID() peer.ID {
+ return p.PeerNetParams.ID
+}
+
+func (p *identity) Address() ma.Multiaddr {
+ return p.Addr
+}
+
+func (p *identity) PrivateKey() ci.PrivKey {
+ return p.PrivKey
+}
+
+func (p *identity) PublicKey() ci.PubKey {
+ return p.PubKey
+}
+
+func testNamekeyPublisher(t *testing.T, keyType int, expectedErr error, expectedExistence bool) {
+ // Context
+ ctx := context.Background()
+
+ // Private key
+ privKey, pubKey, err := ci.GenerateKeyPairWithReader(keyType, 2048, rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // ID
+ id, err := peer.IDFromPublicKey(pubKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Value
+ value := []byte("ipfs/TESTING")
+
+ // Seqnum
+ seqnum := uint64(0)
+
+ // Eol
+ eol := time.Now().Add(24 * time.Hour)
+
+ // Routing value store
+ p := testutil.PeerNetParams{
+ ID: id,
+ PrivKey: privKey,
+ PubKey: pubKey,
+ Addr: testutil.ZeroLocalTCPAddress,
+ }
+
+ dstore := dssync.MutexWrap(ds.NewMapDatastore())
+ serv := mockrouting.NewServer()
+ r := serv.ClientWithDatastore(context.Background(), &identity{p}, dstore)
+
+ entry, err := ipns.Create(privKey, value, seqnum, eol, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = PutRecordToRouting(ctx, r, pubKey, entry)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Check for namekey existence in value store
+ namekey := PkKeyForID(id)
+ _, err = r.GetValue(ctx, namekey)
+ if err != expectedErr {
+ t.Fatal(err)
+ }
+
+ // Also check datastore for completeness
+ key := dshelp.NewKeyFromBinary([]byte(namekey))
+ exists, err := dstore.Has(ctx, key)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if exists != expectedExistence {
+ t.Fatal("Unexpected key existence in datastore")
+ }
+}
+
+func TestRSAPublisher(t *testing.T) {
+ testNamekeyPublisher(t, ci.RSA, nil, true)
+}
+
+func TestEd22519Publisher(t *testing.T) {
+ testNamekeyPublisher(t, ci.Ed25519, ds.ErrNotFound, false)
+}
+
+func TestAsyncDS(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ rt := mockrouting.NewServer().Client(testutil.RandIdentityOrFatal(t))
+ ds := &checkSyncDS{
+ Datastore: ds.NewMapDatastore(),
+ syncKeys: make(map[ds.Key]struct{}),
+ }
+ publisher := NewIpnsPublisher(rt, ds)
+
+ ipnsFakeID := testutil.RandIdentityOrFatal(t)
+ ipnsVal, err := path.ParsePath("/ipns/foo.bar")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := publisher.Publish(ctx, ipnsFakeID.PrivateKey(), ipnsVal); err != nil {
+ t.Fatal(err)
+ }
+
+ ipnsKey := IpnsDsKey(ipnsFakeID.ID())
+
+ for k := range ds.syncKeys {
+ if k.IsAncestorOf(ipnsKey) || k.Equal(ipnsKey) {
+ return
+ }
+ }
+
+ t.Fatal("ipns key not synced")
+}
+
+type checkSyncDS struct {
+ ds.Datastore
+ syncKeys map[ds.Key]struct{}
+}
+
+func (d *checkSyncDS) Sync(ctx context.Context, prefix ds.Key) error {
+ d.syncKeys[prefix] = struct{}{}
+ return d.Datastore.Sync(ctx, prefix)
+}
diff --git a/namesys/republisher/repub.go b/namesys/republisher/repub.go
new file mode 100644
index 0000000000..03426843b9
--- /dev/null
+++ b/namesys/republisher/repub.go
@@ -0,0 +1,186 @@
+// Package republisher provides a utility to automatically re-publish IPNS
+// records related to the keys in a Keystore.
+package republisher
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ keystore "github.com/ipfs/boxo/keystore"
+ "github.com/ipfs/boxo/namesys"
+ "github.com/ipfs/boxo/path"
+ "go.opentelemetry.io/otel/attribute"
+
+ "github.com/gogo/protobuf/proto"
+ opts "github.com/ipfs/boxo/coreiface/options/namesys"
+ "github.com/ipfs/boxo/ipns"
+ pb "github.com/ipfs/boxo/ipns/pb"
+ ds "github.com/ipfs/go-datastore"
+ logging "github.com/ipfs/go-log"
+ "github.com/jbenet/goprocess"
+ gpctx "github.com/jbenet/goprocess/context"
+ ic "github.com/libp2p/go-libp2p/core/crypto"
+ "github.com/libp2p/go-libp2p/core/peer"
+)
+
+var errNoEntry = errors.New("no previous entry")
+
+var log = logging.Logger("ipns-repub")
+
+// DefaultRebroadcastInterval is the default interval at which we rebroadcast IPNS records
+var DefaultRebroadcastInterval = time.Hour * 4
+
+// InitialRebroadcastDelay is the delay before first broadcasting IPNS records on start
+var InitialRebroadcastDelay = time.Minute * 1
+
+// FailureRetryInterval is the interval at which we retry IPNS records broadcasts (when they fail)
+var FailureRetryInterval = time.Minute * 5
+
+// DefaultRecordLifetime is the default lifetime for IPNS records
+const DefaultRecordLifetime = time.Hour * 24
+
+// Republisher facilitates the regular publishing of all the IPNS records
+// associated to keys in a Keystore.
+type Republisher struct {
+ ns namesys.Publisher
+ ds ds.Datastore
+ self ic.PrivKey
+ ks keystore.Keystore
+
+ Interval time.Duration
+
+ // how long records that are republished should be valid for
+ RecordLifetime time.Duration
+}
+
+// NewRepublisher creates a new Republisher
+func NewRepublisher(ns namesys.Publisher, ds ds.Datastore, self ic.PrivKey, ks keystore.Keystore) *Republisher {
+ return &Republisher{
+ ns: ns,
+ ds: ds,
+ self: self,
+ ks: ks,
+ Interval: DefaultRebroadcastInterval,
+ RecordLifetime: DefaultRecordLifetime,
+ }
+}
+
+// Run starts the republisher facility. It can be stopped by stopping the
+// provided proc.
+func (rp *Republisher) Run(proc goprocess.Process) {
+ timer := time.NewTimer(InitialRebroadcastDelay)
+ defer timer.Stop()
+ if rp.Interval < InitialRebroadcastDelay {
+ timer.Reset(rp.Interval)
+ }
+
+ for {
+ select {
+ case <-timer.C:
+ timer.Reset(rp.Interval)
+ err := rp.republishEntries(proc)
+ if err != nil {
+ log.Info("republisher failed to republish: ", err)
+ if FailureRetryInterval < rp.Interval {
+ timer.Reset(FailureRetryInterval)
+ }
+ }
+ case <-proc.Closing():
+ return
+ }
+ }
+}
+
+func (rp *Republisher) republishEntries(p goprocess.Process) error {
+ ctx, cancel := context.WithCancel(gpctx.OnClosingContext(p))
+ defer cancel()
+ ctx, span := namesys.StartSpan(ctx, "Republisher.RepublishEntries")
+ defer span.End()
+
+ // TODO: Use rp.ipns.ListPublished(). We can't currently *do* that
+ // because:
+ // 1. There's no way to get keys from the keystore by ID.
+ // 2. We don't actually have access to the IPNS publisher.
+ err := rp.republishEntry(ctx, rp.self)
+ if err != nil {
+ return err
+ }
+
+ if rp.ks != nil {
+ keyNames, err := rp.ks.List()
+ if err != nil {
+ return err
+ }
+ for _, name := range keyNames {
+ priv, err := rp.ks.Get(name)
+ if err != nil {
+ return err
+ }
+ err = rp.republishEntry(ctx, priv)
+ if err != nil {
+ return err
+ }
+
+ }
+ }
+
+ return nil
+}
+
+func (rp *Republisher) republishEntry(ctx context.Context, priv ic.PrivKey) error {
+ ctx, span := namesys.StartSpan(ctx, "Republisher.RepublishEntry")
+ defer span.End()
+ id, err := peer.IDFromPrivateKey(priv)
+ if err != nil {
+ span.RecordError(err)
+ return err
+ }
+
+ log.Debugf("republishing ipns entry for %s", id)
+
+ // Look for it locally only
+ e, err := rp.getLastIPNSEntry(ctx, id)
+ if err != nil {
+ if err == errNoEntry {
+ span.SetAttributes(attribute.Bool("NoEntry", true))
+ return nil
+ }
+ span.RecordError(err)
+ return err
+ }
+
+ p := path.Path(e.GetValue())
+ prevEol, err := ipns.GetEOL(e)
+ if err != nil {
+ span.RecordError(err)
+ return err
+ }
+
+ // update record with same sequence number
+ eol := time.Now().Add(rp.RecordLifetime)
+ if prevEol.After(eol) {
+ eol = prevEol
+ }
+ err = rp.ns.Publish(ctx, priv, p, opts.PublishWithEOL(eol))
+ span.RecordError(err)
+ return err
+}
+
+func (rp *Republisher) getLastIPNSEntry(ctx context.Context, id peer.ID) (*pb.IpnsEntry, error) {
+ // Look for it locally only
+ val, err := rp.ds.Get(ctx, namesys.IpnsDsKey(id))
+ switch err {
+ case nil:
+ case ds.ErrNotFound:
+ return nil, errNoEntry
+ default:
+ return nil, err
+ }
+
+ e := new(pb.IpnsEntry)
+ if err := proto.Unmarshal(val, e); err != nil {
+ return nil, err
+ }
+ return e, nil
+}
diff --git a/namesys/republisher/repub_test.go b/namesys/republisher/repub_test.go
new file mode 100644
index 0000000000..e07b3a22cf
--- /dev/null
+++ b/namesys/republisher/repub_test.go
@@ -0,0 +1,266 @@
+package republisher_test
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ "github.com/gogo/protobuf/proto"
+
+ "github.com/jbenet/goprocess"
+ "github.com/libp2p/go-libp2p"
+ dht "github.com/libp2p/go-libp2p-kad-dht"
+ ic "github.com/libp2p/go-libp2p/core/crypto"
+ host "github.com/libp2p/go-libp2p/core/host"
+ peer "github.com/libp2p/go-libp2p/core/peer"
+ routing "github.com/libp2p/go-libp2p/core/routing"
+
+ ds "github.com/ipfs/go-datastore"
+ dssync "github.com/ipfs/go-datastore/sync"
+ opts "github.com/ipfs/boxo/coreiface/options/namesys"
+ "github.com/ipfs/boxo/ipns"
+ ipns_pb "github.com/ipfs/boxo/ipns/pb"
+ "github.com/ipfs/boxo/path"
+
+ keystore "github.com/ipfs/boxo/keystore"
+ "github.com/ipfs/boxo/namesys"
+ . "github.com/ipfs/boxo/namesys/republisher"
+)
+
+type mockNode struct {
+ h host.Host
+ id string
+ privKey ic.PrivKey
+ store ds.Batching
+ dht *dht.IpfsDHT
+ keystore keystore.Keystore
+}
+
+func getMockNode(t *testing.T, ctx context.Context) *mockNode {
+ t.Helper()
+
+ dstore := dssync.MutexWrap(ds.NewMapDatastore())
+ var idht *dht.IpfsDHT
+ h, err := libp2p.New(
+ libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0"),
+ libp2p.Routing(func(h host.Host) (routing.PeerRouting, error) {
+ rt, err := dht.New(ctx, h, dht.Mode(dht.ModeServer))
+ idht = rt
+ return rt, err
+ }),
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ return &mockNode{
+ h: h,
+ id: h.ID().Pretty(),
+ privKey: h.Peerstore().PrivKey(h.ID()),
+ store: dstore,
+ dht: idht,
+ keystore: keystore.NewMemKeystore(),
+ }
+}
+
+func TestRepublish(t *testing.T) {
+ // set cache life to zero for testing low-period repubs
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ var nsystems []namesys.NameSystem
+ var nodes []*mockNode
+ for i := 0; i < 10; i++ {
+ n := getMockNode(t, ctx)
+ ns, err := namesys.NewNameSystem(n.dht, namesys.WithDatastore(n.store))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nsystems = append(nsystems, ns)
+ nodes = append(nodes, n)
+ }
+
+ pinfo := host.InfoFromHost(nodes[0].h)
+
+ for _, n := range nodes[1:] {
+ if err := n.h.Connect(ctx, *pinfo); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ // have one node publish a record that is valid for 1 second
+ publisher := nodes[3]
+
+ p := path.FromString("/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn") // does not need to be valid
+ rp := namesys.NewIpnsPublisher(publisher.dht, publisher.store)
+ name := "/ipns/" + publisher.id
+
+ // Retry in case the record expires before we can fetch it. This can
+ // happen when running the test on a slow machine.
+ var expiration time.Time
+ timeout := time.Second
+ for {
+ expiration = time.Now().Add(time.Second)
+ err := rp.Publish(ctx, publisher.privKey, p, opts.PublishWithEOL(expiration))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = verifyResolution(nsystems, name, p)
+ if err == nil {
+ break
+ }
+
+ if time.Now().After(expiration) {
+ timeout *= 2
+ continue
+ }
+ t.Fatal(err)
+ }
+
+ // Now wait a second, the records will be invalid and we should fail to resolve
+ time.Sleep(timeout)
+ if err := verifyResolutionFails(nsystems, name); err != nil {
+ t.Fatal(err)
+ }
+
+ // The republishers that are contained within the nodes have their timeout set
+ // to 12 hours. Instead of trying to tweak those, we're just going to pretend
+ // they don't exist and make our own.
+ repub := NewRepublisher(rp, publisher.store, publisher.privKey, publisher.keystore)
+ repub.Interval = time.Second
+ repub.RecordLifetime = time.Second * 5
+
+ proc := goprocess.Go(repub.Run)
+ defer proc.Close()
+
+ // now wait a couple seconds for it to fire
+ time.Sleep(time.Second * 2)
+
+ // we should be able to resolve them now
+ if err := verifyResolution(nsystems, name, p); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestLongEOLRepublish(t *testing.T) {
+ // set cache life to zero for testing low-period repubs
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ var nsystems []namesys.NameSystem
+ var nodes []*mockNode
+ for i := 0; i < 10; i++ {
+ n := getMockNode(t, ctx)
+ ns, err := namesys.NewNameSystem(n.dht, namesys.WithDatastore(n.store))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nsystems = append(nsystems, ns)
+ nodes = append(nodes, n)
+ }
+
+ pinfo := host.InfoFromHost(nodes[0].h)
+
+ for _, n := range nodes[1:] {
+ if err := n.h.Connect(ctx, *pinfo); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ // have one node publish a record that is valid for 1 second
+ publisher := nodes[3]
+ p := path.FromString("/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn") // does not need to be valid
+ rp := namesys.NewIpnsPublisher(publisher.dht, publisher.store)
+ name := "/ipns/" + publisher.id
+
+ expiration := time.Now().Add(time.Hour)
+ err := rp.Publish(ctx, publisher.privKey, p, opts.PublishWithEOL(expiration))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = verifyResolution(nsystems, name, p)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // The republishers that are contained within the nodes have their timeout set
+ // to 12 hours. Instead of trying to tweak those, we're just going to pretend
+ // they don't exist and make our own.
+ repub := NewRepublisher(rp, publisher.store, publisher.privKey, publisher.keystore)
+ repub.Interval = time.Millisecond * 500
+ repub.RecordLifetime = time.Second
+
+ proc := goprocess.Go(repub.Run)
+ defer proc.Close()
+
+ // now wait a couple seconds for it to fire a few times
+ time.Sleep(time.Second * 2)
+
+ err = verifyResolution(nsystems, name, p)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ entry, err := getLastIPNSEntry(ctx, publisher.store, publisher.h.ID())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ finalEol, err := ipns.GetEOL(entry)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !finalEol.Equal(expiration) {
+ t.Fatal("expiration time modified")
+ }
+}
+
+func getLastIPNSEntry(ctx context.Context, dstore ds.Datastore, id peer.ID) (*ipns_pb.IpnsEntry, error) {
+ // Look for it locally only
+ val, err := dstore.Get(ctx, namesys.IpnsDsKey(id))
+ if err != nil {
+ return nil, err
+ }
+
+ e := new(ipns_pb.IpnsEntry)
+ if err := proto.Unmarshal(val, e); err != nil {
+ return nil, err
+ }
+ return e, nil
+}
+
+func verifyResolution(nsystems []namesys.NameSystem, key string, exp path.Path) error {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ for _, n := range nsystems {
+ val, err := n.Resolve(ctx, key)
+ if err != nil {
+ return err
+ }
+
+ if val != exp {
+ return errors.New("resolved wrong record")
+ }
+ }
+ return nil
+}
+
+func verifyResolutionFails(nsystems []namesys.NameSystem, key string) error {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ for _, n := range nsystems {
+ _, err := n.Resolve(ctx, key)
+ if err == nil {
+ return errors.New("expected resolution to fail")
+ }
+ }
+ return nil
+}
diff --git a/namesys/resolve/resolve.go b/namesys/resolve/resolve.go
new file mode 100644
index 0000000000..b2acf06028
--- /dev/null
+++ b/namesys/resolve/resolve.go
@@ -0,0 +1,56 @@
+package resolve
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/ipfs/boxo/path"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
+
+ "github.com/ipfs/boxo/namesys"
+)
+
+// ErrNoNamesys is an explicit error for when an IPFS node doesn't
+// (yet) have a name system
+var ErrNoNamesys = errors.New(
+ "core/resolve: no Namesys on IpfsNode - can't resolve ipns entry")
+
+// ResolveIPNS resolves /ipns paths
+func ResolveIPNS(ctx context.Context, nsys namesys.NameSystem, p path.Path) (path.Path, error) {
+ ctx, span := namesys.StartSpan(ctx, "ResolveIPNS", trace.WithAttributes(attribute.String("Path", p.String())))
+ defer span.End()
+ if strings.HasPrefix(p.String(), "/ipns/") {
+ // TODO(cryptix): we should be able to query the local cache for the path
+ if nsys == nil {
+ return "", ErrNoNamesys
+ }
+
+ seg := p.Segments()
+
+ if len(seg) < 2 || seg[1] == "" { // just "/ " without further segments
+ err := fmt.Errorf("invalid path %q: ipns path missing IPNS ID", p)
+ return "", err
+ }
+
+ extensions := seg[2:]
+ resolvable, err := path.FromSegments("/", seg[0], seg[1])
+ if err != nil {
+ return "", err
+ }
+
+ respath, err := nsys.Resolve(ctx, resolvable.String())
+ if err != nil {
+ return "", err
+ }
+
+ segments := append(respath.Segments(), extensions...)
+ p, err = path.FromSegments("/", segments...)
+ if err != nil {
+ return "", err
+ }
+ }
+ return p, nil
+}
diff --git a/namesys/resolve_test.go b/namesys/resolve_test.go
new file mode 100644
index 0000000000..d2da312152
--- /dev/null
+++ b/namesys/resolve_test.go
@@ -0,0 +1,122 @@
+package namesys
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ ipns "github.com/ipfs/boxo/ipns"
+ path "github.com/ipfs/boxo/path"
+ mockrouting "github.com/ipfs/boxo/routing/mock"
+ ds "github.com/ipfs/go-datastore"
+ dssync "github.com/ipfs/go-datastore/sync"
+ tnet "github.com/libp2p/go-libp2p-testing/net"
+)
+
+func TestRoutingResolve(t *testing.T) {
+ dstore := dssync.MutexWrap(ds.NewMapDatastore())
+ serv := mockrouting.NewServer()
+ id := tnet.RandIdentityOrFatal(t)
+ d := serv.ClientWithDatastore(context.Background(), id, dstore)
+
+ resolver := NewIpnsResolver(d)
+ publisher := NewIpnsPublisher(d, dstore)
+
+ identity := tnet.RandIdentityOrFatal(t)
+
+ h := path.FromString("/ipfs/QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN")
+ err := publisher.Publish(context.Background(), identity.PrivateKey(), h)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res, err := resolver.Resolve(context.Background(), identity.ID().Pretty())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if res != h {
+ t.Fatal("Got back incorrect value.")
+ }
+}
+
+func TestPrexistingExpiredRecord(t *testing.T) {
+ dstore := dssync.MutexWrap(ds.NewMapDatastore())
+ d := mockrouting.NewServer().ClientWithDatastore(context.Background(), tnet.RandIdentityOrFatal(t), dstore)
+
+ resolver := NewIpnsResolver(d)
+ publisher := NewIpnsPublisher(d, dstore)
+
+ identity := tnet.RandIdentityOrFatal(t)
+
+ // Make an expired record and put it in the datastore
+ h := path.FromString("/ipfs/QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN")
+ eol := time.Now().Add(time.Hour * -1)
+
+ entry, err := ipns.Create(identity.PrivateKey(), []byte(h), 0, eol, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = PutRecordToRouting(context.Background(), d, identity.PublicKey(), entry)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Now, with an old record in the system already, try and publish a new one
+ err = publisher.Publish(context.Background(), identity.PrivateKey(), h)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = verifyCanResolve(resolver, identity.ID().Pretty(), h)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestPrexistingRecord(t *testing.T) {
+ dstore := dssync.MutexWrap(ds.NewMapDatastore())
+ d := mockrouting.NewServer().ClientWithDatastore(context.Background(), tnet.RandIdentityOrFatal(t), dstore)
+
+ resolver := NewIpnsResolver(d)
+ publisher := NewIpnsPublisher(d, dstore)
+
+ identity := tnet.RandIdentityOrFatal(t)
+
+ // Make a good record and put it in the datastore
+ h := path.FromString("/ipfs/QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN")
+ eol := time.Now().Add(time.Hour)
+ entry, err := ipns.Create(identity.PrivateKey(), []byte(h), 0, eol, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = PutRecordToRouting(context.Background(), d, identity.PublicKey(), entry)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Now, with an old record in the system already, try and publish a new one
+ err = publisher.Publish(context.Background(), identity.PrivateKey(), h)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = verifyCanResolve(resolver, identity.ID().Pretty(), h)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func verifyCanResolve(r Resolver, name string, exp path.Path) error {
+ res, err := r.Resolve(context.Background(), name)
+ if err != nil {
+ return err
+ }
+
+ if res != exp {
+ return errors.New("got back wrong record")
+ }
+
+ return nil
+}
diff --git a/namesys/routing.go b/namesys/routing.go
new file mode 100644
index 0000000000..c51d6f72b8
--- /dev/null
+++ b/namesys/routing.go
@@ -0,0 +1,160 @@
+package namesys
+
+import (
+ "context"
+ "strings"
+ "time"
+
+ "github.com/gogo/protobuf/proto"
+ opts "github.com/ipfs/boxo/coreiface/options/namesys"
+ "github.com/ipfs/boxo/ipns"
+ pb "github.com/ipfs/boxo/ipns/pb"
+ "github.com/ipfs/boxo/path"
+ "github.com/ipfs/go-cid"
+ logging "github.com/ipfs/go-log"
+ dht "github.com/libp2p/go-libp2p-kad-dht"
+ "github.com/libp2p/go-libp2p/core/peer"
+ "github.com/libp2p/go-libp2p/core/routing"
+ mh "github.com/multiformats/go-multihash"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
+)
+
+var log = logging.Logger("namesys")
+
+// IpnsResolver implements NSResolver for the main IPFS SFS-like naming
+type IpnsResolver struct {
+ routing routing.ValueStore
+}
+
+// NewIpnsResolver constructs a name resolver using the IPFS Routing system
+// to implement SFS-like naming on top.
+func NewIpnsResolver(route routing.ValueStore) *IpnsResolver {
+ if route == nil {
+ panic("attempt to create resolver with nil routing system")
+ }
+ return &IpnsResolver{
+ routing: route,
+ }
+}
+
+// Resolve implements Resolver.
+func (r *IpnsResolver) Resolve(ctx context.Context, name string, options ...opts.ResolveOpt) (path.Path, error) {
+ ctx, span := StartSpan(ctx, "IpnsResolver.Resolve", trace.WithAttributes(attribute.String("Name", name)))
+ defer span.End()
+ return resolve(ctx, r, name, opts.ProcessOpts(options))
+}
+
+// ResolveAsync implements Resolver.
+func (r *IpnsResolver) ResolveAsync(ctx context.Context, name string, options ...opts.ResolveOpt) <-chan Result {
+ ctx, span := StartSpan(ctx, "IpnsResolver.ResolveAsync", trace.WithAttributes(attribute.String("Name", name)))
+ defer span.End()
+ return resolveAsync(ctx, r, name, opts.ProcessOpts(options))
+}
+
+// resolveOnce implements resolver. Uses the IPFS routing system to
+// resolve SFS-like names.
+func (r *IpnsResolver) resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult {
+ ctx, span := StartSpan(ctx, "IpnsResolver.ResolveOnceAsync", trace.WithAttributes(attribute.String("Name", name)))
+ defer span.End()
+
+ out := make(chan onceResult, 1)
+ log.Debugf("RoutingResolver resolving %s", name)
+ cancel := func() {}
+
+ if options.DhtTimeout != 0 {
+ // Resolution must complete within the timeout
+ ctx, cancel = context.WithTimeout(ctx, options.DhtTimeout)
+ }
+
+ name = strings.TrimPrefix(name, "/ipns/")
+
+ pid, err := peer.Decode(name)
+ if err != nil {
+ log.Debugf("RoutingResolver: could not convert public key hash %s to peer ID: %s\n", name, err)
+ out <- onceResult{err: err}
+ close(out)
+ cancel()
+ return out
+ }
+
+ // Use the routing system to get the name.
+ // Note that the DHT will call the ipns validator when retrieving
+ // the value, which in turn verifies the ipns record signature
+ ipnsKey := ipns.RecordKey(pid)
+
+ vals, err := r.routing.SearchValue(ctx, ipnsKey, dht.Quorum(int(options.DhtRecordCount)))
+ if err != nil {
+ log.Debugf("RoutingResolver: dht get for name %s failed: %s", name, err)
+ out <- onceResult{err: err}
+ close(out)
+ cancel()
+ return out
+ }
+
+ go func() {
+ defer cancel()
+ defer close(out)
+ ctx, span := StartSpan(ctx, "IpnsResolver.ResolveOnceAsync.Worker")
+ defer span.End()
+
+ for {
+ select {
+ case val, ok := <-vals:
+ if !ok {
+ return
+ }
+
+ entry := new(pb.IpnsEntry)
+ err = proto.Unmarshal(val, entry)
+ if err != nil {
+ log.Debugf("RoutingResolver: could not unmarshal value for name %s: %s", name, err)
+ emitOnceResult(ctx, out, onceResult{err: err})
+ return
+ }
+
+ var p path.Path
+ // check for old style record:
+ if valh, err := mh.Cast(entry.GetValue()); err == nil {
+ // Its an old style multihash record
+ log.Debugf("encountered CIDv0 ipns entry: %s", valh)
+ p = path.FromCid(cid.NewCidV0(valh))
+ } else {
+ // Not a multihash, probably a new style record
+ p, err = path.ParsePath(string(entry.GetValue()))
+ if err != nil {
+ emitOnceResult(ctx, out, onceResult{err: err})
+ return
+ }
+ }
+
+ ttl := DefaultResolverCacheTTL
+ if entry.Ttl != nil {
+ ttl = time.Duration(*entry.Ttl)
+ }
+ switch eol, err := ipns.GetEOL(entry); err {
+ case ipns.ErrUnrecognizedValidity:
+ // No EOL.
+ case nil:
+ ttEol := time.Until(eol)
+ if ttEol < 0 {
+ // It *was* valid when we first resolved it.
+ ttl = 0
+ } else if ttEol < ttl {
+ ttl = ttEol
+ }
+ default:
+ log.Errorf("encountered error when parsing EOL: %s", err)
+ emitOnceResult(ctx, out, onceResult{err: err})
+ return
+ }
+
+ emitOnceResult(ctx, out, onceResult{value: p, ttl: ttl})
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+
+ return out
+}
diff --git a/namesys/tracing.go b/namesys/tracing.go
new file mode 100644
index 0000000000..4ef84294ae
--- /dev/null
+++ b/namesys/tracing.go
@@ -0,0 +1,13 @@
+package namesys
+
+import (
+ "context"
+ "fmt"
+
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/trace"
+)
+
+func StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
+ return otel.Tracer("go-namesys").Start(ctx, fmt.Sprintf("Namesys.%s", name))
+}
diff --git a/path/error.go b/path/error.go
new file mode 100644
index 0000000000..dafc446b5a
--- /dev/null
+++ b/path/error.go
@@ -0,0 +1,27 @@
+package path
+
+import (
+ "fmt"
+)
+
+type ErrInvalidPath struct {
+ error error
+ path string
+}
+
+func (e ErrInvalidPath) Error() string {
+ return fmt.Sprintf("invalid path %q: %s", e.path, e.error)
+}
+
+func (e ErrInvalidPath) Unwrap() error {
+ return e.error
+}
+
+func (e ErrInvalidPath) Is(err error) bool {
+ switch err.(type) {
+ case ErrInvalidPath:
+ return true
+ default:
+ return false
+ }
+}
diff --git a/path/error_test.go b/path/error_test.go
new file mode 100644
index 0000000000..07aab64081
--- /dev/null
+++ b/path/error_test.go
@@ -0,0 +1,16 @@
+package path
+
+import (
+ "errors"
+ "testing"
+)
+
+func TestErrorIs(t *testing.T) {
+ if !errors.Is(ErrInvalidPath{path: "foo", error: errors.New("bar")}, ErrInvalidPath{}) {
+ t.Fatal("error must be error")
+ }
+
+ if !errors.Is(&ErrInvalidPath{path: "foo", error: errors.New("bar")}, ErrInvalidPath{}) {
+ t.Fatal("pointer to error must be error")
+ }
+}
diff --git a/path/internal/tracing.go b/path/internal/tracing.go
new file mode 100644
index 0000000000..f9eda2f92c
--- /dev/null
+++ b/path/internal/tracing.go
@@ -0,0 +1,13 @@
+package internal
+
+import (
+ "context"
+ "fmt"
+
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/trace"
+)
+
+func StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
+ return otel.Tracer("go-path").Start(ctx, fmt.Sprintf("Path.%s", name), opts...)
+}
diff --git a/path/path.go b/path/path.go
new file mode 100644
index 0000000000..6d53ade047
--- /dev/null
+++ b/path/path.go
@@ -0,0 +1,190 @@
+// Package path contains utilities to work with ipfs paths.
+package path
+
+import (
+ "fmt"
+ "path"
+ "strings"
+
+ cid "github.com/ipfs/go-cid"
+)
+
+// A Path represents an ipfs content path:
+// - /path/to/file
+// - /ipfs/
+// - /ipns//path/to/folder
+// - etc
+type Path string
+
+// ^^^
+// TODO: debate making this a private struct wrapped in a public interface
+// would allow us to control creation, and cache segments.
+
+// FromString safely converts a string type to a Path type.
+func FromString(s string) Path {
+ return Path(s)
+}
+
+// FromCid safely converts a cid.Cid type to a Path type.
+func FromCid(c cid.Cid) Path {
+ return Path("/ipfs/" + c.String())
+}
+
+// Segments returns the different elements of a path
+// (elements are delimited by a /).
+func (p Path) Segments() []string {
+ cleaned := path.Clean(string(p))
+ segments := strings.Split(cleaned, "/")
+
+ // Ignore leading slash
+ if len(segments[0]) == 0 {
+ segments = segments[1:]
+ }
+
+ return segments
+}
+
+// String converts a path to string.
+func (p Path) String() string {
+ return string(p)
+}
+
+// IsJustAKey returns true if the path is of the form or /ipfs/, or
+// /ipld/
+func (p Path) IsJustAKey() bool {
+ parts := p.Segments()
+ return len(parts) == 2 && (parts[0] == "ipfs" || parts[0] == "ipld")
+}
+
+// PopLastSegment returns a new Path without its final segment, and the final
+// segment, separately. If there is no more to pop (the path is just a key),
+// the original path is returned.
+func (p Path) PopLastSegment() (Path, string, error) {
+
+ if p.IsJustAKey() {
+ return p, "", nil
+ }
+
+ segs := p.Segments()
+ newPath, err := ParsePath("/" + strings.Join(segs[:len(segs)-1], "/"))
+ if err != nil {
+ return "", "", err
+ }
+
+ return newPath, segs[len(segs)-1], nil
+}
+
+// FromSegments returns a path given its different segments.
+func FromSegments(prefix string, seg ...string) (Path, error) {
+ return ParsePath(prefix + strings.Join(seg, "/"))
+}
+
+// ParsePath returns a well-formed ipfs Path.
+// The returned path will always be prefixed with /ipfs/ or /ipns/.
+// The prefix will be added if not present in the given string.
+// This function will return an error when the given string is
+// not a valid ipfs path.
+func ParsePath(txt string) (Path, error) {
+ parts := strings.Split(txt, "/")
+ if len(parts) == 1 {
+ kp, err := ParseCidToPath(txt)
+ if err == nil {
+ return kp, nil
+ }
+ }
+
+ // if the path doesnt begin with a '/'
+ // we expect this to start with a hash, and be an 'ipfs' path
+ if parts[0] != "" {
+ if _, err := decodeCid(parts[0]); err != nil {
+ return "", &ErrInvalidPath{error: err, path: txt}
+ }
+ // The case when the path starts with hash without a protocol prefix
+ return Path("/ipfs/" + txt), nil
+ }
+
+ if len(parts) < 3 {
+ return "", &ErrInvalidPath{error: fmt.Errorf("invalid ipfs path"), path: txt}
+ }
+
+ //TODO: make this smarter
+ switch parts[1] {
+ case "ipfs", "ipld":
+ if parts[2] == "" {
+ return "", &ErrInvalidPath{error: fmt.Errorf("not enough path components"), path: txt}
+ }
+ // Validate Cid.
+ _, err := decodeCid(parts[2])
+ if err != nil {
+ return "", &ErrInvalidPath{error: fmt.Errorf("invalid CID: %w", err), path: txt}
+ }
+ case "ipns":
+ if parts[2] == "" {
+ return "", &ErrInvalidPath{error: fmt.Errorf("not enough path components"), path: txt}
+ }
+ default:
+ return "", &ErrInvalidPath{error: fmt.Errorf("unknown namespace %q", parts[1]), path: txt}
+ }
+
+ return Path(txt), nil
+}
+
+// ParseCidToPath takes a CID in string form and returns a valid ipfs Path.
+func ParseCidToPath(txt string) (Path, error) {
+ if txt == "" {
+ return "", &ErrInvalidPath{error: fmt.Errorf("empty"), path: txt}
+ }
+
+ c, err := decodeCid(txt)
+ if err != nil {
+ return "", &ErrInvalidPath{error: err, path: txt}
+ }
+
+ return FromCid(c), nil
+}
+
+// IsValid checks if a path is a valid ipfs Path.
+func (p *Path) IsValid() error {
+ _, err := ParsePath(p.String())
+ return err
+}
+
+// Join joins strings slices using /
+func Join(pths []string) string {
+ return strings.Join(pths, "/")
+}
+
+// SplitList splits strings usings /
+func SplitList(pth string) []string {
+ return strings.Split(pth, "/")
+}
+
+// SplitAbsPath clean up and split fpath. It extracts the first component (which
+// must be a Multihash) and return it separately.
+func SplitAbsPath(fpath Path) (cid.Cid, []string, error) {
+ parts := fpath.Segments()
+ if parts[0] == "ipfs" || parts[0] == "ipld" {
+ parts = parts[1:]
+ }
+
+ // if nothing, bail.
+ if len(parts) == 0 {
+ return cid.Cid{}, nil, &ErrInvalidPath{error: fmt.Errorf("empty"), path: string(fpath)}
+ }
+
+ c, err := decodeCid(parts[0])
+ // first element in the path is a cid
+ if err != nil {
+ return cid.Cid{}, nil, &ErrInvalidPath{error: fmt.Errorf("invalid CID: %w", err), path: string(fpath)}
+ }
+
+ return c, parts[1:], nil
+}
+
+func decodeCid(cstr string) (cid.Cid, error) {
+ c, err := cid.Decode(cstr)
+ if err != nil && len(cstr) == 46 && cstr[:2] == "qm" { // https://github.com/ipfs/go-ipfs/issues/7792
+ return cid.Cid{}, fmt.Errorf("%v (possible lowercased CIDv0; consider converting to a case-agnostic CIDv1, such as base32)", err)
+ }
+ return c, err
+}
diff --git a/path/path_test.go b/path/path_test.go
new file mode 100644
index 0000000000..2b26a56786
--- /dev/null
+++ b/path/path_test.go
@@ -0,0 +1,128 @@
+package path
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestPathParsing(t *testing.T) {
+ cases := map[string]bool{
+ "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": true,
+ "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a": true,
+ "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/c/d/e/f": true,
+ "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": true,
+ "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a": true,
+ "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/c/d/e/f": true,
+ "/ipns/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/c/d/e/f": true,
+ "/ipns/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": true,
+ "QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/c/d/e/f": true,
+ "QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": true,
+ "/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": false,
+ "/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a": false,
+ "/ipfs/foo": false,
+ "/ipfs/": false,
+ "ipfs/": false,
+ "ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": false,
+ "/ipld/foo": false,
+ "/ipld/": false,
+ "ipld/": false,
+ "ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": false,
+ }
+
+ for p, expected := range cases {
+ _, err := ParsePath(p)
+ valid := err == nil
+ if valid != expected {
+ t.Fatalf("expected %s to have valid == %t", p, expected)
+ }
+ }
+}
+
+func TestNoComponents(t *testing.T) {
+ for _, s := range []string{
+ "/ipfs/",
+ "/ipns/",
+ "/ipld/",
+ } {
+ _, err := ParsePath(s)
+ if err == nil || !strings.Contains(err.Error(), "not enough path components") || !strings.Contains(err.Error(), s) {
+ t.Error("wrong error")
+ }
+ }
+}
+
+func TestInvalidPaths(t *testing.T) {
+ for _, s := range []string{
+ "/ipfs",
+ "/testfs",
+ "/",
+ } {
+ _, err := ParsePath(s)
+ if err == nil || !strings.Contains(err.Error(), "invalid ipfs path") || !strings.Contains(err.Error(), s) {
+ t.Error("wrong error")
+ }
+ }
+}
+
+func TestIsJustAKey(t *testing.T) {
+ cases := map[string]bool{
+ "QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": true,
+ "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": true,
+ "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a": false,
+ "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b": false,
+ "/ipns/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": false,
+ "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b": false,
+ "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": true,
+ }
+
+ for p, expected := range cases {
+ path, err := ParsePath(p)
+ if err != nil {
+ t.Fatalf("ParsePath failed to parse \"%s\", but should have succeeded", p)
+ }
+ result := path.IsJustAKey()
+ if result != expected {
+ t.Fatalf("expected IsJustAKey(%s) to return %v, not %v", p, expected, result)
+ }
+ }
+}
+
+func TestPopLastSegment(t *testing.T) {
+ cases := map[string][]string{
+ "QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", ""},
+ "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", ""},
+ "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a": {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", "a"},
+ "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b": {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a", "b"},
+ "/ipns/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/x/y/z": {"/ipns/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/x/y", "z"},
+ "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/x/y/z": {"/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/x/y", "z"},
+ }
+
+ for p, expected := range cases {
+ path, err := ParsePath(p)
+ if err != nil {
+ t.Fatalf("ParsePath failed to parse \"%s\", but should have succeeded", p)
+ }
+ head, tail, err := path.PopLastSegment()
+ if err != nil {
+ t.Fatalf("PopLastSegment failed, but should have succeeded: %s", err)
+ }
+ headStr := head.String()
+ if headStr != expected[0] {
+ t.Fatalf("expected head of PopLastSegment(%s) to return %v, not %v", p, expected[0], headStr)
+ }
+ if tail != expected[1] {
+ t.Fatalf("expected tail of PopLastSegment(%s) to return %v, not %v", p, expected[1], tail)
+ }
+ }
+}
+
+func TestV0ErrorDueToLowercase(t *testing.T) {
+ badb58 := "/ipfs/qmbwqxbekc3p8tqskc98xmwnzrzdtrlmimpl8wbutgsmnr"
+ _, err := ParsePath(badb58)
+ if err == nil {
+ t.Fatal("should have failed to decode")
+ }
+ if !strings.HasSuffix(err.Error(), "(possible lowercased CIDv0; consider converting to a case-agnostic CIDv1, such as base32)") {
+ t.Fatal("should have meaningful info about case-insensitive fix")
+ }
+}
diff --git a/path/resolver/resolver.go b/path/resolver/resolver.go
new file mode 100644
index 0000000000..8192a9eb0b
--- /dev/null
+++ b/path/resolver/resolver.go
@@ -0,0 +1,322 @@
+// Package resolver implements utilities for resolving paths within ipfs.
+package resolver
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
+
+ "github.com/ipfs/boxo/fetcher"
+ fetcherhelpers "github.com/ipfs/boxo/fetcher/helpers"
+ path "github.com/ipfs/boxo/path"
+ "github.com/ipfs/boxo/path/internal"
+ cid "github.com/ipfs/go-cid"
+ format "github.com/ipfs/go-ipld-format"
+ logging "github.com/ipfs/go-log"
+ "github.com/ipld/go-ipld-prime"
+ cidlink "github.com/ipld/go-ipld-prime/linking/cid"
+ basicnode "github.com/ipld/go-ipld-prime/node/basic"
+ "github.com/ipld/go-ipld-prime/schema"
+ "github.com/ipld/go-ipld-prime/traversal/selector/builder"
+)
+
+var log = logging.Logger("pathresolv")
+
+// ErrNoComponents is used when Paths after a protocol
+// do not contain at least one component
+var ErrNoComponents = errors.New(
+ "path must contain at least one component")
+
+// ErrNoLink is returned when a link is not found in a path
+type ErrNoLink struct {
+ Name string
+ Node cid.Cid
+}
+
+// Error implements the Error interface for ErrNoLink with a useful
+// human readable message.
+func (e ErrNoLink) Error() string {
+ return fmt.Sprintf("no link named %q under %s", e.Name, e.Node.String())
+}
+
+// Resolver provides path resolution to IPFS.
+type Resolver interface {
+ // ResolveToLastNode walks the given path and returns the cid of the
+ // last block referenced by the path, and the path segments to
+ // traverse from the final block boundary to the final node within the
+ // block.
+ ResolveToLastNode(ctx context.Context, fpath path.Path) (cid.Cid, []string, error)
+ // ResolvePath fetches the node for given path. It returns the last
+ // item returned by ResolvePathComponents and the last link traversed
+ // which can be used to recover the block.
+ ResolvePath(ctx context.Context, fpath path.Path) (ipld.Node, ipld.Link, error)
+ // ResolvePathComponents fetches the nodes for each segment of the given path.
+ // It uses the first path component as a hash (key) of the first node, then
+ // resolves all other components walking the links via a selector traversal
+ ResolvePathComponents(ctx context.Context, fpath path.Path) ([]ipld.Node, error)
+}
+
+// basicResolver implements the Resolver interface.
+// It references a FetcherFactory, which is uses to resolve nodes.
+// TODO: now that this is more modular, try to unify this code with the
+//
+// the resolvers in namesys.
+type basicResolver struct {
+ FetcherFactory fetcher.Factory
+}
+
+// NewBasicResolver constructs a new basic resolver.
+func NewBasicResolver(fetcherFactory fetcher.Factory) Resolver {
+ return &basicResolver{
+ FetcherFactory: fetcherFactory,
+ }
+}
+
+// ResolveToLastNode walks the given path and returns the cid of the last
+// block referenced by the path, and the path segments to traverse from the
+// final block boundary to the final node within the block.
+func (r *basicResolver) ResolveToLastNode(ctx context.Context, fpath path.Path) (cid.Cid, []string, error) {
+ ctx, span := internal.StartSpan(ctx, "basicResolver.ResolveToLastNode", trace.WithAttributes(attribute.Stringer("Path", fpath)))
+ defer span.End()
+
+ c, p, err := path.SplitAbsPath(fpath)
+ if err != nil {
+ return cid.Cid{}, nil, err
+ }
+
+ if len(p) == 0 {
+ return c, nil, nil
+ }
+
+ // create a selector to traverse and match all path segments
+ pathSelector := pathAllSelector(p[:len(p)-1])
+
+ // create a new cancellable session
+ ctx, cancel := context.WithTimeout(ctx, time.Minute)
+ defer cancel()
+
+ // resolve node before last path segment
+ nodes, lastCid, depth, err := r.resolveNodes(ctx, c, pathSelector)
+ if err != nil {
+ return cid.Cid{}, nil, err
+ }
+
+ if len(nodes) < 1 {
+ return cid.Cid{}, nil, fmt.Errorf("path %v did not resolve to a node", fpath)
+ } else if len(nodes) < len(p) {
+ return cid.Undef, nil, ErrNoLink{Name: p[len(nodes)-1], Node: lastCid}
+ }
+
+ parent := nodes[len(nodes)-1]
+ lastSegment := p[len(p)-1]
+
+ // find final path segment within node
+ nd, err := parent.LookupBySegment(ipld.ParsePathSegment(lastSegment))
+ switch err.(type) {
+ case nil:
+ case schema.ErrNoSuchField:
+ return cid.Undef, nil, ErrNoLink{Name: lastSegment, Node: lastCid}
+ default:
+ return cid.Cid{}, nil, err
+ }
+
+ // if last node is not a link, just return it's cid, add path to remainder and return
+ if nd.Kind() != ipld.Kind_Link {
+ // return the cid and the remainder of the path
+ return lastCid, p[len(p)-depth-1:], nil
+ }
+
+ lnk, err := nd.AsLink()
+ if err != nil {
+ return cid.Cid{}, nil, err
+ }
+
+ clnk, ok := lnk.(cidlink.Link)
+ if !ok {
+ return cid.Cid{}, nil, fmt.Errorf("path %v resolves to a link that is not a cid link: %v", fpath, lnk)
+ }
+
+ return clnk.Cid, []string{}, nil
+}
+
+// ResolvePath fetches the node for given path. It returns the last item
+// returned by ResolvePathComponents and the last link traversed which can be used to recover the block.
+//
+// Note: if/when the context is cancelled or expires then if a multi-block ADL node is returned then it may not be
+// possible to load certain values.
+func (r *basicResolver) ResolvePath(ctx context.Context, fpath path.Path) (ipld.Node, ipld.Link, error) {
+ ctx, span := internal.StartSpan(ctx, "basicResolver.ResolvePath", trace.WithAttributes(attribute.Stringer("Path", fpath)))
+ defer span.End()
+
+ // validate path
+ if err := fpath.IsValid(); err != nil {
+ return nil, nil, err
+ }
+
+ c, p, err := path.SplitAbsPath(fpath)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // create a selector to traverse all path segments but only match the last
+ pathSelector := pathLeafSelector(p)
+
+ nodes, c, _, err := r.resolveNodes(ctx, c, pathSelector)
+ if err != nil {
+ return nil, nil, err
+ }
+ if len(nodes) < 1 {
+ return nil, nil, fmt.Errorf("path %v did not resolve to a node", fpath)
+ }
+ return nodes[len(nodes)-1], cidlink.Link{Cid: c}, nil
+}
+
+// ResolveSingle simply resolves one hop of a path through a graph with no
+// extra context (does not opaquely resolve through sharded nodes)
+// Deprecated: fetch node as ipld-prime or convert it and then use a selector to traverse through it.
+func ResolveSingle(ctx context.Context, ds format.NodeGetter, nd format.Node, names []string) (*format.Link, []string, error) {
+ _, span := internal.StartSpan(ctx, "ResolveSingle", trace.WithAttributes(attribute.Stringer("CID", nd.Cid())))
+ defer span.End()
+ return nd.ResolveLink(names)
+}
+
+// ResolvePathComponents fetches the nodes for each segment of the given path.
+// It uses the first path component as a hash (key) of the first node, then
+// resolves all other components walking the links via a selector traversal
+//
+// Note: if/when the context is cancelled or expires then if a multi-block ADL node is returned then it may not be
+// possible to load certain values.
+func (r *basicResolver) ResolvePathComponents(ctx context.Context, fpath path.Path) ([]ipld.Node, error) {
+ ctx, span := internal.StartSpan(ctx, "basicResolver.ResolvePathComponents", trace.WithAttributes(attribute.Stringer("Path", fpath)))
+ defer span.End()
+
+ //lint:ignore SA1019 TODO: replace EventBegin
+ evt := log.EventBegin(ctx, "resolvePathComponents", logging.LoggableMap{"fpath": fpath})
+ defer evt.Done()
+
+ // validate path
+ if err := fpath.IsValid(); err != nil {
+ evt.Append(logging.LoggableMap{"error": err.Error()})
+ return nil, err
+ }
+
+ c, p, err := path.SplitAbsPath(fpath)
+ if err != nil {
+ evt.Append(logging.LoggableMap{"error": err.Error()})
+ return nil, err
+ }
+
+ // create a selector to traverse and match all path segments
+ pathSelector := pathAllSelector(p)
+
+ nodes, _, _, err := r.resolveNodes(ctx, c, pathSelector)
+ if err != nil {
+ evt.Append(logging.LoggableMap{"error": err.Error()})
+ }
+
+ return nodes, err
+}
+
+// ResolveLinks iteratively resolves names by walking the link hierarchy.
+// Every node is fetched from the Fetcher, resolving the next name.
+// Returns the list of nodes forming the path, starting with ndd. This list is
+// guaranteed never to be empty.
+//
+// ResolveLinks(nd, []string{"foo", "bar", "baz"})
+// would retrieve "baz" in ("bar" in ("foo" in nd.Links).Links).Links
+//
+// Note: if/when the context is cancelled or expires then if a multi-block ADL node is returned then it may not be
+// possible to load certain values.
+func (r *basicResolver) ResolveLinks(ctx context.Context, ndd ipld.Node, names []string) ([]ipld.Node, error) {
+ ctx, span := internal.StartSpan(ctx, "basicResolver.ResolveLinks")
+ defer span.End()
+
+ //lint:ignore SA1019 TODO: replace EventBegin
+ evt := log.EventBegin(ctx, "resolveLinks", logging.LoggableMap{"names": names})
+ defer evt.Done()
+
+ // create a selector to traverse and match all path segments
+ pathSelector := pathAllSelector(names)
+
+ session := r.FetcherFactory.NewSession(ctx)
+
+ // traverse selector
+ nodes := []ipld.Node{ndd}
+ err := session.NodeMatching(ctx, ndd, pathSelector, func(res fetcher.FetchResult) error {
+ nodes = append(nodes, res.Node)
+ return nil
+ })
+ if err != nil {
+ evt.Append(logging.LoggableMap{"error": err.Error()})
+ return nil, err
+ }
+
+ return nodes, err
+}
+
+// Finds nodes matching the selector starting with a cid. Returns the matched nodes, the cid of the block containing
+// the last node, and the depth of the last node within its block (root is depth 0).
+func (r *basicResolver) resolveNodes(ctx context.Context, c cid.Cid, sel ipld.Node) ([]ipld.Node, cid.Cid, int, error) {
+ ctx, span := internal.StartSpan(ctx, "basicResolver.resolveNodes", trace.WithAttributes(attribute.Stringer("CID", c)))
+ defer span.End()
+ session := r.FetcherFactory.NewSession(ctx)
+
+ // traverse selector
+ lastLink := cid.Undef
+ depth := 0
+ nodes := []ipld.Node{}
+ err := fetcherhelpers.BlockMatching(ctx, session, cidlink.Link{Cid: c}, sel, func(res fetcher.FetchResult) error {
+ if res.LastBlockLink == nil {
+ res.LastBlockLink = cidlink.Link{Cid: c}
+ }
+ cidLnk, ok := res.LastBlockLink.(cidlink.Link)
+ if !ok {
+ return fmt.Errorf("link is not a cidlink: %v", cidLnk)
+ }
+
+ // if we hit a block boundary
+ if !lastLink.Equals(cidLnk.Cid) {
+ depth = 0
+ lastLink = cidLnk.Cid
+ } else {
+ depth++
+ }
+
+ nodes = append(nodes, res.Node)
+ return nil
+ })
+ if err != nil {
+ return nil, cid.Undef, 0, err
+ }
+
+ return nodes, lastLink, depth, nil
+}
+
+func pathLeafSelector(path []string) ipld.Node {
+ ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any)
+ return pathSelector(path, ssb, func(p string, s builder.SelectorSpec) builder.SelectorSpec {
+ return ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) { efsb.Insert(p, s) })
+ })
+}
+
+func pathAllSelector(path []string) ipld.Node {
+ ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any)
+ return pathSelector(path, ssb, func(p string, s builder.SelectorSpec) builder.SelectorSpec {
+ return ssb.ExploreUnion(
+ ssb.Matcher(),
+ ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) { efsb.Insert(p, s) }),
+ )
+ })
+}
+
+func pathSelector(path []string, ssb builder.SelectorSpecBuilder, reduce func(string, builder.SelectorSpec) builder.SelectorSpec) ipld.Node {
+ spec := ssb.Matcher()
+ for i := len(path) - 1; i >= 0; i-- {
+ spec = reduce(path[i], spec)
+ }
+ return spec.Node()
+}
diff --git a/path/resolver/resolver_test.go b/path/resolver/resolver_test.go
new file mode 100644
index 0000000000..580108fe38
--- /dev/null
+++ b/path/resolver/resolver_test.go
@@ -0,0 +1,293 @@
+package resolver_test
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "math/rand"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/ipfs/go-cid"
+ blocks "github.com/ipfs/boxo/blocks"
+ bsfetcher "github.com/ipfs/boxo/fetcher/impl/blockservice"
+ dagpb "github.com/ipld/go-codec-dagpb"
+ "github.com/ipld/go-ipld-prime"
+ cidlink "github.com/ipld/go-ipld-prime/linking/cid"
+ basicnode "github.com/ipld/go-ipld-prime/node/basic"
+ "github.com/ipld/go-ipld-prime/schema"
+ "github.com/multiformats/go-multihash"
+
+ path "github.com/ipfs/boxo/path"
+ "github.com/ipfs/boxo/path/resolver"
+ merkledag "github.com/ipfs/boxo/ipld/merkledag"
+ dagmock "github.com/ipfs/boxo/ipld/merkledag/test"
+ "github.com/ipfs/go-unixfsnode"
+ dagcbor "github.com/ipld/go-ipld-prime/codec/dagcbor"
+ dagjson "github.com/ipld/go-ipld-prime/codec/dagjson"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func randNode() *merkledag.ProtoNode {
+ node := new(merkledag.ProtoNode)
+ node.SetData(make([]byte, 32))
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ r.Read(node.Data())
+ return node
+}
+
+func TestRecurivePathResolution(t *testing.T) {
+ ctx := context.Background()
+ bsrv := dagmock.Bserv()
+
+ a := randNode()
+ b := randNode()
+ c := randNode()
+
+ err := b.AddNodeLink("grandchild", c)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = a.AddNodeLink("child", b)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, n := range []*merkledag.ProtoNode{a, b, c} {
+ err = bsrv.AddBlock(ctx, n)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ aKey := a.Cid()
+
+ segments := []string{aKey.String(), "child", "grandchild"}
+ p, err := path.FromSegments("/ipfs/", segments...)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fetcherFactory := bsfetcher.NewFetcherConfig(bsrv)
+ fetcherFactory.NodeReifier = unixfsnode.Reify
+ fetcherFactory.PrototypeChooser = dagpb.AddSupportToChooser(func(lnk ipld.Link, lnkCtx ipld.LinkContext) (ipld.NodePrototype, error) {
+ if tlnkNd, ok := lnkCtx.LinkNode.(schema.TypedLinkNode); ok {
+ return tlnkNd.LinkTargetNodePrototype(), nil
+ }
+ return basicnode.Prototype.Any, nil
+ })
+ resolver := resolver.NewBasicResolver(fetcherFactory)
+
+ node, lnk, err := resolver.ResolvePath(ctx, p)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ uNode, ok := node.(unixfsnode.PathedPBNode)
+ require.True(t, ok)
+ fd := uNode.FieldData()
+ byts, err := fd.Must().AsBytes()
+ require.NoError(t, err)
+
+ assert.Equal(t, cidlink.Link{Cid: c.Cid()}, lnk)
+
+ assert.Equal(t, c.Data(), byts)
+ cKey := c.Cid()
+
+ rCid, rest, err := resolver.ResolveToLastNode(ctx, p)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(rest) != 0 {
+ t.Error("expected rest to be empty")
+ }
+
+ if rCid.String() != cKey.String() {
+ t.Fatal(fmt.Errorf(
+ "ResolveToLastNode failed for %s: %s != %s",
+ p.String(), rCid.String(), cKey.String()))
+ }
+
+ p2, err := path.FromSegments("/ipfs/", aKey.String())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rCid, rest, err = resolver.ResolveToLastNode(ctx, p2)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(rest) != 0 {
+ t.Error("expected rest to be empty")
+ }
+
+ if rCid.String() != aKey.String() {
+ t.Fatal(fmt.Errorf(
+ "ResolveToLastNode failed for %s: %s != %s",
+ p.String(), rCid.String(), cKey.String()))
+ }
+}
+func TestResolveToLastNode_ErrNoLink(t *testing.T) {
+ ctx := context.Background()
+ bsrv := dagmock.Bserv()
+
+ a := randNode()
+ b := randNode()
+ c := randNode()
+
+ err := b.AddNodeLink("grandchild", c)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = a.AddNodeLink("child", b)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, n := range []*merkledag.ProtoNode{a, b, c} {
+ err = bsrv.AddBlock(ctx, n)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ aKey := a.Cid()
+
+ fetcherFactory := bsfetcher.NewFetcherConfig(bsrv)
+ fetcherFactory.PrototypeChooser = dagpb.AddSupportToChooser(func(lnk ipld.Link, lnkCtx ipld.LinkContext) (ipld.NodePrototype, error) {
+ if tlnkNd, ok := lnkCtx.LinkNode.(schema.TypedLinkNode); ok {
+ return tlnkNd.LinkTargetNodePrototype(), nil
+ }
+ return basicnode.Prototype.Any, nil
+ })
+ fetcherFactory.NodeReifier = unixfsnode.Reify
+ r := resolver.NewBasicResolver(fetcherFactory)
+
+ // test missing link intermediate segment
+ segments := []string{aKey.String(), "cheese", "time"}
+ p, err := path.FromSegments("/ipfs/", segments...)
+ require.NoError(t, err)
+
+ _, _, err = r.ResolveToLastNode(ctx, p)
+ require.EqualError(t, err, resolver.ErrNoLink{Name: "cheese", Node: aKey}.Error())
+
+ // test missing link at end
+ bKey := b.Cid()
+ segments = []string{aKey.String(), "child", "apples"}
+ p, err = path.FromSegments("/ipfs/", segments...)
+ require.NoError(t, err)
+
+ _, _, err = r.ResolveToLastNode(ctx, p)
+ require.EqualError(t, err, resolver.ErrNoLink{Name: "apples", Node: bKey}.Error())
+}
+
+func TestResolveToLastNode_NoUnnecessaryFetching(t *testing.T) {
+ ctx := context.Background()
+ bsrv := dagmock.Bserv()
+
+ a := randNode()
+ b := randNode()
+
+ err := a.AddNodeLink("child", b)
+ require.NoError(t, err)
+
+ err = bsrv.AddBlock(ctx, a)
+ require.NoError(t, err)
+
+ aKey := a.Cid()
+
+ segments := []string{aKey.String(), "child"}
+ p, err := path.FromSegments("/ipfs/", segments...)
+ require.NoError(t, err)
+
+ fetcherFactory := bsfetcher.NewFetcherConfig(bsrv)
+ fetcherFactory.PrototypeChooser = dagpb.AddSupportToChooser(func(lnk ipld.Link, lnkCtx ipld.LinkContext) (ipld.NodePrototype, error) {
+ if tlnkNd, ok := lnkCtx.LinkNode.(schema.TypedLinkNode); ok {
+ return tlnkNd.LinkTargetNodePrototype(), nil
+ }
+ return basicnode.Prototype.Any, nil
+ })
+ fetcherFactory.NodeReifier = unixfsnode.Reify
+ resolver := resolver.NewBasicResolver(fetcherFactory)
+
+ resolvedCID, remainingPath, err := resolver.ResolveToLastNode(ctx, p)
+ require.NoError(t, err)
+
+ require.Equal(t, len(remainingPath), 0, "cannot have remaining path")
+ require.Equal(t, b.Cid(), resolvedCID)
+}
+
+func TestPathRemainder(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ bsrv := dagmock.Bserv()
+
+ nb := basicnode.Prototype.Any.NewBuilder()
+ err := dagjson.Decode(nb, strings.NewReader(`{"foo": {"bar": "baz"}}`))
+ require.NoError(t, err)
+ out := new(bytes.Buffer)
+ err = dagcbor.Encode(nb.Build(), out)
+ require.NoError(t, err)
+ lnk, err := cid.Prefix{
+ Version: 1,
+ Codec: cid.DagCBOR,
+ MhType: multihash.SHA2_256,
+ MhLength: 32,
+ }.Sum(out.Bytes())
+ require.NoError(t, err)
+ blk, err := blocks.NewBlockWithCid(out.Bytes(), lnk)
+ require.NoError(t, err)
+ bsrv.AddBlock(ctx, blk)
+ fetcherFactory := bsfetcher.NewFetcherConfig(bsrv)
+ resolver := resolver.NewBasicResolver(fetcherFactory)
+
+ rp1, remainder, err := resolver.ResolveToLastNode(ctx, path.FromString(lnk.String()+"/foo/bar"))
+ require.NoError(t, err)
+
+ assert.Equal(t, lnk, rp1)
+ require.Equal(t, "foo/bar", path.Join(remainder))
+}
+
+func TestResolveToLastNode_MixedSegmentTypes(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ bsrv := dagmock.Bserv()
+ a := randNode()
+ err := bsrv.AddBlock(ctx, a)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nb := basicnode.Prototype.Any.NewBuilder()
+ json := `{"foo":{"bar":[0,{"boom":["baz",1,2,{"/":"CID"},"blop"]}]}}`
+ json = strings.ReplaceAll(json, "CID", a.Cid().String())
+ err = dagjson.Decode(nb, strings.NewReader(json))
+ require.NoError(t, err)
+ out := new(bytes.Buffer)
+ err = dagcbor.Encode(nb.Build(), out)
+ require.NoError(t, err)
+ lnk, err := cid.Prefix{
+ Version: 1,
+ Codec: cid.DagCBOR,
+ MhType: multihash.SHA2_256,
+ MhLength: 32,
+ }.Sum(out.Bytes())
+ require.NoError(t, err)
+ blk, err := blocks.NewBlockWithCid(out.Bytes(), lnk)
+ require.NoError(t, err)
+ bsrv.AddBlock(ctx, blk)
+ fetcherFactory := bsfetcher.NewFetcherConfig(bsrv)
+ resolver := resolver.NewBasicResolver(fetcherFactory)
+
+ cid, remainder, err := resolver.ResolveToLastNode(ctx, path.FromString(lnk.String()+"/foo/bar/1/boom/3"))
+ require.NoError(t, err)
+
+ assert.Equal(t, 0, len(remainder))
+ assert.True(t, cid.Equals(a.Cid()))
+}
diff --git a/pinning/pinner/.gitignore b/pinning/pinner/.gitignore
new file mode 100644
index 0000000000..3c342889d2
--- /dev/null
+++ b/pinning/pinner/.gitignore
@@ -0,0 +1,8 @@
+*~
+*.log
+
+# Test binary, build with `go test -c`
+*.test
+
+# Output of the go coverage tool
+*.out
diff --git a/pinning/pinner/dsindex/error.go b/pinning/pinner/dsindex/error.go
new file mode 100644
index 0000000000..f3b685bb95
--- /dev/null
+++ b/pinning/pinner/dsindex/error.go
@@ -0,0 +1,8 @@
+package dsindex
+
+import "errors"
+
+var (
+ ErrEmptyKey = errors.New("key is empty")
+ ErrEmptyValue = errors.New("value is empty")
+)
diff --git a/pinning/pinner/dsindex/indexer.go b/pinning/pinner/dsindex/indexer.go
new file mode 100644
index 0000000000..8384ad5d5a
--- /dev/null
+++ b/pinning/pinner/dsindex/indexer.go
@@ -0,0 +1,277 @@
+// Package dsindex provides secondary indexing functionality for a datastore.
+package dsindex
+
+import (
+ "context"
+ "fmt"
+ "path"
+
+ ds "github.com/ipfs/go-datastore"
+ "github.com/ipfs/go-datastore/namespace"
+ "github.com/ipfs/go-datastore/query"
+ "github.com/multiformats/go-multibase"
+)
+
+// Indexer maintains a secondary index. An index is a collection of key-value
+// mappings where the key is the secondary index that maps to one or more
+// values, where each value is a unique key being indexed.
+type Indexer interface {
+ // Add adds the specified value to the key
+ Add(ctx context.Context, key, value string) error
+
+ // Delete deletes the specified value from the key. If the value is not in
+ // the datastore, this method returns no error.
+ Delete(ctx context.Context, key, value string) error
+
+ // DeleteKey deletes all values in the given key. If a key is not in the
+ // datastore, this method returns no error. Returns a count of values that
+ // were deleted.
+ DeleteKey(ctx context.Context, key string) (count int, err error)
+
+ // DeleteAll deletes all keys managed by this Indexer. Returns a count of
+ // the values that were deleted.
+ DeleteAll(ctx context.Context) (count int, err error)
+
+ // ForEach calls the function for each value in the specified key, until
+ // there are no more values, or until the function returns false. If key
+ // is empty string, then all keys are iterated.
+ ForEach(ctx context.Context, key string, fn func(key, value string) bool) error
+
+ // HasValue determines if the key contains the specified value
+ HasValue(ctx context.Context, key, value string) (bool, error)
+
+ // HasAny determines if any value is in the specified key. If key is
+ // empty string, then all values are searched.
+ HasAny(ctx context.Context, key string) (bool, error)
+
+ // Search returns all values for the given key
+ Search(ctx context.Context, key string) (values []string, err error)
+}
+
+// indexer is a simple implementation of Indexer. This implementation relies
+// on the underlying data store to support efficient querying by prefix.
+//
+// TODO: Consider adding caching
+type indexer struct {
+ dstore ds.Datastore
+}
+
+// New creates a new datastore index. All indexes are stored under the
+// specified index name.
+//
+// To persist the actions of calling Indexer functions, it is necessary to call
+// dstore.Sync.
+func New(dstore ds.Datastore, name ds.Key) Indexer {
+ return &indexer{
+ dstore: namespace.Wrap(dstore, name),
+ }
+}
+
+func (x *indexer) Add(ctx context.Context, key, value string) error {
+ if key == "" {
+ return ErrEmptyKey
+ }
+ if value == "" {
+ return ErrEmptyValue
+ }
+ dsKey := ds.NewKey(encode(key)).ChildString(encode(value))
+ return x.dstore.Put(ctx, dsKey, []byte{})
+}
+
+func (x *indexer) Delete(ctx context.Context, key, value string) error {
+ if key == "" {
+ return ErrEmptyKey
+ }
+ if value == "" {
+ return ErrEmptyValue
+ }
+ return x.dstore.Delete(ctx, ds.NewKey(encode(key)).ChildString(encode(value)))
+}
+
+func (x *indexer) DeleteKey(ctx context.Context, key string) (int, error) {
+ if key == "" {
+ return 0, ErrEmptyKey
+ }
+ return x.deletePrefix(ctx, encode(key))
+}
+
+func (x *indexer) DeleteAll(ctx context.Context) (int, error) {
+ return x.deletePrefix(ctx, "")
+}
+
+func (x *indexer) ForEach(ctx context.Context, key string, fn func(key, value string) bool) error {
+ if key != "" {
+ key = encode(key)
+ }
+
+ q := query.Query{
+ Prefix: key,
+ KeysOnly: true,
+ }
+ results, err := x.dstore.Query(ctx, q)
+ if err != nil {
+ return err
+ }
+ defer results.Close()
+
+ for r := range results.Next() {
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+ if r.Error != nil {
+ return fmt.Errorf("cannot read index: %v", r.Error)
+ }
+ ent := r.Entry
+ decIdx, err := decode(path.Base(path.Dir(ent.Key)))
+ if err != nil {
+ return fmt.Errorf("cannot decode index: %v", err)
+ }
+ decKey, err := decode(path.Base(ent.Key))
+ if err != nil {
+ return fmt.Errorf("cannot decode key: %v", err)
+ }
+ if !fn(decIdx, decKey) {
+ return nil
+ }
+ }
+
+ return nil
+}
+
+func (x *indexer) HasValue(ctx context.Context, key, value string) (bool, error) {
+ if key == "" {
+ return false, ErrEmptyKey
+ }
+ if value == "" {
+ return false, ErrEmptyValue
+ }
+ return x.dstore.Has(ctx, ds.NewKey(encode(key)).ChildString(encode(value)))
+}
+
+func (x *indexer) HasAny(ctx context.Context, key string) (bool, error) {
+ var any bool
+ err := x.ForEach(ctx, key, func(key, value string) bool {
+ any = true
+ return false
+ })
+ return any, err
+}
+
+func (x *indexer) Search(ctx context.Context, key string) ([]string, error) {
+ if key == "" {
+ return nil, ErrEmptyKey
+ }
+ ents, err := x.queryPrefix(ctx, encode(key))
+ if err != nil {
+ return nil, err
+ }
+ if len(ents) == 0 {
+ return nil, nil
+ }
+
+ values := make([]string, len(ents))
+ for i := range ents {
+ values[i], err = decode(path.Base(ents[i].Key))
+ if err != nil {
+ return nil, fmt.Errorf("cannot decode value: %v", err)
+ }
+ }
+ return values, nil
+}
+
+// SyncIndex synchronizes the keys in the target Indexer to match those of the
+// ref Indexer. This function does not change this indexer's key root (name
+// passed into New).
+func SyncIndex(ctx context.Context, ref, target Indexer) (bool, error) {
+ // Build reference index map
+ refs := map[string]string{}
+ err := ref.ForEach(ctx, "", func(key, value string) bool {
+ refs[value] = key
+ return true
+ })
+ if err != nil {
+ return false, err
+ }
+ if len(refs) == 0 {
+ return false, nil
+ }
+
+ // Compare current indexes
+ dels := map[string]string{}
+ err = target.ForEach(ctx, "", func(key, value string) bool {
+ refKey, ok := refs[value]
+ if ok && refKey == key {
+ // same in both; delete from refs, do not add to dels
+ delete(refs, value)
+ } else {
+ dels[value] = key
+ }
+ return true
+ })
+ if err != nil {
+ return false, err
+ }
+
+ // Items in dels are keys that no longer exist
+ for value, key := range dels {
+ err = target.Delete(ctx, key, value)
+ if err != nil {
+ return false, err
+ }
+ }
+
+ // What remains in refs are keys that need to be added
+ for value, key := range refs {
+ err = target.Add(ctx, key, value)
+ if err != nil {
+ return false, err
+ }
+ }
+
+ return len(refs) != 0 || len(dels) != 0, nil
+}
+
+func (x *indexer) deletePrefix(ctx context.Context, prefix string) (int, error) {
+ ents, err := x.queryPrefix(ctx, prefix)
+ if err != nil {
+ return 0, err
+ }
+
+ for i := range ents {
+ err = x.dstore.Delete(ctx, ds.NewKey(ents[i].Key))
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ return len(ents), nil
+}
+
+func (x *indexer) queryPrefix(ctx context.Context, prefix string) ([]query.Entry, error) {
+ q := query.Query{
+ Prefix: prefix,
+ KeysOnly: true,
+ }
+ results, err := x.dstore.Query(ctx, q)
+ if err != nil {
+ return nil, err
+ }
+ return results.Rest()
+}
+
+func encode(data string) string {
+ encData, err := multibase.Encode(multibase.Base64url, []byte(data))
+ if err != nil {
+ // programming error; using unsupported encoding
+ panic(err.Error())
+ }
+ return encData
+}
+
+func decode(data string) (string, error) {
+ _, b, err := multibase.Decode(data)
+ if err != nil {
+ return "", err
+ }
+ return string(b), nil
+}
diff --git a/pinning/pinner/dsindex/indexer_test.go b/pinning/pinner/dsindex/indexer_test.go
new file mode 100644
index 0000000000..45372c6053
--- /dev/null
+++ b/pinning/pinner/dsindex/indexer_test.go
@@ -0,0 +1,286 @@
+package dsindex
+
+import (
+ "context"
+ "testing"
+
+ ds "github.com/ipfs/go-datastore"
+)
+
+func createIndexer() Indexer {
+ dstore := ds.NewMapDatastore()
+ nameIndex := New(dstore, ds.NewKey("/data/nameindex"))
+
+ ctx := context.Background()
+ nameIndex.Add(ctx, "alice", "a1")
+ nameIndex.Add(ctx, "bob", "b1")
+ nameIndex.Add(ctx, "bob", "b2")
+ nameIndex.Add(ctx, "cathy", "c1")
+
+ return nameIndex
+}
+
+func TestAdd(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ nameIndex := createIndexer()
+ err := nameIndex.Add(ctx, "someone", "s1")
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = nameIndex.Add(ctx, "someone", "s1")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = nameIndex.Add(ctx, "", "noindex")
+ if err != ErrEmptyKey {
+ t.Fatal("unexpected error:", err)
+ }
+
+ err = nameIndex.Add(ctx, "nokey", "")
+ if err != ErrEmptyValue {
+ t.Fatal("unexpected error:", err)
+ }
+}
+
+func TestHasValue(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ nameIndex := createIndexer()
+
+ ok, err := nameIndex.HasValue(ctx, "bob", "b1")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !ok {
+ t.Fatal("missing index")
+ }
+
+ ok, err = nameIndex.HasValue(ctx, "bob", "b3")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if ok {
+ t.Fatal("should not have index")
+ }
+
+ _, err = nameIndex.HasValue(ctx, "", "b1")
+ if err != ErrEmptyKey {
+ t.Fatal("unexpected error:", err)
+ }
+
+ _, err = nameIndex.HasValue(ctx, "bob", "")
+ if err != ErrEmptyValue {
+ t.Fatal("unexpected error:", err)
+ }
+}
+
+func TestHasAny(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ nameIndex := createIndexer()
+
+ ok, err := nameIndex.HasAny(ctx, "nothere")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if ok {
+ t.Fatal("should return false")
+ }
+
+ for _, idx := range []string{"alice", "bob", ""} {
+ ok, err = nameIndex.HasAny(ctx, idx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !ok {
+ t.Fatal("missing index", idx)
+ }
+ }
+
+ count, err := nameIndex.DeleteAll(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if count != 4 {
+ t.Fatal("expected 4 deletions")
+ }
+
+ ok, err = nameIndex.HasAny(ctx, "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if ok {
+ t.Fatal("should return false")
+ }
+}
+
+func TestForEach(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ nameIndex := createIndexer()
+
+ found := make(map[string]struct{})
+ err := nameIndex.ForEach(ctx, "bob", func(key, value string) bool {
+ found[value] = struct{}{}
+ return true
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, value := range []string{"b1", "b2"} {
+ _, ok := found[value]
+ if !ok {
+ t.Fatal("missing key for value", value)
+ }
+ }
+
+ values := map[string]string{}
+ err = nameIndex.ForEach(ctx, "", func(key, value string) bool {
+ values[value] = key
+ return true
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(values) != 4 {
+ t.Fatal("expected 4 keys")
+ }
+
+ if values["a1"] != "alice" {
+ t.Error("expected a1: alice")
+ }
+ if values["b1"] != "bob" {
+ t.Error("expected b1: bob")
+ }
+ if values["b2"] != "bob" {
+ t.Error("expected b2: bob")
+ }
+ if values["c1"] != "cathy" {
+ t.Error("expected c1: cathy")
+ }
+}
+
+func TestSearch(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ nameIndex := createIndexer()
+
+ ids, err := nameIndex.Search(ctx, "bob")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(ids) != 2 {
+ t.Fatal("wrong number of ids - expected 2 got", ids)
+ }
+ for _, id := range ids {
+ if id != "b1" && id != "b2" {
+ t.Fatal("wrong value in id set")
+ }
+ }
+ if ids[0] == ids[1] {
+ t.Fatal("duplicate id")
+ }
+
+ ids, err = nameIndex.Search(ctx, "cathy")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(ids) != 1 || ids[0] != "c1" {
+ t.Fatal("wrong ids")
+ }
+
+ ids, err = nameIndex.Search(ctx, "amit")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(ids) != 0 {
+ t.Fatal("unexpected ids returned")
+ }
+}
+
+func TestDelete(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ nameIndex := createIndexer()
+
+ err := nameIndex.Delete(ctx, "bob", "b3")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = nameIndex.Delete(ctx, "alice", "a1")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ok, err := nameIndex.HasValue(ctx, "alice", "a1")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if ok {
+ t.Fatal("index key should have been deleted")
+ }
+
+ count, err := nameIndex.DeleteKey(ctx, "bob")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if count != 2 {
+ t.Fatal("wrong deleted count")
+ }
+ ok, _ = nameIndex.HasValue(ctx, "bob", "b1")
+ if ok {
+ t.Fatal("index not deleted")
+ }
+}
+
+func TestSyncIndex(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ nameIndex := createIndexer()
+
+ dstore := ds.NewMapDatastore()
+ refIndex := New(dstore, ds.NewKey("/ref"))
+ refIndex.Add(ctx, "alice", "a1")
+ refIndex.Add(ctx, "cathy", "zz")
+ refIndex.Add(ctx, "dennis", "d1")
+
+ changed, err := SyncIndex(ctx, refIndex, nameIndex)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !changed {
+ t.Error("change was not indicated")
+ }
+
+ // Create map of id->index in sync target
+ syncs := map[string]string{}
+ err = nameIndex.ForEach(ctx, "", func(key, value string) bool {
+ syncs[value] = key
+ return true
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Iterate items in sync source and make sure they appear in target
+ var itemCount int
+ err = refIndex.ForEach(ctx, "", func(key, value string) bool {
+ itemCount++
+ syncKey, ok := syncs[value]
+ if !ok || key != syncKey {
+ t.Fatal("key", key, "-->", value, "was not synced")
+ }
+ return true
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if itemCount != len(syncs) {
+ t.Fatal("different number of items in sync source and target")
+ }
+}
diff --git a/pinning/pinner/dspinner/pin.go b/pinning/pinner/dspinner/pin.go
new file mode 100644
index 0000000000..8168535652
--- /dev/null
+++ b/pinning/pinner/dspinner/pin.go
@@ -0,0 +1,1031 @@
+// Package dspinner implements structures and methods to keep track of
+// which objects a user wants to keep stored locally. This implementation
+// stores pin data in a datastore.
+package dspinner
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "path"
+ "sync"
+
+ "github.com/ipfs/go-cid"
+ ds "github.com/ipfs/go-datastore"
+ "github.com/ipfs/go-datastore/query"
+ ipld "github.com/ipfs/go-ipld-format"
+ "github.com/ipfs/boxo/ipld/merkledag"
+ "github.com/ipfs/boxo/ipld/merkledag/dagutils"
+ logging "github.com/ipfs/go-log"
+ "github.com/polydawn/refmt/cbor"
+ "github.com/polydawn/refmt/obj/atlas"
+
+ ipfspinner "github.com/ipfs/boxo/pinning/pinner"
+ "github.com/ipfs/boxo/pinning/pinner/dsindex"
+)
+
+const (
+ basePath = "/pins"
+ pinKeyPath = "/pins/pin"
+ indexKeyPath = "/pins/index"
+ dirtyKeyPath = "/pins/state/dirty"
+)
+
+var (
+ log logging.StandardLogger = logging.Logger("pin")
+
+ linkDirect, linkRecursive string
+
+ pinCidDIndexPath string
+ pinCidRIndexPath string
+ pinNameIndexPath string
+
+ dirtyKey = ds.NewKey(dirtyKeyPath)
+
+ pinAtl atlas.Atlas
+)
+
+func init() {
+ directStr, ok := ipfspinner.ModeToString(ipfspinner.Direct)
+ if !ok {
+ panic("could not find Direct pin enum")
+ }
+ linkDirect = directStr
+
+ recursiveStr, ok := ipfspinner.ModeToString(ipfspinner.Recursive)
+ if !ok {
+ panic("could not find Recursive pin enum")
+ }
+ linkRecursive = recursiveStr
+
+ pinCidRIndexPath = path.Join(indexKeyPath, "cidRindex")
+ pinCidDIndexPath = path.Join(indexKeyPath, "cidDindex")
+ pinNameIndexPath = path.Join(indexKeyPath, "nameIndex")
+
+ pinAtl = atlas.MustBuild(
+ atlas.BuildEntry(pin{}).StructMap().
+ AddField("Cid", atlas.StructMapEntry{SerialName: "cid"}).
+ AddField("Metadata", atlas.StructMapEntry{SerialName: "metadata", OmitEmpty: true}).
+ AddField("Mode", atlas.StructMapEntry{SerialName: "mode"}).
+ AddField("Name", atlas.StructMapEntry{SerialName: "name", OmitEmpty: true}).
+ Complete(),
+ atlas.BuildEntry(cid.Cid{}).Transform().
+ TransformMarshal(atlas.MakeMarshalTransformFunc(func(live cid.Cid) ([]byte, error) { return live.MarshalBinary() })).
+ TransformUnmarshal(atlas.MakeUnmarshalTransformFunc(func(serializable []byte) (cid.Cid, error) {
+ c := cid.Cid{}
+ err := c.UnmarshalBinary(serializable)
+ if err != nil {
+ return cid.Cid{}, err
+ }
+ return c, nil
+ })).Complete(),
+ )
+ pinAtl = pinAtl.WithMapMorphism(atlas.MapMorphism{KeySortMode: atlas.KeySortMode_Strings})
+}
+
+// pinner implements the Pinner interface
+type pinner struct {
+ autoSync bool
+ lock sync.RWMutex
+
+ dserv ipld.DAGService
+ dstore ds.Datastore
+
+ cidDIndex dsindex.Indexer
+ cidRIndex dsindex.Indexer
+ nameIndex dsindex.Indexer
+
+ clean int64
+ dirty int64
+}
+
+var _ ipfspinner.Pinner = (*pinner)(nil)
+
+type pin struct {
+ Id string
+ Cid cid.Cid
+ Metadata map[string]interface{}
+ Mode ipfspinner.Mode
+ Name string
+}
+
+func (p *pin) dsKey() ds.Key {
+ return ds.NewKey(path.Join(pinKeyPath, p.Id))
+}
+
+func newPin(c cid.Cid, mode ipfspinner.Mode, name string) *pin {
+ return &pin{
+ Id: path.Base(ds.RandomKey().String()),
+ Cid: c,
+ Name: name,
+ Mode: mode,
+ }
+}
+
+type syncDAGService interface {
+ ipld.DAGService
+ Sync() error
+}
+
+// New creates a new pinner and loads its keysets from the given datastore. If
+// there is no data present in the datastore, then an empty pinner is returned.
+//
+// By default, changes are automatically flushed to the datastore. This can be
+// disabled by calling SetAutosync(false), which will require that Flush be
+// called explicitly.
+func New(ctx context.Context, dstore ds.Datastore, dserv ipld.DAGService) (*pinner, error) {
+ p := &pinner{
+ autoSync: true,
+ cidDIndex: dsindex.New(dstore, ds.NewKey(pinCidDIndexPath)),
+ cidRIndex: dsindex.New(dstore, ds.NewKey(pinCidRIndexPath)),
+ nameIndex: dsindex.New(dstore, ds.NewKey(pinNameIndexPath)),
+ dserv: dserv,
+ dstore: dstore,
+ }
+
+ data, err := dstore.Get(ctx, dirtyKey)
+ if err != nil {
+ if err == ds.ErrNotFound {
+ return p, nil
+ }
+ return nil, fmt.Errorf("cannot load dirty flag: %v", err)
+ }
+ if data[0] == 1 {
+ p.dirty = 1
+
+ err = p.rebuildIndexes(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("cannot rebuild indexes: %v", err)
+ }
+ }
+
+ return p, nil
+}
+
+// SetAutosync allows auto-syncing to be enabled or disabled during runtime.
+// This may be used to turn off autosync before doing many repeated pinning
+// operations, and then turn it on after. Returns the previous value.
+func (p *pinner) SetAutosync(auto bool) bool {
+ p.lock.Lock()
+ defer p.lock.Unlock()
+
+ p.autoSync, auto = auto, p.autoSync
+ return auto
+}
+
+// Pin the given node, optionally recursive
+func (p *pinner) Pin(ctx context.Context, node ipld.Node, recurse bool) error {
+ err := p.dserv.Add(ctx, node)
+ if err != nil {
+ return err
+ }
+
+ if recurse {
+ return p.doPinRecursive(ctx, node.Cid(), true)
+ } else {
+ return p.doPinDirect(ctx, node.Cid())
+ }
+}
+
+func (p *pinner) doPinRecursive(ctx context.Context, c cid.Cid, fetch bool) error {
+ cidKey := c.KeyString()
+
+ p.lock.Lock()
+ defer p.lock.Unlock()
+
+ found, err := p.cidRIndex.HasAny(ctx, cidKey)
+ if err != nil {
+ return err
+ }
+ if found {
+ return nil
+ }
+
+ dirtyBefore := p.dirty
+
+ if fetch {
+ // temporary unlock to fetch the entire graph
+ p.lock.Unlock()
+ // Fetch graph starting at node identified by cid
+ err = merkledag.FetchGraph(ctx, c, p.dserv)
+ p.lock.Lock()
+ if err != nil {
+ return err
+ }
+ }
+
+ // If autosyncing, sync dag service before making any change to pins
+ err = p.flushDagService(ctx, false)
+ if err != nil {
+ return err
+ }
+
+ // Only look again if something has changed.
+ if p.dirty != dirtyBefore {
+ found, err = p.cidRIndex.HasAny(ctx, cidKey)
+ if err != nil {
+ return err
+ }
+ if found {
+ return nil
+ }
+ }
+
+ // TODO: remove this to support multiple pins per CID
+ found, err = p.cidDIndex.HasAny(ctx, cidKey)
+ if err != nil {
+ return err
+ }
+ if found {
+ _, err = p.removePinsForCid(ctx, c, ipfspinner.Direct)
+ if err != nil {
+ return err
+ }
+ }
+
+ _, err = p.addPin(ctx, c, ipfspinner.Recursive, "")
+ if err != nil {
+ return err
+ }
+ return p.flushPins(ctx, false)
+}
+
+func (p *pinner) doPinDirect(ctx context.Context, c cid.Cid) error {
+ cidKey := c.KeyString()
+
+ p.lock.Lock()
+ defer p.lock.Unlock()
+
+ found, err := p.cidRIndex.HasAny(ctx, cidKey)
+ if err != nil {
+ return err
+ }
+ if found {
+ return fmt.Errorf("%s already pinned recursively", c.String())
+ }
+
+ _, err = p.addPin(ctx, c, ipfspinner.Direct, "")
+ if err != nil {
+ return err
+ }
+
+ return p.flushPins(ctx, false)
+}
+
+func (p *pinner) addPin(ctx context.Context, c cid.Cid, mode ipfspinner.Mode, name string) (string, error) {
+ // Create new pin and store in datastore
+ pp := newPin(c, mode, name)
+
+ // Serialize pin
+ pinData, err := encodePin(pp)
+ if err != nil {
+ return "", fmt.Errorf("could not encode pin: %v", err)
+ }
+
+ p.setDirty(ctx)
+
+ // Store the pin
+ err = p.dstore.Put(ctx, pp.dsKey(), pinData)
+ if err != nil {
+ return "", err
+ }
+
+ // Store CID index
+ switch mode {
+ case ipfspinner.Recursive:
+ err = p.cidRIndex.Add(ctx, c.KeyString(), pp.Id)
+ case ipfspinner.Direct:
+ err = p.cidDIndex.Add(ctx, c.KeyString(), pp.Id)
+ default:
+ panic("pin mode must be recursive or direct")
+ }
+ if err != nil {
+ return "", fmt.Errorf("could not add pin cid index: %v", err)
+ }
+
+ if name != "" {
+ // Store name index
+ err = p.nameIndex.Add(ctx, name, pp.Id)
+ if err != nil {
+ if mode == ipfspinner.Recursive {
+ e := p.cidRIndex.Delete(ctx, c.KeyString(), pp.Id)
+ if e != nil {
+ log.Errorf("error deleting index: %s", e)
+ }
+ } else {
+ e := p.cidDIndex.Delete(ctx, c.KeyString(), pp.Id)
+ if e != nil {
+ log.Errorf("error deleting index: %s", e)
+ }
+ }
+ return "", fmt.Errorf("could not add pin name index: %v", err)
+ }
+ }
+
+ return pp.Id, nil
+}
+
+func (p *pinner) removePin(ctx context.Context, pp *pin) error {
+ p.setDirty(ctx)
+ var err error
+
+ // Remove cid index from datastore
+ if pp.Mode == ipfspinner.Recursive {
+ err = p.cidRIndex.Delete(ctx, pp.Cid.KeyString(), pp.Id)
+ } else {
+ err = p.cidDIndex.Delete(ctx, pp.Cid.KeyString(), pp.Id)
+ }
+ if err != nil {
+ return err
+ }
+
+ if pp.Name != "" {
+ // Remove name index from datastore
+ err = p.nameIndex.Delete(ctx, pp.Name, pp.Id)
+ if err != nil {
+ return err
+ }
+ }
+
+ // The pin is removed last so that an incomplete remove is detected by a
+ // pin that has a missing index.
+ err = p.dstore.Delete(ctx, pp.dsKey())
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// Unpin a given key
+func (p *pinner) Unpin(ctx context.Context, c cid.Cid, recursive bool) error {
+ cidKey := c.KeyString()
+
+ p.lock.Lock()
+ defer p.lock.Unlock()
+
+ // TODO: use Ls() to lookup pins when new pinning API available
+ /*
+ matchSpec := map[string][]string {
+ "cid": []string{c.String}
+ }
+ matches := p.Ls(matchSpec)
+ */
+ has, err := p.cidRIndex.HasAny(ctx, cidKey)
+ if err != nil {
+ return err
+ }
+
+ if has {
+ if !recursive {
+ return fmt.Errorf("%s is pinned recursively", c.String())
+ }
+ } else {
+ has, err = p.cidDIndex.HasAny(ctx, cidKey)
+ if err != nil {
+ return err
+ }
+ if !has {
+ return ipfspinner.ErrNotPinned
+ }
+ }
+
+ removed, err := p.removePinsForCid(ctx, c, ipfspinner.Any)
+ if err != nil {
+ return err
+ }
+ if !removed {
+ return nil
+ }
+
+ return p.flushPins(ctx, false)
+}
+
+// IsPinned returns whether or not the given key is pinned
+// and an explanation of why its pinned
+func (p *pinner) IsPinned(ctx context.Context, c cid.Cid) (string, bool, error) {
+ p.lock.RLock()
+ defer p.lock.RUnlock()
+ return p.isPinnedWithType(ctx, c, ipfspinner.Any)
+}
+
+// IsPinnedWithType returns whether or not the given cid is pinned with the
+// given pin type, as well as returning the type of pin its pinned with.
+func (p *pinner) IsPinnedWithType(ctx context.Context, c cid.Cid, mode ipfspinner.Mode) (string, bool, error) {
+ p.lock.RLock()
+ defer p.lock.RUnlock()
+ return p.isPinnedWithType(ctx, c, mode)
+}
+
+func (p *pinner) isPinnedWithType(ctx context.Context, c cid.Cid, mode ipfspinner.Mode) (string, bool, error) {
+ cidKey := c.KeyString()
+ switch mode {
+ case ipfspinner.Recursive:
+ has, err := p.cidRIndex.HasAny(ctx, cidKey)
+ if err != nil {
+ return "", false, err
+ }
+ if has {
+ return linkRecursive, true, nil
+ }
+ return "", false, nil
+ case ipfspinner.Direct:
+ has, err := p.cidDIndex.HasAny(ctx, cidKey)
+ if err != nil {
+ return "", false, err
+ }
+ if has {
+ return linkDirect, true, nil
+ }
+ return "", false, nil
+ case ipfspinner.Internal:
+ return "", false, nil
+ case ipfspinner.Indirect:
+ case ipfspinner.Any:
+ has, err := p.cidRIndex.HasAny(ctx, cidKey)
+ if err != nil {
+ return "", false, err
+ }
+ if has {
+ return linkRecursive, true, nil
+ }
+ has, err = p.cidDIndex.HasAny(ctx, cidKey)
+ if err != nil {
+ return "", false, err
+ }
+ if has {
+ return linkDirect, true, nil
+ }
+ default:
+ err := fmt.Errorf(
+ "invalid Pin Mode '%d', must be one of {%d, %d, %d, %d, %d}",
+ mode, ipfspinner.Direct, ipfspinner.Indirect, ipfspinner.Recursive,
+ ipfspinner.Internal, ipfspinner.Any)
+ return "", false, err
+ }
+
+ // Default is Indirect
+ visitedSet := cid.NewSet()
+
+ // No index for given CID, so search children of all recursive pinned CIDs
+ var has bool
+ var rc cid.Cid
+ var e error
+ err := p.cidRIndex.ForEach(ctx, "", func(key, value string) bool {
+ rc, e = cid.Cast([]byte(key))
+ if e != nil {
+ return false
+ }
+ has, e = hasChild(ctx, p.dserv, rc, c, visitedSet.Visit)
+ if e != nil {
+ return false
+ }
+ if has {
+ return false
+ }
+ return true
+ })
+ if err != nil {
+ return "", false, err
+ }
+ if e != nil {
+ return "", false, e
+ }
+
+ if has {
+ return rc.String(), true, nil
+ }
+
+ return "", false, nil
+}
+
+// CheckIfPinned checks if a set of keys are pinned, more efficient than
+// calling IsPinned for each key, returns the pinned status of cid(s)
+//
+// TODO: If a CID is pinned by multiple pins, should they all be reported?
+func (p *pinner) CheckIfPinned(ctx context.Context, cids ...cid.Cid) ([]ipfspinner.Pinned, error) {
+ pinned := make([]ipfspinner.Pinned, 0, len(cids))
+ toCheck := cid.NewSet()
+
+ p.lock.RLock()
+ defer p.lock.RUnlock()
+
+ // First check for non-Indirect pins directly
+ for _, c := range cids {
+ cidKey := c.KeyString()
+ has, err := p.cidRIndex.HasAny(ctx, cidKey)
+ if err != nil {
+ return nil, err
+ }
+ if has {
+ pinned = append(pinned, ipfspinner.Pinned{Key: c, Mode: ipfspinner.Recursive})
+ } else {
+ has, err = p.cidDIndex.HasAny(ctx, cidKey)
+ if err != nil {
+ return nil, err
+ }
+ if has {
+ pinned = append(pinned, ipfspinner.Pinned{Key: c, Mode: ipfspinner.Direct})
+ } else {
+ toCheck.Add(c)
+ }
+ }
+ }
+
+ var e error
+ visited := cid.NewSet()
+ err := p.cidRIndex.ForEach(ctx, "", func(key, value string) bool {
+ var rk cid.Cid
+ rk, e = cid.Cast([]byte(key))
+ if e != nil {
+ return false
+ }
+ e = merkledag.Walk(ctx, merkledag.GetLinksWithDAG(p.dserv), rk, func(c cid.Cid) bool {
+ if toCheck.Len() == 0 || !visited.Visit(c) {
+ return false
+ }
+
+ if toCheck.Has(c) {
+ pinned = append(pinned, ipfspinner.Pinned{Key: c, Mode: ipfspinner.Indirect, Via: rk})
+ toCheck.Remove(c)
+ }
+
+ return true
+ }, merkledag.Concurrent())
+ if e != nil {
+ return false
+ }
+ return toCheck.Len() > 0
+ })
+ if err != nil {
+ return nil, err
+ }
+ if e != nil {
+ return nil, e
+ }
+
+ // Anything left in toCheck is not pinned
+ for _, k := range toCheck.Keys() {
+ pinned = append(pinned, ipfspinner.Pinned{Key: k, Mode: ipfspinner.NotPinned})
+ }
+
+ return pinned, nil
+}
+
+// removePinsForCid removes all pins for a cid that has the specified mode.
+// Returns true if any pins, and all corresponding CID index entries, were
+// removed. Otherwise, returns false.
+func (p *pinner) removePinsForCid(ctx context.Context, c cid.Cid, mode ipfspinner.Mode) (bool, error) {
+ // Search for pins by CID
+ var ids []string
+ var err error
+ cidKey := c.KeyString()
+ switch mode {
+ case ipfspinner.Recursive:
+ ids, err = p.cidRIndex.Search(ctx, cidKey)
+ case ipfspinner.Direct:
+ ids, err = p.cidDIndex.Search(ctx, cidKey)
+ case ipfspinner.Any:
+ ids, err = p.cidRIndex.Search(ctx, cidKey)
+ if err != nil {
+ return false, err
+ }
+ dIds, err := p.cidDIndex.Search(ctx, cidKey)
+ if err != nil {
+ return false, err
+ }
+ if len(dIds) != 0 {
+ ids = append(ids, dIds...)
+ }
+ }
+ if err != nil {
+ return false, err
+ }
+
+ var removed bool
+
+ // Remove the pin with the requested mode
+ for _, pid := range ids {
+ var pp *pin
+ pp, err = p.loadPin(ctx, pid)
+ if err != nil {
+ if err == ds.ErrNotFound {
+ p.setDirty(ctx)
+ // Fix index; remove index for pin that does not exist
+ switch mode {
+ case ipfspinner.Recursive:
+ _, err = p.cidRIndex.DeleteKey(ctx, cidKey)
+ if err != nil {
+ return false, fmt.Errorf("error deleting index: %s", err)
+ }
+ case ipfspinner.Direct:
+ _, err = p.cidDIndex.DeleteKey(ctx, cidKey)
+ if err != nil {
+ return false, fmt.Errorf("error deleting index: %s", err)
+ }
+ case ipfspinner.Any:
+ _, err = p.cidRIndex.DeleteKey(ctx, cidKey)
+ if err != nil {
+ return false, fmt.Errorf("error deleting index: %s", err)
+ }
+ _, err = p.cidDIndex.DeleteKey(ctx, cidKey)
+ if err != nil {
+ return false, fmt.Errorf("error deleting index: %s", err)
+ }
+ }
+ if err = p.flushPins(ctx, true); err != nil {
+ return false, err
+ }
+ // Mark this as removed since it removed an index, which is
+ // what prevents determines if an item is pinned.
+ removed = true
+ log.Error("found CID index with missing pin")
+ continue
+ }
+ return false, err
+ }
+ if mode == ipfspinner.Any || pp.Mode == mode {
+ err = p.removePin(ctx, pp)
+ if err != nil {
+ return false, err
+ }
+ removed = true
+ }
+ }
+ return removed, nil
+}
+
+// loadPin loads a single pin from the datastore.
+func (p *pinner) loadPin(ctx context.Context, pid string) (*pin, error) {
+ pinData, err := p.dstore.Get(ctx, ds.NewKey(path.Join(pinKeyPath, pid)))
+ if err != nil {
+ return nil, err
+ }
+ return decodePin(pid, pinData)
+}
+
+// DirectKeys returns a slice containing the directly pinned keys
+func (p *pinner) DirectKeys(ctx context.Context) ([]cid.Cid, error) {
+ p.lock.RLock()
+ defer p.lock.RUnlock()
+
+ cidSet := cid.NewSet()
+ var e error
+ err := p.cidDIndex.ForEach(ctx, "", func(key, value string) bool {
+ var c cid.Cid
+ c, e = cid.Cast([]byte(key))
+ if e != nil {
+ return false
+ }
+ cidSet.Add(c)
+ return true
+ })
+ if err != nil {
+ return nil, err
+ }
+ if e != nil {
+ return nil, e
+ }
+
+ return cidSet.Keys(), nil
+}
+
+// RecursiveKeys returns a slice containing the recursively pinned keys
+func (p *pinner) RecursiveKeys(ctx context.Context) ([]cid.Cid, error) {
+ p.lock.RLock()
+ defer p.lock.RUnlock()
+
+ cidSet := cid.NewSet()
+ var e error
+ err := p.cidRIndex.ForEach(ctx, "", func(key, value string) bool {
+ var c cid.Cid
+ c, e = cid.Cast([]byte(key))
+ if e != nil {
+ return false
+ }
+ cidSet.Add(c)
+ return true
+ })
+ if err != nil {
+ return nil, err
+ }
+ if e != nil {
+ return nil, e
+ }
+
+ return cidSet.Keys(), nil
+}
+
+// InternalPins returns all cids kept pinned for the internal state of the
+// pinner
+func (p *pinner) InternalPins(ctx context.Context) ([]cid.Cid, error) {
+ return nil, nil
+}
+
+// Update updates a recursive pin from one cid to another. This is equivalent
+// to pinning the new one and unpinning the old one.
+//
+// TODO: This will not work when multiple pins are supported
+func (p *pinner) Update(ctx context.Context, from, to cid.Cid, unpin bool) error {
+ p.lock.Lock()
+ defer p.lock.Unlock()
+
+ found, err := p.cidRIndex.HasAny(ctx, from.KeyString())
+ if err != nil {
+ return err
+ }
+ if !found {
+ return errors.New("'from' cid was not recursively pinned already")
+ }
+
+ // If `from` already recursively pinned and `to` is the same, then all done
+ if from == to {
+ return nil
+ }
+
+ // Check if the `to` cid is already recursively pinned
+ found, err = p.cidRIndex.HasAny(ctx, to.KeyString())
+ if err != nil {
+ return err
+ }
+ if found {
+ return errors.New("'to' cid was already recursively pinned")
+ }
+
+ // Temporarily unlock while we fetch the differences.
+ p.lock.Unlock()
+ err = dagutils.DiffEnumerate(ctx, p.dserv, from, to)
+ p.lock.Lock()
+
+ if err != nil {
+ return err
+ }
+
+ _, err = p.addPin(ctx, to, ipfspinner.Recursive, "")
+ if err != nil {
+ return err
+ }
+
+ if unpin {
+ _, err = p.removePinsForCid(ctx, from, ipfspinner.Recursive)
+ if err != nil {
+ return err
+ }
+ }
+
+ return p.flushPins(ctx, false)
+}
+
+func (p *pinner) flushDagService(ctx context.Context, force bool) error {
+ if !p.autoSync && !force {
+ return nil
+ }
+ if syncDServ, ok := p.dserv.(syncDAGService); ok {
+ if err := syncDServ.Sync(); err != nil {
+ return fmt.Errorf("cannot sync pinned data: %v", err)
+ }
+ }
+ return nil
+}
+
+func (p *pinner) flushPins(ctx context.Context, force bool) error {
+ if !p.autoSync && !force {
+ return nil
+ }
+ if err := p.dstore.Sync(ctx, ds.NewKey(basePath)); err != nil {
+ return fmt.Errorf("cannot sync pin state: %v", err)
+ }
+ p.setClean(ctx)
+ return nil
+}
+
+// Flush encodes and writes pinner keysets to the datastore
+func (p *pinner) Flush(ctx context.Context) error {
+ p.lock.Lock()
+ defer p.lock.Unlock()
+
+ err := p.flushDagService(ctx, true)
+ if err != nil {
+ return err
+ }
+
+ return p.flushPins(ctx, true)
+}
+
+// PinWithMode allows the user to have fine grained control over pin
+// counts
+func (p *pinner) PinWithMode(ctx context.Context, c cid.Cid, mode ipfspinner.Mode) error {
+ // TODO: remove his to support multiple pins per CID
+ switch mode {
+ case ipfspinner.Recursive:
+ return p.doPinRecursive(ctx, c, false)
+ case ipfspinner.Direct:
+ return p.doPinDirect(ctx, c)
+ default:
+ return fmt.Errorf("unrecognized pin mode")
+ }
+}
+
+// hasChild recursively looks for a Cid among the children of a root Cid.
+// The visit function can be used to shortcut already-visited branches.
+func hasChild(ctx context.Context, ng ipld.NodeGetter, root cid.Cid, child cid.Cid, visit func(cid.Cid) bool) (bool, error) {
+ links, err := ipld.GetLinks(ctx, ng, root)
+ if err != nil {
+ return false, err
+ }
+ for _, lnk := range links {
+ c := lnk.Cid
+ if lnk.Cid.Equals(child) {
+ return true, nil
+ }
+ if visit(c) {
+ has, err := hasChild(ctx, ng, c, child, visit)
+ if err != nil {
+ return false, err
+ }
+
+ if has {
+ return has, nil
+ }
+ }
+ }
+ return false, nil
+}
+
+func encodePin(p *pin) ([]byte, error) {
+ b, err := cbor.MarshalAtlased(p, pinAtl)
+ if err != nil {
+ return nil, err
+ }
+ return b, nil
+}
+
+func decodePin(pid string, data []byte) (*pin, error) {
+ p := &pin{Id: pid}
+ err := cbor.UnmarshalAtlased(cbor.DecodeOptions{}, data, p, pinAtl)
+ if err != nil {
+ return nil, err
+ }
+ return p, nil
+}
+
+// setDirty updates the dirty counter and saves a dirty state in the datastore
+// if the state was previously clean
+func (p *pinner) setDirty(ctx context.Context) {
+ wasClean := p.dirty == p.clean
+ p.dirty++
+
+ if !wasClean {
+ return // do not save; was already dirty
+ }
+
+ data := []byte{1}
+ err := p.dstore.Put(ctx, dirtyKey, data)
+ if err != nil {
+ log.Errorf("failed to set pin dirty flag: %s", err)
+ return
+ }
+ err = p.dstore.Sync(ctx, dirtyKey)
+ if err != nil {
+ log.Errorf("failed to sync pin dirty flag: %s", err)
+ }
+}
+
+// setClean saves a clean state value in the datastore if the state was
+// previously dirty
+func (p *pinner) setClean(ctx context.Context) {
+ if p.dirty == p.clean {
+ return // already clean
+ }
+
+ data := []byte{0}
+ err := p.dstore.Put(ctx, dirtyKey, data)
+ if err != nil {
+ log.Errorf("failed to set clear dirty flag: %s", err)
+ return
+ }
+ if err = p.dstore.Sync(ctx, dirtyKey); err != nil {
+ log.Errorf("failed to sync cleared pin dirty flag: %s", err)
+ return
+ }
+ p.clean = p.dirty // set clean
+}
+
+// sync datastore after every 50 cid repairs
+const syncRepairFrequency = 50
+
+// rebuildIndexes uses the stored pins to rebuild secondary indexes. This
+// resolves any discrepancy between secondary indexes and pins that could
+// result from a program termination between saving the two.
+func (p *pinner) rebuildIndexes(ctx context.Context) error {
+ // Load all pins from the datastore.
+ q := query.Query{
+ Prefix: pinKeyPath,
+ }
+ results, err := p.dstore.Query(ctx, q)
+ if err != nil {
+ return err
+ }
+ defer results.Close()
+
+ var checkedCount, repairedCount int
+
+ // Iterate all pins and check if the corresponding recursive or direct
+ // index is missing. If the index is missing then create the index.
+ for r := range results.Next() {
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+ if r.Error != nil {
+ return fmt.Errorf("cannot read index: %v", r.Error)
+ }
+ ent := r.Entry
+ pp, err := decodePin(path.Base(ent.Key), ent.Value)
+ if err != nil {
+ return err
+ }
+
+ indexKey := pp.Cid.KeyString()
+
+ var indexer, staleIndexer dsindex.Indexer
+ var idxrName, staleIdxrName string
+ if pp.Mode == ipfspinner.Recursive {
+ indexer = p.cidRIndex
+ staleIndexer = p.cidDIndex
+ idxrName = linkRecursive
+ staleIdxrName = linkDirect
+ } else if pp.Mode == ipfspinner.Direct {
+ indexer = p.cidDIndex
+ staleIndexer = p.cidRIndex
+ idxrName = linkDirect
+ staleIdxrName = linkRecursive
+ } else {
+ log.Error("unrecognized pin mode:", pp.Mode)
+ continue
+ }
+
+ // Remove any stale index from unused indexer
+ ok, err := staleIndexer.HasValue(ctx, indexKey, pp.Id)
+ if err != nil {
+ return err
+ }
+ if ok {
+ // Delete any stale index
+ log.Errorf("deleting stale %s pin index for cid %v", staleIdxrName, pp.Cid.String())
+ if err = staleIndexer.Delete(ctx, indexKey, pp.Id); err != nil {
+ return err
+ }
+ }
+
+ // Check that the indexer indexes this pin
+ ok, err = indexer.HasValue(ctx, indexKey, pp.Id)
+ if err != nil {
+ return err
+ }
+
+ var repaired bool
+ if !ok {
+ // Do not rebuild if index has an old value with leading slash
+ ok, err = indexer.HasValue(ctx, indexKey, "/"+pp.Id)
+ if err != nil {
+ return err
+ }
+ if !ok {
+ log.Errorf("repairing %s pin index for cid: %s", idxrName, pp.Cid.String())
+ // There was no index found for this pin. This was either an
+ // incomplete add or and incomplete delete of a pin. Either
+ // way, restore the index to complete the add or to undo the
+ // incomplete delete.
+ if err = indexer.Add(ctx, indexKey, pp.Id); err != nil {
+ return err
+ }
+ repaired = true
+ }
+ }
+ // Check for missing name index
+ if pp.Name != "" {
+ ok, err = p.nameIndex.HasValue(ctx, pp.Name, pp.Id)
+ if err != nil {
+ return err
+ }
+ if !ok {
+ log.Errorf("repairing name pin index for cid: %s", pp.Cid.String())
+ if err = p.nameIndex.Add(ctx, pp.Name, pp.Id); err != nil {
+ return err
+ }
+ }
+ repaired = true
+ }
+
+ if repaired {
+ repairedCount++
+ }
+ checkedCount++
+ if checkedCount%syncRepairFrequency == 0 {
+ p.flushPins(ctx, true)
+ }
+ }
+
+ log.Errorf("checked %d pins for invalid indexes, repaired %d pins", checkedCount, repairedCount)
+ return p.flushPins(ctx, true)
+}
diff --git a/pinning/pinner/dspinner/pin_test.go b/pinning/pinner/dspinner/pin_test.go
new file mode 100644
index 0000000000..edf315fcbb
--- /dev/null
+++ b/pinning/pinner/dspinner/pin_test.go
@@ -0,0 +1,1347 @@
+package dspinner
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "path"
+ "testing"
+ "time"
+
+ bs "github.com/ipfs/boxo/blockservice"
+ mdag "github.com/ipfs/boxo/ipld/merkledag"
+
+ blockstore "github.com/ipfs/boxo/blockstore"
+ offline "github.com/ipfs/boxo/exchange/offline"
+ util "github.com/ipfs/boxo/util"
+ cid "github.com/ipfs/go-cid"
+ ds "github.com/ipfs/go-datastore"
+ "github.com/ipfs/go-datastore/query"
+ dssync "github.com/ipfs/go-datastore/sync"
+ lds "github.com/ipfs/go-ds-leveldb"
+ ipld "github.com/ipfs/go-ipld-format"
+ logging "github.com/ipfs/go-log"
+
+ ipfspin "github.com/ipfs/boxo/pinning/pinner"
+)
+
+var rand = util.NewTimeSeededRand()
+
+type fakeLogger struct {
+ logging.StandardLogger
+ lastError error
+}
+
+func (f *fakeLogger) Error(args ...interface{}) {
+ f.lastError = errors.New(fmt.Sprint(args...))
+}
+
+func (f *fakeLogger) Errorf(format string, args ...interface{}) {
+ f.lastError = fmt.Errorf(format, args...)
+}
+
+func randNode() (*mdag.ProtoNode, cid.Cid) {
+ nd := new(mdag.ProtoNode)
+ nd.SetData(make([]byte, 32))
+ _, err := io.ReadFull(rand, nd.Data())
+ if err != nil {
+ panic(err)
+ }
+ k := nd.Cid()
+ return nd, k
+}
+
+func assertPinned(t *testing.T, p ipfspin.Pinner, c cid.Cid, failmsg string) {
+ _, pinned, err := p.IsPinned(context.Background(), c)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !pinned {
+ t.Fatal(failmsg)
+ }
+}
+
+func assertPinnedWithType(t *testing.T, p ipfspin.Pinner, c cid.Cid, mode ipfspin.Mode, failmsg string) {
+ modeText, pinned, err := p.IsPinnedWithType(context.Background(), c, mode)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expect, ok := ipfspin.ModeToString(mode)
+ if !ok {
+ t.Fatal("unrecognized pin mode")
+ }
+
+ if !pinned {
+ t.Fatal(failmsg)
+ }
+
+ if mode == ipfspin.Any {
+ return
+ }
+
+ if expect != modeText {
+ t.Fatal("expected", expect, "pin, got", modeText)
+ }
+}
+
+func assertUnpinned(t *testing.T, p ipfspin.Pinner, c cid.Cid, failmsg string) {
+ _, pinned, err := p.IsPinned(context.Background(), c)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if pinned {
+ t.Fatal(failmsg)
+ }
+}
+
+func TestPinnerBasic(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dstore := dssync.MutexWrap(ds.NewMapDatastore())
+ bstore := blockstore.NewBlockstore(dstore)
+ bserv := bs.New(bstore, offline.Exchange(bstore))
+
+ dserv := mdag.NewDAGService(bserv)
+
+ p, err := New(ctx, dstore, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ a, ak := randNode()
+ err = dserv.Add(ctx, a)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Pin A{}
+ err = p.Pin(ctx, a, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertPinned(t, p, ak, "Failed to find key")
+ assertPinnedWithType(t, p, ak, ipfspin.Direct, "Expected direct pin")
+
+ // create new node c, to be indirectly pinned through b
+ c, _ := randNode()
+ err = dserv.Add(ctx, c)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ck := c.Cid()
+
+ // Create new node b, to be parent to a and c
+ b, _ := randNode()
+ err = b.AddNodeLink("child", a)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = b.AddNodeLink("otherchild", c)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = dserv.Add(ctx, b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ bk := b.Cid()
+
+ // recursively pin B{A,C}
+ err = p.Pin(ctx, b, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertPinned(t, p, ck, "child of recursively pinned node not found")
+
+ assertPinned(t, p, bk, "Pinned node not found")
+ assertPinnedWithType(t, p, bk, ipfspin.Recursive, "Recursively pinned node not found")
+
+ d, _ := randNode()
+ err = d.AddNodeLink("a", a)
+ if err != nil {
+ panic(err)
+ }
+ err = d.AddNodeLink("c", c)
+ if err != nil {
+ panic(err)
+ }
+
+ e, _ := randNode()
+ err = d.AddNodeLink("e", e)
+ if err != nil {
+ panic(err)
+ }
+
+ // Must be in dagserv for unpin to work
+ err = dserv.Add(ctx, e)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = dserv.Add(ctx, d)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Add D{A,C,E}
+ err = p.Pin(ctx, d, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ dk := d.Cid()
+ assertPinned(t, p, dk, "pinned node not found.")
+
+ cids, err := p.RecursiveKeys(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(cids) != 2 {
+ t.Error("expected 2 recursive pins")
+ }
+ if !(bk == cids[0] || bk == cids[1]) {
+ t.Error("expected recursive pin of B")
+ }
+ if !(dk == cids[0] || dk == cids[1]) {
+ t.Error("expected recursive pin of D")
+ }
+
+ pinned, err := p.CheckIfPinned(ctx, ak, bk, ck, dk)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(pinned) != 4 {
+ t.Error("incorrect number of results")
+ }
+ for _, pn := range pinned {
+ switch pn.Key {
+ case ak:
+ if pn.Mode != ipfspin.Direct {
+ t.Error("A pinned with wrong mode")
+ }
+ case bk:
+ if pn.Mode != ipfspin.Recursive {
+ t.Error("B pinned with wrong mode")
+ }
+ case ck:
+ if pn.Mode != ipfspin.Indirect {
+ t.Error("C should be pinned indirectly")
+ }
+ if pn.Via != dk && pn.Via != bk {
+ t.Error("C should be pinned via D or B")
+ }
+ case dk:
+ if pn.Mode != ipfspin.Recursive {
+ t.Error("D pinned with wrong mode")
+ }
+ }
+ }
+
+ cids, err = p.DirectKeys(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(cids) != 1 {
+ t.Error("expected 1 direct pin")
+ }
+ if cids[0] != ak {
+ t.Error("wrong direct pin")
+ }
+
+ cids, _ = p.InternalPins(ctx)
+ if len(cids) != 0 {
+ t.Error("shound not have internal keys")
+ }
+
+ err = p.Unpin(ctx, dk, false)
+ if err == nil {
+ t.Fatal("expected error unpinning recursive pin without specifying recursive")
+ }
+
+ // Test recursive unpin
+ err = p.Unpin(ctx, dk, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = p.Unpin(ctx, dk, true)
+ if err != ipfspin.ErrNotPinned {
+ t.Fatal("expected error:", ipfspin.ErrNotPinned)
+ }
+
+ err = p.Flush(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ p, err = New(ctx, dstore, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Test directly pinned
+ assertPinned(t, p, ak, "Could not find pinned node!")
+
+ // Test recursively pinned
+ assertPinned(t, p, bk, "could not find recursively pinned node")
+
+ // Remove the pin but not the index to simulate corruption
+ ids, err := p.cidDIndex.Search(ctx, ak.KeyString())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(ids) == 0 {
+ t.Fatal("did not find pin for cid", ak.String())
+ }
+ pp, err := p.loadPin(ctx, ids[0])
+ if err != nil {
+ t.Fatal(err)
+ }
+ if pp.Mode != ipfspin.Direct {
+ t.Error("loaded pin has wrong mode")
+ }
+ if pp.Cid != ak {
+ t.Error("loaded pin has wrong cid")
+ }
+ err = p.dstore.Delete(ctx, pp.dsKey())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ realLog := log
+ fakeLog := &fakeLogger{}
+ fakeLog.StandardLogger = log
+ log = fakeLog
+ err = p.Pin(ctx, a, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if fakeLog.lastError == nil {
+ t.Error("expected error to be logged")
+ } else if fakeLog.lastError.Error() != "found CID index with missing pin" {
+ t.Error("did not get expected log message")
+ }
+
+ log = realLog
+}
+
+func TestAddLoadPin(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dstore := dssync.MutexWrap(ds.NewMapDatastore())
+ bstore := blockstore.NewBlockstore(dstore)
+ bserv := bs.New(bstore, offline.Exchange(bstore))
+
+ dserv := mdag.NewDAGService(bserv)
+
+ p, err := New(ctx, dstore, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ a, ak := randNode()
+ err = dserv.Add(ctx, a)
+ if err != nil {
+ panic(err)
+ }
+
+ mode := ipfspin.Recursive
+ name := "my-pin"
+ pid, err := p.addPin(ctx, ak, mode, name)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Load pin and check that data decoded correctly
+ pinData, err := p.loadPin(ctx, pid)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if pinData.Mode != mode {
+ t.Error("worng pin mode")
+ }
+ if pinData.Cid != ak {
+ t.Error("wrong pin cid")
+ }
+ if pinData.Name != name {
+ t.Error("wrong pin name; expected", name, "got", pinData.Name)
+ }
+}
+
+func TestIsPinnedLookup(t *testing.T) {
+ // Test that lookups work in pins which share
+ // the same branches. For that construct this tree:
+ //
+ // A5->A4->A3->A2->A1->A0
+ // / /
+ // B------- /
+ // \ /
+ // C---------------
+ //
+ // This ensures that IsPinned works for all objects both when they
+ // are pinned and once they have been unpinned.
+ aBranchLen := 6
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ dstore := dssync.MutexWrap(ds.NewMapDatastore())
+ bstore := blockstore.NewBlockstore(dstore)
+ bserv := bs.New(bstore, offline.Exchange(bstore))
+
+ dserv := mdag.NewDAGService(bserv)
+
+ // Create new pinner. New will not load anything since there are
+ // no pins saved in the datastore yet.
+ p, err := New(ctx, dstore, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ aKeys, bk, ck, err := makeTree(ctx, aBranchLen, dserv, p)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertPinned(t, p, aKeys[0], "A0 should be pinned")
+ assertPinned(t, p, aKeys[1], "A1 should be pinned")
+ assertPinned(t, p, ck, "C should be pinned")
+ assertPinned(t, p, bk, "B should be pinned")
+
+ // Unpin A5 recursively
+ if err = p.Unpin(ctx, aKeys[5], true); err != nil {
+ t.Fatal(err)
+ }
+
+ assertPinned(t, p, aKeys[0], "A0 should still be pinned through B")
+ assertUnpinned(t, p, aKeys[4], "A4 should be unpinned")
+
+ // Unpin B recursively
+ if err = p.Unpin(ctx, bk, true); err != nil {
+ t.Fatal(err)
+ }
+ assertUnpinned(t, p, bk, "B should be unpinned")
+ assertUnpinned(t, p, aKeys[1], "A1 should be unpinned")
+ assertPinned(t, p, aKeys[0], "A0 should still be pinned through C")
+}
+
+func TestDuplicateSemantics(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ dstore := dssync.MutexWrap(ds.NewMapDatastore())
+ bstore := blockstore.NewBlockstore(dstore)
+ bserv := bs.New(bstore, offline.Exchange(bstore))
+
+ dserv := mdag.NewDAGService(bserv)
+
+ p, err := New(ctx, dstore, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ a, _ := randNode()
+ err = dserv.Add(ctx, a)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // pin is recursively
+ err = p.Pin(ctx, a, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // pinning directly should fail
+ err = p.Pin(ctx, a, false)
+ if err == nil {
+ t.Fatal("expected direct pin to fail")
+ }
+
+ // pinning recursively again should succeed
+ err = p.Pin(ctx, a, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestFlush(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ dstore := dssync.MutexWrap(ds.NewMapDatastore())
+ bstore := blockstore.NewBlockstore(dstore)
+ bserv := bs.New(bstore, offline.Exchange(bstore))
+
+ dserv := mdag.NewDAGService(bserv)
+ p, err := New(ctx, dstore, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, k := randNode()
+
+ p.PinWithMode(ctx, k, ipfspin.Recursive)
+ if err = p.Flush(ctx); err != nil {
+ t.Fatal(err)
+ }
+ assertPinned(t, p, k, "expected key to still be pinned")
+}
+
+func TestPinRecursiveFail(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ dstore := dssync.MutexWrap(ds.NewMapDatastore())
+ bstore := blockstore.NewBlockstore(dstore)
+ bserv := bs.New(bstore, offline.Exchange(bstore))
+ dserv := mdag.NewDAGService(bserv)
+
+ p, err := New(ctx, dstore, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ a, _ := randNode()
+ b, _ := randNode()
+ err = a.AddNodeLink("child", b)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // NOTE: This isnt a time based test, we expect the pin to fail
+ mctx, cancel := context.WithTimeout(ctx, time.Millisecond)
+ defer cancel()
+
+ err = p.Pin(mctx, a, true)
+ if err == nil {
+ t.Fatal("should have failed to pin here")
+ }
+
+ err = dserv.Add(ctx, b)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = dserv.Add(ctx, a)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // this one is time based... but shouldnt cause any issues
+ mctx, cancel = context.WithTimeout(ctx, time.Second)
+ defer cancel()
+ err = p.Pin(mctx, a, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestPinUpdate(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dstore := dssync.MutexWrap(ds.NewMapDatastore())
+ bstore := blockstore.NewBlockstore(dstore)
+ bserv := bs.New(bstore, offline.Exchange(bstore))
+
+ dserv := mdag.NewDAGService(bserv)
+ p, err := New(ctx, dstore, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+ n1, c1 := randNode()
+ n2, c2 := randNode()
+ _, c3 := randNode()
+
+ if err = dserv.Add(ctx, n1); err != nil {
+ t.Fatal(err)
+ }
+ if err = dserv.Add(ctx, n2); err != nil {
+ t.Fatal(err)
+ }
+
+ if err = p.Pin(ctx, n1, true); err != nil {
+ t.Fatal(err)
+ }
+
+ if err = p.Update(ctx, c1, c2, true); err != nil {
+ t.Fatal(err)
+ }
+
+ assertPinned(t, p, c2, "c2 should be pinned now")
+ assertUnpinned(t, p, c1, "c1 should no longer be pinned")
+
+ if err = p.Update(ctx, c2, c1, false); err != nil {
+ t.Fatal(err)
+ }
+
+ // Test updating same pin that is already pinned.
+ if err = p.Update(ctx, c2, c2, true); err != nil {
+ t.Fatal(err)
+ }
+ // Check that pin is still pinned.
+ _, ok, err := p.IsPinned(ctx, c2)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !ok {
+ t.Fatal("c2 should still be pinned")
+ }
+
+ // Test updating same pin that is not pinned.
+ if err = p.Update(ctx, c3, c3, false); err == nil {
+ t.Fatal("expected error updating unpinned cid")
+ }
+ _, ok, err = p.IsPinned(ctx, c3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if ok {
+ t.Fatal("c3 should not be pinned")
+ }
+
+ assertPinned(t, p, c2, "c2 should be pinned still")
+ assertPinned(t, p, c1, "c1 should be pinned now")
+}
+
+func TestLoadDirty(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dstore := dssync.MutexWrap(ds.NewMapDatastore())
+ bstore := blockstore.NewBlockstore(dstore)
+ bserv := bs.New(bstore, offline.Exchange(bstore))
+ dserv := mdag.NewDAGService(bserv)
+
+ p, err := New(ctx, dstore, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+ prev := p.SetAutosync(false)
+ if !prev {
+ t.Fatal("expected previous autosync to be true")
+ }
+ prev = p.SetAutosync(false)
+ if prev {
+ t.Fatal("expected previous autosync to be false")
+ }
+ prev = p.SetAutosync(true)
+ if prev {
+ t.Fatal("expected previous autosync to be false")
+ }
+
+ a, ak := randNode()
+ err = dserv.Add(ctx, a)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, bk := randNode()
+
+ err = p.Pin(ctx, a, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cidAKey := ak.KeyString()
+ cidBKey := bk.KeyString()
+
+ // Corrupt index
+ cidRIndex := p.cidRIndex
+ _, err = cidRIndex.DeleteKey(ctx, cidAKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = cidRIndex.Add(ctx, cidBKey, "not-a-pin-id")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Force dirty, since Pin syncs automatically
+ p.setDirty(ctx)
+
+ // Verify dirty
+ data, err := dstore.Get(ctx, dirtyKey)
+ if err != nil {
+ t.Fatalf("could not read dirty flag: %v", err)
+ }
+ if data[0] != 1 {
+ t.Fatal("dirty flag not set")
+ }
+
+ has, err := cidRIndex.HasAny(ctx, cidAKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if has {
+ t.Fatal("index should be deleted")
+ }
+
+ // Create new pinner on same datastore that was never flushed. This should
+ // detect the dirty flag and repair the indexes.
+ p, err = New(ctx, dstore, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Verify not dirty
+ data, err = dstore.Get(ctx, dirtyKey)
+ if err != nil {
+ t.Fatalf("could not read dirty flag: %v", err)
+ }
+ if data[0] != 0 {
+ t.Fatal("dirty flag is set")
+ }
+
+ // Verify index rebuilt
+ cidRIndex = p.cidRIndex
+ has, err = cidRIndex.HasAny(ctx, cidAKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !has {
+ t.Fatal("index should have been rebuilt")
+ }
+
+ has, err = p.removePinsForCid(ctx, bk, ipfspin.Any)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !has {
+ t.Fatal("expected Unpin to return true since index removed")
+ }
+}
+
+func TestEncodeDecodePin(t *testing.T) {
+ _, c := randNode()
+
+ pin := newPin(c, ipfspin.Recursive, "testpin")
+ pin.Metadata = make(map[string]interface{}, 2)
+ pin.Metadata["hello"] = "world"
+ pin.Metadata["foo"] = "bar"
+
+ encBytes, err := encodePin(pin)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ decPin, err := decodePin(pin.Id, encBytes)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if decPin.Id != pin.Id {
+ t.Errorf("wrong pin id: expect %q got %q", pin.Id, decPin.Id)
+ }
+ if decPin.Cid != pin.Cid {
+ t.Errorf("wrong pin cid: expect %q got %q", pin.Cid.String(), decPin.Cid.String())
+ }
+ if decPin.Mode != pin.Mode {
+ expect, _ := ipfspin.ModeToString(pin.Mode)
+ got, _ := ipfspin.ModeToString(decPin.Mode)
+ t.Errorf("wrong pin mode: expect %s got %s", expect, got)
+ }
+ if decPin.Name != pin.Name {
+ t.Errorf("wrong pin name: expect %q got %q", pin.Name, decPin.Name)
+ }
+ for key, val := range pin.Metadata {
+ dval, ok := decPin.Metadata[key]
+ if !ok {
+ t.Errorf("decoded pin missing metadata key %q", key)
+ }
+ if dval != val {
+ t.Errorf("wrong metadata value: expected %q got %q", val, dval)
+ }
+ }
+}
+
+func makeTree(ctx context.Context, aBranchLen int, dserv ipld.DAGService, p ipfspin.Pinner) (aKeys []cid.Cid, bk cid.Cid, ck cid.Cid, err error) {
+ if aBranchLen < 3 {
+ err = errors.New("set aBranchLen to at least 3")
+ return
+ }
+
+ aNodes := make([]*mdag.ProtoNode, aBranchLen)
+ aKeys = make([]cid.Cid, aBranchLen)
+ for i := 0; i < aBranchLen; i++ {
+ a, _ := randNode()
+ if i >= 1 {
+ if err = a.AddNodeLink("child", aNodes[i-1]); err != nil {
+ return
+ }
+ }
+
+ if err = dserv.Add(ctx, a); err != nil {
+ return
+ }
+ aNodes[i] = a
+ aKeys[i] = a.Cid()
+ }
+
+ // Pin last A recursively
+ if err = p.Pin(ctx, aNodes[aBranchLen-1], true); err != nil {
+ return
+ }
+
+ // Create node B and add A3 as child
+ b, _ := randNode()
+ if err = b.AddNodeLink("mychild", aNodes[3]); err != nil {
+ return
+ }
+
+ // Create C node
+ c, _ := randNode()
+ // Add A0 as child of C
+ if err = c.AddNodeLink("child", aNodes[0]); err != nil {
+ return
+ }
+
+ // Add C
+ if err = dserv.Add(ctx, c); err != nil {
+ return
+ }
+ ck = c.Cid()
+
+ // Add C to B and Add B
+ if err = b.AddNodeLink("myotherchild", c); err != nil {
+ return
+ }
+ if err = dserv.Add(ctx, b); err != nil {
+ return
+ }
+ bk = b.Cid()
+
+ // Pin C recursively
+ if err = p.Pin(ctx, c, true); err != nil {
+ return
+ }
+
+ // Pin B recursively
+ if err = p.Pin(ctx, b, true); err != nil {
+ return
+ }
+
+ if err = p.Flush(ctx); err != nil {
+ return
+ }
+
+ return
+}
+
+func makeNodes(count int, dserv ipld.DAGService) []ipld.Node {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ nodes := make([]ipld.Node, count)
+ for i := 0; i < count; i++ {
+ n, _ := randNode()
+ err := dserv.Add(ctx, n)
+ if err != nil {
+ panic(err)
+ }
+ nodes[i] = n
+ }
+ return nodes
+}
+
+func pinNodes(nodes []ipld.Node, p ipfspin.Pinner, recursive bool) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ var err error
+
+ for i := range nodes {
+ err = p.Pin(ctx, nodes[i], recursive)
+ if err != nil {
+ panic(err)
+ }
+ }
+ err = p.Flush(ctx)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func unpinNodes(nodes []ipld.Node, p ipfspin.Pinner) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ var err error
+
+ for i := range nodes {
+ err = p.Unpin(ctx, nodes[i].Cid(), true)
+ if err != nil {
+ panic(err)
+ }
+ }
+ err = p.Flush(ctx)
+ if err != nil {
+ panic(err)
+ }
+}
+
+type batchWrap struct {
+ ds.Datastore
+}
+
+func (d *batchWrap) Batch(_ context.Context) (ds.Batch, error) {
+ return ds.NewBasicBatch(d), nil
+}
+
+func makeStore() (ds.Datastore, ipld.DAGService) {
+ ldstore, err := lds.NewDatastore("", nil)
+ if err != nil {
+ panic(err)
+ }
+ dstore := &batchWrap{ldstore}
+ bstore := blockstore.NewBlockstore(dstore)
+ bserv := bs.New(bstore, offline.Exchange(bstore))
+ dserv := mdag.NewDAGService(bserv)
+ return dstore, dserv
+}
+
+// BenchmarkLoadRebuild loads a pinner that has some number of saved pins, and
+// compares the load time when rebuilding indexes to loading without rebuilding
+// indexes.
+func BenchmarkLoad(b *testing.B) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dstore, dserv := makeStore()
+ pinner, err := New(ctx, dstore, dserv)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ nodes := makeNodes(4096, dserv)
+ pinNodes(nodes, pinner, true)
+
+ b.Run("RebuildTrue", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ err = dstore.Put(ctx, dirtyKey, []byte{1})
+ if err != nil {
+ panic(err.Error())
+ }
+
+ _, err = New(ctx, dstore, dserv)
+ if err != nil {
+ panic(err.Error())
+ }
+ }
+ })
+
+ b.Run("RebuildFalse", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ err = dstore.Put(ctx, dirtyKey, []byte{0})
+ if err != nil {
+ panic(err.Error())
+ }
+
+ _, err = New(ctx, dstore, dserv)
+ if err != nil {
+ panic(err.Error())
+ }
+ }
+ })
+}
+
+// BenchmarkNthPins shows the time it takes to create/save 1 pin when a number
+// of other pins already exist. Each run in the series shows performance for
+// creating a pin in a larger number of existing pins.
+func BenchmarkNthPin(b *testing.B) {
+ dstore, dserv := makeStore()
+ pinner, err := New(context.Background(), dstore, dserv)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ for count := 1000; count <= 10000; count += 1000 {
+ b.Run(fmt.Sprint("PinDS-", count), func(b *testing.B) {
+ benchmarkNthPin(b, count, pinner, dserv)
+ })
+ }
+}
+
+func benchmarkNthPin(b *testing.B, count int, pinner ipfspin.Pinner, dserv ipld.DAGService) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ nodes := makeNodes(count, dserv)
+ pinNodes(nodes[:count-1], pinner, true)
+ b.ResetTimer()
+
+ which := count - 1
+ for i := 0; i < b.N; i++ {
+ // Pin the Nth node and Flush
+ err := pinner.Pin(ctx, nodes[which], true)
+ if err != nil {
+ panic(err)
+ }
+ err = pinner.Flush(ctx)
+ if err != nil {
+ panic(err)
+ }
+ // Unpin the nodes so that it can pinned next iter.
+ b.StopTimer()
+ err = pinner.Unpin(ctx, nodes[which].Cid(), true)
+ if err != nil {
+ panic(err)
+ }
+ err = pinner.Flush(ctx)
+ if err != nil {
+ panic(err)
+ }
+ b.StartTimer()
+ }
+}
+
+// BenchmarkNPins demonstrates creating individual pins. Each run in the
+// series shows performance for a larger number of individual pins.
+func BenchmarkNPins(b *testing.B) {
+ for count := 128; count < 16386; count <<= 1 {
+ b.Run(fmt.Sprint("PinDS-", count), func(b *testing.B) {
+ dstore, dserv := makeStore()
+ pinner, err := New(context.Background(), dstore, dserv)
+ if err != nil {
+ panic(err.Error())
+ }
+ benchmarkNPins(b, count, pinner, dserv)
+ })
+ }
+}
+
+func benchmarkNPins(b *testing.B, count int, pinner ipfspin.Pinner, dserv ipld.DAGService) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ nodes := makeNodes(count, dserv)
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ // Pin all the nodes one at a time.
+ for j := range nodes {
+ err := pinner.Pin(ctx, nodes[j], true)
+ if err != nil {
+ panic(err)
+ }
+ err = pinner.Flush(ctx)
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ // Unpin all nodes so that they can be pinned next iter.
+ b.StopTimer()
+ unpinNodes(nodes, pinner)
+ b.StartTimer()
+ }
+}
+
+// BenchmarkNUnpins demonstrates unpinning individual pins. Each run in the
+// series shows performance for a larger number of individual unpins.
+func BenchmarkNUnpins(b *testing.B) {
+ for count := 128; count < 16386; count <<= 1 {
+ b.Run(fmt.Sprint("UnpinDS-", count), func(b *testing.B) {
+ dstore, dserv := makeStore()
+ pinner, err := New(context.Background(), dstore, dserv)
+ if err != nil {
+ panic(err.Error())
+ }
+ benchmarkNUnpins(b, count, pinner, dserv)
+ })
+ }
+}
+
+func benchmarkNUnpins(b *testing.B, count int, pinner ipfspin.Pinner, dserv ipld.DAGService) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ nodes := makeNodes(count, dserv)
+ pinNodes(nodes, pinner, true)
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ for j := range nodes {
+ // Unpin nodes one at a time.
+ err := pinner.Unpin(ctx, nodes[j].Cid(), true)
+ if err != nil {
+ panic(err)
+ }
+ err = pinner.Flush(ctx)
+ if err != nil {
+ panic(err)
+ }
+ }
+ // Pin all nodes so that they can be unpinned next iter.
+ b.StopTimer()
+ pinNodes(nodes, pinner, true)
+ b.StartTimer()
+ }
+}
+
+// BenchmarkPinAllSeries shows times to pin all nodes with only one Flush at
+// the end.
+func BenchmarkPinAll(b *testing.B) {
+ for count := 128; count < 16386; count <<= 1 {
+ b.Run(fmt.Sprint("PinAllDS-", count), func(b *testing.B) {
+ dstore, dserv := makeStore()
+ pinner, err := New(context.Background(), dstore, dserv)
+ if err != nil {
+ panic(err)
+ }
+ benchmarkPinAll(b, count, pinner, dserv)
+ })
+ }
+}
+
+func benchmarkPinAll(b *testing.B, count int, pinner ipfspin.Pinner, dserv ipld.DAGService) {
+ nodes := makeNodes(count, dserv)
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ pinNodes(nodes, pinner, true)
+
+ b.StopTimer()
+ unpinNodes(nodes, pinner)
+ b.StartTimer()
+ }
+}
+
+func BenchmarkRebuild(b *testing.B) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dstore, dserv := makeStore()
+ pinIncr := 32768
+
+ for pins := pinIncr; pins <= pinIncr*5; pins += pinIncr {
+ pinner, err := New(ctx, dstore, dserv)
+ if err != nil {
+ panic(err.Error())
+ }
+ nodes := makeNodes(pinIncr, dserv)
+ pinNodes(nodes, pinner, true)
+
+ b.Run(fmt.Sprintf("Rebuild %d", pins), func(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ err = dstore.Put(ctx, dirtyKey, []byte{1})
+ if err != nil {
+ panic(err.Error())
+ }
+
+ _, err = New(ctx, dstore, dserv)
+ if err != nil {
+ panic(err.Error())
+ }
+ }
+ })
+ }
+}
+
+func TestCidIndex(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dstore, dserv := makeStore()
+ pinner, err := New(ctx, dstore, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+ nodes := makeNodes(1, dserv)
+ node := nodes[0]
+
+ c := node.Cid()
+ cidKey := c.KeyString()
+
+ // Pin the cid
+ pid, err := pinner.addPin(ctx, c, ipfspin.Recursive, "")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ t.Log("Added pin:", pid)
+ t.Log("CID index:", c.String(), "-->", pid)
+
+ // Check that the index exists
+ ok, err := pinner.cidRIndex.HasAny(ctx, cidKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !ok {
+ t.Fatal("R-index has no value for", cidKey)
+ }
+
+ // Check that searching for the cid returns a value
+ values, err := pinner.cidRIndex.Search(ctx, cidKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(values) != 1 {
+ t.Fatal("expect index to return one value")
+ }
+ if values[0] != pid {
+ t.Fatal("indexer should have has value", cidKey, "-->", pid)
+ }
+
+ // Check that index has specific value
+ ok, err = pinner.cidRIndex.HasValue(ctx, cidKey, pid)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !ok {
+ t.Fatal("indexer should have has value", cidKey, "-->", pid)
+ }
+
+ // Iterate values of index
+ var seen bool
+ err = pinner.cidRIndex.ForEach(ctx, "", func(key, value string) bool {
+ if seen {
+ t.Fatal("expected one key-value pair")
+ }
+ if key != cidKey {
+ t.Fatal("unexpected key:", key)
+ }
+ if value != pid {
+ t.Fatal("unexpected value:", value)
+ }
+ seen = true
+ return true
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Load all pins from the datastore.
+ q := query.Query{
+ Prefix: pinKeyPath,
+ }
+ results, err := pinner.dstore.Query(ctx, q)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer results.Close()
+
+ // Iterate all pins and check if the corresponding recursive or direct
+ // index is missing. If the index is missing then create the index.
+ seen = false
+ for r := range results.Next() {
+ if seen {
+ t.Fatal("has more than one pin")
+ }
+ if r.Error != nil {
+ t.Fatal(fmt.Errorf("cannot read index: %v", r.Error))
+ }
+ ent := r.Entry
+ pp, err := decodePin(path.Base(ent.Key), ent.Value)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Log("Found pin:", pp.Id)
+ if pp.Id != pid {
+ t.Fatal("ID of loaded pin is not the same known to indexer")
+ }
+ seen = true
+ }
+}
+
+func TestRebuild(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dstore, dserv := makeStore()
+ pinner, err := New(ctx, dstore, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+ nodes := makeNodes(3, dserv)
+ pinNodes(nodes, pinner, true)
+
+ c1 := nodes[0].Cid()
+ cid1Key := c1.KeyString()
+ c2 := nodes[1].Cid()
+ cid2Key := c2.KeyString()
+ c3 := nodes[2].Cid()
+ cid3Key := c3.KeyString()
+
+ // Get pin IDs
+ values, err := pinner.cidRIndex.Search(ctx, cid1Key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ pid1 := values[0]
+ values, err = pinner.cidRIndex.Search(ctx, cid2Key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ pid2 := values[0]
+ values, err = pinner.cidRIndex.Search(ctx, cid3Key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ pid3 := values[0]
+
+ // Corrupt by adding direct index when there is already a recursive index
+ err = pinner.cidDIndex.Add(ctx, cid1Key, pid1)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Corrupt index by deleting cid index 2 to simulate an incomplete add or delete
+ _, err = pinner.cidRIndex.DeleteKey(ctx, cid2Key)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Corrupt index by deleting pin to simulate corruption
+ var pp *pin
+ pp, err = pinner.loadPin(ctx, pid3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = pinner.dstore.Delete(ctx, pp.dsKey())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ pinner.setDirty(ctx)
+
+ // Rebuild indexes
+ pinner, err = New(ctx, dstore, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Verify that indexes have same values as before
+ err = verifyIndexValue(ctx, pinner, cid1Key, pid1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = verifyIndexValue(ctx, pinner, cid2Key, pid2)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = verifyIndexValue(ctx, pinner, cid3Key, pid3)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func verifyIndexValue(ctx context.Context, pinner *pinner, cidKey, expectedPid string) error {
+ values, err := pinner.cidRIndex.Search(ctx, cidKey)
+ if err != nil {
+ return err
+ }
+ if len(values) != 1 {
+ return errors.New("expected 1 value")
+ }
+ if expectedPid != values[0] {
+ return errors.New("index has wrong value")
+ }
+ ok, err := pinner.cidDIndex.HasAny(ctx, cidKey)
+ if err != nil {
+ return err
+ }
+ if ok {
+ return errors.New("should not have a direct index")
+ }
+ return nil
+}
diff --git a/pinning/pinner/dspinner/sync_test.go b/pinning/pinner/dspinner/sync_test.go
new file mode 100644
index 0000000000..311833a95c
--- /dev/null
+++ b/pinning/pinner/dspinner/sync_test.go
@@ -0,0 +1,88 @@
+package dspinner
+
+import (
+ "context"
+ "os"
+ "testing"
+
+ bs "github.com/ipfs/boxo/blockservice"
+ blockstore "github.com/ipfs/boxo/blockstore"
+ offline "github.com/ipfs/boxo/exchange/offline"
+ ds "github.com/ipfs/go-datastore"
+ bds "github.com/ipfs/go-ds-badger"
+ lds "github.com/ipfs/go-ds-leveldb"
+ ipld "github.com/ipfs/go-ipld-format"
+ mdag "github.com/ipfs/boxo/ipld/merkledag"
+)
+
+func makeStoreLevelDB(dir string) (ds.Datastore, ipld.DAGService) {
+ ldstore, err := lds.NewDatastore(dir, nil)
+ if err != nil {
+ panic(err)
+ }
+ // dstore := &batchWrap{ldstore}
+ dstore := ldstore
+ bstore := blockstore.NewBlockstore(dstore)
+ bserv := bs.New(bstore, offline.Exchange(bstore))
+ dserv := mdag.NewDAGService(bserv)
+ return dstore, dserv
+}
+
+func makeStoreBadger(dir string) (ds.Datastore, ipld.DAGService) {
+ bdstore, err := bds.NewDatastore(dir, nil)
+ if err != nil {
+ panic(err)
+ }
+ dstore := &batchWrap{bdstore}
+ bstore := blockstore.NewBlockstore(dstore)
+ bserv := bs.New(bstore, offline.Exchange(bstore))
+ dserv := mdag.NewDAGService(bserv)
+ return dstore, dserv
+}
+
+func benchAutoSync(b *testing.B, N int, auto bool, dstore ds.Datastore, dserv ipld.DAGService) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ pinner, err := New(ctx, dstore, dserv)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ nodes := makeNodes(N, dserv)
+
+ pinner.SetAutosync(auto)
+ pinNodes(nodes, pinner, true)
+}
+
+func BenchmarkSyncOnceBadger(b *testing.B) {
+ const dsDir = "b-once"
+ dstoreB1, dservB1 := makeStoreBadger(dsDir)
+ defer os.RemoveAll(dsDir)
+ benchAutoSync(b, b.N, false, dstoreB1, dservB1)
+ dstoreB1.Close()
+}
+
+func BenchmarkSyncEveryBadger(b *testing.B) {
+ const dsDir = "b-every"
+ dstoreB2, dservB2 := makeStoreBadger(dsDir)
+ defer os.RemoveAll(dsDir)
+ benchAutoSync(b, b.N, true, dstoreB2, dservB2)
+ dstoreB2.Close()
+}
+
+func BenchmarkSyncOnceLevelDB(b *testing.B) {
+ const dsDir = "l-once"
+ dstoreL1, dservL1 := makeStoreLevelDB(dsDir)
+ defer os.RemoveAll(dsDir)
+ benchAutoSync(b, b.N, false, dstoreL1, dservL1)
+ dstoreL1.Close()
+}
+
+func BenchmarkSyncEveryLevelDB(b *testing.B) {
+ const dsDir = "l-every"
+ dstoreL2, dservL2 := makeStoreLevelDB(dsDir)
+ defer os.RemoveAll(dsDir)
+ benchAutoSync(b, b.N, true, dstoreL2, dservL2)
+ dstoreL2.Close()
+}
diff --git a/pinning/pinner/pin.go b/pinning/pinner/pin.go
new file mode 100644
index 0000000000..fcf7d764a7
--- /dev/null
+++ b/pinning/pinner/pin.go
@@ -0,0 +1,158 @@
+// Package pin implements structures and methods to keep track of
+// which objects a user wants to keep stored locally.
+package pin
+
+import (
+ "context"
+ "fmt"
+
+ cid "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+const (
+ linkRecursive = "recursive"
+ linkDirect = "direct"
+ linkIndirect = "indirect"
+ linkInternal = "internal"
+ linkNotPinned = "not pinned"
+ linkAny = "any"
+ linkAll = "all"
+)
+
+// Mode allows to specify different types of pin (recursive, direct etc.).
+// See the Pin Modes constants for a full list.
+type Mode int
+
+// Pin Modes
+const (
+ // Recursive pins pin the target cids along with any reachable children.
+ Recursive Mode = iota
+
+ // Direct pins pin just the target cid.
+ Direct
+
+ // Indirect pins are cids who have some ancestor pinned recursively.
+ Indirect
+
+ // Internal pins are cids used to keep the internal state of the pinner.
+ Internal
+
+ // NotPinned
+ NotPinned
+
+ // Any refers to any pinned cid
+ Any
+)
+
+// ModeToString returns a human-readable name for the Mode.
+func ModeToString(mode Mode) (string, bool) {
+ m := map[Mode]string{
+ Recursive: linkRecursive,
+ Direct: linkDirect,
+ Indirect: linkIndirect,
+ Internal: linkInternal,
+ NotPinned: linkNotPinned,
+ Any: linkAny,
+ }
+ s, ok := m[mode]
+ return s, ok
+}
+
+// StringToMode parses the result of ModeToString() back to a Mode.
+// It returns a boolean which is set to false if the mode is unknown.
+func StringToMode(s string) (Mode, bool) {
+ m := map[string]Mode{
+ linkRecursive: Recursive,
+ linkDirect: Direct,
+ linkIndirect: Indirect,
+ linkInternal: Internal,
+ linkNotPinned: NotPinned,
+ linkAny: Any,
+ linkAll: Any, // "all" and "any" means the same thing
+ }
+ mode, ok := m[s]
+ return mode, ok
+}
+
+// ErrNotPinned is returned when trying to unpin items that are not pinned.
+var ErrNotPinned = fmt.Errorf("not pinned or pinned indirectly")
+
+// A Pinner provides the necessary methods to keep track of Nodes which are
+// to be kept locally, according to a pin mode. In practice, a Pinner is in
+// in charge of keeping the list of items from the local storage that should
+// not be garbage-collected.
+type Pinner interface {
+ // IsPinned returns whether or not the given cid is pinned
+ // and an explanation of why its pinned
+ IsPinned(ctx context.Context, c cid.Cid) (string, bool, error)
+
+ // IsPinnedWithType returns whether or not the given cid is pinned with the
+ // given pin type, as well as returning the type of pin its pinned with.
+ IsPinnedWithType(ctx context.Context, c cid.Cid, mode Mode) (string, bool, error)
+
+ // Pin the given node, optionally recursively.
+ // Pin will make sure that the given node and its children if recursive is set
+ // are stored locally.
+ Pin(ctx context.Context, node ipld.Node, recursive bool) error
+
+ // Unpin the given cid. If recursive is true, removes either a recursive or
+ // a direct pin. If recursive is false, only removes a direct pin.
+ // If the pin doesn't exist, return ErrNotPinned
+ Unpin(ctx context.Context, cid cid.Cid, recursive bool) error
+
+ // Update updates a recursive pin from one cid to another
+ // this is more efficient than simply pinning the new one and unpinning the
+ // old one
+ Update(ctx context.Context, from, to cid.Cid, unpin bool) error
+
+ // Check if a set of keys are pinned, more efficient than
+ // calling IsPinned for each key
+ CheckIfPinned(ctx context.Context, cids ...cid.Cid) ([]Pinned, error)
+
+ // PinWithMode is for manually editing the pin structure. Use with
+ // care! If used improperly, garbage collection may not be
+ // successful.
+ PinWithMode(context.Context, cid.Cid, Mode) error
+
+ // Flush writes the pin state to the backing datastore
+ Flush(ctx context.Context) error
+
+ // DirectKeys returns all directly pinned cids
+ DirectKeys(ctx context.Context) ([]cid.Cid, error)
+
+ // RecursiveKeys returns all recursively pinned cids
+ RecursiveKeys(ctx context.Context) ([]cid.Cid, error)
+
+ // InternalPins returns all cids kept pinned for the internal state of the
+ // pinner
+ InternalPins(ctx context.Context) ([]cid.Cid, error)
+}
+
+// Pinned represents CID which has been pinned with a pinning strategy.
+// The Via field allows to identify the pinning parent of this CID, in the
+// case that the item is not pinned directly (but rather pinned recursively
+// by some ascendant).
+type Pinned struct {
+ Key cid.Cid
+ Mode Mode
+ Via cid.Cid
+}
+
+// Pinned returns whether or not the given cid is pinned
+func (p Pinned) Pinned() bool {
+ return p.Mode != NotPinned
+}
+
+// String Returns pin status as string
+func (p Pinned) String() string {
+ switch p.Mode {
+ case NotPinned:
+ return "not pinned"
+ case Indirect:
+ return fmt.Sprintf("pinned via %s", p.Via)
+ default:
+ modeStr, _ := ModeToString(p.Mode)
+ return fmt.Sprintf("pinned: %s", modeStr)
+ }
+}
diff --git a/pinning/remote/client/.gitignore b/pinning/remote/client/.gitignore
new file mode 100644
index 0000000000..66fd13c903
--- /dev/null
+++ b/pinning/remote/client/.gitignore
@@ -0,0 +1,15 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
diff --git a/pinning/remote/client/README.md b/pinning/remote/client/README.md
new file mode 100644
index 0000000000..717d9538b8
--- /dev/null
+++ b/pinning/remote/client/README.md
@@ -0,0 +1,16 @@
+## Updating Pinning Service Spec
+
+Download the openapi-generator from https://github.com/OpenAPITools/openapi-generator and generate the code using:
+
+Current code generated with: openapi-generator 5.0.0-beta
+
+```
+openapi-generator generate -g go-experimental -i https://raw.githubusercontent.com/ipfs/pinning-services-api-spec/master/ipfs-pinning-service.yaml -o openapi
+rm openapi/go.mod openapi/go.sum
+```
+
+Notes:
+Due to https://github.com/OpenAPITools/openapi-generator/issues/7473 the code generator the http error codes processing
+may need some manual editing.
+
+`go-experimental` is becoming mainstream and so in later versions will be replaced with `go`
diff --git a/pinning/remote/client/client.go b/pinning/remote/client/client.go
new file mode 100644
index 0000000000..6c869d40f1
--- /dev/null
+++ b/pinning/remote/client/client.go
@@ -0,0 +1,428 @@
+package go_pinning_service_http_client
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/pkg/errors"
+
+ "github.com/ipfs/boxo/pinning/remote/client/openapi"
+ "github.com/ipfs/go-cid"
+ "github.com/multiformats/go-multiaddr"
+ "github.com/multiformats/go-multibase"
+
+ logging "github.com/ipfs/go-log/v2"
+)
+
+var logger = logging.Logger("pinning-service-http-client")
+
+const UserAgent = "go-pinning-service-http-client"
+
+type Client struct {
+ client *openapi.APIClient
+}
+
+func NewClient(url, bearerToken string) *Client {
+ config := openapi.NewConfiguration()
+ config.UserAgent = UserAgent
+ config.AddDefaultHeader("Authorization", "Bearer "+bearerToken)
+ config.Servers = openapi.ServerConfigurations{
+ openapi.ServerConfiguration{
+ URL: url,
+ },
+ }
+
+ return &Client{client: openapi.NewAPIClient(config)}
+}
+
+// TODO: We should probably make sure there are no duplicates sent
+type lsSettings struct {
+ cids []string
+ name string
+ status []Status
+ before *time.Time
+ after *time.Time
+ limit *int32
+ meta map[string]string
+}
+
+type LsOption func(options *lsSettings) error
+
+var PinOpts = pinOpts{}
+
+type pinOpts struct {
+ pinLsOpts
+ pinAddOpts
+}
+
+type pinLsOpts struct{}
+
+func (pinLsOpts) FilterCIDs(cids ...cid.Cid) LsOption {
+ return func(options *lsSettings) error {
+ enc := getCIDEncoder()
+ for _, c := range cids {
+ options.cids = append(options.cids, c.Encode(enc))
+ }
+ return nil
+ }
+}
+
+const maxNameSize = 255
+
+func (pinLsOpts) FilterName(name string) LsOption {
+ return func(options *lsSettings) error {
+ if len(name) > maxNameSize {
+ return fmt.Errorf("name cannot be longer than %d", maxNameSize)
+ }
+ options.name = name
+ return nil
+ }
+}
+
+func (pinLsOpts) FilterStatus(statuses ...Status) LsOption {
+ return func(options *lsSettings) error {
+ for _, s := range statuses {
+ valid := false
+ for _, existing := range validStatuses {
+ if existing == s {
+ valid = true
+ break
+ }
+ }
+ if !valid {
+ return fmt.Errorf("invalid status %s", s)
+ }
+ }
+ options.status = append(options.status, statuses...)
+ return nil
+ }
+}
+
+func (pinLsOpts) FilterBefore(t time.Time) LsOption {
+ return func(options *lsSettings) error {
+ options.before = &t
+ return nil
+ }
+}
+
+func (pinLsOpts) FilterAfter(t time.Time) LsOption {
+ return func(options *lsSettings) error {
+ options.after = &t
+ return nil
+ }
+}
+
+const recordLimit = 1000
+const defaultLimit = 10
+
+func (pinLsOpts) Limit(limit int) LsOption {
+ return func(options *lsSettings) error {
+ if limit > recordLimit {
+ return fmt.Errorf("limit exceeded maximum record limit of %d", recordLimit)
+ }
+ limitCasted := int32(limit)
+ options.limit = &limitCasted
+ return nil
+ }
+}
+
+func (pinLsOpts) LsMeta(meta map[string]string) LsOption {
+ return func(options *lsSettings) error {
+ options.meta = meta
+ return nil
+ }
+}
+
+type pinResults = openapi.PinResults
+
+func (c *Client) Ls(ctx context.Context, opts ...LsOption) (chan PinStatusGetter, chan error) {
+ res := make(chan PinStatusGetter, 1)
+ errs := make(chan error, 1)
+
+ settings := new(lsSettings)
+ for _, o := range opts {
+ if err := o(settings); err != nil {
+ close(res)
+ errs <- err
+ close(errs)
+ return res, errs
+ }
+ }
+
+ go func() {
+ defer func() {
+ if r := recover(); r != nil {
+ var err error
+ switch x := r.(type) {
+ case string:
+ err = fmt.Errorf("unexpected error while listing remote pins: %s", x)
+ case error:
+ err = fmt.Errorf("unexpected error while listing remote pins: %w", x)
+ default:
+ err = errors.New("unknown panic while listing remote pins")
+ }
+ errs <- err
+ }
+ close(errs)
+ close(res)
+ }()
+
+ for {
+ pinRes, err := c.lsInternal(ctx, settings)
+ if err != nil {
+ errs <- err
+ return
+ }
+
+ results := pinRes.GetResults()
+ for _, r := range results {
+ select {
+ case res <- &pinStatusObject{r}:
+ case <-ctx.Done():
+ errs <- ctx.Err()
+ return
+ }
+ }
+
+ batchSize := len(results)
+ if int(pinRes.Count) == batchSize {
+ // no more batches
+ return
+ }
+
+ // Better DX/UX for cases like https://github.com/application-research/estuary/issues/124
+ if batchSize == 0 && int(pinRes.Count) != 0 {
+ errs <- fmt.Errorf("invalid pinning service response: PinResults.count=%d but no PinResults.results", int(pinRes.Count))
+ return
+ }
+
+ oldestResult := results[batchSize-1]
+ settings.before = &oldestResult.Created
+ }
+ }()
+
+ return res, errs
+}
+
+func (c *Client) LsSync(ctx context.Context, opts ...LsOption) ([]PinStatusGetter, error) {
+ resCh, errCh := c.Ls(ctx, opts...)
+
+ var res []PinStatusGetter
+ for r := range resCh {
+ res = append(res, r)
+ }
+
+ return res, <-errCh
+}
+
+// Manual version of Ls that returns a single batch of results and int with total count
+func (c *Client) LsBatchSync(ctx context.Context, opts ...LsOption) ([]PinStatusGetter, int, error) {
+ var res []PinStatusGetter
+
+ settings := new(lsSettings)
+ for _, o := range opts {
+ if err := o(settings); err != nil {
+ return nil, 0, err
+ }
+ }
+
+ pinRes, err := c.lsInternal(ctx, settings)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ results := pinRes.GetResults()
+ for _, r := range results {
+ res = append(res, &pinStatusObject{r})
+ }
+
+ return res, int(pinRes.Count), nil
+}
+
+func (c *Client) lsInternal(ctx context.Context, settings *lsSettings) (pinResults, error) {
+ getter := c.client.PinsApi.PinsGet(ctx)
+ if len(settings.cids) > 0 {
+ getter = getter.Cid(settings.cids)
+ }
+ if len(settings.status) > 0 {
+ statuses := make([]openapi.Status, len(settings.status))
+ for i := 0; i < len(statuses); i++ {
+ statuses[i] = openapi.Status(settings.status[i])
+ }
+ getter = getter.Status(statuses)
+ }
+ if settings.limit == nil {
+ getter = getter.Limit(defaultLimit)
+ } else {
+ getter = getter.Limit(*settings.limit)
+ }
+ if len(settings.name) > 0 {
+ getter = getter.Name(settings.name)
+ }
+ if settings.before != nil {
+ getter = getter.Before(*settings.before)
+ }
+ if settings.after != nil {
+ getter = getter.After(*settings.after)
+ }
+ if settings.meta != nil {
+ getter = getter.Meta(settings.meta)
+ }
+
+ // TODO: Ignoring HTTP Response OK?
+ results, httpresp, err := getter.Execute()
+ if err != nil {
+ err := httperr(httpresp, err)
+ return pinResults{}, err
+ }
+
+ return results, nil
+}
+
+// TODO: We should probably make sure there are no duplicates sent
+type addSettings struct {
+ name string
+ origins []string
+ meta map[string]string
+}
+
+type AddOption func(options *addSettings) error
+
+type pinAddOpts struct{}
+
+func (pinAddOpts) WithName(name string) AddOption {
+ return func(options *addSettings) error {
+ if len(name) > maxNameSize {
+ return fmt.Errorf("name cannot be longer than %d", maxNameSize)
+ }
+ options.name = name
+ return nil
+ }
+}
+
+func (pinAddOpts) WithOrigins(origins ...multiaddr.Multiaddr) AddOption {
+ return func(options *addSettings) error {
+ for _, o := range origins {
+ options.origins = append(options.origins, o.String())
+ }
+ return nil
+ }
+}
+
+func (pinAddOpts) AddMeta(meta map[string]string) AddOption {
+ return func(options *addSettings) error {
+ options.meta = meta
+ return nil
+ }
+}
+
+func (c *Client) Add(ctx context.Context, cid cid.Cid, opts ...AddOption) (PinStatusGetter, error) {
+ settings := new(addSettings)
+ for _, o := range opts {
+ if err := o(settings); err != nil {
+ return nil, err
+ }
+ }
+
+ adder := c.client.PinsApi.PinsPost(ctx)
+ p := openapi.Pin{
+ Cid: cid.Encode(getCIDEncoder()),
+ }
+
+ if len(settings.origins) > 0 {
+ p.SetOrigins(settings.origins)
+ }
+ if settings.meta != nil {
+ p.SetMeta(settings.meta)
+ }
+ if len(settings.name) > 0 {
+ p.SetName(settings.name)
+ }
+
+ result, httpresp, err := adder.Pin(p).Execute()
+ if err != nil {
+ err := httperr(httpresp, err)
+ return nil, err
+ }
+
+ return &pinStatusObject{result}, nil
+}
+
+func (c *Client) GetStatusByID(ctx context.Context, pinID string) (PinStatusGetter, error) {
+ getter := c.client.PinsApi.PinsRequestidGet(ctx, pinID)
+ result, httpresp, err := getter.Execute()
+ if err != nil {
+ err := httperr(httpresp, err)
+ return nil, err
+ }
+
+ return &pinStatusObject{result}, nil
+}
+
+func (c *Client) DeleteByID(ctx context.Context, pinID string) error {
+ deleter := c.client.PinsApi.PinsRequestidDelete(ctx, pinID)
+ httpresp, err := deleter.Execute()
+ if err != nil {
+ err := httperr(httpresp, err)
+ return err
+ }
+ return nil
+}
+
+func (c *Client) Replace(ctx context.Context, pinID string, cid cid.Cid, opts ...AddOption) (PinStatusGetter, error) {
+ settings := new(addSettings)
+ for _, o := range opts {
+ if err := o(settings); err != nil {
+ return nil, err
+ }
+ }
+
+ adder := c.client.PinsApi.PinsRequestidPost(ctx, pinID)
+ p := openapi.Pin{
+ Cid: cid.Encode(getCIDEncoder()),
+ }
+
+ if len(settings.origins) > 0 {
+ p.SetOrigins(settings.origins)
+ }
+ if settings.meta != nil {
+ p.SetMeta(settings.meta)
+ }
+ if len(settings.name) > 0 {
+ p.SetName(settings.name)
+ }
+
+ result, httpresp, err := adder.Pin(p).Execute()
+ if err != nil {
+ err := httperr(httpresp, err)
+ return nil, err
+ }
+
+ return &pinStatusObject{result}, nil
+}
+
+func getCIDEncoder() multibase.Encoder {
+ enc, err := multibase.NewEncoder(multibase.Base32)
+ if err != nil {
+ panic(err)
+ }
+ return enc
+}
+
+func httperr(resp *http.Response, e error) error {
+ oerr, ok := e.(openapi.GenericOpenAPIError)
+ if ok {
+ ferr, ok := oerr.Model().(openapi.Failure)
+ if ok {
+ return errors.Wrapf(e, "reason: %q, details: %q", ferr.Error.GetReason(), ferr.Error.GetDetails())
+ }
+ }
+
+ if resp == nil {
+ return errors.Wrapf(e, "empty response from remote pinning service")
+ }
+
+ return errors.Wrapf(e, "remote pinning service returned http error %d", resp.StatusCode)
+}
diff --git a/pinning/remote/client/cmd/main.go b/pinning/remote/client/cmd/main.go
new file mode 100644
index 0000000000..c85f51effa
--- /dev/null
+++ b/pinning/remote/client/cmd/main.go
@@ -0,0 +1,92 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "time"
+
+ pinclient "github.com/ipfs/boxo/pinning/remote/client"
+ "github.com/ipfs/go-cid"
+)
+
+func main() {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ url, ok := os.LookupEnv("PS_URL")
+ if !ok {
+ panic("No Pinning Service URL found")
+ }
+
+ key, ok := os.LookupEnv("PS_KEY")
+ if !ok {
+ panic("No Pinning Service API Key found")
+ }
+
+ c := pinclient.NewClient(url, key)
+
+ ipfsPgCid, err := cid.Parse("bafybeiayvrj27f65vbecspbnuavehcb3znvnt2strop2rfbczupudoizya")
+ if err != nil {
+ panic(err)
+ }
+
+ libp2pCid, err := cid.Parse("bafybeiejgrxo4p4uofgfzvlg5twrg5w7tfwpf7aciiswfacfbdpevg2xfy")
+ if err != nil {
+ panic(err)
+ }
+ _ = ipfsPgCid
+
+ listPins(ctx, c)
+
+ fmt.Println("Adding libp2p home page")
+ ps, err := c.Add(ctx, libp2pCid, pinclient.PinOpts.WithName("libp2p"))
+ if err == nil {
+ fmt.Printf("PinStatus: %v \n", ps)
+ } else {
+ fmt.Println(err)
+ }
+
+ listPins(ctx, c)
+
+ fmt.Println("Check on pin status")
+ if ps == nil {
+ panic("Skipping pin status check because the pin is null")
+ }
+
+ var pinned bool
+ for !pinned {
+ status, err := c.GetStatusByID(ctx, ps.GetRequestId())
+ if err == nil {
+ fmt.Println(status.GetStatus())
+ pinned = status.GetStatus() == pinclient.StatusPinned
+ } else {
+ fmt.Println(err)
+ }
+ time.Sleep(time.Millisecond * 500)
+ }
+
+ listPins(ctx, c)
+
+ fmt.Println("Delete pin")
+ err = c.DeleteByID(ctx, ps.GetRequestId())
+ if err == nil {
+ fmt.Println("Successfully deleted pin")
+ } else {
+ fmt.Println(err)
+ }
+
+ listPins(ctx, c)
+}
+
+func listPins(ctx context.Context, c *pinclient.Client) {
+ fmt.Println("List all pins")
+ pins, err := c.LsSync(ctx)
+ if err != nil {
+ fmt.Println(err)
+ } else {
+ for _, p := range pins {
+ fmt.Printf("Pin: %v \n", p)
+ }
+ }
+}
diff --git a/pinning/remote/client/model.go b/pinning/remote/client/model.go
new file mode 100644
index 0000000000..60d6144778
--- /dev/null
+++ b/pinning/remote/client/model.go
@@ -0,0 +1,167 @@
+package go_pinning_service_http_client
+
+import (
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/ipfs/boxo/pinning/remote/client/openapi"
+ "github.com/ipfs/go-cid"
+ "github.com/multiformats/go-multiaddr"
+)
+
+// PinGetter Getter for Pin object
+type PinGetter interface {
+ fmt.Stringer
+ json.Marshaler
+ // CID to be pinned recursively
+ GetCid() cid.Cid
+ // Optional name for pinned data; can be used for lookups later
+ GetName() string
+ // Optional list of multiaddrs known to provide the data
+ GetOrigins() []string
+ // Optional metadata for pin object
+ GetMeta() map[string]string
+}
+
+type pinObject struct {
+ openapi.Pin
+}
+
+func (p *pinObject) MarshalJSON() ([]byte, error) {
+ var originsStr string
+ if o := p.GetOrigins(); o != nil {
+ originsBytes, err := json.Marshal(o)
+ if err == nil {
+ originsStr = string(originsBytes)
+ }
+ }
+
+ metaStr := "{}"
+ if meta := p.GetMeta(); meta != nil {
+ metaBytes, err := json.Marshal(meta)
+ if err == nil {
+ metaStr = string(metaBytes)
+ }
+ }
+
+ str := fmt.Sprintf("{ \"Cid\" : \"%v\", \"Name\" : \"%s\", \"Origins\" : %v, \"Meta\" : %v }",
+ p.GetCid(), p.GetName(), originsStr, metaStr)
+ return []byte(str), nil
+}
+
+func (p *pinObject) String() string {
+ marshalled, err := json.MarshalIndent(p, "", "\t")
+ if err != nil {
+ return ""
+ }
+
+ return string(marshalled)
+}
+
+func (p *pinObject) GetCid() cid.Cid {
+ c, err := cid.Parse(p.Pin.Cid)
+ if err != nil {
+ return cid.Undef
+ }
+ return c
+}
+
+type Status string
+
+const (
+ StatusUnknown Status = ""
+ StatusQueued Status = Status(openapi.QUEUED)
+ StatusPinning Status = Status(openapi.PINNING)
+ StatusPinned Status = Status(openapi.PINNED)
+ StatusFailed Status = Status(openapi.FAILED)
+)
+
+func (s Status) String() string {
+ switch s {
+ case StatusQueued, StatusPinning, StatusPinned, StatusFailed:
+ return string(s)
+ default:
+ return string(StatusUnknown)
+ }
+}
+
+var validStatuses = []Status{"queued", "pinning", "pinned", "failed"}
+
+// PinStatusGetter Getter for Pin object with status
+type PinStatusGetter interface {
+ fmt.Stringer
+ json.Marshaler
+ // Globally unique ID of the pin request; can be used to check the status of ongoing pinning, modification of pin object, or pin removal
+ GetRequestId() string
+ GetStatus() Status
+ // Immutable timestamp indicating when a pin request entered a pinning service; can be used for filtering results and pagination
+ GetCreated() time.Time
+ GetPin() PinGetter
+ // List of multiaddrs designated by pinning service for transferring any new data from external peers
+ GetDelegates() []multiaddr.Multiaddr
+ // Optional info for PinStatus response
+ GetInfo() map[string]string
+}
+
+type pinStatusObject struct {
+ openapi.PinStatus
+}
+
+func (p *pinStatusObject) GetDelegates() []multiaddr.Multiaddr {
+ delegates := p.PinStatus.GetDelegates()
+ addrs := make([]multiaddr.Multiaddr, 0, len(delegates))
+ for _, d := range delegates {
+ a, err := multiaddr.NewMultiaddr(d)
+ if err != nil {
+ logger.Errorf("returned delegate is an invalid multiaddr: %w", err)
+ continue
+ }
+ addrs = append(addrs, a)
+ }
+ return addrs
+}
+
+func (p *pinStatusObject) GetPin() PinGetter {
+ return &pinObject{p.Pin}
+}
+
+func (p *pinStatusObject) GetStatus() Status {
+ return Status(p.PinStatus.GetStatus())
+}
+
+func (p *pinStatusObject) GetRequestId() string {
+ return p.GetRequestid()
+}
+
+func (p *pinStatusObject) MarshalJSON() ([]byte, error) {
+ var delegatesStr string
+ if d := p.GetDelegates(); d != nil {
+ delegatesBytes, err := json.Marshal(d)
+ if err == nil {
+ delegatesStr = string(delegatesBytes)
+ }
+ }
+
+ infoStr := "{}"
+ if info := p.GetInfo(); info != nil {
+ infoBytes, err := json.Marshal(info)
+ if err == nil {
+ infoStr = string(infoBytes)
+ }
+ }
+
+ str := fmt.Sprintf("{\"Pin\" : %v, \"RequestID\" : \"%s\", \"Status\" : \"%s\", \"Created\" : \"%v\", \"Delegates\" : %v, \"Info\" : %v }",
+ p.GetPin(), p.GetRequestId(), p.GetStatus(), p.GetCreated(), delegatesStr, infoStr)
+
+ return []byte(str), nil
+}
+
+func (p *pinStatusObject) String() string {
+ marshalled, err := json.MarshalIndent(p, "", "\t")
+ if err != nil {
+ return ""
+ }
+
+ return string(marshalled)
+}
diff --git a/pinning/remote/client/openapi/README.md b/pinning/remote/client/openapi/README.md
new file mode 100644
index 0000000000..fe21b9a009
--- /dev/null
+++ b/pinning/remote/client/openapi/README.md
@@ -0,0 +1,217 @@
+# Go API client for openapi
+
+
+
+## About this spec
+The IPFS Pinning Service API is intended to be an implementation-agnostic API:
+- For use and implementation by pinning service providers
+- For use in client mode by IPFS nodes and GUI-based applications
+
+> **Note**: while ready for implementation, this spec is still a work in progress! 🏗️ **Your input and feedback are welcome and valuable as we develop this API spec. Please join the design discussion at [github.com/ipfs/pinning-services-api-spec](https://github.com/ipfs/pinning-services-api-spec).**
+
+# Schemas
+This section describes the most important object types and conventions.
+
+A full list of fields and schemas can be found in the `schemas` section of the [YAML file](https://github.com/ipfs/pinning-services-api-spec/blob/master/ipfs-pinning-service.yaml).
+
+## Identifiers
+### cid
+[Content Identifier (CID)](https://docs.ipfs.io/concepts/content-addressing/) points at the root of a DAG that is pinned recursively.
+### requestid
+Unique identifier of a pin request.
+
+When a pin is created, the service responds with unique `requestid` that can be later used for pin removal. When the same `cid` is pinned again, a different `requestid` is returned to differentiate between those pin requests.
+
+Service implementation should use UUID, `hash(accessToken,Pin,PinStatus.created)`, or any other opaque identifier that provides equally strong protection against race conditions.
+
+## Objects
+### Pin object
+
+![pin object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pin.png)
+
+The `Pin` object is a representation of a pin request.
+
+It includes the `cid` of data to be pinned, as well as optional metadata in `name`, `origins`, and `meta`.
+
+### Pin status response
+
+![pin status response object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pinstatus.png)
+
+The `PinStatus` object is a representation of the current state of a pinning operation.
+It includes the original `pin` object, along with the current `status` and globally unique `requestid` of the entire pinning request, which can be used for future status checks and management. Addresses in the `delegates` array are peers delegated by the pinning service for facilitating direct file transfers (more details in the provider hints section). Any additional vendor-specific information is returned in optional `info`.
+
+## The pin lifecycle
+
+![pinning service objects and lifecycle](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/lifecycle.png)
+
+### Creating a new pin object
+The user sends a `Pin` object to `POST /pins` and receives a `PinStatus` response:
+- `requestid` in `PinStatus` is the identifier of the pin operation, which can can be used for checking status, and removing the pin in the future
+- `status` in `PinStatus` indicates the current state of a pin
+
+### Checking status of in-progress pinning
+`status` (in `PinStatus`) may indicate a pending state (`queued` or `pinning`). This means the data behind `Pin.cid` was not found on the pinning service and is being fetched from the IPFS network at large, which may take time.
+
+In this case, the user can periodically check pinning progress via `GET /pins/{requestid}` until pinning is successful, or the user decides to remove the pending pin.
+
+### Replacing an existing pin object
+The user can replace an existing pin object via `POST /pins/{requestid}`. This is a shortcut for removing a pin object identified by `requestid` and creating a new one in a single API call that protects against undesired garbage collection of blocks common to both pins. Useful when updating a pin representing a huge dataset where most of blocks did not change. The new pin object `requestid` is returned in the `PinStatus` response. The old pin object is deleted automatically.
+
+### Removing a pin object
+A pin object can be removed via `DELETE /pins/{requestid}`.
+
+
+## Provider hints
+Pinning of new data can be accelerated by providing a list of known data sources in `Pin.origins`, and connecting at least one of them to pinning service nodes at `PinStatus.delegates`.
+
+The most common scenario is a client putting its own IPFS node's multiaddrs in `Pin.origins`, and then directly connecting to every multiaddr returned by a pinning service in `PinStatus.delegates` to initiate transfer.
+
+This ensures data transfer starts immediately (without waiting for provider discovery over DHT), and direct dial from a client works around peer routing issues in restrictive network topologies such as NATs.
+
+## Custom metadata
+Pinning services are encouraged to add support for additional features by leveraging the optional `Pin.meta` and `PinStatus.info` fields. While these attributes can be application- or vendor-specific, we encourage the community at large to leverage these attributes as a sandbox to come up with conventions that could become part of future revisions of this API.
+### Pin metadata
+String keys and values passed in `Pin.meta` are persisted with the pin object.
+
+Potential uses:
+- `Pin.meta[app_id]`: Attaching a unique identifier to pins created by an app enables filtering pins per app via `?meta={\"app_id\":}`
+- `Pin.meta[vendor_policy]`: Vendor-specific policy (for example: which region to use, how many copies to keep)
+
+Note that it is OK for a client to omit or ignore these optional attributes; doing so should not impact the basic pinning functionality.
+
+### Pin status info
+Additional `PinStatus.info` can be returned by pinning service.
+
+Potential uses:
+- `PinStatus.info[status_details]`: more info about the current status (queue position, percentage of transferred data, summary of where data is stored, etc); when `PinStatus.status=failed`, it could provide a reason why a pin operation failed (e.g. lack of funds, DAG too big, etc.)
+- `PinStatus.info[dag_size]`: the size of pinned data, along with DAG overhead
+- `PinStatus.info[raw_size]`: the size of data without DAG overhead (eg. unixfs)
+- `PinStatus.info[pinned_until]`: if vendor supports time-bound pins, this could indicate when the pin will expire
+
+# Pagination and filtering
+Pin objects can be listed by executing `GET /pins` with optional parameters:
+
+- When no filters are provided, the endpoint will return a small batch of the 10 most recently created items, from the latest to the oldest.
+- The number of returned items can be adjusted with the `limit` parameter (implicit default is 10).
+- If the value in `PinResults.count` is bigger than the length of `PinResults.results`, the client can infer there are more results that can be queried.
+- To read more items, pass the `before` filter with the timestamp from `PinStatus.created` found in the oldest item in the current batch of results. Repeat to read all results.
+- Returned results can be fine-tuned by applying optional `after`, `cid`, `name`, `status`, or `meta` filters.
+
+> **Note**: pagination by the `created` timestamp requires each value to be globally unique. Any future considerations to add support for bulk creation must account for this.
+
+
+
+## Overview
+This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [OpenAPI-spec](https://www.openapis.org/) from a remote server, you can easily generate an API client.
+
+- API version: 0.1.1
+- Package version: 1.0.0
+- Build package: org.openapitools.codegen.languages.GoClientExperimentalCodegen
+
+## Installation
+
+Install the following dependencies:
+
+```shell
+go get github.com/stretchr/testify/assert
+go get golang.org/x/oauth2
+go get golang.org/x/net/context
+```
+
+Put the package under your project folder and add the following in import:
+
+```golang
+import sw "./openapi"
+```
+
+## Configuration of Server URL
+
+Default configuration comes with `Servers` field that contains server objects as defined in the OpenAPI specification.
+
+### Select Server Configuration
+
+For using other server than the one defined on index 0 set context value `sw.ContextServerIndex` of type `int`.
+
+```golang
+ctx := context.WithValue(context.Background(), sw.ContextServerIndex, 1)
+```
+
+### Templated Server URL
+
+Templated server URL is formatted using default variables from configuration or from context value `sw.ContextServerVariables` of type `map[string]string`.
+
+```golang
+ctx := context.WithValue(context.Background(), sw.ContextServerVariables, map[string]string{
+ "basePath": "v2",
+})
+```
+
+Note, enum values are always validated and all unused variables are silently ignored.
+
+### URLs Configuration per Operation
+
+Each operation can use different server URL defined using `OperationServers` map in the `Configuration`.
+An operation is uniquely identifield by `"{classname}Service.{nickname}"` string.
+Similar rules for overriding default operation server index and variables applies by using `sw.ContextOperationServerIndices` and `sw.ContextOperationServerVariables` context maps.
+
+```
+ctx := context.WithValue(context.Background(), sw.ContextOperationServerIndices, map[string]int{
+ "{classname}Service.{nickname}": 2,
+})
+ctx = context.WithValue(context.Background(), sw.ContextOperationServerVariables, map[string]map[string]string{
+ "{classname}Service.{nickname}": {
+ "port": "8443",
+ },
+})
+```
+
+## Documentation for API Endpoints
+
+All URIs are relative to *https://pinning-service.example.com*
+
+Class | Method | HTTP request | Description
+------------ | ------------- | ------------- | -------------
+*PinsApi* | [**PinsGet**](docs/PinsApi.md#pinsget) | **Get** /pins | List pin objects
+*PinsApi* | [**PinsPost**](docs/PinsApi.md#pinspost) | **Post** /pins | Add pin object
+*PinsApi* | [**PinsRequestidDelete**](docs/PinsApi.md#pinsrequestiddelete) | **Delete** /pins/{requestid} | Remove pin object
+*PinsApi* | [**PinsRequestidGet**](docs/PinsApi.md#pinsrequestidget) | **Get** /pins/{requestid} | Get pin object
+*PinsApi* | [**PinsRequestidPost**](docs/PinsApi.md#pinsrequestidpost) | **Post** /pins/{requestid} | Replace pin object
+
+
+## Documentation For Models
+
+ - [Failure](docs/Failure.md)
+ - [FailureError](docs/FailureError.md)
+ - [Pin](docs/Pin.md)
+ - [PinResults](docs/PinResults.md)
+ - [PinStatus](docs/PinStatus.md)
+ - [Status](docs/Status.md)
+
+
+## Documentation For Authorization
+
+
+
+### accessToken
+
+
+## Documentation for Utility Methods
+
+Due to the fact that model structure members are all pointers, this package contains
+a number of utility functions to easily obtain pointers to values of basic types.
+Each of these functions takes a value of the given basic type and returns a pointer to it:
+
+* `PtrBool`
+* `PtrInt`
+* `PtrInt32`
+* `PtrInt64`
+* `PtrFloat`
+* `PtrFloat32`
+* `PtrFloat64`
+* `PtrString`
+* `PtrTime`
+
+## Author
+
+
+
diff --git a/pinning/remote/client/openapi/api_pins.go b/pinning/remote/client/openapi/api_pins.go
new file mode 100644
index 0000000000..b2858c5eea
--- /dev/null
+++ b/pinning/remote/client/openapi/api_pins.go
@@ -0,0 +1,654 @@
+/*
+ * IPFS Pinning Service API
+ *
+ * ## About this spec The IPFS Pinning Service API is intended to be an implementation-agnostic API: - For use and implementation by pinning service providers - For use in client mode by IPFS nodes and GUI-based applications > **Note**: while ready for implementation, this spec is still a work in progress! 🏗️ **Your input and feedback are welcome and valuable as we develop this API spec. Please join the design discussion at [github.com/ipfs/pinning-services-api-spec](https://github.com/ipfs/pinning-services-api-spec).** # Schemas This section describes the most important object types and conventions. A full list of fields and schemas can be found in the `schemas` section of the [YAML file](https://github.com/ipfs/pinning-services-api-spec/blob/master/ipfs-pinning-service.yaml). ## Identifiers ### cid [Content Identifier (CID)](https://docs.ipfs.io/concepts/content-addressing/) points at the root of a DAG that is pinned recursively. ### requestid Unique identifier of a pin request. When a pin is created, the service responds with unique `requestid` that can be later used for pin removal. When the same `cid` is pinned again, a different `requestid` is returned to differentiate between those pin requests. Service implementation should use UUID, `hash(accessToken,Pin,PinStatus.created)`, or any other opaque identifier that provides equally strong protection against race conditions. ## Objects ### Pin object ![pin object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pin.png) The `Pin` object is a representation of a pin request. It includes the `cid` of data to be pinned, as well as optional metadata in `name`, `origins`, and `meta`. ### Pin status response ![pin status response object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pinstatus.png) The `PinStatus` object is a representation of the current state of a pinning operation. It includes the original `pin` object, along with the current `status` and globally unique `requestid` of the entire pinning request, which can be used for future status checks and management. Addresses in the `delegates` array are peers delegated by the pinning service for facilitating direct file transfers (more details in the provider hints section). Any additional vendor-specific information is returned in optional `info`. ## The pin lifecycle ![pinning service objects and lifecycle](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/lifecycle.png) ### Creating a new pin object The user sends a `Pin` object to `POST /pins` and receives a `PinStatus` response: - `requestid` in `PinStatus` is the identifier of the pin operation, which can can be used for checking status, and removing the pin in the future - `status` in `PinStatus` indicates the current state of a pin ### Checking status of in-progress pinning `status` (in `PinStatus`) may indicate a pending state (`queued` or `pinning`). This means the data behind `Pin.cid` was not found on the pinning service and is being fetched from the IPFS network at large, which may take time. In this case, the user can periodically check pinning progress via `GET /pins/{requestid}` until pinning is successful, or the user decides to remove the pending pin. ### Replacing an existing pin object The user can replace an existing pin object via `POST /pins/{requestid}`. This is a shortcut for removing a pin object identified by `requestid` and creating a new one in a single API call that protects against undesired garbage collection of blocks common to both pins. Useful when updating a pin representing a huge dataset where most of blocks did not change. The new pin object `requestid` is returned in the `PinStatus` response. The old pin object is deleted automatically. ### Removing a pin object A pin object can be removed via `DELETE /pins/{requestid}`. ## Provider hints Pinning of new data can be accelerated by providing a list of known data sources in `Pin.origins`, and connecting at least one of them to pinning service nodes at `PinStatus.delegates`. The most common scenario is a client putting its own IPFS node's multiaddrs in `Pin.origins`, and then directly connecting to every multiaddr returned by a pinning service in `PinStatus.delegates` to initiate transfer. This ensures data transfer starts immediately (without waiting for provider discovery over DHT), and direct dial from a client works around peer routing issues in restrictive network topologies such as NATs. ## Custom metadata Pinning services are encouraged to add support for additional features by leveraging the optional `Pin.meta` and `PinStatus.info` fields. While these attributes can be application- or vendor-specific, we encourage the community at large to leverage these attributes as a sandbox to come up with conventions that could become part of future revisions of this API. ### Pin metadata String keys and values passed in `Pin.meta` are persisted with the pin object. Potential uses: - `Pin.meta[app_id]`: Attaching a unique identifier to pins created by an app enables filtering pins per app via `?meta={\"app_id\":}` - `Pin.meta[vendor_policy]`: Vendor-specific policy (for example: which region to use, how many copies to keep) Note that it is OK for a client to omit or ignore these optional attributes; doing so should not impact the basic pinning functionality. ### Pin status info Additional `PinStatus.info` can be returned by pinning service. Potential uses: - `PinStatus.info[status_details]`: more info about the current status (queue position, percentage of transferred data, summary of where data is stored, etc); when `PinStatus.status=failed`, it could provide a reason why a pin operation failed (e.g. lack of funds, DAG too big, etc.) - `PinStatus.info[dag_size]`: the size of pinned data, along with DAG overhead - `PinStatus.info[raw_size]`: the size of data without DAG overhead (eg. unixfs) - `PinStatus.info[pinned_until]`: if vendor supports time-bound pins, this could indicate when the pin will expire # Pagination and filtering Pin objects can be listed by executing `GET /pins` with optional parameters: - When no filters are provided, the endpoint will return a small batch of the 10 most recently created items, from the latest to the oldest. - The number of returned items can be adjusted with the `limit` parameter (implicit default is 10). - If the value in `PinResults.count` is bigger than the length of `PinResults.results`, the client can infer there are more results that can be queried. - To read more items, pass the `before` filter with the timestamp from `PinStatus.created` found in the oldest item in the current batch of results. Repeat to read all results. - Returned results can be fine-tuned by applying optional `after`, `cid`, `name`, `status`, or `meta` filters. > **Note**: pagination by the `created` timestamp requires each value to be globally unique. Any future considerations to add support for bulk creation must account for this.
+ *
+ * API version: 0.1.1
+ * Generated by: OpenAPI Generator (https://openapi-generator.tech)
+ */
+
+package openapi
+
+import (
+ _context "context"
+ _io "io"
+ _nethttp "net/http"
+ _neturl "net/url"
+ "strings"
+ "time"
+)
+
+// Linger please
+var (
+ _ _context.Context
+)
+
+// PinsApiService PinsApi service
+type PinsApiService service
+
+type apiPinsGetRequest struct {
+ ctx _context.Context
+ apiService *PinsApiService
+ cid *[]string
+ name *string
+ status *[]Status
+ before *time.Time
+ after *time.Time
+ limit *int32
+ meta *map[string]string
+}
+
+func (r apiPinsGetRequest) Cid(cid []string) apiPinsGetRequest {
+ r.cid = &cid
+ return r
+}
+
+func (r apiPinsGetRequest) Name(name string) apiPinsGetRequest {
+ r.name = &name
+ return r
+}
+
+func (r apiPinsGetRequest) Status(status []Status) apiPinsGetRequest {
+ r.status = &status
+ return r
+}
+
+func (r apiPinsGetRequest) Before(before time.Time) apiPinsGetRequest {
+ r.before = &before
+ return r
+}
+
+func (r apiPinsGetRequest) After(after time.Time) apiPinsGetRequest {
+ r.after = &after
+ return r
+}
+
+func (r apiPinsGetRequest) Limit(limit int32) apiPinsGetRequest {
+ r.limit = &limit
+ return r
+}
+
+func (r apiPinsGetRequest) Meta(meta map[string]string) apiPinsGetRequest {
+ r.meta = &meta
+ return r
+}
+
+/*
+PinsGet List pin objects
+List all the pin objects, matching optional filters; when no filter is provided, only successful pins are returned
+ - @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
+
+@return apiPinsGetRequest
+*/
+func (a *PinsApiService) PinsGet(ctx _context.Context) apiPinsGetRequest {
+ return apiPinsGetRequest{
+ apiService: a,
+ ctx: ctx,
+ }
+}
+
+/*
+Execute executes the request
+
+ @return PinResults
+*/
+func (r apiPinsGetRequest) Execute() (PinResults, *_nethttp.Response, error) {
+ var (
+ localVarHTTPMethod = _nethttp.MethodGet
+ localVarPostBody interface{}
+ localVarFormFileName string
+ localVarFileName string
+ localVarFileBytes []byte
+ localVarReturnValue PinResults
+ )
+
+ localBasePath, err := r.apiService.client.cfg.ServerURLWithContext(r.ctx, "PinsApiService.PinsGet")
+ if err != nil {
+ return localVarReturnValue, nil, GenericOpenAPIError{error: err.Error()}
+ }
+
+ localVarPath := localBasePath + "/pins"
+
+ localVarHeaderParams := make(map[string]string)
+ localVarQueryParams := _neturl.Values{}
+ localVarFormParams := _neturl.Values{}
+
+ if r.cid != nil {
+ localVarQueryParams.Add("cid", parameterToString(*r.cid, "csv"))
+ }
+ if r.name != nil {
+ localVarQueryParams.Add("name", parameterToString(*r.name, ""))
+ }
+ if r.status != nil {
+ localVarQueryParams.Add("status", parameterToString(*r.status, "csv"))
+ }
+ if r.before != nil {
+ localVarQueryParams.Add("before", parameterToString(*r.before, ""))
+ }
+ if r.after != nil {
+ localVarQueryParams.Add("after", parameterToString(*r.after, ""))
+ }
+ if r.limit != nil {
+ localVarQueryParams.Add("limit", parameterToString(*r.limit, ""))
+ }
+ if r.meta != nil {
+ localVarQueryParams.Add("meta", parameterToString(*r.meta, ""))
+ }
+ // to determine the Content-Type header
+ localVarHTTPContentTypes := []string{}
+
+ // set Content-Type header
+ localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
+ if localVarHTTPContentType != "" {
+ localVarHeaderParams["Content-Type"] = localVarHTTPContentType
+ }
+
+ // to determine the Accept header
+ localVarHTTPHeaderAccepts := []string{"application/json"}
+
+ // set Accept header
+ localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
+ if localVarHTTPHeaderAccept != "" {
+ localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
+ }
+ req, err := r.apiService.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes)
+ if err != nil {
+ return localVarReturnValue, nil, err
+ }
+
+ localVarHTTPResponse, err := r.apiService.client.callAPI(req)
+ if err != nil || localVarHTTPResponse == nil {
+ return localVarReturnValue, localVarHTTPResponse, err
+ }
+
+ localVarBody, err := _io.ReadAll(localVarHTTPResponse.Body)
+ localVarHTTPResponse.Body.Close()
+ if err != nil {
+ return localVarReturnValue, localVarHTTPResponse, err
+ }
+
+ if localVarHTTPResponse.StatusCode >= 300 {
+ newErr := GenericOpenAPIError{
+ body: localVarBody,
+ error: localVarHTTPResponse.Status,
+ }
+ if localVarHTTPResponse.StatusCode >= 400 && localVarHTTPResponse.StatusCode <= 600 {
+ var v Failure
+ err = r.apiService.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
+ if err != nil {
+ newErr.error = err.Error()
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+ newErr.model = v
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+
+ err = r.apiService.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
+ if err != nil {
+ newErr := GenericOpenAPIError{
+ body: localVarBody,
+ error: err.Error(),
+ }
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+
+ return localVarReturnValue, localVarHTTPResponse, nil
+}
+
+type apiPinsPostRequest struct {
+ ctx _context.Context
+ apiService *PinsApiService
+ pin *Pin
+}
+
+func (r apiPinsPostRequest) Pin(pin Pin) apiPinsPostRequest {
+ r.pin = &pin
+ return r
+}
+
+/*
+PinsPost Add pin object
+Add a new pin object for the current access token
+ - @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
+
+@return apiPinsPostRequest
+*/
+func (a *PinsApiService) PinsPost(ctx _context.Context) apiPinsPostRequest {
+ return apiPinsPostRequest{
+ apiService: a,
+ ctx: ctx,
+ }
+}
+
+/*
+Execute executes the request
+
+ @return PinStatus
+*/
+func (r apiPinsPostRequest) Execute() (PinStatus, *_nethttp.Response, error) {
+ var (
+ localVarHTTPMethod = _nethttp.MethodPost
+ localVarPostBody interface{}
+ localVarFormFileName string
+ localVarFileName string
+ localVarFileBytes []byte
+ localVarReturnValue PinStatus
+ )
+
+ localBasePath, err := r.apiService.client.cfg.ServerURLWithContext(r.ctx, "PinsApiService.PinsPost")
+ if err != nil {
+ return localVarReturnValue, nil, GenericOpenAPIError{error: err.Error()}
+ }
+
+ localVarPath := localBasePath + "/pins"
+
+ localVarHeaderParams := make(map[string]string)
+ localVarQueryParams := _neturl.Values{}
+ localVarFormParams := _neturl.Values{}
+
+ if r.pin == nil {
+ return localVarReturnValue, nil, reportError("pin is required and must be specified")
+ }
+
+ // to determine the Content-Type header
+ localVarHTTPContentTypes := []string{"application/json"}
+
+ // set Content-Type header
+ localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
+ if localVarHTTPContentType != "" {
+ localVarHeaderParams["Content-Type"] = localVarHTTPContentType
+ }
+
+ // to determine the Accept header
+ localVarHTTPHeaderAccepts := []string{"application/json"}
+
+ // set Accept header
+ localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
+ if localVarHTTPHeaderAccept != "" {
+ localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
+ }
+ // body params
+ localVarPostBody = r.pin
+ req, err := r.apiService.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes)
+ if err != nil {
+ return localVarReturnValue, nil, err
+ }
+
+ localVarHTTPResponse, err := r.apiService.client.callAPI(req)
+ if err != nil || localVarHTTPResponse == nil {
+ return localVarReturnValue, localVarHTTPResponse, err
+ }
+
+ localVarBody, err := _io.ReadAll(localVarHTTPResponse.Body)
+ localVarHTTPResponse.Body.Close()
+ if err != nil {
+ return localVarReturnValue, localVarHTTPResponse, err
+ }
+
+ if localVarHTTPResponse.StatusCode >= 300 {
+ newErr := GenericOpenAPIError{
+ body: localVarBody,
+ error: localVarHTTPResponse.Status,
+ }
+ if localVarHTTPResponse.StatusCode >= 400 && localVarHTTPResponse.StatusCode <= 600 {
+ var v Failure
+ err = r.apiService.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
+ if err != nil {
+ newErr.error = err.Error()
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+ newErr.model = v
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+
+ err = r.apiService.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
+ if err != nil {
+ newErr := GenericOpenAPIError{
+ body: localVarBody,
+ error: err.Error(),
+ }
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+
+ return localVarReturnValue, localVarHTTPResponse, nil
+}
+
+type apiPinsRequestidDeleteRequest struct {
+ ctx _context.Context
+ apiService *PinsApiService
+ requestid string
+}
+
+/*
+PinsRequestidDelete Remove pin object
+Remove a pin object
+ - @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
+ - @param requestid
+
+@return apiPinsRequestidDeleteRequest
+*/
+func (a *PinsApiService) PinsRequestidDelete(ctx _context.Context, requestid string) apiPinsRequestidDeleteRequest {
+ return apiPinsRequestidDeleteRequest{
+ apiService: a,
+ ctx: ctx,
+ requestid: requestid,
+ }
+}
+
+/*
+Execute executes the request
+*/
+func (r apiPinsRequestidDeleteRequest) Execute() (*_nethttp.Response, error) {
+ var (
+ localVarHTTPMethod = _nethttp.MethodDelete
+ localVarPostBody interface{}
+ localVarFormFileName string
+ localVarFileName string
+ localVarFileBytes []byte
+ )
+
+ localBasePath, err := r.apiService.client.cfg.ServerURLWithContext(r.ctx, "PinsApiService.PinsRequestidDelete")
+ if err != nil {
+ return nil, GenericOpenAPIError{error: err.Error()}
+ }
+
+ localVarPath := localBasePath + "/pins/{requestid}"
+ localVarPath = strings.Replace(localVarPath, "{"+"requestid"+"}", _neturl.PathEscape(parameterToString(r.requestid, "")), -1)
+
+ localVarHeaderParams := make(map[string]string)
+ localVarQueryParams := _neturl.Values{}
+ localVarFormParams := _neturl.Values{}
+
+ // to determine the Content-Type header
+ localVarHTTPContentTypes := []string{}
+
+ // set Content-Type header
+ localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
+ if localVarHTTPContentType != "" {
+ localVarHeaderParams["Content-Type"] = localVarHTTPContentType
+ }
+
+ // to determine the Accept header
+ localVarHTTPHeaderAccepts := []string{"application/json"}
+
+ // set Accept header
+ localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
+ if localVarHTTPHeaderAccept != "" {
+ localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
+ }
+ req, err := r.apiService.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes)
+ if err != nil {
+ return nil, err
+ }
+
+ localVarHTTPResponse, err := r.apiService.client.callAPI(req)
+ if err != nil || localVarHTTPResponse == nil {
+ return localVarHTTPResponse, err
+ }
+
+ localVarBody, err := _io.ReadAll(localVarHTTPResponse.Body)
+ localVarHTTPResponse.Body.Close()
+ if err != nil {
+ return localVarHTTPResponse, err
+ }
+
+ if localVarHTTPResponse.StatusCode >= 300 {
+ newErr := GenericOpenAPIError{
+ body: localVarBody,
+ error: localVarHTTPResponse.Status,
+ }
+ if localVarHTTPResponse.StatusCode >= 400 && localVarHTTPResponse.StatusCode <= 600 {
+ var v Failure
+ err = r.apiService.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
+ if err != nil {
+ newErr.error = err.Error()
+ return localVarHTTPResponse, newErr
+ }
+ newErr.model = v
+ return localVarHTTPResponse, newErr
+ }
+ return localVarHTTPResponse, newErr
+ }
+
+ return localVarHTTPResponse, nil
+}
+
+type apiPinsRequestidGetRequest struct {
+ ctx _context.Context
+ apiService *PinsApiService
+ requestid string
+}
+
+/*
+PinsRequestidGet Get pin object
+Get a pin object and its status
+ - @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
+ - @param requestid
+
+@return apiPinsRequestidGetRequest
+*/
+func (a *PinsApiService) PinsRequestidGet(ctx _context.Context, requestid string) apiPinsRequestidGetRequest {
+ return apiPinsRequestidGetRequest{
+ apiService: a,
+ ctx: ctx,
+ requestid: requestid,
+ }
+}
+
+/*
+Execute executes the request
+
+ @return PinStatus
+*/
+func (r apiPinsRequestidGetRequest) Execute() (PinStatus, *_nethttp.Response, error) {
+ var (
+ localVarHTTPMethod = _nethttp.MethodGet
+ localVarPostBody interface{}
+ localVarFormFileName string
+ localVarFileName string
+ localVarFileBytes []byte
+ localVarReturnValue PinStatus
+ )
+
+ localBasePath, err := r.apiService.client.cfg.ServerURLWithContext(r.ctx, "PinsApiService.PinsRequestidGet")
+ if err != nil {
+ return localVarReturnValue, nil, GenericOpenAPIError{error: err.Error()}
+ }
+
+ localVarPath := localBasePath + "/pins/{requestid}"
+ localVarPath = strings.Replace(localVarPath, "{"+"requestid"+"}", _neturl.PathEscape(parameterToString(r.requestid, "")), -1)
+
+ localVarHeaderParams := make(map[string]string)
+ localVarQueryParams := _neturl.Values{}
+ localVarFormParams := _neturl.Values{}
+
+ // to determine the Content-Type header
+ localVarHTTPContentTypes := []string{}
+
+ // set Content-Type header
+ localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
+ if localVarHTTPContentType != "" {
+ localVarHeaderParams["Content-Type"] = localVarHTTPContentType
+ }
+
+ // to determine the Accept header
+ localVarHTTPHeaderAccepts := []string{"application/json"}
+
+ // set Accept header
+ localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
+ if localVarHTTPHeaderAccept != "" {
+ localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
+ }
+ req, err := r.apiService.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes)
+ if err != nil {
+ return localVarReturnValue, nil, err
+ }
+
+ localVarHTTPResponse, err := r.apiService.client.callAPI(req)
+ if err != nil || localVarHTTPResponse == nil {
+ return localVarReturnValue, localVarHTTPResponse, err
+ }
+
+ localVarBody, err := _io.ReadAll(localVarHTTPResponse.Body)
+ localVarHTTPResponse.Body.Close()
+ if err != nil {
+ return localVarReturnValue, localVarHTTPResponse, err
+ }
+
+ if localVarHTTPResponse.StatusCode >= 300 {
+ newErr := GenericOpenAPIError{
+ body: localVarBody,
+ error: localVarHTTPResponse.Status,
+ }
+ if localVarHTTPResponse.StatusCode >= 400 && localVarHTTPResponse.StatusCode <= 600 {
+ var v Failure
+ err = r.apiService.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
+ if err != nil {
+ newErr.error = err.Error()
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+ newErr.model = v
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+
+ err = r.apiService.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
+ if err != nil {
+ newErr := GenericOpenAPIError{
+ body: localVarBody,
+ error: err.Error(),
+ }
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+
+ return localVarReturnValue, localVarHTTPResponse, nil
+}
+
+type apiPinsRequestidPostRequest struct {
+ ctx _context.Context
+ apiService *PinsApiService
+ requestid string
+ pin *Pin
+}
+
+func (r apiPinsRequestidPostRequest) Pin(pin Pin) apiPinsRequestidPostRequest {
+ r.pin = &pin
+ return r
+}
+
+/*
+PinsRequestidPost Replace pin object
+Replace an existing pin object (shortcut for executing remove and add operations in one step to avoid unnecessary garbage collection of blocks present in both recursive pins)
+ - @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
+ - @param requestid
+
+@return apiPinsRequestidPostRequest
+*/
+func (a *PinsApiService) PinsRequestidPost(ctx _context.Context, requestid string) apiPinsRequestidPostRequest {
+ return apiPinsRequestidPostRequest{
+ apiService: a,
+ ctx: ctx,
+ requestid: requestid,
+ }
+}
+
+/*
+Execute executes the request
+
+ @return PinStatus
+*/
+func (r apiPinsRequestidPostRequest) Execute() (PinStatus, *_nethttp.Response, error) {
+ var (
+ localVarHTTPMethod = _nethttp.MethodPost
+ localVarPostBody interface{}
+ localVarFormFileName string
+ localVarFileName string
+ localVarFileBytes []byte
+ localVarReturnValue PinStatus
+ )
+
+ localBasePath, err := r.apiService.client.cfg.ServerURLWithContext(r.ctx, "PinsApiService.PinsRequestidPost")
+ if err != nil {
+ return localVarReturnValue, nil, GenericOpenAPIError{error: err.Error()}
+ }
+
+ localVarPath := localBasePath + "/pins/{requestid}"
+ localVarPath = strings.Replace(localVarPath, "{"+"requestid"+"}", _neturl.PathEscape(parameterToString(r.requestid, "")), -1)
+
+ localVarHeaderParams := make(map[string]string)
+ localVarQueryParams := _neturl.Values{}
+ localVarFormParams := _neturl.Values{}
+
+ if r.pin == nil {
+ return localVarReturnValue, nil, reportError("pin is required and must be specified")
+ }
+
+ // to determine the Content-Type header
+ localVarHTTPContentTypes := []string{"application/json"}
+
+ // set Content-Type header
+ localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
+ if localVarHTTPContentType != "" {
+ localVarHeaderParams["Content-Type"] = localVarHTTPContentType
+ }
+
+ // to determine the Accept header
+ localVarHTTPHeaderAccepts := []string{"application/json"}
+
+ // set Accept header
+ localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
+ if localVarHTTPHeaderAccept != "" {
+ localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
+ }
+ // body params
+ localVarPostBody = r.pin
+ req, err := r.apiService.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes)
+ if err != nil {
+ return localVarReturnValue, nil, err
+ }
+
+ localVarHTTPResponse, err := r.apiService.client.callAPI(req)
+ if err != nil || localVarHTTPResponse == nil {
+ return localVarReturnValue, localVarHTTPResponse, err
+ }
+
+ localVarBody, err := _io.ReadAll(localVarHTTPResponse.Body)
+ localVarHTTPResponse.Body.Close()
+ if err != nil {
+ return localVarReturnValue, localVarHTTPResponse, err
+ }
+
+ if localVarHTTPResponse.StatusCode >= 300 {
+ newErr := GenericOpenAPIError{
+ body: localVarBody,
+ error: localVarHTTPResponse.Status,
+ }
+ if localVarHTTPResponse.StatusCode >= 400 && localVarHTTPResponse.StatusCode <= 600 {
+ var v Failure
+ err = r.apiService.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
+ if err != nil {
+ newErr.error = err.Error()
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+ newErr.model = v
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+
+ err = r.apiService.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
+ if err != nil {
+ newErr := GenericOpenAPIError{
+ body: localVarBody,
+ error: err.Error(),
+ }
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+
+ return localVarReturnValue, localVarHTTPResponse, nil
+}
diff --git a/pinning/remote/client/openapi/client.go b/pinning/remote/client/openapi/client.go
new file mode 100644
index 0000000000..b3cea998b5
--- /dev/null
+++ b/pinning/remote/client/openapi/client.go
@@ -0,0 +1,498 @@
+/*
+ * IPFS Pinning Service API
+ *
+ * ## About this spec The IPFS Pinning Service API is intended to be an implementation-agnostic API: - For use and implementation by pinning service providers - For use in client mode by IPFS nodes and GUI-based applications > **Note**: while ready for implementation, this spec is still a work in progress! 🏗️ **Your input and feedback are welcome and valuable as we develop this API spec. Please join the design discussion at [github.com/ipfs/pinning-services-api-spec](https://github.com/ipfs/pinning-services-api-spec).** # Schemas This section describes the most important object types and conventions. A full list of fields and schemas can be found in the `schemas` section of the [YAML file](https://github.com/ipfs/pinning-services-api-spec/blob/master/ipfs-pinning-service.yaml). ## Identifiers ### cid [Content Identifier (CID)](https://docs.ipfs.io/concepts/content-addressing/) points at the root of a DAG that is pinned recursively. ### requestid Unique identifier of a pin request. When a pin is created, the service responds with unique `requestid` that can be later used for pin removal. When the same `cid` is pinned again, a different `requestid` is returned to differentiate between those pin requests. Service implementation should use UUID, `hash(accessToken,Pin,PinStatus.created)`, or any other opaque identifier that provides equally strong protection against race conditions. ## Objects ### Pin object ![pin object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pin.png) The `Pin` object is a representation of a pin request. It includes the `cid` of data to be pinned, as well as optional metadata in `name`, `origins`, and `meta`. ### Pin status response ![pin status response object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pinstatus.png) The `PinStatus` object is a representation of the current state of a pinning operation. It includes the original `pin` object, along with the current `status` and globally unique `requestid` of the entire pinning request, which can be used for future status checks and management. Addresses in the `delegates` array are peers delegated by the pinning service for facilitating direct file transfers (more details in the provider hints section). Any additional vendor-specific information is returned in optional `info`. ## The pin lifecycle ![pinning service objects and lifecycle](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/lifecycle.png) ### Creating a new pin object The user sends a `Pin` object to `POST /pins` and receives a `PinStatus` response: - `requestid` in `PinStatus` is the identifier of the pin operation, which can can be used for checking status, and removing the pin in the future - `status` in `PinStatus` indicates the current state of a pin ### Checking status of in-progress pinning `status` (in `PinStatus`) may indicate a pending state (`queued` or `pinning`). This means the data behind `Pin.cid` was not found on the pinning service and is being fetched from the IPFS network at large, which may take time. In this case, the user can periodically check pinning progress via `GET /pins/{requestid}` until pinning is successful, or the user decides to remove the pending pin. ### Replacing an existing pin object The user can replace an existing pin object via `POST /pins/{requestid}`. This is a shortcut for removing a pin object identified by `requestid` and creating a new one in a single API call that protects against undesired garbage collection of blocks common to both pins. Useful when updating a pin representing a huge dataset where most of blocks did not change. The new pin object `requestid` is returned in the `PinStatus` response. The old pin object is deleted automatically. ### Removing a pin object A pin object can be removed via `DELETE /pins/{requestid}`. ## Provider hints Pinning of new data can be accelerated by providing a list of known data sources in `Pin.origins`, and connecting at least one of them to pinning service nodes at `PinStatus.delegates`. The most common scenario is a client putting its own IPFS node's multiaddrs in `Pin.origins`, and then directly connecting to every multiaddr returned by a pinning service in `PinStatus.delegates` to initiate transfer. This ensures data transfer starts immediately (without waiting for provider discovery over DHT), and direct dial from a client works around peer routing issues in restrictive network topologies such as NATs. ## Custom metadata Pinning services are encouraged to add support for additional features by leveraging the optional `Pin.meta` and `PinStatus.info` fields. While these attributes can be application- or vendor-specific, we encourage the community at large to leverage these attributes as a sandbox to come up with conventions that could become part of future revisions of this API. ### Pin metadata String keys and values passed in `Pin.meta` are persisted with the pin object. Potential uses: - `Pin.meta[app_id]`: Attaching a unique identifier to pins created by an app enables filtering pins per app via `?meta={\"app_id\":}` - `Pin.meta[vendor_policy]`: Vendor-specific policy (for example: which region to use, how many copies to keep) Note that it is OK for a client to omit or ignore these optional attributes; doing so should not impact the basic pinning functionality. ### Pin status info Additional `PinStatus.info` can be returned by pinning service. Potential uses: - `PinStatus.info[status_details]`: more info about the current status (queue position, percentage of transferred data, summary of where data is stored, etc); when `PinStatus.status=failed`, it could provide a reason why a pin operation failed (e.g. lack of funds, DAG too big, etc.) - `PinStatus.info[dag_size]`: the size of pinned data, along with DAG overhead - `PinStatus.info[raw_size]`: the size of data without DAG overhead (eg. unixfs) - `PinStatus.info[pinned_until]`: if vendor supports time-bound pins, this could indicate when the pin will expire # Pagination and filtering Pin objects can be listed by executing `GET /pins` with optional parameters: - When no filters are provided, the endpoint will return a small batch of the 10 most recently created items, from the latest to the oldest. - The number of returned items can be adjusted with the `limit` parameter (implicit default is 10). - If the value in `PinResults.count` is bigger than the length of `PinResults.results`, the client can infer there are more results that can be queried. - To read more items, pass the `before` filter with the timestamp from `PinStatus.created` found in the oldest item in the current batch of results. Repeat to read all results. - Returned results can be fine-tuned by applying optional `after`, `cid`, `name`, `status`, or `meta` filters. > **Note**: pagination by the `created` timestamp requires each value to be globally unique. Any future considerations to add support for bulk creation must account for this.
+ *
+ * API version: 0.1.1
+ * Generated by: OpenAPI Generator (https://openapi-generator.tech)
+ */
+
+package openapi
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+ "log"
+ "mime/multipart"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+ "os"
+ "path/filepath"
+ "reflect"
+ "regexp"
+ "strings"
+ "time"
+
+ "golang.org/x/oauth2"
+)
+
+var (
+ jsonCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:vnd\.[^;]+\+)?json)`)
+ xmlCheck = regexp.MustCompile(`(?i:(?:application|text)/xml)`)
+)
+
+// APIClient manages communication with the IPFS Pinning Service API API v0.1.1
+// In most cases there should be only one, shared, APIClient.
+type APIClient struct {
+ cfg *Configuration
+ common service // Reuse a single struct instead of allocating one for each service on the heap.
+
+ // API Services
+
+ PinsApi *PinsApiService
+}
+
+type service struct {
+ client *APIClient
+}
+
+// NewAPIClient creates a new API client. Requires a userAgent string describing your application.
+// optionally a custom http.Client to allow for advanced features such as caching.
+func NewAPIClient(cfg *Configuration) *APIClient {
+ if cfg.HTTPClient == nil {
+ cfg.HTTPClient = http.DefaultClient
+ }
+
+ c := &APIClient{}
+ c.cfg = cfg
+ c.common.client = c
+
+ // API Services
+ c.PinsApi = (*PinsApiService)(&c.common)
+
+ return c
+}
+
+// selectHeaderContentType select a content type from the available list.
+func selectHeaderContentType(contentTypes []string) string {
+ if len(contentTypes) == 0 {
+ return ""
+ }
+ if contains(contentTypes, "application/json") {
+ return "application/json"
+ }
+ return contentTypes[0] // use the first content type specified in 'consumes'
+}
+
+// selectHeaderAccept join all accept types and return
+func selectHeaderAccept(accepts []string) string {
+ if len(accepts) == 0 {
+ return ""
+ }
+
+ if contains(accepts, "application/json") {
+ return "application/json"
+ }
+
+ return strings.Join(accepts, ",")
+}
+
+// contains is a case insenstive match, finding needle in a haystack
+func contains(haystack []string, needle string) bool {
+ for _, a := range haystack {
+ if strings.EqualFold(a, needle) {
+ return true
+ }
+ }
+ return false
+}
+
+// parameterToString convert interface{} parameters to string, using a delimiter if format is provided.
+func parameterToString(obj interface{}, collectionFormat string) string {
+ var delimiter string
+
+ switch collectionFormat {
+ case "pipes":
+ delimiter = "|"
+ case "ssv":
+ delimiter = " "
+ case "tsv":
+ delimiter = "\t"
+ case "csv":
+ delimiter = ","
+ }
+
+ if reflect.TypeOf(obj).Kind() == reflect.Slice {
+ return strings.Trim(strings.Replace(fmt.Sprint(obj), " ", delimiter, -1), "[]")
+ } else if t, ok := obj.(time.Time); ok {
+ return t.Format(time.RFC3339Nano)
+ }
+
+ return fmt.Sprintf("%v", obj)
+}
+
+// callAPI do the request.
+func (c *APIClient) callAPI(request *http.Request) (*http.Response, error) {
+ if c.cfg.Debug {
+ dump, err := httputil.DumpRequestOut(request, true)
+ if err != nil {
+ return nil, err
+ }
+ log.Printf("\n%s\n", string(dump))
+ }
+
+ resp, err := c.cfg.HTTPClient.Do(request)
+ if err != nil {
+ return resp, err
+ }
+
+ if c.cfg.Debug {
+ dump, err := httputil.DumpResponse(resp, true)
+ if err != nil {
+ return resp, err
+ }
+ log.Printf("\n%s\n", string(dump))
+ }
+ return resp, err
+}
+
+// Allow modification of underlying config for alternate implementations and testing
+// Caution: modifying the configuration while live can cause data races and potentially unwanted behavior
+func (c *APIClient) GetConfig() *Configuration {
+ return c.cfg
+}
+
+// prepareRequest build the request
+func (c *APIClient) prepareRequest(
+ ctx context.Context,
+ path string, method string,
+ postBody interface{},
+ headerParams map[string]string,
+ queryParams url.Values,
+ formParams url.Values,
+ formFileName string,
+ fileName string,
+ fileBytes []byte) (localVarRequest *http.Request, err error) {
+
+ var body *bytes.Buffer
+
+ // Detect postBody type and post.
+ if postBody != nil {
+ contentType := headerParams["Content-Type"]
+ if contentType == "" {
+ contentType = detectContentType(postBody)
+ headerParams["Content-Type"] = contentType
+ }
+
+ body, err = setBody(postBody, contentType)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // add form parameters and file if available.
+ if strings.HasPrefix(headerParams["Content-Type"], "multipart/form-data") && len(formParams) > 0 || (len(fileBytes) > 0 && fileName != "") {
+ if body != nil {
+ return nil, errors.New("cannot specify postBody and multipart form at the same time")
+ }
+ body = &bytes.Buffer{}
+ w := multipart.NewWriter(body)
+
+ for k, v := range formParams {
+ for _, iv := range v {
+ if strings.HasPrefix(k, "@") { // file
+ err = addFile(w, k[1:], iv)
+ if err != nil {
+ return nil, err
+ }
+ } else { // form value
+ w.WriteField(k, iv)
+ }
+ }
+ }
+ if len(fileBytes) > 0 && fileName != "" {
+ w.Boundary()
+ //_, fileNm := filepath.Split(fileName)
+ part, err := w.CreateFormFile(formFileName, filepath.Base(fileName))
+ if err != nil {
+ return nil, err
+ }
+ _, err = part.Write(fileBytes)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // Set the Boundary in the Content-Type
+ headerParams["Content-Type"] = w.FormDataContentType()
+
+ // Set Content-Length
+ headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len())
+ w.Close()
+ }
+
+ if strings.HasPrefix(headerParams["Content-Type"], "application/x-www-form-urlencoded") && len(formParams) > 0 {
+ if body != nil {
+ return nil, errors.New("cannot specify postBody and x-www-form-urlencoded form at the same time")
+ }
+ body = &bytes.Buffer{}
+ body.WriteString(formParams.Encode())
+ // Set Content-Length
+ headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len())
+ }
+
+ // Setup path and query parameters
+ url, err := url.Parse(path)
+ if err != nil {
+ return nil, err
+ }
+
+ // Override request host, if applicable
+ if c.cfg.Host != "" {
+ url.Host = c.cfg.Host
+ }
+
+ // Override request scheme, if applicable
+ if c.cfg.Scheme != "" {
+ url.Scheme = c.cfg.Scheme
+ }
+
+ // Adding Query Param
+ query := url.Query()
+ for k, v := range queryParams {
+ for _, iv := range v {
+ query.Add(k, iv)
+ }
+ }
+
+ // Encode the parameters.
+ url.RawQuery = query.Encode()
+
+ // Generate a new request
+ if body != nil {
+ localVarRequest, err = http.NewRequest(method, url.String(), body)
+ } else {
+ localVarRequest, err = http.NewRequest(method, url.String(), nil)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ // add header parameters, if any
+ if len(headerParams) > 0 {
+ headers := http.Header{}
+ for h, v := range headerParams {
+ headers.Set(h, v)
+ }
+ localVarRequest.Header = headers
+ }
+
+ // Add the user agent to the request.
+ localVarRequest.Header.Add("User-Agent", c.cfg.UserAgent)
+
+ if ctx != nil {
+ // add context to the request
+ localVarRequest = localVarRequest.WithContext(ctx)
+
+ // Walk through any authentication.
+
+ // OAuth2 authentication
+ if tok, ok := ctx.Value(ContextOAuth2).(oauth2.TokenSource); ok {
+ // We were able to grab an oauth2 token from the context
+ var latestToken *oauth2.Token
+ if latestToken, err = tok.Token(); err != nil {
+ return nil, err
+ }
+
+ latestToken.SetAuthHeader(localVarRequest)
+ }
+
+ // Basic HTTP Authentication
+ if auth, ok := ctx.Value(ContextBasicAuth).(BasicAuth); ok {
+ localVarRequest.SetBasicAuth(auth.UserName, auth.Password)
+ }
+
+ // AccessToken Authentication
+ if auth, ok := ctx.Value(ContextAccessToken).(string); ok {
+ localVarRequest.Header.Add("Authorization", "Bearer "+auth)
+ }
+ }
+
+ for header, value := range c.cfg.DefaultHeader {
+ localVarRequest.Header.Add(header, value)
+ }
+ return localVarRequest, nil
+}
+
+func (c *APIClient) decode(v interface{}, b []byte, contentType string) (err error) {
+ if len(b) == 0 {
+ return nil
+ }
+ if s, ok := v.(*string); ok {
+ *s = string(b)
+ return nil
+ }
+ if xmlCheck.MatchString(contentType) {
+ if err = xml.Unmarshal(b, v); err != nil {
+ return err
+ }
+ return nil
+ }
+ if jsonCheck.MatchString(contentType) {
+ if actualObj, ok := v.(interface{ GetActualInstance() interface{} }); ok { // oneOf, anyOf schemas
+ if unmarshalObj, ok := actualObj.(interface{ UnmarshalJSON([]byte) error }); ok { // make sure it has UnmarshalJSON defined
+ if err = unmarshalObj.UnmarshalJSON(b); err != nil {
+ return err
+ }
+ } else {
+ return errors.New("unknown type with GetActualInstance but no unmarshalObj.UnmarshalJSON defined")
+ }
+ } else if err = json.Unmarshal(b, v); err != nil { // simple model
+ return err
+ }
+ return nil
+ }
+ return errors.New("undefined response type")
+}
+
+// Add a file to the multipart request
+func addFile(w *multipart.Writer, fieldName, path string) error {
+ file, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ part, err := w.CreateFormFile(fieldName, filepath.Base(path))
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(part, file)
+
+ return err
+}
+
+// Prevent trying to import "fmt"
+func reportError(format string, a ...interface{}) error {
+ return fmt.Errorf(format, a...)
+}
+
+// Set request body from an interface{}
+func setBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) {
+ if bodyBuf == nil {
+ bodyBuf = &bytes.Buffer{}
+ }
+
+ if reader, ok := body.(io.Reader); ok {
+ _, err = bodyBuf.ReadFrom(reader)
+ } else if b, ok := body.([]byte); ok {
+ _, err = bodyBuf.Write(b)
+ } else if s, ok := body.(string); ok {
+ _, err = bodyBuf.WriteString(s)
+ } else if s, ok := body.(*string); ok {
+ _, err = bodyBuf.WriteString(*s)
+ } else if jsonCheck.MatchString(contentType) {
+ err = json.NewEncoder(bodyBuf).Encode(body)
+ } else if xmlCheck.MatchString(contentType) {
+ err = xml.NewEncoder(bodyBuf).Encode(body)
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ if bodyBuf.Len() == 0 {
+ err = fmt.Errorf("invalid body type %s", contentType)
+ return nil, err
+ }
+ return bodyBuf, nil
+}
+
+// detectContentType method is used to figure out `Request.Body` content type for request header
+func detectContentType(body interface{}) string {
+ contentType := "text/plain; charset=utf-8"
+ kind := reflect.TypeOf(body).Kind()
+
+ switch kind {
+ case reflect.Struct, reflect.Map, reflect.Ptr:
+ contentType = "application/json; charset=utf-8"
+ case reflect.String:
+ contentType = "text/plain; charset=utf-8"
+ default:
+ if b, ok := body.([]byte); ok {
+ contentType = http.DetectContentType(b)
+ } else if kind == reflect.Slice {
+ contentType = "application/json; charset=utf-8"
+ }
+ }
+
+ return contentType
+}
+
+// Ripped from https://github.com/gregjones/httpcache/blob/master/httpcache.go
+type cacheControl map[string]string
+
+func parseCacheControl(headers http.Header) cacheControl {
+ cc := cacheControl{}
+ ccHeader := headers.Get("Cache-Control")
+ for _, part := range strings.Split(ccHeader, ",") {
+ part = strings.Trim(part, " ")
+ if part == "" {
+ continue
+ }
+ if strings.ContainsRune(part, '=') {
+ keyval := strings.Split(part, "=")
+ cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",")
+ } else {
+ cc[part] = ""
+ }
+ }
+ return cc
+}
+
+// CacheExpires helper function to determine remaining time before repeating a request.
+func CacheExpires(r *http.Response) time.Time {
+ // Figure out when the cache expires.
+ var expires time.Time
+ now, err := time.Parse(time.RFC1123, r.Header.Get("date"))
+ if err != nil {
+ return time.Now()
+ }
+ respCacheControl := parseCacheControl(r.Header)
+
+ if maxAge, ok := respCacheControl["max-age"]; ok {
+ lifetime, err := time.ParseDuration(maxAge + "s")
+ if err != nil {
+ expires = now
+ } else {
+ expires = now.Add(lifetime)
+ }
+ } else {
+ expiresHeader := r.Header.Get("Expires")
+ if expiresHeader != "" {
+ expires, err = time.Parse(time.RFC1123, expiresHeader)
+ if err != nil {
+ expires = now
+ }
+ }
+ }
+ return expires
+}
+
+// GenericOpenAPIError Provides access to the body, error and model on returned errors.
+type GenericOpenAPIError struct {
+ body []byte
+ error string
+ model interface{}
+}
+
+// Error returns non-empty string if there was an error.
+func (e GenericOpenAPIError) Error() string {
+ return e.error
+}
+
+// Body returns the raw bytes of the response
+func (e GenericOpenAPIError) Body() []byte {
+ return e.body
+}
+
+// Model returns the unpacked model of the error
+func (e GenericOpenAPIError) Model() interface{} {
+ return e.model
+}
diff --git a/pinning/remote/client/openapi/configuration.go b/pinning/remote/client/openapi/configuration.go
new file mode 100644
index 0000000000..bb0a8e5073
--- /dev/null
+++ b/pinning/remote/client/openapi/configuration.go
@@ -0,0 +1,228 @@
+/*
+ * IPFS Pinning Service API
+ *
+ * ## About this spec The IPFS Pinning Service API is intended to be an implementation-agnostic API: - For use and implementation by pinning service providers - For use in client mode by IPFS nodes and GUI-based applications > **Note**: while ready for implementation, this spec is still a work in progress! 🏗️ **Your input and feedback are welcome and valuable as we develop this API spec. Please join the design discussion at [github.com/ipfs/pinning-services-api-spec](https://github.com/ipfs/pinning-services-api-spec).** # Schemas This section describes the most important object types and conventions. A full list of fields and schemas can be found in the `schemas` section of the [YAML file](https://github.com/ipfs/pinning-services-api-spec/blob/master/ipfs-pinning-service.yaml). ## Identifiers ### cid [Content Identifier (CID)](https://docs.ipfs.io/concepts/content-addressing/) points at the root of a DAG that is pinned recursively. ### requestid Unique identifier of a pin request. When a pin is created, the service responds with unique `requestid` that can be later used for pin removal. When the same `cid` is pinned again, a different `requestid` is returned to differentiate between those pin requests. Service implementation should use UUID, `hash(accessToken,Pin,PinStatus.created)`, or any other opaque identifier that provides equally strong protection against race conditions. ## Objects ### Pin object ![pin object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pin.png) The `Pin` object is a representation of a pin request. It includes the `cid` of data to be pinned, as well as optional metadata in `name`, `origins`, and `meta`. ### Pin status response ![pin status response object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pinstatus.png) The `PinStatus` object is a representation of the current state of a pinning operation. It includes the original `pin` object, along with the current `status` and globally unique `requestid` of the entire pinning request, which can be used for future status checks and management. Addresses in the `delegates` array are peers delegated by the pinning service for facilitating direct file transfers (more details in the provider hints section). Any additional vendor-specific information is returned in optional `info`. ## The pin lifecycle ![pinning service objects and lifecycle](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/lifecycle.png) ### Creating a new pin object The user sends a `Pin` object to `POST /pins` and receives a `PinStatus` response: - `requestid` in `PinStatus` is the identifier of the pin operation, which can can be used for checking status, and removing the pin in the future - `status` in `PinStatus` indicates the current state of a pin ### Checking status of in-progress pinning `status` (in `PinStatus`) may indicate a pending state (`queued` or `pinning`). This means the data behind `Pin.cid` was not found on the pinning service and is being fetched from the IPFS network at large, which may take time. In this case, the user can periodically check pinning progress via `GET /pins/{requestid}` until pinning is successful, or the user decides to remove the pending pin. ### Replacing an existing pin object The user can replace an existing pin object via `POST /pins/{requestid}`. This is a shortcut for removing a pin object identified by `requestid` and creating a new one in a single API call that protects against undesired garbage collection of blocks common to both pins. Useful when updating a pin representing a huge dataset where most of blocks did not change. The new pin object `requestid` is returned in the `PinStatus` response. The old pin object is deleted automatically. ### Removing a pin object A pin object can be removed via `DELETE /pins/{requestid}`. ## Provider hints Pinning of new data can be accelerated by providing a list of known data sources in `Pin.origins`, and connecting at least one of them to pinning service nodes at `PinStatus.delegates`. The most common scenario is a client putting its own IPFS node's multiaddrs in `Pin.origins`, and then directly connecting to every multiaddr returned by a pinning service in `PinStatus.delegates` to initiate transfer. This ensures data transfer starts immediately (without waiting for provider discovery over DHT), and direct dial from a client works around peer routing issues in restrictive network topologies such as NATs. ## Custom metadata Pinning services are encouraged to add support for additional features by leveraging the optional `Pin.meta` and `PinStatus.info` fields. While these attributes can be application- or vendor-specific, we encourage the community at large to leverage these attributes as a sandbox to come up with conventions that could become part of future revisions of this API. ### Pin metadata String keys and values passed in `Pin.meta` are persisted with the pin object. Potential uses: - `Pin.meta[app_id]`: Attaching a unique identifier to pins created by an app enables filtering pins per app via `?meta={\"app_id\":}` - `Pin.meta[vendor_policy]`: Vendor-specific policy (for example: which region to use, how many copies to keep) Note that it is OK for a client to omit or ignore these optional attributes; doing so should not impact the basic pinning functionality. ### Pin status info Additional `PinStatus.info` can be returned by pinning service. Potential uses: - `PinStatus.info[status_details]`: more info about the current status (queue position, percentage of transferred data, summary of where data is stored, etc); when `PinStatus.status=failed`, it could provide a reason why a pin operation failed (e.g. lack of funds, DAG too big, etc.) - `PinStatus.info[dag_size]`: the size of pinned data, along with DAG overhead - `PinStatus.info[raw_size]`: the size of data without DAG overhead (eg. unixfs) - `PinStatus.info[pinned_until]`: if vendor supports time-bound pins, this could indicate when the pin will expire # Pagination and filtering Pin objects can be listed by executing `GET /pins` with optional parameters: - When no filters are provided, the endpoint will return a small batch of the 10 most recently created items, from the latest to the oldest. - The number of returned items can be adjusted with the `limit` parameter (implicit default is 10). - If the value in `PinResults.count` is bigger than the length of `PinResults.results`, the client can infer there are more results that can be queried. - To read more items, pass the `before` filter with the timestamp from `PinStatus.created` found in the oldest item in the current batch of results. Repeat to read all results. - Returned results can be fine-tuned by applying optional `after`, `cid`, `name`, `status`, or `meta` filters. > **Note**: pagination by the `created` timestamp requires each value to be globally unique. Any future considerations to add support for bulk creation must account for this.
+ *
+ * API version: 0.1.1
+ * Generated by: OpenAPI Generator (https://openapi-generator.tech)
+ */
+
+package openapi
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "strings"
+)
+
+// contextKeys are used to identify the type of value in the context.
+// Since these are string, it is possible to get a short description of the
+// context key for logging and debugging using key.String().
+
+type contextKey string
+
+func (c contextKey) String() string {
+ return "auth " + string(c)
+}
+
+var (
+ // ContextOAuth2 takes an oauth2.TokenSource as authentication for the request.
+ ContextOAuth2 = contextKey("token")
+
+ // ContextBasicAuth takes BasicAuth as authentication for the request.
+ ContextBasicAuth = contextKey("basic")
+
+ // ContextAccessToken takes a string oauth2 access token as authentication for the request.
+ ContextAccessToken = contextKey("accesstoken")
+
+ // ContextAPIKeys takes a string apikey as authentication for the request
+ ContextAPIKeys = contextKey("apiKeys")
+
+ // ContextHttpSignatureAuth takes HttpSignatureAuth as authentication for the request.
+ ContextHttpSignatureAuth = contextKey("httpsignature")
+
+ // ContextServerIndex uses a server configuration from the index.
+ ContextServerIndex = contextKey("serverIndex")
+
+ // ContextOperationServerIndices uses a server configuration from the index mapping.
+ ContextOperationServerIndices = contextKey("serverOperationIndices")
+
+ // ContextServerVariables overrides a server configuration variables.
+ ContextServerVariables = contextKey("serverVariables")
+
+ // ContextOperationServerVariables overrides a server configuration variables using operation specific values.
+ ContextOperationServerVariables = contextKey("serverOperationVariables")
+)
+
+// BasicAuth provides basic http authentication to a request passed via context using ContextBasicAuth
+type BasicAuth struct {
+ UserName string `json:"userName,omitempty"`
+ Password string `json:"password,omitempty"`
+}
+
+// APIKey provides API key based authentication to a request passed via context using ContextAPIKey
+type APIKey struct {
+ Key string
+ Prefix string
+}
+
+// ServerVariable stores the information about a server variable
+type ServerVariable struct {
+ Description string
+ DefaultValue string
+ EnumValues []string
+}
+
+// ServerConfiguration stores the information about a server
+type ServerConfiguration struct {
+ URL string
+ Description string
+ Variables map[string]ServerVariable
+}
+
+// ServerConfigurations stores multiple ServerConfiguration items
+type ServerConfigurations []ServerConfiguration
+
+// Configuration stores the configuration of the API client
+type Configuration struct {
+ Host string `json:"host,omitempty"`
+ Scheme string `json:"scheme,omitempty"`
+ DefaultHeader map[string]string `json:"defaultHeader,omitempty"`
+ UserAgent string `json:"userAgent,omitempty"`
+ Debug bool `json:"debug,omitempty"`
+ Servers ServerConfigurations
+ OperationServers map[string]ServerConfigurations
+ HTTPClient *http.Client
+}
+
+// NewConfiguration returns a new Configuration object
+func NewConfiguration() *Configuration {
+ cfg := &Configuration{
+ DefaultHeader: make(map[string]string),
+ UserAgent: "OpenAPI-Generator/1.0.0/go",
+ Debug: false,
+ Servers: ServerConfigurations{
+ {
+ URL: "https://pinning-service.example.com",
+ Description: "No description provided",
+ },
+ },
+ OperationServers: map[string]ServerConfigurations{},
+ }
+ return cfg
+}
+
+// AddDefaultHeader adds a new HTTP header to the default header in the request
+func (c *Configuration) AddDefaultHeader(key string, value string) {
+ c.DefaultHeader[key] = value
+}
+
+// URL formats template on a index using given variables
+func (sc ServerConfigurations) URL(index int, variables map[string]string) (string, error) {
+ if index < 0 || len(sc) <= index {
+ return "", fmt.Errorf("index %v out of range %v", index, len(sc)-1)
+ }
+ server := sc[index]
+ url := server.URL
+
+ // go through variables and replace placeholders
+ for name, variable := range server.Variables {
+ if value, ok := variables[name]; ok {
+ found := bool(len(variable.EnumValues) == 0)
+ for _, enumValue := range variable.EnumValues {
+ if value == enumValue {
+ found = true
+ }
+ }
+ if !found {
+ return "", fmt.Errorf("the variable %s in the server URL has invalid value %v. Must be %v", name, value, variable.EnumValues)
+ }
+ url = strings.Replace(url, "{"+name+"}", value, -1)
+ } else {
+ url = strings.Replace(url, "{"+name+"}", variable.DefaultValue, -1)
+ }
+ }
+ return url, nil
+}
+
+// ServerURL returns URL based on server settings
+func (c *Configuration) ServerURL(index int, variables map[string]string) (string, error) {
+ return c.Servers.URL(index, variables)
+}
+
+func getServerIndex(ctx context.Context) (int, error) {
+ si := ctx.Value(ContextServerIndex)
+ if si != nil {
+ if index, ok := si.(int); ok {
+ return index, nil
+ }
+ return 0, reportError("Invalid type %T should be int", si)
+ }
+ return 0, nil
+}
+
+func getServerOperationIndex(ctx context.Context, endpoint string) (int, error) {
+ osi := ctx.Value(ContextOperationServerIndices)
+ if osi != nil {
+ if operationIndices, ok := osi.(map[string]int); !ok {
+ return 0, reportError("Invalid type %T should be map[string]int", osi)
+ } else {
+ index, ok := operationIndices[endpoint]
+ if ok {
+ return index, nil
+ }
+ }
+ }
+ return getServerIndex(ctx)
+}
+
+func getServerVariables(ctx context.Context) (map[string]string, error) {
+ sv := ctx.Value(ContextServerVariables)
+ if sv != nil {
+ if variables, ok := sv.(map[string]string); ok {
+ return variables, nil
+ }
+ return nil, reportError("ctx value of ContextServerVariables has invalid type %T should be map[string]string", sv)
+ }
+ return nil, nil
+}
+
+func getServerOperationVariables(ctx context.Context, endpoint string) (map[string]string, error) {
+ osv := ctx.Value(ContextOperationServerVariables)
+ if osv != nil {
+ if operationVariables, ok := osv.(map[string]map[string]string); !ok {
+ return nil, reportError("ctx value of ContextOperationServerVariables has invalid type %T should be map[string]map[string]string", osv)
+ } else {
+ variables, ok := operationVariables[endpoint]
+ if ok {
+ return variables, nil
+ }
+ }
+ }
+ return getServerVariables(ctx)
+}
+
+// ServerURLWithContext returns a new server URL given an endpoint
+func (c *Configuration) ServerURLWithContext(ctx context.Context, endpoint string) (string, error) {
+ sc, ok := c.OperationServers[endpoint]
+ if !ok {
+ sc = c.Servers
+ }
+
+ if ctx == nil {
+ return sc.URL(0, nil)
+ }
+
+ index, err := getServerOperationIndex(ctx, endpoint)
+ if err != nil {
+ return "", err
+ }
+
+ variables, err := getServerOperationVariables(ctx, endpoint)
+ if err != nil {
+ return "", err
+ }
+
+ return sc.URL(index, variables)
+}
diff --git a/pinning/remote/client/openapi/docs/Failure.md b/pinning/remote/client/openapi/docs/Failure.md
new file mode 100644
index 0000000000..c899f7138a
--- /dev/null
+++ b/pinning/remote/client/openapi/docs/Failure.md
@@ -0,0 +1,51 @@
+# Failure
+
+## Properties
+
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**Error** | [**FailureError**](Failure_error.md) | |
+
+## Methods
+
+### NewFailure
+
+`func NewFailure(error_ FailureError, ) *Failure`
+
+NewFailure instantiates a new Failure object
+This constructor will assign default values to properties that have it defined,
+and makes sure properties required by API are set, but the set of arguments
+will change when the set of required properties is changed
+
+### NewFailureWithDefaults
+
+`func NewFailureWithDefaults() *Failure`
+
+NewFailureWithDefaults instantiates a new Failure object
+This constructor will only assign default values to properties that have it defined,
+but it doesn't guarantee that properties required by API are set
+
+### GetError
+
+`func (o *Failure) GetError() FailureError`
+
+GetError returns the Error field if non-nil, zero value otherwise.
+
+### GetErrorOk
+
+`func (o *Failure) GetErrorOk() (*FailureError, bool)`
+
+GetErrorOk returns a tuple with the Error field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetError
+
+`func (o *Failure) SetError(v FailureError)`
+
+SetError sets Error field to given value.
+
+
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/pinning/remote/client/openapi/docs/FailureError.md b/pinning/remote/client/openapi/docs/FailureError.md
new file mode 100644
index 0000000000..478f1b942a
--- /dev/null
+++ b/pinning/remote/client/openapi/docs/FailureError.md
@@ -0,0 +1,77 @@
+# FailureError
+
+## Properties
+
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**Reason** | **string** | Mandatory string identifying the type of error |
+**Details** | Pointer to **string** | Optional, longer description of the error; may include UUID of transaction for support, links to documentation etc | [optional]
+
+## Methods
+
+### NewFailureError
+
+`func NewFailureError(reason string, ) *FailureError`
+
+NewFailureError instantiates a new FailureError object
+This constructor will assign default values to properties that have it defined,
+and makes sure properties required by API are set, but the set of arguments
+will change when the set of required properties is changed
+
+### NewFailureErrorWithDefaults
+
+`func NewFailureErrorWithDefaults() *FailureError`
+
+NewFailureErrorWithDefaults instantiates a new FailureError object
+This constructor will only assign default values to properties that have it defined,
+but it doesn't guarantee that properties required by API are set
+
+### GetReason
+
+`func (o *FailureError) GetReason() string`
+
+GetReason returns the Reason field if non-nil, zero value otherwise.
+
+### GetReasonOk
+
+`func (o *FailureError) GetReasonOk() (*string, bool)`
+
+GetReasonOk returns a tuple with the Reason field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetReason
+
+`func (o *FailureError) SetReason(v string)`
+
+SetReason sets Reason field to given value.
+
+
+### GetDetails
+
+`func (o *FailureError) GetDetails() string`
+
+GetDetails returns the Details field if non-nil, zero value otherwise.
+
+### GetDetailsOk
+
+`func (o *FailureError) GetDetailsOk() (*string, bool)`
+
+GetDetailsOk returns a tuple with the Details field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetDetails
+
+`func (o *FailureError) SetDetails(v string)`
+
+SetDetails sets Details field to given value.
+
+### HasDetails
+
+`func (o *FailureError) HasDetails() bool`
+
+HasDetails returns a boolean if a field has been set.
+
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/pinning/remote/client/openapi/docs/Pin.md b/pinning/remote/client/openapi/docs/Pin.md
new file mode 100644
index 0000000000..e5d3e0f18f
--- /dev/null
+++ b/pinning/remote/client/openapi/docs/Pin.md
@@ -0,0 +1,129 @@
+# Pin
+
+## Properties
+
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**Cid** | **string** | Content Identifier (CID) to be pinned recursively |
+**Name** | Pointer to **string** | Optional name for pinned data; can be used for lookups later | [optional]
+**Origins** | Pointer to **[]string** | Optional list of multiaddrs known to provide the data | [optional]
+**Meta** | Pointer to **map[string]string** | Optional metadata for pin object | [optional]
+
+## Methods
+
+### NewPin
+
+`func NewPin(cid string, ) *Pin`
+
+NewPin instantiates a new Pin object
+This constructor will assign default values to properties that have it defined,
+and makes sure properties required by API are set, but the set of arguments
+will change when the set of required properties is changed
+
+### NewPinWithDefaults
+
+`func NewPinWithDefaults() *Pin`
+
+NewPinWithDefaults instantiates a new Pin object
+This constructor will only assign default values to properties that have it defined,
+but it doesn't guarantee that properties required by API are set
+
+### GetCid
+
+`func (o *Pin) GetCid() string`
+
+GetCid returns the Cid field if non-nil, zero value otherwise.
+
+### GetCidOk
+
+`func (o *Pin) GetCidOk() (*string, bool)`
+
+GetCidOk returns a tuple with the Cid field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetCid
+
+`func (o *Pin) SetCid(v string)`
+
+SetCid sets Cid field to given value.
+
+
+### GetName
+
+`func (o *Pin) GetName() string`
+
+GetName returns the Name field if non-nil, zero value otherwise.
+
+### GetNameOk
+
+`func (o *Pin) GetNameOk() (*string, bool)`
+
+GetNameOk returns a tuple with the Name field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetName
+
+`func (o *Pin) SetName(v string)`
+
+SetName sets Name field to given value.
+
+### HasName
+
+`func (o *Pin) HasName() bool`
+
+HasName returns a boolean if a field has been set.
+
+### GetOrigins
+
+`func (o *Pin) GetOrigins() []string`
+
+GetOrigins returns the Origins field if non-nil, zero value otherwise.
+
+### GetOriginsOk
+
+`func (o *Pin) GetOriginsOk() (*[]string, bool)`
+
+GetOriginsOk returns a tuple with the Origins field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetOrigins
+
+`func (o *Pin) SetOrigins(v []string)`
+
+SetOrigins sets Origins field to given value.
+
+### HasOrigins
+
+`func (o *Pin) HasOrigins() bool`
+
+HasOrigins returns a boolean if a field has been set.
+
+### GetMeta
+
+`func (o *Pin) GetMeta() map[string]string`
+
+GetMeta returns the Meta field if non-nil, zero value otherwise.
+
+### GetMetaOk
+
+`func (o *Pin) GetMetaOk() (*map[string]string, bool)`
+
+GetMetaOk returns a tuple with the Meta field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetMeta
+
+`func (o *Pin) SetMeta(v map[string]string)`
+
+SetMeta sets Meta field to given value.
+
+### HasMeta
+
+`func (o *Pin) HasMeta() bool`
+
+HasMeta returns a boolean if a field has been set.
+
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/pinning/remote/client/openapi/docs/PinResults.md b/pinning/remote/client/openapi/docs/PinResults.md
new file mode 100644
index 0000000000..1982bfddb0
--- /dev/null
+++ b/pinning/remote/client/openapi/docs/PinResults.md
@@ -0,0 +1,72 @@
+# PinResults
+
+## Properties
+
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**Count** | **int32** | The total number of pin objects that exist for passed query filters |
+**Results** | [**[]PinStatus**](PinStatus.md) | An array of PinStatus results |
+
+## Methods
+
+### NewPinResults
+
+`func NewPinResults(count int32, results []PinStatus, ) *PinResults`
+
+NewPinResults instantiates a new PinResults object
+This constructor will assign default values to properties that have it defined,
+and makes sure properties required by API are set, but the set of arguments
+will change when the set of required properties is changed
+
+### NewPinResultsWithDefaults
+
+`func NewPinResultsWithDefaults() *PinResults`
+
+NewPinResultsWithDefaults instantiates a new PinResults object
+This constructor will only assign default values to properties that have it defined,
+but it doesn't guarantee that properties required by API are set
+
+### GetCount
+
+`func (o *PinResults) GetCount() int32`
+
+GetCount returns the Count field if non-nil, zero value otherwise.
+
+### GetCountOk
+
+`func (o *PinResults) GetCountOk() (*int32, bool)`
+
+GetCountOk returns a tuple with the Count field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetCount
+
+`func (o *PinResults) SetCount(v int32)`
+
+SetCount sets Count field to given value.
+
+
+### GetResults
+
+`func (o *PinResults) GetResults() []PinStatus`
+
+GetResults returns the Results field if non-nil, zero value otherwise.
+
+### GetResultsOk
+
+`func (o *PinResults) GetResultsOk() (*[]PinStatus, bool)`
+
+GetResultsOk returns a tuple with the Results field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetResults
+
+`func (o *PinResults) SetResults(v []PinStatus)`
+
+SetResults sets Results field to given value.
+
+
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/pinning/remote/client/openapi/docs/PinStatus.md b/pinning/remote/client/openapi/docs/PinStatus.md
new file mode 100644
index 0000000000..40ae992ab0
--- /dev/null
+++ b/pinning/remote/client/openapi/docs/PinStatus.md
@@ -0,0 +1,161 @@
+# PinStatus
+
+## Properties
+
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**Requestid** | **string** | Globally unique identifier of the pin request; can be used to check the status of ongoing pinning, or pin removal |
+**Status** | [**Status**](Status.md) | |
+**Created** | [**time.Time**](time.Time.md) | Immutable timestamp indicating when a pin request entered a pinning service; can be used for filtering results and pagination |
+**Pin** | [**Pin**](Pin.md) | |
+**Delegates** | **[]string** | List of multiaddrs designated by pinning service for transferring any new data from external peers |
+**Info** | Pointer to **map[string]string** | Optional info for PinStatus response | [optional]
+
+## Methods
+
+### NewPinStatus
+
+`func NewPinStatus(requestid string, status Status, created time.Time, pin Pin, delegates []string, ) *PinStatus`
+
+NewPinStatus instantiates a new PinStatus object
+This constructor will assign default values to properties that have it defined,
+and makes sure properties required by API are set, but the set of arguments
+will change when the set of required properties is changed
+
+### NewPinStatusWithDefaults
+
+`func NewPinStatusWithDefaults() *PinStatus`
+
+NewPinStatusWithDefaults instantiates a new PinStatus object
+This constructor will only assign default values to properties that have it defined,
+but it doesn't guarantee that properties required by API are set
+
+### GetRequestid
+
+`func (o *PinStatus) GetRequestid() string`
+
+GetRequestid returns the Requestid field if non-nil, zero value otherwise.
+
+### GetRequestidOk
+
+`func (o *PinStatus) GetRequestidOk() (*string, bool)`
+
+GetRequestidOk returns a tuple with the Requestid field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetRequestid
+
+`func (o *PinStatus) SetRequestid(v string)`
+
+SetRequestid sets Requestid field to given value.
+
+
+### GetStatus
+
+`func (o *PinStatus) GetStatus() Status`
+
+GetStatus returns the Status field if non-nil, zero value otherwise.
+
+### GetStatusOk
+
+`func (o *PinStatus) GetStatusOk() (*Status, bool)`
+
+GetStatusOk returns a tuple with the Status field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetStatus
+
+`func (o *PinStatus) SetStatus(v Status)`
+
+SetStatus sets Status field to given value.
+
+
+### GetCreated
+
+`func (o *PinStatus) GetCreated() time.Time`
+
+GetCreated returns the Created field if non-nil, zero value otherwise.
+
+### GetCreatedOk
+
+`func (o *PinStatus) GetCreatedOk() (*time.Time, bool)`
+
+GetCreatedOk returns a tuple with the Created field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetCreated
+
+`func (o *PinStatus) SetCreated(v time.Time)`
+
+SetCreated sets Created field to given value.
+
+
+### GetPin
+
+`func (o *PinStatus) GetPin() Pin`
+
+GetPin returns the Pin field if non-nil, zero value otherwise.
+
+### GetPinOk
+
+`func (o *PinStatus) GetPinOk() (*Pin, bool)`
+
+GetPinOk returns a tuple with the Pin field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetPin
+
+`func (o *PinStatus) SetPin(v Pin)`
+
+SetPin sets Pin field to given value.
+
+
+### GetDelegates
+
+`func (o *PinStatus) GetDelegates() []string`
+
+GetDelegates returns the Delegates field if non-nil, zero value otherwise.
+
+### GetDelegatesOk
+
+`func (o *PinStatus) GetDelegatesOk() (*[]string, bool)`
+
+GetDelegatesOk returns a tuple with the Delegates field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetDelegates
+
+`func (o *PinStatus) SetDelegates(v []string)`
+
+SetDelegates sets Delegates field to given value.
+
+
+### GetInfo
+
+`func (o *PinStatus) GetInfo() map[string]string`
+
+GetInfo returns the Info field if non-nil, zero value otherwise.
+
+### GetInfoOk
+
+`func (o *PinStatus) GetInfoOk() (*map[string]string, bool)`
+
+GetInfoOk returns a tuple with the Info field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetInfo
+
+`func (o *PinStatus) SetInfo(v map[string]string)`
+
+SetInfo sets Info field to given value.
+
+### HasInfo
+
+`func (o *PinStatus) HasInfo() bool`
+
+HasInfo returns a boolean if a field has been set.
+
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/pinning/remote/client/openapi/docs/PinsApi.md b/pinning/remote/client/openapi/docs/PinsApi.md
new file mode 100644
index 0000000000..00cff667b6
--- /dev/null
+++ b/pinning/remote/client/openapi/docs/PinsApi.md
@@ -0,0 +1,367 @@
+# \PinsApi
+
+All URIs are relative to *https://pinning-service.example.com*
+
+Method | HTTP request | Description
+------------- | ------------- | -------------
+[**PinsGet**](PinsApi.md#PinsGet) | **Get** /pins | List pin objects
+[**PinsPost**](PinsApi.md#PinsPost) | **Post** /pins | Add pin object
+[**PinsRequestidDelete**](PinsApi.md#PinsRequestidDelete) | **Delete** /pins/{requestid} | Remove pin object
+[**PinsRequestidGet**](PinsApi.md#PinsRequestidGet) | **Get** /pins/{requestid} | Get pin object
+[**PinsRequestidPost**](PinsApi.md#PinsRequestidPost) | **Post** /pins/{requestid} | Replace pin object
+
+
+
+## PinsGet
+
+> PinResults PinsGet(ctx).Cid(cid).Name(name).Status(status).Before(before).After(after).Limit(limit).Meta(meta).Execute()
+
+List pin objects
+
+
+
+### Example
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ openapiclient "./openapi"
+)
+
+func main() {
+ cid := []string{"Inner_example"} // []string | Return pin objects responsible for pinning the specified CID(s); be aware that using longer hash functions introduces further constraints on the number of CIDs that will fit under the limit of 2000 characters per URL in browser contexts (optional)
+ name := "name_example" // string | Return pin objects with names that contain provided value (case-insensitive, partial or full match) (optional)
+ status := []Status{openapiclient.Status{}} // []Status | Return pin objects for pins with the specified status (optional)
+ before := Get-Date // time.Time | Return results created (queued) before provided timestamp (optional)
+ after := Get-Date // time.Time | Return results created (queued) after provided timestamp (optional)
+ limit := 987 // int32 | Max records to return (optional) (default to 10)
+ meta := map[string]string{ "Key" = "Value" } // map[string]string | Return pin objects that match specified metadata (optional)
+
+ configuration := openapiclient.NewConfiguration()
+ api_client := openapiclient.NewAPIClient(configuration)
+ resp, r, err := api_client.PinsApi.PinsGet(context.Background(), ).Cid(cid).Name(name).Status(status).Before(before).After(after).Limit(limit).Meta(meta).Execute()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error when calling `PinsApi.PinsGet``: %v\n", err)
+ fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r)
+ }
+ // response from `PinsGet`: PinResults
+ fmt.Fprintf(os.Stdout, "Response from `PinsApi.PinsGet`: %v\n", resp)
+}
+```
+
+### Path Parameters
+
+
+
+### Other Parameters
+
+Other parameters are passed through a pointer to a apiPinsGetRequest struct via the builder pattern
+
+
+Name | Type | Description | Notes
+------------- | ------------- | ------------- | -------------
+ **cid** | [**[]string**](string.md) | Return pin objects responsible for pinning the specified CID(s); be aware that using longer hash functions introduces further constraints on the number of CIDs that will fit under the limit of 2000 characters per URL in browser contexts |
+ **name** | **string** | Return pin objects with names that contain provided value (case-insensitive, partial or full match) |
+ **status** | [**[]Status**](Status.md) | Return pin objects for pins with the specified status |
+ **before** | **time.Time** | Return results created (queued) before provided timestamp |
+ **after** | **time.Time** | Return results created (queued) after provided timestamp |
+ **limit** | **int32** | Max records to return | [default to 10]
+ **meta** | [**map[string]string**](string.md) | Return pin objects that match specified metadata |
+
+### Return type
+
+[**PinResults**](PinResults.md)
+
+### Authorization
+
+[accessToken](../README.md#accessToken)
+
+### HTTP request headers
+
+- **Content-Type**: Not defined
+- **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints)
+[[Back to Model list]](../README.md#documentation-for-models)
+[[Back to README]](../README.md)
+
+
+## PinsPost
+
+> PinStatus PinsPost(ctx).Pin(pin).Execute()
+
+Add pin object
+
+
+
+### Example
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ openapiclient "./openapi"
+)
+
+func main() {
+ pin := openapiclient.Pin{Cid: "Cid_example", Name: "Name_example", Origins: []string{"Origins_example"), Meta: map[string]string{ "Key" = "Value" }} // Pin |
+
+ configuration := openapiclient.NewConfiguration()
+ api_client := openapiclient.NewAPIClient(configuration)
+ resp, r, err := api_client.PinsApi.PinsPost(context.Background(), pin).Execute()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error when calling `PinsApi.PinsPost``: %v\n", err)
+ fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r)
+ }
+ // response from `PinsPost`: PinStatus
+ fmt.Fprintf(os.Stdout, "Response from `PinsApi.PinsPost`: %v\n", resp)
+}
+```
+
+### Path Parameters
+
+
+
+### Other Parameters
+
+Other parameters are passed through a pointer to a apiPinsPostRequest struct via the builder pattern
+
+
+Name | Type | Description | Notes
+------------- | ------------- | ------------- | -------------
+ **pin** | [**Pin**](Pin.md) | |
+
+### Return type
+
+[**PinStatus**](PinStatus.md)
+
+### Authorization
+
+[accessToken](../README.md#accessToken)
+
+### HTTP request headers
+
+- **Content-Type**: application/json
+- **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints)
+[[Back to Model list]](../README.md#documentation-for-models)
+[[Back to README]](../README.md)
+
+
+## PinsRequestidDelete
+
+> PinsRequestidDelete(ctx, requestid).Execute()
+
+Remove pin object
+
+
+
+### Example
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ openapiclient "./openapi"
+)
+
+func main() {
+ requestid := "requestid_example" // string |
+
+ configuration := openapiclient.NewConfiguration()
+ api_client := openapiclient.NewAPIClient(configuration)
+ resp, r, err := api_client.PinsApi.PinsRequestidDelete(context.Background(), requestid).Execute()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error when calling `PinsApi.PinsRequestidDelete``: %v\n", err)
+ fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r)
+ }
+}
+```
+
+### Path Parameters
+
+
+Name | Type | Description | Notes
+------------- | ------------- | ------------- | -------------
+**ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc.
+**requestid** | **string** | |
+
+### Other Parameters
+
+Other parameters are passed through a pointer to a apiPinsRequestidDeleteRequest struct via the builder pattern
+
+
+Name | Type | Description | Notes
+------------- | ------------- | ------------- | -------------
+
+
+### Return type
+
+ (empty response body)
+
+### Authorization
+
+[accessToken](../README.md#accessToken)
+
+### HTTP request headers
+
+- **Content-Type**: Not defined
+- **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints)
+[[Back to Model list]](../README.md#documentation-for-models)
+[[Back to README]](../README.md)
+
+
+## PinsRequestidGet
+
+> PinStatus PinsRequestidGet(ctx, requestid).Execute()
+
+Get pin object
+
+
+
+### Example
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ openapiclient "./openapi"
+)
+
+func main() {
+ requestid := "requestid_example" // string |
+
+ configuration := openapiclient.NewConfiguration()
+ api_client := openapiclient.NewAPIClient(configuration)
+ resp, r, err := api_client.PinsApi.PinsRequestidGet(context.Background(), requestid).Execute()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error when calling `PinsApi.PinsRequestidGet``: %v\n", err)
+ fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r)
+ }
+ // response from `PinsRequestidGet`: PinStatus
+ fmt.Fprintf(os.Stdout, "Response from `PinsApi.PinsRequestidGet`: %v\n", resp)
+}
+```
+
+### Path Parameters
+
+
+Name | Type | Description | Notes
+------------- | ------------- | ------------- | -------------
+**ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc.
+**requestid** | **string** | |
+
+### Other Parameters
+
+Other parameters are passed through a pointer to a apiPinsRequestidGetRequest struct via the builder pattern
+
+
+Name | Type | Description | Notes
+------------- | ------------- | ------------- | -------------
+
+
+### Return type
+
+[**PinStatus**](PinStatus.md)
+
+### Authorization
+
+[accessToken](../README.md#accessToken)
+
+### HTTP request headers
+
+- **Content-Type**: Not defined
+- **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints)
+[[Back to Model list]](../README.md#documentation-for-models)
+[[Back to README]](../README.md)
+
+
+## PinsRequestidPost
+
+> PinStatus PinsRequestidPost(ctx, requestid).Pin(pin).Execute()
+
+Replace pin object
+
+
+
+### Example
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ openapiclient "./openapi"
+)
+
+func main() {
+ requestid := "requestid_example" // string |
+ pin := openapiclient.Pin{Cid: "Cid_example", Name: "Name_example", Origins: []string{"Origins_example"), Meta: map[string]string{ "Key" = "Value" }} // Pin |
+
+ configuration := openapiclient.NewConfiguration()
+ api_client := openapiclient.NewAPIClient(configuration)
+ resp, r, err := api_client.PinsApi.PinsRequestidPost(context.Background(), requestid, pin).Execute()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error when calling `PinsApi.PinsRequestidPost``: %v\n", err)
+ fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r)
+ }
+ // response from `PinsRequestidPost`: PinStatus
+ fmt.Fprintf(os.Stdout, "Response from `PinsApi.PinsRequestidPost`: %v\n", resp)
+}
+```
+
+### Path Parameters
+
+
+Name | Type | Description | Notes
+------------- | ------------- | ------------- | -------------
+**ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc.
+**requestid** | **string** | |
+
+### Other Parameters
+
+Other parameters are passed through a pointer to a apiPinsRequestidPostRequest struct via the builder pattern
+
+
+Name | Type | Description | Notes
+------------- | ------------- | ------------- | -------------
+
+ **pin** | [**Pin**](Pin.md) | |
+
+### Return type
+
+[**PinStatus**](PinStatus.md)
+
+### Authorization
+
+[accessToken](../README.md#accessToken)
+
+### HTTP request headers
+
+- **Content-Type**: application/json
+- **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints)
+[[Back to Model list]](../README.md#documentation-for-models)
+[[Back to README]](../README.md)
+
diff --git a/pinning/remote/client/openapi/docs/Status.md b/pinning/remote/client/openapi/docs/Status.md
new file mode 100644
index 0000000000..01176af11e
--- /dev/null
+++ b/pinning/remote/client/openapi/docs/Status.md
@@ -0,0 +1,11 @@
+# Status
+
+## Properties
+
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/pinning/remote/client/openapi/model_failure.go b/pinning/remote/client/openapi/model_failure.go
new file mode 100644
index 0000000000..ec1fc8268a
--- /dev/null
+++ b/pinning/remote/client/openapi/model_failure.go
@@ -0,0 +1,105 @@
+/*
+ * IPFS Pinning Service API
+ *
+ * ## About this spec The IPFS Pinning Service API is intended to be an implementation-agnostic API: - For use and implementation by pinning service providers - For use in client mode by IPFS nodes and GUI-based applications > **Note**: while ready for implementation, this spec is still a work in progress! 🏗️ **Your input and feedback are welcome and valuable as we develop this API spec. Please join the design discussion at [github.com/ipfs/pinning-services-api-spec](https://github.com/ipfs/pinning-services-api-spec).** # Schemas This section describes the most important object types and conventions. A full list of fields and schemas can be found in the `schemas` section of the [YAML file](https://github.com/ipfs/pinning-services-api-spec/blob/master/ipfs-pinning-service.yaml). ## Identifiers ### cid [Content Identifier (CID)](https://docs.ipfs.io/concepts/content-addressing/) points at the root of a DAG that is pinned recursively. ### requestid Unique identifier of a pin request. When a pin is created, the service responds with unique `requestid` that can be later used for pin removal. When the same `cid` is pinned again, a different `requestid` is returned to differentiate between those pin requests. Service implementation should use UUID, `hash(accessToken,Pin,PinStatus.created)`, or any other opaque identifier that provides equally strong protection against race conditions. ## Objects ### Pin object ![pin object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pin.png) The `Pin` object is a representation of a pin request. It includes the `cid` of data to be pinned, as well as optional metadata in `name`, `origins`, and `meta`. ### Pin status response ![pin status response object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pinstatus.png) The `PinStatus` object is a representation of the current state of a pinning operation. It includes the original `pin` object, along with the current `status` and globally unique `requestid` of the entire pinning request, which can be used for future status checks and management. Addresses in the `delegates` array are peers delegated by the pinning service for facilitating direct file transfers (more details in the provider hints section). Any additional vendor-specific information is returned in optional `info`. ## The pin lifecycle ![pinning service objects and lifecycle](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/lifecycle.png) ### Creating a new pin object The user sends a `Pin` object to `POST /pins` and receives a `PinStatus` response: - `requestid` in `PinStatus` is the identifier of the pin operation, which can can be used for checking status, and removing the pin in the future - `status` in `PinStatus` indicates the current state of a pin ### Checking status of in-progress pinning `status` (in `PinStatus`) may indicate a pending state (`queued` or `pinning`). This means the data behind `Pin.cid` was not found on the pinning service and is being fetched from the IPFS network at large, which may take time. In this case, the user can periodically check pinning progress via `GET /pins/{requestid}` until pinning is successful, or the user decides to remove the pending pin. ### Replacing an existing pin object The user can replace an existing pin object via `POST /pins/{requestid}`. This is a shortcut for removing a pin object identified by `requestid` and creating a new one in a single API call that protects against undesired garbage collection of blocks common to both pins. Useful when updating a pin representing a huge dataset where most of blocks did not change. The new pin object `requestid` is returned in the `PinStatus` response. The old pin object is deleted automatically. ### Removing a pin object A pin object can be removed via `DELETE /pins/{requestid}`. ## Provider hints Pinning of new data can be accelerated by providing a list of known data sources in `Pin.origins`, and connecting at least one of them to pinning service nodes at `PinStatus.delegates`. The most common scenario is a client putting its own IPFS node's multiaddrs in `Pin.origins`, and then directly connecting to every multiaddr returned by a pinning service in `PinStatus.delegates` to initiate transfer. This ensures data transfer starts immediately (without waiting for provider discovery over DHT), and direct dial from a client works around peer routing issues in restrictive network topologies such as NATs. ## Custom metadata Pinning services are encouraged to add support for additional features by leveraging the optional `Pin.meta` and `PinStatus.info` fields. While these attributes can be application- or vendor-specific, we encourage the community at large to leverage these attributes as a sandbox to come up with conventions that could become part of future revisions of this API. ### Pin metadata String keys and values passed in `Pin.meta` are persisted with the pin object. Potential uses: - `Pin.meta[app_id]`: Attaching a unique identifier to pins created by an app enables filtering pins per app via `?meta={\"app_id\":}` - `Pin.meta[vendor_policy]`: Vendor-specific policy (for example: which region to use, how many copies to keep) Note that it is OK for a client to omit or ignore these optional attributes; doing so should not impact the basic pinning functionality. ### Pin status info Additional `PinStatus.info` can be returned by pinning service. Potential uses: - `PinStatus.info[status_details]`: more info about the current status (queue position, percentage of transferred data, summary of where data is stored, etc); when `PinStatus.status=failed`, it could provide a reason why a pin operation failed (e.g. lack of funds, DAG too big, etc.) - `PinStatus.info[dag_size]`: the size of pinned data, along with DAG overhead - `PinStatus.info[raw_size]`: the size of data without DAG overhead (eg. unixfs) - `PinStatus.info[pinned_until]`: if vendor supports time-bound pins, this could indicate when the pin will expire # Pagination and filtering Pin objects can be listed by executing `GET /pins` with optional parameters: - When no filters are provided, the endpoint will return a small batch of the 10 most recently created items, from the latest to the oldest. - The number of returned items can be adjusted with the `limit` parameter (implicit default is 10). - If the value in `PinResults.count` is bigger than the length of `PinResults.results`, the client can infer there are more results that can be queried. - To read more items, pass the `before` filter with the timestamp from `PinStatus.created` found in the oldest item in the current batch of results. Repeat to read all results. - Returned results can be fine-tuned by applying optional `after`, `cid`, `name`, `status`, or `meta` filters. > **Note**: pagination by the `created` timestamp requires each value to be globally unique. Any future considerations to add support for bulk creation must account for this.
+ *
+ * API version: 0.1.1
+ * Generated by: OpenAPI Generator (https://openapi-generator.tech)
+ */
+
+package openapi
+
+import (
+ "encoding/json"
+)
+
+// Failure Response for a failed request
+type Failure struct {
+ Error FailureError `json:"error"`
+}
+
+// NewFailure instantiates a new Failure object
+// This constructor will assign default values to properties that have it defined,
+// and makes sure properties required by API are set, but the set of arguments
+// will change when the set of required properties is changed
+func NewFailure(error_ FailureError) *Failure {
+ this := Failure{}
+ this.Error = error_
+ return &this
+}
+
+// NewFailureWithDefaults instantiates a new Failure object
+// This constructor will only assign default values to properties that have it defined,
+// but it doesn't guarantee that properties required by API are set
+func NewFailureWithDefaults() *Failure {
+ this := Failure{}
+ return &this
+}
+
+// GetError returns the Error field value
+func (o *Failure) GetError() FailureError {
+ if o == nil {
+ var ret FailureError
+ return ret
+ }
+
+ return o.Error
+}
+
+// GetErrorOk returns a tuple with the Error field value
+// and a boolean to check if the value has been set.
+func (o *Failure) GetErrorOk() (*FailureError, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Error, true
+}
+
+// SetError sets field value
+func (o *Failure) SetError(v FailureError) {
+ o.Error = v
+}
+
+func (o Failure) MarshalJSON() ([]byte, error) {
+ toSerialize := map[string]interface{}{}
+ if true {
+ toSerialize["error"] = o.Error
+ }
+ return json.Marshal(toSerialize)
+}
+
+type NullableFailure struct {
+ value *Failure
+ isSet bool
+}
+
+func (v NullableFailure) Get() *Failure {
+ return v.value
+}
+
+func (v *NullableFailure) Set(val *Failure) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableFailure) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableFailure) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableFailure(val *Failure) *NullableFailure {
+ return &NullableFailure{value: val, isSet: true}
+}
+
+func (v NullableFailure) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableFailure) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
diff --git a/pinning/remote/client/openapi/model_failure_error.go b/pinning/remote/client/openapi/model_failure_error.go
new file mode 100644
index 0000000000..4f38acdf96
--- /dev/null
+++ b/pinning/remote/client/openapi/model_failure_error.go
@@ -0,0 +1,143 @@
+/*
+ * IPFS Pinning Service API
+ *
+ * ## About this spec The IPFS Pinning Service API is intended to be an implementation-agnostic API: - For use and implementation by pinning service providers - For use in client mode by IPFS nodes and GUI-based applications > **Note**: while ready for implementation, this spec is still a work in progress! 🏗️ **Your input and feedback are welcome and valuable as we develop this API spec. Please join the design discussion at [github.com/ipfs/pinning-services-api-spec](https://github.com/ipfs/pinning-services-api-spec).** # Schemas This section describes the most important object types and conventions. A full list of fields and schemas can be found in the `schemas` section of the [YAML file](https://github.com/ipfs/pinning-services-api-spec/blob/master/ipfs-pinning-service.yaml). ## Identifiers ### cid [Content Identifier (CID)](https://docs.ipfs.io/concepts/content-addressing/) points at the root of a DAG that is pinned recursively. ### requestid Unique identifier of a pin request. When a pin is created, the service responds with unique `requestid` that can be later used for pin removal. When the same `cid` is pinned again, a different `requestid` is returned to differentiate between those pin requests. Service implementation should use UUID, `hash(accessToken,Pin,PinStatus.created)`, or any other opaque identifier that provides equally strong protection against race conditions. ## Objects ### Pin object ![pin object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pin.png) The `Pin` object is a representation of a pin request. It includes the `cid` of data to be pinned, as well as optional metadata in `name`, `origins`, and `meta`. ### Pin status response ![pin status response object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pinstatus.png) The `PinStatus` object is a representation of the current state of a pinning operation. It includes the original `pin` object, along with the current `status` and globally unique `requestid` of the entire pinning request, which can be used for future status checks and management. Addresses in the `delegates` array are peers delegated by the pinning service for facilitating direct file transfers (more details in the provider hints section). Any additional vendor-specific information is returned in optional `info`. ## The pin lifecycle ![pinning service objects and lifecycle](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/lifecycle.png) ### Creating a new pin object The user sends a `Pin` object to `POST /pins` and receives a `PinStatus` response: - `requestid` in `PinStatus` is the identifier of the pin operation, which can can be used for checking status, and removing the pin in the future - `status` in `PinStatus` indicates the current state of a pin ### Checking status of in-progress pinning `status` (in `PinStatus`) may indicate a pending state (`queued` or `pinning`). This means the data behind `Pin.cid` was not found on the pinning service and is being fetched from the IPFS network at large, which may take time. In this case, the user can periodically check pinning progress via `GET /pins/{requestid}` until pinning is successful, or the user decides to remove the pending pin. ### Replacing an existing pin object The user can replace an existing pin object via `POST /pins/{requestid}`. This is a shortcut for removing a pin object identified by `requestid` and creating a new one in a single API call that protects against undesired garbage collection of blocks common to both pins. Useful when updating a pin representing a huge dataset where most of blocks did not change. The new pin object `requestid` is returned in the `PinStatus` response. The old pin object is deleted automatically. ### Removing a pin object A pin object can be removed via `DELETE /pins/{requestid}`. ## Provider hints Pinning of new data can be accelerated by providing a list of known data sources in `Pin.origins`, and connecting at least one of them to pinning service nodes at `PinStatus.delegates`. The most common scenario is a client putting its own IPFS node's multiaddrs in `Pin.origins`, and then directly connecting to every multiaddr returned by a pinning service in `PinStatus.delegates` to initiate transfer. This ensures data transfer starts immediately (without waiting for provider discovery over DHT), and direct dial from a client works around peer routing issues in restrictive network topologies such as NATs. ## Custom metadata Pinning services are encouraged to add support for additional features by leveraging the optional `Pin.meta` and `PinStatus.info` fields. While these attributes can be application- or vendor-specific, we encourage the community at large to leverage these attributes as a sandbox to come up with conventions that could become part of future revisions of this API. ### Pin metadata String keys and values passed in `Pin.meta` are persisted with the pin object. Potential uses: - `Pin.meta[app_id]`: Attaching a unique identifier to pins created by an app enables filtering pins per app via `?meta={\"app_id\":}` - `Pin.meta[vendor_policy]`: Vendor-specific policy (for example: which region to use, how many copies to keep) Note that it is OK for a client to omit or ignore these optional attributes; doing so should not impact the basic pinning functionality. ### Pin status info Additional `PinStatus.info` can be returned by pinning service. Potential uses: - `PinStatus.info[status_details]`: more info about the current status (queue position, percentage of transferred data, summary of where data is stored, etc); when `PinStatus.status=failed`, it could provide a reason why a pin operation failed (e.g. lack of funds, DAG too big, etc.) - `PinStatus.info[dag_size]`: the size of pinned data, along with DAG overhead - `PinStatus.info[raw_size]`: the size of data without DAG overhead (eg. unixfs) - `PinStatus.info[pinned_until]`: if vendor supports time-bound pins, this could indicate when the pin will expire # Pagination and filtering Pin objects can be listed by executing `GET /pins` with optional parameters: - When no filters are provided, the endpoint will return a small batch of the 10 most recently created items, from the latest to the oldest. - The number of returned items can be adjusted with the `limit` parameter (implicit default is 10). - If the value in `PinResults.count` is bigger than the length of `PinResults.results`, the client can infer there are more results that can be queried. - To read more items, pass the `before` filter with the timestamp from `PinStatus.created` found in the oldest item in the current batch of results. Repeat to read all results. - Returned results can be fine-tuned by applying optional `after`, `cid`, `name`, `status`, or `meta` filters. > **Note**: pagination by the `created` timestamp requires each value to be globally unique. Any future considerations to add support for bulk creation must account for this.
+ *
+ * API version: 0.1.1
+ * Generated by: OpenAPI Generator (https://openapi-generator.tech)
+ */
+
+package openapi
+
+import (
+ "encoding/json"
+)
+
+// FailureError struct for FailureError
+type FailureError struct {
+ // Mandatory string identifying the type of error
+ Reason string `json:"reason"`
+ // Optional, longer description of the error; may include UUID of transaction for support, links to documentation etc
+ Details *string `json:"details,omitempty"`
+}
+
+// NewFailureError instantiates a new FailureError object
+// This constructor will assign default values to properties that have it defined,
+// and makes sure properties required by API are set, but the set of arguments
+// will change when the set of required properties is changed
+func NewFailureError(reason string) *FailureError {
+ this := FailureError{}
+ this.Reason = reason
+ return &this
+}
+
+// NewFailureErrorWithDefaults instantiates a new FailureError object
+// This constructor will only assign default values to properties that have it defined,
+// but it doesn't guarantee that properties required by API are set
+func NewFailureErrorWithDefaults() *FailureError {
+ this := FailureError{}
+ return &this
+}
+
+// GetReason returns the Reason field value
+func (o *FailureError) GetReason() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.Reason
+}
+
+// GetReasonOk returns a tuple with the Reason field value
+// and a boolean to check if the value has been set.
+func (o *FailureError) GetReasonOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Reason, true
+}
+
+// SetReason sets field value
+func (o *FailureError) SetReason(v string) {
+ o.Reason = v
+}
+
+// GetDetails returns the Details field value if set, zero value otherwise.
+func (o *FailureError) GetDetails() string {
+ if o == nil || o.Details == nil {
+ var ret string
+ return ret
+ }
+ return *o.Details
+}
+
+// GetDetailsOk returns a tuple with the Details field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *FailureError) GetDetailsOk() (*string, bool) {
+ if o == nil || o.Details == nil {
+ return nil, false
+ }
+ return o.Details, true
+}
+
+// HasDetails returns a boolean if a field has been set.
+func (o *FailureError) HasDetails() bool {
+ if o != nil && o.Details != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetDetails gets a reference to the given string and assigns it to the Details field.
+func (o *FailureError) SetDetails(v string) {
+ o.Details = &v
+}
+
+func (o FailureError) MarshalJSON() ([]byte, error) {
+ toSerialize := map[string]interface{}{}
+ if true {
+ toSerialize["reason"] = o.Reason
+ }
+ if o.Details != nil {
+ toSerialize["details"] = o.Details
+ }
+ return json.Marshal(toSerialize)
+}
+
+type NullableFailureError struct {
+ value *FailureError
+ isSet bool
+}
+
+func (v NullableFailureError) Get() *FailureError {
+ return v.value
+}
+
+func (v *NullableFailureError) Set(val *FailureError) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableFailureError) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableFailureError) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableFailureError(val *FailureError) *NullableFailureError {
+ return &NullableFailureError{value: val, isSet: true}
+}
+
+func (v NullableFailureError) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableFailureError) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
diff --git a/pinning/remote/client/openapi/model_pin.go b/pinning/remote/client/openapi/model_pin.go
new file mode 100644
index 0000000000..0152d43bbf
--- /dev/null
+++ b/pinning/remote/client/openapi/model_pin.go
@@ -0,0 +1,217 @@
+/*
+ * IPFS Pinning Service API
+ *
+ * ## About this spec The IPFS Pinning Service API is intended to be an implementation-agnostic API: - For use and implementation by pinning service providers - For use in client mode by IPFS nodes and GUI-based applications > **Note**: while ready for implementation, this spec is still a work in progress! 🏗️ **Your input and feedback are welcome and valuable as we develop this API spec. Please join the design discussion at [github.com/ipfs/pinning-services-api-spec](https://github.com/ipfs/pinning-services-api-spec).** # Schemas This section describes the most important object types and conventions. A full list of fields and schemas can be found in the `schemas` section of the [YAML file](https://github.com/ipfs/pinning-services-api-spec/blob/master/ipfs-pinning-service.yaml). ## Identifiers ### cid [Content Identifier (CID)](https://docs.ipfs.io/concepts/content-addressing/) points at the root of a DAG that is pinned recursively. ### requestid Unique identifier of a pin request. When a pin is created, the service responds with unique `requestid` that can be later used for pin removal. When the same `cid` is pinned again, a different `requestid` is returned to differentiate between those pin requests. Service implementation should use UUID, `hash(accessToken,Pin,PinStatus.created)`, or any other opaque identifier that provides equally strong protection against race conditions. ## Objects ### Pin object ![pin object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pin.png) The `Pin` object is a representation of a pin request. It includes the `cid` of data to be pinned, as well as optional metadata in `name`, `origins`, and `meta`. ### Pin status response ![pin status response object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pinstatus.png) The `PinStatus` object is a representation of the current state of a pinning operation. It includes the original `pin` object, along with the current `status` and globally unique `requestid` of the entire pinning request, which can be used for future status checks and management. Addresses in the `delegates` array are peers delegated by the pinning service for facilitating direct file transfers (more details in the provider hints section). Any additional vendor-specific information is returned in optional `info`. ## The pin lifecycle ![pinning service objects and lifecycle](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/lifecycle.png) ### Creating a new pin object The user sends a `Pin` object to `POST /pins` and receives a `PinStatus` response: - `requestid` in `PinStatus` is the identifier of the pin operation, which can can be used for checking status, and removing the pin in the future - `status` in `PinStatus` indicates the current state of a pin ### Checking status of in-progress pinning `status` (in `PinStatus`) may indicate a pending state (`queued` or `pinning`). This means the data behind `Pin.cid` was not found on the pinning service and is being fetched from the IPFS network at large, which may take time. In this case, the user can periodically check pinning progress via `GET /pins/{requestid}` until pinning is successful, or the user decides to remove the pending pin. ### Replacing an existing pin object The user can replace an existing pin object via `POST /pins/{requestid}`. This is a shortcut for removing a pin object identified by `requestid` and creating a new one in a single API call that protects against undesired garbage collection of blocks common to both pins. Useful when updating a pin representing a huge dataset where most of blocks did not change. The new pin object `requestid` is returned in the `PinStatus` response. The old pin object is deleted automatically. ### Removing a pin object A pin object can be removed via `DELETE /pins/{requestid}`. ## Provider hints Pinning of new data can be accelerated by providing a list of known data sources in `Pin.origins`, and connecting at least one of them to pinning service nodes at `PinStatus.delegates`. The most common scenario is a client putting its own IPFS node's multiaddrs in `Pin.origins`, and then directly connecting to every multiaddr returned by a pinning service in `PinStatus.delegates` to initiate transfer. This ensures data transfer starts immediately (without waiting for provider discovery over DHT), and direct dial from a client works around peer routing issues in restrictive network topologies such as NATs. ## Custom metadata Pinning services are encouraged to add support for additional features by leveraging the optional `Pin.meta` and `PinStatus.info` fields. While these attributes can be application- or vendor-specific, we encourage the community at large to leverage these attributes as a sandbox to come up with conventions that could become part of future revisions of this API. ### Pin metadata String keys and values passed in `Pin.meta` are persisted with the pin object. Potential uses: - `Pin.meta[app_id]`: Attaching a unique identifier to pins created by an app enables filtering pins per app via `?meta={\"app_id\":}` - `Pin.meta[vendor_policy]`: Vendor-specific policy (for example: which region to use, how many copies to keep) Note that it is OK for a client to omit or ignore these optional attributes; doing so should not impact the basic pinning functionality. ### Pin status info Additional `PinStatus.info` can be returned by pinning service. Potential uses: - `PinStatus.info[status_details]`: more info about the current status (queue position, percentage of transferred data, summary of where data is stored, etc); when `PinStatus.status=failed`, it could provide a reason why a pin operation failed (e.g. lack of funds, DAG too big, etc.) - `PinStatus.info[dag_size]`: the size of pinned data, along with DAG overhead - `PinStatus.info[raw_size]`: the size of data without DAG overhead (eg. unixfs) - `PinStatus.info[pinned_until]`: if vendor supports time-bound pins, this could indicate when the pin will expire # Pagination and filtering Pin objects can be listed by executing `GET /pins` with optional parameters: - When no filters are provided, the endpoint will return a small batch of the 10 most recently created items, from the latest to the oldest. - The number of returned items can be adjusted with the `limit` parameter (implicit default is 10). - If the value in `PinResults.count` is bigger than the length of `PinResults.results`, the client can infer there are more results that can be queried. - To read more items, pass the `before` filter with the timestamp from `PinStatus.created` found in the oldest item in the current batch of results. Repeat to read all results. - Returned results can be fine-tuned by applying optional `after`, `cid`, `name`, `status`, or `meta` filters. > **Note**: pagination by the `created` timestamp requires each value to be globally unique. Any future considerations to add support for bulk creation must account for this.
+ *
+ * API version: 0.1.1
+ * Generated by: OpenAPI Generator (https://openapi-generator.tech)
+ */
+
+package openapi
+
+import (
+ "encoding/json"
+)
+
+// Pin Pin object
+type Pin struct {
+ // Content Identifier (CID) to be pinned recursively
+ Cid string `json:"cid"`
+ // Optional name for pinned data; can be used for lookups later
+ Name *string `json:"name,omitempty"`
+ // Optional list of multiaddrs known to provide the data
+ Origins *[]string `json:"origins,omitempty"`
+ // Optional metadata for pin object
+ Meta *map[string]string `json:"meta,omitempty"`
+}
+
+// NewPin instantiates a new Pin object
+// This constructor will assign default values to properties that have it defined,
+// and makes sure properties required by API are set, but the set of arguments
+// will change when the set of required properties is changed
+func NewPin(cid string) *Pin {
+ this := Pin{}
+ this.Cid = cid
+ return &this
+}
+
+// NewPinWithDefaults instantiates a new Pin object
+// This constructor will only assign default values to properties that have it defined,
+// but it doesn't guarantee that properties required by API are set
+func NewPinWithDefaults() *Pin {
+ this := Pin{}
+ return &this
+}
+
+// GetCid returns the Cid field value
+func (o *Pin) GetCid() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.Cid
+}
+
+// GetCidOk returns a tuple with the Cid field value
+// and a boolean to check if the value has been set.
+func (o *Pin) GetCidOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Cid, true
+}
+
+// SetCid sets field value
+func (o *Pin) SetCid(v string) {
+ o.Cid = v
+}
+
+// GetName returns the Name field value if set, zero value otherwise.
+func (o *Pin) GetName() string {
+ if o == nil || o.Name == nil {
+ var ret string
+ return ret
+ }
+ return *o.Name
+}
+
+// GetNameOk returns a tuple with the Name field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *Pin) GetNameOk() (*string, bool) {
+ if o == nil || o.Name == nil {
+ return nil, false
+ }
+ return o.Name, true
+}
+
+// HasName returns a boolean if a field has been set.
+func (o *Pin) HasName() bool {
+ if o != nil && o.Name != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetName gets a reference to the given string and assigns it to the Name field.
+func (o *Pin) SetName(v string) {
+ o.Name = &v
+}
+
+// GetOrigins returns the Origins field value if set, zero value otherwise.
+func (o *Pin) GetOrigins() []string {
+ if o == nil || o.Origins == nil {
+ var ret []string
+ return ret
+ }
+ return *o.Origins
+}
+
+// GetOriginsOk returns a tuple with the Origins field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *Pin) GetOriginsOk() (*[]string, bool) {
+ if o == nil || o.Origins == nil {
+ return nil, false
+ }
+ return o.Origins, true
+}
+
+// HasOrigins returns a boolean if a field has been set.
+func (o *Pin) HasOrigins() bool {
+ if o != nil && o.Origins != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetOrigins gets a reference to the given []string and assigns it to the Origins field.
+func (o *Pin) SetOrigins(v []string) {
+ o.Origins = &v
+}
+
+// GetMeta returns the Meta field value if set, zero value otherwise.
+func (o *Pin) GetMeta() map[string]string {
+ if o == nil || o.Meta == nil {
+ var ret map[string]string
+ return ret
+ }
+ return *o.Meta
+}
+
+// GetMetaOk returns a tuple with the Meta field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *Pin) GetMetaOk() (*map[string]string, bool) {
+ if o == nil || o.Meta == nil {
+ return nil, false
+ }
+ return o.Meta, true
+}
+
+// HasMeta returns a boolean if a field has been set.
+func (o *Pin) HasMeta() bool {
+ if o != nil && o.Meta != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetMeta gets a reference to the given map[string]string and assigns it to the Meta field.
+func (o *Pin) SetMeta(v map[string]string) {
+ o.Meta = &v
+}
+
+func (o Pin) MarshalJSON() ([]byte, error) {
+ toSerialize := map[string]interface{}{}
+ if true {
+ toSerialize["cid"] = o.Cid
+ }
+ if o.Name != nil {
+ toSerialize["name"] = o.Name
+ }
+ if o.Origins != nil {
+ toSerialize["origins"] = o.Origins
+ }
+ if o.Meta != nil {
+ toSerialize["meta"] = o.Meta
+ }
+ return json.Marshal(toSerialize)
+}
+
+type NullablePin struct {
+ value *Pin
+ isSet bool
+}
+
+func (v NullablePin) Get() *Pin {
+ return v.value
+}
+
+func (v *NullablePin) Set(val *Pin) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullablePin) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullablePin) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullablePin(val *Pin) *NullablePin {
+ return &NullablePin{value: val, isSet: true}
+}
+
+func (v NullablePin) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullablePin) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
diff --git a/pinning/remote/client/openapi/model_pin_results.go b/pinning/remote/client/openapi/model_pin_results.go
new file mode 100644
index 0000000000..eacb5e0212
--- /dev/null
+++ b/pinning/remote/client/openapi/model_pin_results.go
@@ -0,0 +1,136 @@
+/*
+ * IPFS Pinning Service API
+ *
+ * ## About this spec The IPFS Pinning Service API is intended to be an implementation-agnostic API: - For use and implementation by pinning service providers - For use in client mode by IPFS nodes and GUI-based applications > **Note**: while ready for implementation, this spec is still a work in progress! 🏗️ **Your input and feedback are welcome and valuable as we develop this API spec. Please join the design discussion at [github.com/ipfs/pinning-services-api-spec](https://github.com/ipfs/pinning-services-api-spec).** # Schemas This section describes the most important object types and conventions. A full list of fields and schemas can be found in the `schemas` section of the [YAML file](https://github.com/ipfs/pinning-services-api-spec/blob/master/ipfs-pinning-service.yaml). ## Identifiers ### cid [Content Identifier (CID)](https://docs.ipfs.io/concepts/content-addressing/) points at the root of a DAG that is pinned recursively. ### requestid Unique identifier of a pin request. When a pin is created, the service responds with unique `requestid` that can be later used for pin removal. When the same `cid` is pinned again, a different `requestid` is returned to differentiate between those pin requests. Service implementation should use UUID, `hash(accessToken,Pin,PinStatus.created)`, or any other opaque identifier that provides equally strong protection against race conditions. ## Objects ### Pin object ![pin object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pin.png) The `Pin` object is a representation of a pin request. It includes the `cid` of data to be pinned, as well as optional metadata in `name`, `origins`, and `meta`. ### Pin status response ![pin status response object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pinstatus.png) The `PinStatus` object is a representation of the current state of a pinning operation. It includes the original `pin` object, along with the current `status` and globally unique `requestid` of the entire pinning request, which can be used for future status checks and management. Addresses in the `delegates` array are peers delegated by the pinning service for facilitating direct file transfers (more details in the provider hints section). Any additional vendor-specific information is returned in optional `info`. ## The pin lifecycle ![pinning service objects and lifecycle](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/lifecycle.png) ### Creating a new pin object The user sends a `Pin` object to `POST /pins` and receives a `PinStatus` response: - `requestid` in `PinStatus` is the identifier of the pin operation, which can can be used for checking status, and removing the pin in the future - `status` in `PinStatus` indicates the current state of a pin ### Checking status of in-progress pinning `status` (in `PinStatus`) may indicate a pending state (`queued` or `pinning`). This means the data behind `Pin.cid` was not found on the pinning service and is being fetched from the IPFS network at large, which may take time. In this case, the user can periodically check pinning progress via `GET /pins/{requestid}` until pinning is successful, or the user decides to remove the pending pin. ### Replacing an existing pin object The user can replace an existing pin object via `POST /pins/{requestid}`. This is a shortcut for removing a pin object identified by `requestid` and creating a new one in a single API call that protects against undesired garbage collection of blocks common to both pins. Useful when updating a pin representing a huge dataset where most of blocks did not change. The new pin object `requestid` is returned in the `PinStatus` response. The old pin object is deleted automatically. ### Removing a pin object A pin object can be removed via `DELETE /pins/{requestid}`. ## Provider hints Pinning of new data can be accelerated by providing a list of known data sources in `Pin.origins`, and connecting at least one of them to pinning service nodes at `PinStatus.delegates`. The most common scenario is a client putting its own IPFS node's multiaddrs in `Pin.origins`, and then directly connecting to every multiaddr returned by a pinning service in `PinStatus.delegates` to initiate transfer. This ensures data transfer starts immediately (without waiting for provider discovery over DHT), and direct dial from a client works around peer routing issues in restrictive network topologies such as NATs. ## Custom metadata Pinning services are encouraged to add support for additional features by leveraging the optional `Pin.meta` and `PinStatus.info` fields. While these attributes can be application- or vendor-specific, we encourage the community at large to leverage these attributes as a sandbox to come up with conventions that could become part of future revisions of this API. ### Pin metadata String keys and values passed in `Pin.meta` are persisted with the pin object. Potential uses: - `Pin.meta[app_id]`: Attaching a unique identifier to pins created by an app enables filtering pins per app via `?meta={\"app_id\":}` - `Pin.meta[vendor_policy]`: Vendor-specific policy (for example: which region to use, how many copies to keep) Note that it is OK for a client to omit or ignore these optional attributes; doing so should not impact the basic pinning functionality. ### Pin status info Additional `PinStatus.info` can be returned by pinning service. Potential uses: - `PinStatus.info[status_details]`: more info about the current status (queue position, percentage of transferred data, summary of where data is stored, etc); when `PinStatus.status=failed`, it could provide a reason why a pin operation failed (e.g. lack of funds, DAG too big, etc.) - `PinStatus.info[dag_size]`: the size of pinned data, along with DAG overhead - `PinStatus.info[raw_size]`: the size of data without DAG overhead (eg. unixfs) - `PinStatus.info[pinned_until]`: if vendor supports time-bound pins, this could indicate when the pin will expire # Pagination and filtering Pin objects can be listed by executing `GET /pins` with optional parameters: - When no filters are provided, the endpoint will return a small batch of the 10 most recently created items, from the latest to the oldest. - The number of returned items can be adjusted with the `limit` parameter (implicit default is 10). - If the value in `PinResults.count` is bigger than the length of `PinResults.results`, the client can infer there are more results that can be queried. - To read more items, pass the `before` filter with the timestamp from `PinStatus.created` found in the oldest item in the current batch of results. Repeat to read all results. - Returned results can be fine-tuned by applying optional `after`, `cid`, `name`, `status`, or `meta` filters. > **Note**: pagination by the `created` timestamp requires each value to be globally unique. Any future considerations to add support for bulk creation must account for this.
+ *
+ * API version: 0.1.1
+ * Generated by: OpenAPI Generator (https://openapi-generator.tech)
+ */
+
+package openapi
+
+import (
+ "encoding/json"
+)
+
+// PinResults Response used for listing pin objects matching request
+type PinResults struct {
+ // The total number of pin objects that exist for passed query filters
+ Count int32 `json:"count"`
+ // An array of PinStatus results
+ Results []PinStatus `json:"results"`
+}
+
+// NewPinResults instantiates a new PinResults object
+// This constructor will assign default values to properties that have it defined,
+// and makes sure properties required by API are set, but the set of arguments
+// will change when the set of required properties is changed
+func NewPinResults(count int32, results []PinStatus) *PinResults {
+ this := PinResults{}
+ this.Count = count
+ this.Results = results
+ return &this
+}
+
+// NewPinResultsWithDefaults instantiates a new PinResults object
+// This constructor will only assign default values to properties that have it defined,
+// but it doesn't guarantee that properties required by API are set
+func NewPinResultsWithDefaults() *PinResults {
+ this := PinResults{}
+ return &this
+}
+
+// GetCount returns the Count field value
+func (o *PinResults) GetCount() int32 {
+ if o == nil {
+ var ret int32
+ return ret
+ }
+
+ return o.Count
+}
+
+// GetCountOk returns a tuple with the Count field value
+// and a boolean to check if the value has been set.
+func (o *PinResults) GetCountOk() (*int32, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Count, true
+}
+
+// SetCount sets field value
+func (o *PinResults) SetCount(v int32) {
+ o.Count = v
+}
+
+// GetResults returns the Results field value
+func (o *PinResults) GetResults() []PinStatus {
+ if o == nil {
+ var ret []PinStatus
+ return ret
+ }
+
+ return o.Results
+}
+
+// GetResultsOk returns a tuple with the Results field value
+// and a boolean to check if the value has been set.
+func (o *PinResults) GetResultsOk() (*[]PinStatus, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Results, true
+}
+
+// SetResults sets field value
+func (o *PinResults) SetResults(v []PinStatus) {
+ o.Results = v
+}
+
+func (o PinResults) MarshalJSON() ([]byte, error) {
+ toSerialize := map[string]interface{}{}
+ if true {
+ toSerialize["count"] = o.Count
+ }
+ if true {
+ toSerialize["results"] = o.Results
+ }
+ return json.Marshal(toSerialize)
+}
+
+type NullablePinResults struct {
+ value *PinResults
+ isSet bool
+}
+
+func (v NullablePinResults) Get() *PinResults {
+ return v.value
+}
+
+func (v *NullablePinResults) Set(val *PinResults) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullablePinResults) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullablePinResults) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullablePinResults(val *PinResults) *NullablePinResults {
+ return &NullablePinResults{value: val, isSet: true}
+}
+
+func (v NullablePinResults) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullablePinResults) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
diff --git a/pinning/remote/client/openapi/model_pin_status.go b/pinning/remote/client/openapi/model_pin_status.go
new file mode 100644
index 0000000000..0f44e62c2d
--- /dev/null
+++ b/pinning/remote/client/openapi/model_pin_status.go
@@ -0,0 +1,262 @@
+/*
+ * IPFS Pinning Service API
+ *
+ * ## About this spec The IPFS Pinning Service API is intended to be an implementation-agnostic API: - For use and implementation by pinning service providers - For use in client mode by IPFS nodes and GUI-based applications > **Note**: while ready for implementation, this spec is still a work in progress! 🏗️ **Your input and feedback are welcome and valuable as we develop this API spec. Please join the design discussion at [github.com/ipfs/pinning-services-api-spec](https://github.com/ipfs/pinning-services-api-spec).** # Schemas This section describes the most important object types and conventions. A full list of fields and schemas can be found in the `schemas` section of the [YAML file](https://github.com/ipfs/pinning-services-api-spec/blob/master/ipfs-pinning-service.yaml). ## Identifiers ### cid [Content Identifier (CID)](https://docs.ipfs.io/concepts/content-addressing/) points at the root of a DAG that is pinned recursively. ### requestid Unique identifier of a pin request. When a pin is created, the service responds with unique `requestid` that can be later used for pin removal. When the same `cid` is pinned again, a different `requestid` is returned to differentiate between those pin requests. Service implementation should use UUID, `hash(accessToken,Pin,PinStatus.created)`, or any other opaque identifier that provides equally strong protection against race conditions. ## Objects ### Pin object ![pin object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pin.png) The `Pin` object is a representation of a pin request. It includes the `cid` of data to be pinned, as well as optional metadata in `name`, `origins`, and `meta`. ### Pin status response ![pin status response object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pinstatus.png) The `PinStatus` object is a representation of the current state of a pinning operation. It includes the original `pin` object, along with the current `status` and globally unique `requestid` of the entire pinning request, which can be used for future status checks and management. Addresses in the `delegates` array are peers delegated by the pinning service for facilitating direct file transfers (more details in the provider hints section). Any additional vendor-specific information is returned in optional `info`. ## The pin lifecycle ![pinning service objects and lifecycle](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/lifecycle.png) ### Creating a new pin object The user sends a `Pin` object to `POST /pins` and receives a `PinStatus` response: - `requestid` in `PinStatus` is the identifier of the pin operation, which can can be used for checking status, and removing the pin in the future - `status` in `PinStatus` indicates the current state of a pin ### Checking status of in-progress pinning `status` (in `PinStatus`) may indicate a pending state (`queued` or `pinning`). This means the data behind `Pin.cid` was not found on the pinning service and is being fetched from the IPFS network at large, which may take time. In this case, the user can periodically check pinning progress via `GET /pins/{requestid}` until pinning is successful, or the user decides to remove the pending pin. ### Replacing an existing pin object The user can replace an existing pin object via `POST /pins/{requestid}`. This is a shortcut for removing a pin object identified by `requestid` and creating a new one in a single API call that protects against undesired garbage collection of blocks common to both pins. Useful when updating a pin representing a huge dataset where most of blocks did not change. The new pin object `requestid` is returned in the `PinStatus` response. The old pin object is deleted automatically. ### Removing a pin object A pin object can be removed via `DELETE /pins/{requestid}`. ## Provider hints Pinning of new data can be accelerated by providing a list of known data sources in `Pin.origins`, and connecting at least one of them to pinning service nodes at `PinStatus.delegates`. The most common scenario is a client putting its own IPFS node's multiaddrs in `Pin.origins`, and then directly connecting to every multiaddr returned by a pinning service in `PinStatus.delegates` to initiate transfer. This ensures data transfer starts immediately (without waiting for provider discovery over DHT), and direct dial from a client works around peer routing issues in restrictive network topologies such as NATs. ## Custom metadata Pinning services are encouraged to add support for additional features by leveraging the optional `Pin.meta` and `PinStatus.info` fields. While these attributes can be application- or vendor-specific, we encourage the community at large to leverage these attributes as a sandbox to come up with conventions that could become part of future revisions of this API. ### Pin metadata String keys and values passed in `Pin.meta` are persisted with the pin object. Potential uses: - `Pin.meta[app_id]`: Attaching a unique identifier to pins created by an app enables filtering pins per app via `?meta={\"app_id\":}` - `Pin.meta[vendor_policy]`: Vendor-specific policy (for example: which region to use, how many copies to keep) Note that it is OK for a client to omit or ignore these optional attributes; doing so should not impact the basic pinning functionality. ### Pin status info Additional `PinStatus.info` can be returned by pinning service. Potential uses: - `PinStatus.info[status_details]`: more info about the current status (queue position, percentage of transferred data, summary of where data is stored, etc); when `PinStatus.status=failed`, it could provide a reason why a pin operation failed (e.g. lack of funds, DAG too big, etc.) - `PinStatus.info[dag_size]`: the size of pinned data, along with DAG overhead - `PinStatus.info[raw_size]`: the size of data without DAG overhead (eg. unixfs) - `PinStatus.info[pinned_until]`: if vendor supports time-bound pins, this could indicate when the pin will expire # Pagination and filtering Pin objects can be listed by executing `GET /pins` with optional parameters: - When no filters are provided, the endpoint will return a small batch of the 10 most recently created items, from the latest to the oldest. - The number of returned items can be adjusted with the `limit` parameter (implicit default is 10). - If the value in `PinResults.count` is bigger than the length of `PinResults.results`, the client can infer there are more results that can be queried. - To read more items, pass the `before` filter with the timestamp from `PinStatus.created` found in the oldest item in the current batch of results. Repeat to read all results. - Returned results can be fine-tuned by applying optional `after`, `cid`, `name`, `status`, or `meta` filters. > **Note**: pagination by the `created` timestamp requires each value to be globally unique. Any future considerations to add support for bulk creation must account for this.
+ *
+ * API version: 0.1.1
+ * Generated by: OpenAPI Generator (https://openapi-generator.tech)
+ */
+
+package openapi
+
+import (
+ "encoding/json"
+ "time"
+)
+
+// PinStatus Pin object with status
+type PinStatus struct {
+ // Globally unique identifier of the pin request; can be used to check the status of ongoing pinning, or pin removal
+ Requestid string `json:"requestid"`
+ Status Status `json:"status"`
+ // Immutable timestamp indicating when a pin request entered a pinning service; can be used for filtering results and pagination
+ Created time.Time `json:"created"`
+ Pin Pin `json:"pin"`
+ // List of multiaddrs designated by pinning service for transferring any new data from external peers
+ Delegates []string `json:"delegates"`
+ // Optional info for PinStatus response
+ Info *map[string]string `json:"info,omitempty"`
+}
+
+// NewPinStatus instantiates a new PinStatus object
+// This constructor will assign default values to properties that have it defined,
+// and makes sure properties required by API are set, but the set of arguments
+// will change when the set of required properties is changed
+func NewPinStatus(requestid string, status Status, created time.Time, pin Pin, delegates []string) *PinStatus {
+ this := PinStatus{}
+ this.Requestid = requestid
+ this.Status = status
+ this.Created = created
+ this.Pin = pin
+ this.Delegates = delegates
+ return &this
+}
+
+// NewPinStatusWithDefaults instantiates a new PinStatus object
+// This constructor will only assign default values to properties that have it defined,
+// but it doesn't guarantee that properties required by API are set
+func NewPinStatusWithDefaults() *PinStatus {
+ this := PinStatus{}
+ return &this
+}
+
+// GetRequestid returns the Requestid field value
+func (o *PinStatus) GetRequestid() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.Requestid
+}
+
+// GetRequestidOk returns a tuple with the Requestid field value
+// and a boolean to check if the value has been set.
+func (o *PinStatus) GetRequestidOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Requestid, true
+}
+
+// SetRequestid sets field value
+func (o *PinStatus) SetRequestid(v string) {
+ o.Requestid = v
+}
+
+// GetStatus returns the Status field value
+func (o *PinStatus) GetStatus() Status {
+ if o == nil {
+ var ret Status
+ return ret
+ }
+
+ return o.Status
+}
+
+// GetStatusOk returns a tuple with the Status field value
+// and a boolean to check if the value has been set.
+func (o *PinStatus) GetStatusOk() (*Status, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Status, true
+}
+
+// SetStatus sets field value
+func (o *PinStatus) SetStatus(v Status) {
+ o.Status = v
+}
+
+// GetCreated returns the Created field value
+func (o *PinStatus) GetCreated() time.Time {
+ if o == nil {
+ var ret time.Time
+ return ret
+ }
+
+ return o.Created
+}
+
+// GetCreatedOk returns a tuple with the Created field value
+// and a boolean to check if the value has been set.
+func (o *PinStatus) GetCreatedOk() (*time.Time, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Created, true
+}
+
+// SetCreated sets field value
+func (o *PinStatus) SetCreated(v time.Time) {
+ o.Created = v
+}
+
+// GetPin returns the Pin field value
+func (o *PinStatus) GetPin() Pin {
+ if o == nil {
+ var ret Pin
+ return ret
+ }
+
+ return o.Pin
+}
+
+// GetPinOk returns a tuple with the Pin field value
+// and a boolean to check if the value has been set.
+func (o *PinStatus) GetPinOk() (*Pin, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Pin, true
+}
+
+// SetPin sets field value
+func (o *PinStatus) SetPin(v Pin) {
+ o.Pin = v
+}
+
+// GetDelegates returns the Delegates field value
+func (o *PinStatus) GetDelegates() []string {
+ if o == nil {
+ var ret []string
+ return ret
+ }
+
+ return o.Delegates
+}
+
+// GetDelegatesOk returns a tuple with the Delegates field value
+// and a boolean to check if the value has been set.
+func (o *PinStatus) GetDelegatesOk() (*[]string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Delegates, true
+}
+
+// SetDelegates sets field value
+func (o *PinStatus) SetDelegates(v []string) {
+ o.Delegates = v
+}
+
+// GetInfo returns the Info field value if set, zero value otherwise.
+func (o *PinStatus) GetInfo() map[string]string {
+ if o == nil || o.Info == nil {
+ var ret map[string]string
+ return ret
+ }
+ return *o.Info
+}
+
+// GetInfoOk returns a tuple with the Info field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *PinStatus) GetInfoOk() (*map[string]string, bool) {
+ if o == nil || o.Info == nil {
+ return nil, false
+ }
+ return o.Info, true
+}
+
+// HasInfo returns a boolean if a field has been set.
+func (o *PinStatus) HasInfo() bool {
+ if o != nil && o.Info != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetInfo gets a reference to the given map[string]string and assigns it to the Info field.
+func (o *PinStatus) SetInfo(v map[string]string) {
+ o.Info = &v
+}
+
+func (o PinStatus) MarshalJSON() ([]byte, error) {
+ toSerialize := map[string]interface{}{}
+ if true {
+ toSerialize["requestid"] = o.Requestid
+ }
+ if true {
+ toSerialize["status"] = o.Status
+ }
+ if true {
+ toSerialize["created"] = o.Created
+ }
+ if true {
+ toSerialize["pin"] = o.Pin
+ }
+ if true {
+ toSerialize["delegates"] = o.Delegates
+ }
+ if o.Info != nil {
+ toSerialize["info"] = o.Info
+ }
+ return json.Marshal(toSerialize)
+}
+
+type NullablePinStatus struct {
+ value *PinStatus
+ isSet bool
+}
+
+func (v NullablePinStatus) Get() *PinStatus {
+ return v.value
+}
+
+func (v *NullablePinStatus) Set(val *PinStatus) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullablePinStatus) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullablePinStatus) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullablePinStatus(val *PinStatus) *NullablePinStatus {
+ return &NullablePinStatus{value: val, isSet: true}
+}
+
+func (v NullablePinStatus) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullablePinStatus) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
diff --git a/pinning/remote/client/openapi/model_status.go b/pinning/remote/client/openapi/model_status.go
new file mode 100644
index 0000000000..56944819fa
--- /dev/null
+++ b/pinning/remote/client/openapi/model_status.go
@@ -0,0 +1,84 @@
+/*
+ * IPFS Pinning Service API
+ *
+ * ## About this spec The IPFS Pinning Service API is intended to be an implementation-agnostic API: - For use and implementation by pinning service providers - For use in client mode by IPFS nodes and GUI-based applications > **Note**: while ready for implementation, this spec is still a work in progress! 🏗️ **Your input and feedback are welcome and valuable as we develop this API spec. Please join the design discussion at [github.com/ipfs/pinning-services-api-spec](https://github.com/ipfs/pinning-services-api-spec).** # Schemas This section describes the most important object types and conventions. A full list of fields and schemas can be found in the `schemas` section of the [YAML file](https://github.com/ipfs/pinning-services-api-spec/blob/master/ipfs-pinning-service.yaml). ## Identifiers ### cid [Content Identifier (CID)](https://docs.ipfs.io/concepts/content-addressing/) points at the root of a DAG that is pinned recursively. ### requestid Unique identifier of a pin request. When a pin is created, the service responds with unique `requestid` that can be later used for pin removal. When the same `cid` is pinned again, a different `requestid` is returned to differentiate between those pin requests. Service implementation should use UUID, `hash(accessToken,Pin,PinStatus.created)`, or any other opaque identifier that provides equally strong protection against race conditions. ## Objects ### Pin object ![pin object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pin.png) The `Pin` object is a representation of a pin request. It includes the `cid` of data to be pinned, as well as optional metadata in `name`, `origins`, and `meta`. ### Pin status response ![pin status response object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pinstatus.png) The `PinStatus` object is a representation of the current state of a pinning operation. It includes the original `pin` object, along with the current `status` and globally unique `requestid` of the entire pinning request, which can be used for future status checks and management. Addresses in the `delegates` array are peers delegated by the pinning service for facilitating direct file transfers (more details in the provider hints section). Any additional vendor-specific information is returned in optional `info`. ## The pin lifecycle ![pinning service objects and lifecycle](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/lifecycle.png) ### Creating a new pin object The user sends a `Pin` object to `POST /pins` and receives a `PinStatus` response: - `requestid` in `PinStatus` is the identifier of the pin operation, which can can be used for checking status, and removing the pin in the future - `status` in `PinStatus` indicates the current state of a pin ### Checking status of in-progress pinning `status` (in `PinStatus`) may indicate a pending state (`queued` or `pinning`). This means the data behind `Pin.cid` was not found on the pinning service and is being fetched from the IPFS network at large, which may take time. In this case, the user can periodically check pinning progress via `GET /pins/{requestid}` until pinning is successful, or the user decides to remove the pending pin. ### Replacing an existing pin object The user can replace an existing pin object via `POST /pins/{requestid}`. This is a shortcut for removing a pin object identified by `requestid` and creating a new one in a single API call that protects against undesired garbage collection of blocks common to both pins. Useful when updating a pin representing a huge dataset where most of blocks did not change. The new pin object `requestid` is returned in the `PinStatus` response. The old pin object is deleted automatically. ### Removing a pin object A pin object can be removed via `DELETE /pins/{requestid}`. ## Provider hints Pinning of new data can be accelerated by providing a list of known data sources in `Pin.origins`, and connecting at least one of them to pinning service nodes at `PinStatus.delegates`. The most common scenario is a client putting its own IPFS node's multiaddrs in `Pin.origins`, and then directly connecting to every multiaddr returned by a pinning service in `PinStatus.delegates` to initiate transfer. This ensures data transfer starts immediately (without waiting for provider discovery over DHT), and direct dial from a client works around peer routing issues in restrictive network topologies such as NATs. ## Custom metadata Pinning services are encouraged to add support for additional features by leveraging the optional `Pin.meta` and `PinStatus.info` fields. While these attributes can be application- or vendor-specific, we encourage the community at large to leverage these attributes as a sandbox to come up with conventions that could become part of future revisions of this API. ### Pin metadata String keys and values passed in `Pin.meta` are persisted with the pin object. Potential uses: - `Pin.meta[app_id]`: Attaching a unique identifier to pins created by an app enables filtering pins per app via `?meta={\"app_id\":}` - `Pin.meta[vendor_policy]`: Vendor-specific policy (for example: which region to use, how many copies to keep) Note that it is OK for a client to omit or ignore these optional attributes; doing so should not impact the basic pinning functionality. ### Pin status info Additional `PinStatus.info` can be returned by pinning service. Potential uses: - `PinStatus.info[status_details]`: more info about the current status (queue position, percentage of transferred data, summary of where data is stored, etc); when `PinStatus.status=failed`, it could provide a reason why a pin operation failed (e.g. lack of funds, DAG too big, etc.) - `PinStatus.info[dag_size]`: the size of pinned data, along with DAG overhead - `PinStatus.info[raw_size]`: the size of data without DAG overhead (eg. unixfs) - `PinStatus.info[pinned_until]`: if vendor supports time-bound pins, this could indicate when the pin will expire # Pagination and filtering Pin objects can be listed by executing `GET /pins` with optional parameters: - When no filters are provided, the endpoint will return a small batch of the 10 most recently created items, from the latest to the oldest. - The number of returned items can be adjusted with the `limit` parameter (implicit default is 10). - If the value in `PinResults.count` is bigger than the length of `PinResults.results`, the client can infer there are more results that can be queried. - To read more items, pass the `before` filter with the timestamp from `PinStatus.created` found in the oldest item in the current batch of results. Repeat to read all results. - Returned results can be fine-tuned by applying optional `after`, `cid`, `name`, `status`, or `meta` filters. > **Note**: pagination by the `created` timestamp requires each value to be globally unique. Any future considerations to add support for bulk creation must account for this.
+ *
+ * API version: 0.1.1
+ * Generated by: OpenAPI Generator (https://openapi-generator.tech)
+ */
+
+package openapi
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// Status Status a pin object can have at a pinning service
+type Status string
+
+// List of Status
+const (
+ QUEUED Status = "queued"
+ PINNING Status = "pinning"
+ PINNED Status = "pinned"
+ FAILED Status = "failed"
+)
+
+func (v *Status) UnmarshalJSON(src []byte) error {
+ var value string
+ err := json.Unmarshal(src, &value)
+ if err != nil {
+ return err
+ }
+ enumTypeValue := Status(value)
+ for _, existing := range []Status{"queued", "pinning", "pinned", "failed"} {
+ if existing == enumTypeValue {
+ *v = enumTypeValue
+ return nil
+ }
+ }
+
+ return fmt.Errorf("%+v is not a valid Status", value)
+}
+
+// Ptr returns reference to Status value
+func (v Status) Ptr() *Status {
+ return &v
+}
+
+type NullableStatus struct {
+ value *Status
+ isSet bool
+}
+
+func (v NullableStatus) Get() *Status {
+ return v.value
+}
+
+func (v *NullableStatus) Set(val *Status) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableStatus) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableStatus) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableStatus(val *Status) *NullableStatus {
+ return &NullableStatus{value: val, isSet: true}
+}
+
+func (v NullableStatus) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableStatus) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
diff --git a/pinning/remote/client/openapi/response.go b/pinning/remote/client/openapi/response.go
new file mode 100644
index 0000000000..8f9fb0b081
--- /dev/null
+++ b/pinning/remote/client/openapi/response.go
@@ -0,0 +1,46 @@
+/*
+ * IPFS Pinning Service API
+ *
+ * ## About this spec The IPFS Pinning Service API is intended to be an implementation-agnostic API: - For use and implementation by pinning service providers - For use in client mode by IPFS nodes and GUI-based applications > **Note**: while ready for implementation, this spec is still a work in progress! 🏗️ **Your input and feedback are welcome and valuable as we develop this API spec. Please join the design discussion at [github.com/ipfs/pinning-services-api-spec](https://github.com/ipfs/pinning-services-api-spec).** # Schemas This section describes the most important object types and conventions. A full list of fields and schemas can be found in the `schemas` section of the [YAML file](https://github.com/ipfs/pinning-services-api-spec/blob/master/ipfs-pinning-service.yaml). ## Identifiers ### cid [Content Identifier (CID)](https://docs.ipfs.io/concepts/content-addressing/) points at the root of a DAG that is pinned recursively. ### requestid Unique identifier of a pin request. When a pin is created, the service responds with unique `requestid` that can be later used for pin removal. When the same `cid` is pinned again, a different `requestid` is returned to differentiate between those pin requests. Service implementation should use UUID, `hash(accessToken,Pin,PinStatus.created)`, or any other opaque identifier that provides equally strong protection against race conditions. ## Objects ### Pin object ![pin object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pin.png) The `Pin` object is a representation of a pin request. It includes the `cid` of data to be pinned, as well as optional metadata in `name`, `origins`, and `meta`. ### Pin status response ![pin status response object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pinstatus.png) The `PinStatus` object is a representation of the current state of a pinning operation. It includes the original `pin` object, along with the current `status` and globally unique `requestid` of the entire pinning request, which can be used for future status checks and management. Addresses in the `delegates` array are peers delegated by the pinning service for facilitating direct file transfers (more details in the provider hints section). Any additional vendor-specific information is returned in optional `info`. ## The pin lifecycle ![pinning service objects and lifecycle](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/lifecycle.png) ### Creating a new pin object The user sends a `Pin` object to `POST /pins` and receives a `PinStatus` response: - `requestid` in `PinStatus` is the identifier of the pin operation, which can can be used for checking status, and removing the pin in the future - `status` in `PinStatus` indicates the current state of a pin ### Checking status of in-progress pinning `status` (in `PinStatus`) may indicate a pending state (`queued` or `pinning`). This means the data behind `Pin.cid` was not found on the pinning service and is being fetched from the IPFS network at large, which may take time. In this case, the user can periodically check pinning progress via `GET /pins/{requestid}` until pinning is successful, or the user decides to remove the pending pin. ### Replacing an existing pin object The user can replace an existing pin object via `POST /pins/{requestid}`. This is a shortcut for removing a pin object identified by `requestid` and creating a new one in a single API call that protects against undesired garbage collection of blocks common to both pins. Useful when updating a pin representing a huge dataset where most of blocks did not change. The new pin object `requestid` is returned in the `PinStatus` response. The old pin object is deleted automatically. ### Removing a pin object A pin object can be removed via `DELETE /pins/{requestid}`. ## Provider hints Pinning of new data can be accelerated by providing a list of known data sources in `Pin.origins`, and connecting at least one of them to pinning service nodes at `PinStatus.delegates`. The most common scenario is a client putting its own IPFS node's multiaddrs in `Pin.origins`, and then directly connecting to every multiaddr returned by a pinning service in `PinStatus.delegates` to initiate transfer. This ensures data transfer starts immediately (without waiting for provider discovery over DHT), and direct dial from a client works around peer routing issues in restrictive network topologies such as NATs. ## Custom metadata Pinning services are encouraged to add support for additional features by leveraging the optional `Pin.meta` and `PinStatus.info` fields. While these attributes can be application- or vendor-specific, we encourage the community at large to leverage these attributes as a sandbox to come up with conventions that could become part of future revisions of this API. ### Pin metadata String keys and values passed in `Pin.meta` are persisted with the pin object. Potential uses: - `Pin.meta[app_id]`: Attaching a unique identifier to pins created by an app enables filtering pins per app via `?meta={\"app_id\":}` - `Pin.meta[vendor_policy]`: Vendor-specific policy (for example: which region to use, how many copies to keep) Note that it is OK for a client to omit or ignore these optional attributes; doing so should not impact the basic pinning functionality. ### Pin status info Additional `PinStatus.info` can be returned by pinning service. Potential uses: - `PinStatus.info[status_details]`: more info about the current status (queue position, percentage of transferred data, summary of where data is stored, etc); when `PinStatus.status=failed`, it could provide a reason why a pin operation failed (e.g. lack of funds, DAG too big, etc.) - `PinStatus.info[dag_size]`: the size of pinned data, along with DAG overhead - `PinStatus.info[raw_size]`: the size of data without DAG overhead (eg. unixfs) - `PinStatus.info[pinned_until]`: if vendor supports time-bound pins, this could indicate when the pin will expire # Pagination and filtering Pin objects can be listed by executing `GET /pins` with optional parameters: - When no filters are provided, the endpoint will return a small batch of the 10 most recently created items, from the latest to the oldest. - The number of returned items can be adjusted with the `limit` parameter (implicit default is 10). - If the value in `PinResults.count` is bigger than the length of `PinResults.results`, the client can infer there are more results that can be queried. - To read more items, pass the `before` filter with the timestamp from `PinStatus.created` found in the oldest item in the current batch of results. Repeat to read all results. - Returned results can be fine-tuned by applying optional `after`, `cid`, `name`, `status`, or `meta` filters. > **Note**: pagination by the `created` timestamp requires each value to be globally unique. Any future considerations to add support for bulk creation must account for this.
+ *
+ * API version: 0.1.1
+ * Generated by: OpenAPI Generator (https://openapi-generator.tech)
+ */
+
+package openapi
+
+import (
+ "net/http"
+)
+
+// APIResponse stores the API response returned by the server.
+type APIResponse struct {
+ *http.Response `json:"-"`
+ Message string `json:"message,omitempty"`
+ // Operation is the name of the OpenAPI operation.
+ Operation string `json:"operation,omitempty"`
+ // RequestURL is the request URL. This value is always available, even if the
+ // embedded *http.Response is nil.
+ RequestURL string `json:"url,omitempty"`
+ // Method is the HTTP method used for the request. This value is always
+ // available, even if the embedded *http.Response is nil.
+ Method string `json:"method,omitempty"`
+ // Payload holds the contents of the response body (which may be nil or empty).
+ // This is provided here as the raw response.Body() reader will have already
+ // been drained.
+ Payload []byte `json:"-"`
+}
+
+// NewAPIResponse returns a new APIResonse object.
+func NewAPIResponse(r *http.Response) *APIResponse {
+
+ response := &APIResponse{Response: r}
+ return response
+}
+
+// NewAPIResponseWithError returns a new APIResponse object with the provided error message.
+func NewAPIResponseWithError(errorMessage string) *APIResponse {
+
+ response := &APIResponse{Message: errorMessage}
+ return response
+}
diff --git a/pinning/remote/client/openapi/utils.go b/pinning/remote/client/openapi/utils.go
new file mode 100644
index 0000000000..25d36f11b6
--- /dev/null
+++ b/pinning/remote/client/openapi/utils.go
@@ -0,0 +1,327 @@
+/*
+ * IPFS Pinning Service API
+ *
+ * ## About this spec The IPFS Pinning Service API is intended to be an implementation-agnostic API: - For use and implementation by pinning service providers - For use in client mode by IPFS nodes and GUI-based applications > **Note**: while ready for implementation, this spec is still a work in progress! 🏗️ **Your input and feedback are welcome and valuable as we develop this API spec. Please join the design discussion at [github.com/ipfs/pinning-services-api-spec](https://github.com/ipfs/pinning-services-api-spec).** # Schemas This section describes the most important object types and conventions. A full list of fields and schemas can be found in the `schemas` section of the [YAML file](https://github.com/ipfs/pinning-services-api-spec/blob/master/ipfs-pinning-service.yaml). ## Identifiers ### cid [Content Identifier (CID)](https://docs.ipfs.io/concepts/content-addressing/) points at the root of a DAG that is pinned recursively. ### requestid Unique identifier of a pin request. When a pin is created, the service responds with unique `requestid` that can be later used for pin removal. When the same `cid` is pinned again, a different `requestid` is returned to differentiate between those pin requests. Service implementation should use UUID, `hash(accessToken,Pin,PinStatus.created)`, or any other opaque identifier that provides equally strong protection against race conditions. ## Objects ### Pin object ![pin object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pin.png) The `Pin` object is a representation of a pin request. It includes the `cid` of data to be pinned, as well as optional metadata in `name`, `origins`, and `meta`. ### Pin status response ![pin status response object](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/pinstatus.png) The `PinStatus` object is a representation of the current state of a pinning operation. It includes the original `pin` object, along with the current `status` and globally unique `requestid` of the entire pinning request, which can be used for future status checks and management. Addresses in the `delegates` array are peers delegated by the pinning service for facilitating direct file transfers (more details in the provider hints section). Any additional vendor-specific information is returned in optional `info`. ## The pin lifecycle ![pinning service objects and lifecycle](https://bafybeideck2fchyxna4wqwc2mo67yriokehw3yujboc5redjdaajrk2fjq.ipfs.dweb.link/lifecycle.png) ### Creating a new pin object The user sends a `Pin` object to `POST /pins` and receives a `PinStatus` response: - `requestid` in `PinStatus` is the identifier of the pin operation, which can can be used for checking status, and removing the pin in the future - `status` in `PinStatus` indicates the current state of a pin ### Checking status of in-progress pinning `status` (in `PinStatus`) may indicate a pending state (`queued` or `pinning`). This means the data behind `Pin.cid` was not found on the pinning service and is being fetched from the IPFS network at large, which may take time. In this case, the user can periodically check pinning progress via `GET /pins/{requestid}` until pinning is successful, or the user decides to remove the pending pin. ### Replacing an existing pin object The user can replace an existing pin object via `POST /pins/{requestid}`. This is a shortcut for removing a pin object identified by `requestid` and creating a new one in a single API call that protects against undesired garbage collection of blocks common to both pins. Useful when updating a pin representing a huge dataset where most of blocks did not change. The new pin object `requestid` is returned in the `PinStatus` response. The old pin object is deleted automatically. ### Removing a pin object A pin object can be removed via `DELETE /pins/{requestid}`. ## Provider hints Pinning of new data can be accelerated by providing a list of known data sources in `Pin.origins`, and connecting at least one of them to pinning service nodes at `PinStatus.delegates`. The most common scenario is a client putting its own IPFS node's multiaddrs in `Pin.origins`, and then directly connecting to every multiaddr returned by a pinning service in `PinStatus.delegates` to initiate transfer. This ensures data transfer starts immediately (without waiting for provider discovery over DHT), and direct dial from a client works around peer routing issues in restrictive network topologies such as NATs. ## Custom metadata Pinning services are encouraged to add support for additional features by leveraging the optional `Pin.meta` and `PinStatus.info` fields. While these attributes can be application- or vendor-specific, we encourage the community at large to leverage these attributes as a sandbox to come up with conventions that could become part of future revisions of this API. ### Pin metadata String keys and values passed in `Pin.meta` are persisted with the pin object. Potential uses: - `Pin.meta[app_id]`: Attaching a unique identifier to pins created by an app enables filtering pins per app via `?meta={\"app_id\":}` - `Pin.meta[vendor_policy]`: Vendor-specific policy (for example: which region to use, how many copies to keep) Note that it is OK for a client to omit or ignore these optional attributes; doing so should not impact the basic pinning functionality. ### Pin status info Additional `PinStatus.info` can be returned by pinning service. Potential uses: - `PinStatus.info[status_details]`: more info about the current status (queue position, percentage of transferred data, summary of where data is stored, etc); when `PinStatus.status=failed`, it could provide a reason why a pin operation failed (e.g. lack of funds, DAG too big, etc.) - `PinStatus.info[dag_size]`: the size of pinned data, along with DAG overhead - `PinStatus.info[raw_size]`: the size of data without DAG overhead (eg. unixfs) - `PinStatus.info[pinned_until]`: if vendor supports time-bound pins, this could indicate when the pin will expire # Pagination and filtering Pin objects can be listed by executing `GET /pins` with optional parameters: - When no filters are provided, the endpoint will return a small batch of the 10 most recently created items, from the latest to the oldest. - The number of returned items can be adjusted with the `limit` parameter (implicit default is 10). - If the value in `PinResults.count` is bigger than the length of `PinResults.results`, the client can infer there are more results that can be queried. - To read more items, pass the `before` filter with the timestamp from `PinStatus.created` found in the oldest item in the current batch of results. Repeat to read all results. - Returned results can be fine-tuned by applying optional `after`, `cid`, `name`, `status`, or `meta` filters. > **Note**: pagination by the `created` timestamp requires each value to be globally unique. Any future considerations to add support for bulk creation must account for this.
+ *
+ * API version: 0.1.1
+ * Generated by: OpenAPI Generator (https://openapi-generator.tech)
+ */
+
+package openapi
+
+import (
+ "encoding/json"
+ "time"
+)
+
+// PtrBool is a helper routine that returns a pointer to given integer value.
+func PtrBool(v bool) *bool { return &v }
+
+// PtrInt is a helper routine that returns a pointer to given integer value.
+func PtrInt(v int) *int { return &v }
+
+// PtrInt32 is a helper routine that returns a pointer to given integer value.
+func PtrInt32(v int32) *int32 { return &v }
+
+// PtrInt64 is a helper routine that returns a pointer to given integer value.
+func PtrInt64(v int64) *int64 { return &v }
+
+// PtrFloat32 is a helper routine that returns a pointer to given float value.
+func PtrFloat32(v float32) *float32 { return &v }
+
+// PtrFloat64 is a helper routine that returns a pointer to given float value.
+func PtrFloat64(v float64) *float64 { return &v }
+
+// PtrString is a helper routine that returns a pointer to given string value.
+func PtrString(v string) *string { return &v }
+
+// PtrTime is helper routine that returns a pointer to given Time value.
+func PtrTime(v time.Time) *time.Time { return &v }
+
+type NullableBool struct {
+ value *bool
+ isSet bool
+}
+
+func (v NullableBool) Get() *bool {
+ return v.value
+}
+
+func (v *NullableBool) Set(val *bool) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableBool) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableBool) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableBool(val *bool) *NullableBool {
+ return &NullableBool{value: val, isSet: true}
+}
+
+func (v NullableBool) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableBool) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
+
+type NullableInt struct {
+ value *int
+ isSet bool
+}
+
+func (v NullableInt) Get() *int {
+ return v.value
+}
+
+func (v *NullableInt) Set(val *int) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableInt) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableInt) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableInt(val *int) *NullableInt {
+ return &NullableInt{value: val, isSet: true}
+}
+
+func (v NullableInt) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableInt) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
+
+type NullableInt32 struct {
+ value *int32
+ isSet bool
+}
+
+func (v NullableInt32) Get() *int32 {
+ return v.value
+}
+
+func (v *NullableInt32) Set(val *int32) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableInt32) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableInt32) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableInt32(val *int32) *NullableInt32 {
+ return &NullableInt32{value: val, isSet: true}
+}
+
+func (v NullableInt32) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableInt32) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
+
+type NullableInt64 struct {
+ value *int64
+ isSet bool
+}
+
+func (v NullableInt64) Get() *int64 {
+ return v.value
+}
+
+func (v *NullableInt64) Set(val *int64) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableInt64) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableInt64) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableInt64(val *int64) *NullableInt64 {
+ return &NullableInt64{value: val, isSet: true}
+}
+
+func (v NullableInt64) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableInt64) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
+
+type NullableFloat32 struct {
+ value *float32
+ isSet bool
+}
+
+func (v NullableFloat32) Get() *float32 {
+ return v.value
+}
+
+func (v *NullableFloat32) Set(val *float32) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableFloat32) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableFloat32) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableFloat32(val *float32) *NullableFloat32 {
+ return &NullableFloat32{value: val, isSet: true}
+}
+
+func (v NullableFloat32) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableFloat32) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
+
+type NullableFloat64 struct {
+ value *float64
+ isSet bool
+}
+
+func (v NullableFloat64) Get() *float64 {
+ return v.value
+}
+
+func (v *NullableFloat64) Set(val *float64) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableFloat64) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableFloat64) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableFloat64(val *float64) *NullableFloat64 {
+ return &NullableFloat64{value: val, isSet: true}
+}
+
+func (v NullableFloat64) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableFloat64) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
+
+type NullableString struct {
+ value *string
+ isSet bool
+}
+
+func (v NullableString) Get() *string {
+ return v.value
+}
+
+func (v *NullableString) Set(val *string) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableString) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableString) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableString(val *string) *NullableString {
+ return &NullableString{value: val, isSet: true}
+}
+
+func (v NullableString) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableString) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
+
+type NullableTime struct {
+ value *time.Time
+ isSet bool
+}
+
+func (v NullableTime) Get() *time.Time {
+ return v.value
+}
+
+func (v *NullableTime) Set(val *time.Time) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableTime) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableTime) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableTime(val *time.Time) *NullableTime {
+ return &NullableTime{value: val, isSet: true}
+}
+
+func (v NullableTime) MarshalJSON() ([]byte, error) {
+ return v.value.MarshalJSON()
+}
+
+func (v *NullableTime) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
diff --git a/provider/README.md b/provider/README.md
new file mode 100644
index 0000000000..0e4f4650d2
--- /dev/null
+++ b/provider/README.md
@@ -0,0 +1,30 @@
+## Usage
+
+Here's how you create, start, interact with, and stop the provider system:
+
+```golang
+import (
+ "context"
+ "time"
+
+ "github.com/ipfs/boxo/provider"
+ "github.com/ipfs/boxo/provider/queue"
+ "github.com/ipfs/boxo/provider/simple"
+)
+
+rsys := (your routing system here)
+dstore := (your datastore here)
+cid := (your cid to provide here)
+
+q := queue.NewQueue(context.Background(), "example", dstore)
+
+reprov := simple.NewReprovider(context.Background(), time.Hour * 12, rsys, simple.NewBlockstoreProvider(dstore))
+prov := simple.NewProvider(context.Background(), q, rsys)
+sys := provider.NewSystem(prov, reprov)
+
+sys.Run()
+
+sys.Provide(cid)
+
+sys.Close()
+```
diff --git a/provider/batched/system.go b/provider/batched/system.go
new file mode 100644
index 0000000000..e3cb0325a0
--- /dev/null
+++ b/provider/batched/system.go
@@ -0,0 +1,420 @@
+package batched
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strconv"
+ "sync"
+ "time"
+
+ provider "github.com/ipfs/boxo/provider"
+ "github.com/ipfs/boxo/provider/queue"
+ "github.com/ipfs/boxo/provider/simple"
+ "github.com/ipfs/boxo/verifcid"
+ "github.com/ipfs/go-cid"
+ "github.com/ipfs/go-datastore"
+ logging "github.com/ipfs/go-log"
+ "github.com/multiformats/go-multihash"
+)
+
+var log = logging.Logger("provider.batched")
+
+type BatchProvidingSystem struct {
+ ctx context.Context
+ close context.CancelFunc
+ closewg sync.WaitGroup
+
+ reprovideInterval time.Duration
+ initalReprovideDelay time.Duration
+ initialReprovideDelaySet bool
+
+ rsys provideMany
+ keyProvider simple.KeyChanFunc
+
+ q *queue.Queue
+ ds datastore.Batching
+
+ reprovideCh chan cid.Cid
+
+ totalProvides, lastReprovideBatchSize int
+ avgProvideDuration, lastReprovideDuration time.Duration
+}
+
+var _ provider.System = (*BatchProvidingSystem)(nil)
+
+type provideMany interface {
+ ProvideMany(ctx context.Context, keys []multihash.Multihash) error
+ Ready() bool
+}
+
+// Option defines the functional option type that can be used to configure
+// BatchProvidingSystem instances
+type Option func(system *BatchProvidingSystem) error
+
+var lastReprovideKey = datastore.NewKey("/provider/reprovide/lastreprovide")
+
+func New(provider provideMany, q *queue.Queue, opts ...Option) (*BatchProvidingSystem, error) {
+ s := &BatchProvidingSystem{
+ reprovideInterval: time.Hour * 24,
+ rsys: provider,
+ keyProvider: nil,
+ q: q,
+ ds: datastore.NewMapDatastore(),
+ reprovideCh: make(chan cid.Cid),
+ }
+
+ for _, o := range opts {
+ if err := o(s); err != nil {
+ return nil, err
+ }
+ }
+
+ // Setup default behavior for the initial reprovide delay
+ //
+ // If the reprovide ticker is larger than a minute (likely),
+ // provide once after we've been up a minute.
+ //
+ // Don't provide _immediately_ as we might be just about to stop.
+ if !s.initialReprovideDelaySet && s.reprovideInterval > time.Minute {
+ s.initalReprovideDelay = time.Minute
+ s.initialReprovideDelaySet = true
+ }
+
+ if s.keyProvider == nil {
+ s.keyProvider = func(ctx context.Context) (<-chan cid.Cid, error) {
+ ch := make(chan cid.Cid)
+ close(ch)
+ return ch, nil
+ }
+ }
+
+ // This is after the options processing so we do not have to worry about leaking a context if there is an
+ // initialization error processing the options
+ ctx, cancel := context.WithCancel(context.Background())
+ s.ctx = ctx
+ s.close = cancel
+
+ return s, nil
+}
+
+func Datastore(batching datastore.Batching) Option {
+ return func(system *BatchProvidingSystem) error {
+ system.ds = batching
+ return nil
+ }
+}
+
+func ReproviderInterval(duration time.Duration) Option {
+ return func(system *BatchProvidingSystem) error {
+ system.reprovideInterval = duration
+ return nil
+ }
+}
+
+func KeyProvider(fn simple.KeyChanFunc) Option {
+ return func(system *BatchProvidingSystem) error {
+ system.keyProvider = fn
+ return nil
+ }
+}
+
+func initialReprovideDelay(duration time.Duration) Option {
+ return func(system *BatchProvidingSystem) error {
+ system.initialReprovideDelaySet = true
+ system.initalReprovideDelay = duration
+ return nil
+ }
+}
+
+func (s *BatchProvidingSystem) Run() {
+ // how long we wait between the first provider we hear about and batching up the provides to send out
+ const pauseDetectionThreshold = time.Millisecond * 500
+ // how long we are willing to collect providers for the batch after we receive the first one
+ const maxCollectionDuration = time.Minute * 10
+
+ provCh := s.q.Dequeue()
+
+ s.closewg.Add(1)
+ go func() {
+ defer s.closewg.Done()
+
+ m := make(map[cid.Cid]struct{})
+
+ // setup stopped timers
+ maxCollectionDurationTimer := time.NewTimer(time.Hour)
+ pauseDetectTimer := time.NewTimer(time.Hour)
+ stopAndEmptyTimer(maxCollectionDurationTimer)
+ stopAndEmptyTimer(pauseDetectTimer)
+
+ // make sure timers are cleaned up
+ defer maxCollectionDurationTimer.Stop()
+ defer pauseDetectTimer.Stop()
+
+ resetTimersAfterReceivingProvide := func() {
+ firstProvide := len(m) == 0
+ if firstProvide {
+ // after receiving the first provider start up the timers
+ maxCollectionDurationTimer.Reset(maxCollectionDuration)
+ pauseDetectTimer.Reset(pauseDetectionThreshold)
+ } else {
+ // otherwise just do a full restart of the pause timer
+ stopAndEmptyTimer(pauseDetectTimer)
+ pauseDetectTimer.Reset(pauseDetectionThreshold)
+ }
+ }
+
+ for {
+ performedReprovide := false
+
+ // at the start of every loop the maxCollectionDurationTimer and pauseDetectTimer should be already be
+ // stopped and have empty channels
+ loop:
+ for {
+ select {
+ case <-maxCollectionDurationTimer.C:
+ // if this timer has fired then the pause timer has started so let's stop and empty it
+ stopAndEmptyTimer(pauseDetectTimer)
+ break loop
+ default:
+ }
+
+ select {
+ case c := <-provCh:
+ resetTimersAfterReceivingProvide()
+ m[c] = struct{}{}
+ continue
+ default:
+ }
+
+ select {
+ case c := <-provCh:
+ resetTimersAfterReceivingProvide()
+ m[c] = struct{}{}
+ case c := <-s.reprovideCh:
+ resetTimersAfterReceivingProvide()
+ m[c] = struct{}{}
+ performedReprovide = true
+ case <-pauseDetectTimer.C:
+ // if this timer has fired then the max collection timer has started so let's stop and empty it
+ stopAndEmptyTimer(maxCollectionDurationTimer)
+ break loop
+ case <-maxCollectionDurationTimer.C:
+ // if this timer has fired then the pause timer has started so let's stop and empty it
+ stopAndEmptyTimer(pauseDetectTimer)
+ break loop
+ case <-s.ctx.Done():
+ return
+ }
+ }
+
+ if len(m) == 0 {
+ continue
+ }
+
+ keys := make([]multihash.Multihash, 0, len(m))
+ for c := range m {
+ delete(m, c)
+
+ // hash security
+ if err := verifcid.ValidateCid(c); err != nil {
+ log.Errorf("insecure hash in reprovider, %s (%s)", c, err)
+ continue
+ }
+
+ keys = append(keys, c.Hash())
+ }
+
+ // in case after removing all the invalid CIDs there are no valid ones left
+ if len(keys) == 0 {
+ continue
+ }
+
+ for !s.rsys.Ready() {
+ log.Debugf("reprovider system not ready")
+ select {
+ case <-time.After(time.Minute):
+ case <-s.ctx.Done():
+ return
+ }
+ }
+
+ log.Debugf("starting provide of %d keys", len(keys))
+ start := time.Now()
+ err := s.rsys.ProvideMany(s.ctx, keys)
+ if err != nil {
+ log.Debugf("providing failed %v", err)
+ continue
+ }
+ dur := time.Since(start)
+
+ totalProvideTime := int64(s.totalProvides) * int64(s.avgProvideDuration)
+ recentAvgProvideDuration := time.Duration(int64(dur) / int64(len(keys)))
+ s.avgProvideDuration = time.Duration((totalProvideTime + int64(dur)) / int64(s.totalProvides+len(keys)))
+ s.totalProvides += len(keys)
+
+ log.Debugf("finished providing of %d keys. It took %v with an average of %v per provide", len(keys), dur, recentAvgProvideDuration)
+
+ if performedReprovide {
+ s.lastReprovideBatchSize = len(keys)
+ s.lastReprovideDuration = dur
+
+ if err := s.ds.Put(s.ctx, lastReprovideKey, storeTime(time.Now())); err != nil {
+ log.Errorf("could not store last reprovide time: %v", err)
+ }
+ if err := s.ds.Sync(s.ctx, lastReprovideKey); err != nil {
+ log.Errorf("could not perform sync of last reprovide time: %v", err)
+ }
+ }
+ }
+ }()
+
+ s.closewg.Add(1)
+ go func() {
+ defer s.closewg.Done()
+
+ var initialReprovideCh, reprovideCh <-chan time.Time
+
+ // If reproviding is enabled (non-zero)
+ if s.reprovideInterval > 0 {
+ reprovideTicker := time.NewTicker(s.reprovideInterval)
+ defer reprovideTicker.Stop()
+ reprovideCh = reprovideTicker.C
+
+ // if there is a non-zero initial reprovide time that was set in the initializer or if the fallback has been
+ if s.initialReprovideDelaySet {
+ initialReprovideTimer := time.NewTimer(s.initalReprovideDelay)
+ defer initialReprovideTimer.Stop()
+
+ initialReprovideCh = initialReprovideTimer.C
+ }
+ }
+
+ for s.ctx.Err() == nil {
+ select {
+ case <-initialReprovideCh:
+ case <-reprovideCh:
+ case <-s.ctx.Done():
+ return
+ }
+
+ err := s.reprovide(s.ctx, false)
+
+ // only log if we've hit an actual error, otherwise just tell the client we're shutting down
+ if s.ctx.Err() == nil && err != nil {
+ log.Errorf("failed to reprovide: %s", err)
+ }
+ }
+ }()
+}
+
+func stopAndEmptyTimer(t *time.Timer) {
+ if !t.Stop() {
+ <-t.C
+ }
+}
+
+func storeTime(t time.Time) []byte {
+ val := []byte(fmt.Sprintf("%d", t.UnixNano()))
+ return val
+}
+
+func parseTime(b []byte) (time.Time, error) {
+ tns, err := strconv.ParseInt(string(b), 10, 64)
+ if err != nil {
+ return time.Time{}, err
+ }
+ return time.Unix(0, tns), nil
+}
+
+func (s *BatchProvidingSystem) Close() error {
+ s.close()
+ err := s.q.Close()
+ s.closewg.Wait()
+ return err
+}
+
+func (s *BatchProvidingSystem) Provide(cid cid.Cid) error {
+ return s.q.Enqueue(cid)
+}
+
+func (s *BatchProvidingSystem) Reprovide(ctx context.Context) error {
+ return s.reprovide(ctx, true)
+}
+
+func (s *BatchProvidingSystem) reprovide(ctx context.Context, force bool) error {
+ if !s.shouldReprovide() && !force {
+ return nil
+ }
+
+ kch, err := s.keyProvider(ctx)
+ if err != nil {
+ return err
+ }
+
+reprovideCidLoop:
+ for {
+ select {
+ case c, ok := <-kch:
+ if !ok {
+ break reprovideCidLoop
+ }
+
+ select {
+ case s.reprovideCh <- c:
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+
+ return nil
+}
+
+func (s *BatchProvidingSystem) getLastReprovideTime() (time.Time, error) {
+ val, err := s.ds.Get(s.ctx, lastReprovideKey)
+ if errors.Is(err, datastore.ErrNotFound) {
+ return time.Time{}, nil
+ }
+ if err != nil {
+ return time.Time{}, fmt.Errorf("could not get last reprovide time")
+ }
+
+ t, err := parseTime(val)
+ if err != nil {
+ return time.Time{}, fmt.Errorf("could not decode last reprovide time, got %q", string(val))
+ }
+
+ return t, nil
+}
+
+func (s *BatchProvidingSystem) shouldReprovide() bool {
+ t, err := s.getLastReprovideTime()
+ if err != nil {
+ log.Debugf("getting last reprovide time failed: %s", err)
+ return false
+ }
+
+ if time.Since(t) < time.Duration(float64(s.reprovideInterval)*0.5) {
+ return false
+ }
+ return true
+}
+
+type BatchedProviderStats struct {
+ TotalProvides, LastReprovideBatchSize int
+ AvgProvideDuration, LastReprovideDuration time.Duration
+}
+
+// Stat returns various stats about this provider system
+func (s *BatchProvidingSystem) Stat(ctx context.Context) (BatchedProviderStats, error) {
+ // TODO: Does it matter that there is no locking around the total+average values?
+ return BatchedProviderStats{
+ TotalProvides: s.totalProvides,
+ LastReprovideBatchSize: s.lastReprovideBatchSize,
+ AvgProvideDuration: s.avgProvideDuration,
+ LastReprovideDuration: s.lastReprovideDuration,
+ }, nil
+}
diff --git a/provider/batched/system_test.go b/provider/batched/system_test.go
new file mode 100644
index 0000000000..4fe0624aff
--- /dev/null
+++ b/provider/batched/system_test.go
@@ -0,0 +1,117 @@
+package batched
+
+import (
+ "context"
+ "strconv"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/ipfs/go-cid"
+ "github.com/ipfs/go-datastore"
+ dssync "github.com/ipfs/go-datastore/sync"
+ mh "github.com/multiformats/go-multihash"
+
+ q "github.com/ipfs/boxo/provider/queue"
+)
+
+type mockProvideMany struct {
+ lk sync.Mutex
+ keys []mh.Multihash
+}
+
+func (m *mockProvideMany) ProvideMany(ctx context.Context, keys []mh.Multihash) error {
+ m.lk.Lock()
+ defer m.lk.Unlock()
+ m.keys = keys
+ return nil
+}
+
+func (m *mockProvideMany) Ready() bool {
+ return true
+}
+
+func (m *mockProvideMany) GetKeys() []mh.Multihash {
+ m.lk.Lock()
+ defer m.lk.Unlock()
+ return m.keys[:]
+}
+
+var _ provideMany = (*mockProvideMany)(nil)
+
+func TestBatched(t *testing.T) {
+ ctx := context.Background()
+ defer ctx.Done()
+
+ ds := dssync.MutexWrap(datastore.NewMapDatastore())
+ queue, err := q.NewQueue(ctx, "test", ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ provider := &mockProvideMany{}
+
+ ctx, cancel := context.WithTimeout(ctx, time.Second*10)
+ defer cancel()
+
+ const numProvides = 100
+ keysToProvide := make(map[cid.Cid]int)
+ for i := 0; i < numProvides; i++ {
+ h, err := mh.Sum([]byte(strconv.Itoa(i)), mh.SHA2_256, -1)
+ if err != nil {
+ panic(err)
+ }
+ c := cid.NewCidV1(cid.Raw, h)
+ keysToProvide[c] = i
+ }
+
+ batchSystem, err := New(provider, queue, KeyProvider(func(ctx context.Context) (<-chan cid.Cid, error) {
+ ch := make(chan cid.Cid)
+ go func() {
+ for k := range keysToProvide {
+ select {
+ case ch <- k:
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+ return ch, nil
+ }), initialReprovideDelay(0))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ batchSystem.Run()
+
+ var keys []mh.Multihash
+ for {
+ if ctx.Err() != nil {
+ t.Fatal("test hung")
+ }
+ keys = provider.GetKeys()
+ if len(keys) != 0 {
+ break
+ }
+ time.Sleep(time.Millisecond * 100)
+ }
+
+ if len(keys) != numProvides {
+ t.Fatalf("expected %d provider keys, got %d", numProvides, len(keys))
+ }
+
+ provMap := make(map[string]struct{})
+ for _, k := range keys {
+ provMap[string(k)] = struct{}{}
+ }
+
+ for i := 0; i < numProvides; i++ {
+ h, err := mh.Sum([]byte(strconv.Itoa(i)), mh.SHA2_256, -1)
+ if err != nil {
+ panic(err)
+ }
+ if _, found := provMap[string(h)]; !found {
+ t.Fatalf("could not find provider with value %d", i)
+ }
+ }
+}
diff --git a/provider/offline.go b/provider/offline.go
new file mode 100644
index 0000000000..030a70ab13
--- /dev/null
+++ b/provider/offline.go
@@ -0,0 +1,29 @@
+package provider
+
+import (
+ "context"
+
+ "github.com/ipfs/go-cid"
+)
+
+type offlineProvider struct{}
+
+// NewOfflineProvider creates a ProviderSystem that does nothing
+func NewOfflineProvider() System {
+ return &offlineProvider{}
+}
+
+func (op *offlineProvider) Run() {
+}
+
+func (op *offlineProvider) Close() error {
+ return nil
+}
+
+func (op *offlineProvider) Provide(cid.Cid) error {
+ return nil
+}
+
+func (op *offlineProvider) Reprovide(context.Context) error {
+ return nil
+}
diff --git a/provider/provider.go b/provider/provider.go
new file mode 100644
index 0000000000..3b9c6ba3eb
--- /dev/null
+++ b/provider/provider.go
@@ -0,0 +1,27 @@
+package provider
+
+import (
+ "context"
+
+ "github.com/ipfs/go-cid"
+)
+
+// Provider announces blocks to the network
+type Provider interface {
+ // Run is used to begin processing the provider work
+ Run()
+ // Provide takes a cid and makes an attempt to announce it to the network
+ Provide(cid.Cid) error
+ // Close stops the provider
+ Close() error
+}
+
+// Reprovider reannounces blocks to the network
+type Reprovider interface {
+ // Run is used to begin processing the reprovider work and waiting for reprovide triggers
+ Run()
+ // Trigger a reprovide
+ Trigger(context.Context) error
+ // Close stops the reprovider
+ Close() error
+}
diff --git a/provider/queue/queue.go b/provider/queue/queue.go
new file mode 100644
index 0000000000..618256bbee
--- /dev/null
+++ b/provider/queue/queue.go
@@ -0,0 +1,156 @@
+package queue
+
+import (
+ "context"
+ "fmt"
+ cid "github.com/ipfs/go-cid"
+ datastore "github.com/ipfs/go-datastore"
+ namespace "github.com/ipfs/go-datastore/namespace"
+ query "github.com/ipfs/go-datastore/query"
+ logging "github.com/ipfs/go-log"
+)
+
+var log = logging.Logger("provider.queue")
+
+// Queue provides a best-effort durability, FIFO interface to the datastore for storing cids
+//
+// Best-effort durability just means that cids in the process of being provided when a
+// crash or shutdown occurs may be in the queue when the node is brought back online
+// depending on whether the underlying datastore has synchronous or asynchronous writes.
+type Queue struct {
+ // used to differentiate queues in datastore
+ // e.g. provider vs reprovider
+ name string
+ ctx context.Context
+ ds datastore.Datastore // Must be threadsafe
+ dequeue chan cid.Cid
+ enqueue chan cid.Cid
+ close context.CancelFunc
+ closed chan struct{}
+
+ counter uint64
+}
+
+// NewQueue creates a queue for cids
+func NewQueue(ctx context.Context, name string, ds datastore.Datastore) (*Queue, error) {
+ namespaced := namespace.Wrap(ds, datastore.NewKey("/"+name+"/queue/"))
+ cancelCtx, cancel := context.WithCancel(ctx)
+ q := &Queue{
+ name: name,
+ ctx: cancelCtx,
+ ds: namespaced,
+ dequeue: make(chan cid.Cid),
+ enqueue: make(chan cid.Cid),
+ close: cancel,
+ closed: make(chan struct{}, 1),
+ }
+ q.work()
+ return q, nil
+}
+
+// Close stops the queue
+func (q *Queue) Close() error {
+ q.close()
+ <-q.closed
+ return nil
+}
+
+// Enqueue puts a cid in the queue
+func (q *Queue) Enqueue(cid cid.Cid) error {
+ select {
+ case q.enqueue <- cid:
+ return nil
+ case <-q.ctx.Done():
+ return fmt.Errorf("failed to enqueue CID: shutting down")
+ }
+}
+
+// Dequeue returns a channel that if listened to will remove entries from the queue
+func (q *Queue) Dequeue() <-chan cid.Cid {
+ return q.dequeue
+}
+
+// Run dequeues and enqueues when available.
+func (q *Queue) work() {
+ go func() {
+ var k datastore.Key = datastore.Key{}
+ var c cid.Cid = cid.Undef
+
+ defer func() {
+ // also cancels any in-progess enqueue tasks.
+ q.close()
+ // unblocks anyone waiting
+ close(q.dequeue)
+ // unblocks the close call
+ close(q.closed)
+ }()
+
+ for {
+ if c == cid.Undef {
+ head, err := q.getQueueHead()
+
+ if err != nil {
+ log.Errorf("error querying for head of queue: %s, stopping provider", err)
+ return
+ } else if head != nil {
+ k = datastore.NewKey(head.Key)
+ c, err = cid.Parse(head.Value)
+ if err != nil {
+ log.Warnf("error parsing queue entry cid with key (%s), removing it from queue: %s", head.Key, err)
+ err = q.ds.Delete(q.ctx, k)
+ if err != nil {
+ log.Errorf("error deleting queue entry with key (%s), due to error (%s), stopping provider", head.Key, err)
+ return
+ }
+ continue
+ }
+ } else {
+ c = cid.Undef
+ }
+ }
+
+ // If c != cid.Undef set dequeue and attempt write, otherwise wait for enqueue
+ var dequeue chan cid.Cid
+ if c != cid.Undef {
+ dequeue = q.dequeue
+ }
+
+ select {
+ case toQueue := <-q.enqueue:
+ keyPath := fmt.Sprintf("%020d/%s", q.counter, c.String())
+ q.counter++
+ nextKey := datastore.NewKey(keyPath)
+
+ if err := q.ds.Put(q.ctx, nextKey, toQueue.Bytes()); err != nil {
+ log.Errorf("Failed to enqueue cid: %s", err)
+ continue
+ }
+ case dequeue <- c:
+ err := q.ds.Delete(q.ctx, k)
+
+ if err != nil {
+ log.Errorf("Failed to delete queued cid %s with key %s: %s", c, k, err)
+ continue
+ }
+ c = cid.Undef
+ case <-q.ctx.Done():
+ return
+ }
+ }
+ }()
+}
+
+func (q *Queue) getQueueHead() (*query.Entry, error) {
+ qry := query.Query{Orders: []query.Order{query.OrderByKey{}}, Limit: 1}
+ results, err := q.ds.Query(q.ctx, qry)
+ if err != nil {
+ return nil, err
+ }
+ defer results.Close()
+ r, ok := results.NextSync()
+ if !ok {
+ return nil, nil
+ }
+
+ return &r.Entry, r.Error
+}
diff --git a/provider/queue/queue_test.go b/provider/queue/queue_test.go
new file mode 100644
index 0000000000..a0fa36c3ad
--- /dev/null
+++ b/provider/queue/queue_test.go
@@ -0,0 +1,133 @@
+package queue
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/ipfs/go-cid"
+ "github.com/ipfs/go-datastore"
+ "github.com/ipfs/go-datastore/sync"
+ blocksutil "github.com/ipfs/go-ipfs-blocksutil"
+)
+
+var blockGenerator = blocksutil.NewBlockGenerator()
+
+func makeCids(n int) []cid.Cid {
+ cids := make([]cid.Cid, 0, n)
+ for i := 0; i < n; i++ {
+ c := blockGenerator.Next().Cid()
+ cids = append(cids, c)
+ }
+ return cids
+}
+
+func assertOrdered(cids []cid.Cid, q *Queue, t *testing.T) {
+ for _, c := range cids {
+ select {
+ case dequeued := <-q.dequeue:
+ if c != dequeued {
+ t.Fatalf("Error in ordering of CIDs retrieved from queue. Expected: %s, got: %s", c, dequeued)
+ }
+
+ case <-time.After(time.Second * 1):
+ t.Fatal("Timeout waiting for cids to be provided.")
+ }
+ }
+}
+
+func TestBasicOperation(t *testing.T) {
+ ctx := context.Background()
+ defer ctx.Done()
+
+ ds := sync.MutexWrap(datastore.NewMapDatastore())
+ queue, err := NewQueue(ctx, "test", ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cids := makeCids(10)
+
+ for _, c := range cids {
+ queue.Enqueue(c)
+ }
+
+ assertOrdered(cids, queue, t)
+}
+
+func TestMangledData(t *testing.T) {
+ ctx := context.Background()
+ defer ctx.Done()
+
+ ds := sync.MutexWrap(datastore.NewMapDatastore())
+ queue, err := NewQueue(ctx, "test", ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cids := makeCids(10)
+ for _, c := range cids {
+ queue.Enqueue(c)
+ }
+
+ // put bad data in the queue
+ queueKey := datastore.NewKey("/test/0")
+ err = queue.ds.Put(ctx, queueKey, []byte("borked"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // expect to only see the valid cids we entered
+ expected := cids
+ assertOrdered(expected, queue, t)
+}
+
+func TestInitialization(t *testing.T) {
+ ctx := context.Background()
+ defer ctx.Done()
+
+ ds := sync.MutexWrap(datastore.NewMapDatastore())
+ queue, err := NewQueue(ctx, "test", ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cids := makeCids(10)
+ for _, c := range cids {
+ queue.Enqueue(c)
+ }
+
+ assertOrdered(cids[:5], queue, t)
+
+ // make a new queue, same data
+ queue, err = NewQueue(ctx, "test", ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertOrdered(cids[5:], queue, t)
+}
+
+func TestInitializationWithManyCids(t *testing.T) {
+ ctx := context.Background()
+ defer ctx.Done()
+
+ ds := sync.MutexWrap(datastore.NewMapDatastore())
+ queue, err := NewQueue(ctx, "test", ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cids := makeCids(25)
+ for _, c := range cids {
+ queue.Enqueue(c)
+ }
+
+ // make a new queue, same data
+ queue, err = NewQueue(ctx, "test", ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertOrdered(cids, queue, t)
+}
diff --git a/provider/simple/provider.go b/provider/simple/provider.go
new file mode 100644
index 0000000000..63de031ad0
--- /dev/null
+++ b/provider/simple/provider.go
@@ -0,0 +1,116 @@
+// Package simple implements structures and methods to provide blocks,
+// keep track of which blocks are provided, and to allow those blocks to
+// be reprovided.
+package simple
+
+import (
+ "context"
+ "time"
+
+ q "github.com/ipfs/boxo/provider/queue"
+ "github.com/ipfs/go-cid"
+ logging "github.com/ipfs/go-log"
+ "github.com/libp2p/go-libp2p/core/routing"
+)
+
+var logP = logging.Logger("provider.simple")
+
+// Provider announces blocks to the network
+type Provider struct {
+ ctx context.Context
+ // the CIDs for which provide announcements should be made
+ queue *q.Queue
+ // used to announce providing to the network
+ contentRouting routing.ContentRouting
+ // how long to wait for announce to complete before giving up
+ timeout time.Duration
+ // how many workers concurrently work through thhe queue
+ workerLimit int
+}
+
+// Option defines the functional option type that can be used to configure
+// provider instances
+type Option func(*Provider)
+
+// WithTimeout is an option to set a timeout on a provider
+func WithTimeout(timeout time.Duration) Option {
+ return func(p *Provider) {
+ p.timeout = timeout
+ }
+}
+
+// MaxWorkers is an option to set the max workers on a provider
+func MaxWorkers(count int) Option {
+ return func(p *Provider) {
+ p.workerLimit = count
+ }
+}
+
+// NewProvider creates a provider that announces blocks to the network using a content router
+func NewProvider(ctx context.Context, queue *q.Queue, contentRouting routing.ContentRouting, options ...Option) *Provider {
+ p := &Provider{
+ ctx: ctx,
+ queue: queue,
+ contentRouting: contentRouting,
+ workerLimit: 8,
+ }
+
+ for _, option := range options {
+ option(p)
+ }
+
+ return p
+}
+
+// Close stops the provider
+func (p *Provider) Close() error {
+ return p.queue.Close()
+}
+
+// Run workers to handle provide requests.
+func (p *Provider) Run() {
+ p.handleAnnouncements()
+}
+
+// Provide the given cid using specified strategy.
+func (p *Provider) Provide(root cid.Cid) error {
+ return p.queue.Enqueue(root)
+}
+
+// Handle all outgoing cids by providing (announcing) them
+func (p *Provider) handleAnnouncements() {
+ for workers := 0; workers < p.workerLimit; workers++ {
+ go func() {
+ for p.ctx.Err() == nil {
+ select {
+ case <-p.ctx.Done():
+ return
+ case c, ok := <-p.queue.Dequeue():
+ if !ok {
+ // queue closed.
+ return
+ }
+
+ p.doProvide(c)
+ }
+ }
+ }()
+ }
+}
+
+func (p *Provider) doProvide(c cid.Cid) {
+ ctx := p.ctx
+ if p.timeout > 0 {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, p.timeout)
+ defer cancel()
+ } else {
+ ctx = p.ctx
+ }
+
+ logP.Info("announce - start - ", c)
+ if err := p.contentRouting.Provide(ctx, c, true); err != nil {
+ logP.Warnf("Unable to provide entry: %s, %s", c, err)
+ }
+ logP.Info("announce - end - ", c)
+}
diff --git a/provider/simple/provider_test.go b/provider/simple/provider_test.go
new file mode 100644
index 0000000000..96f009937b
--- /dev/null
+++ b/provider/simple/provider_test.go
@@ -0,0 +1,162 @@
+package simple_test
+
+import (
+ "context"
+ "math/rand"
+ "testing"
+ "time"
+
+ "github.com/ipfs/go-cid"
+ "github.com/ipfs/go-datastore"
+ "github.com/ipfs/go-datastore/sync"
+ blocksutil "github.com/ipfs/go-ipfs-blocksutil"
+ "github.com/libp2p/go-libp2p/core/peer"
+
+ q "github.com/ipfs/boxo/provider/queue"
+
+ . "github.com/ipfs/boxo/provider/simple"
+)
+
+var blockGenerator = blocksutil.NewBlockGenerator()
+
+type mockRouting struct {
+ provided chan cid.Cid
+}
+
+func (r *mockRouting) Provide(ctx context.Context, cid cid.Cid, recursive bool) error {
+ select {
+ case r.provided <- cid:
+ case <-ctx.Done():
+ panic("context cancelled, but shouldn't have")
+ }
+ return nil
+}
+
+func (r *mockRouting) FindProvidersAsync(ctx context.Context, cid cid.Cid, timeout int) <-chan peer.AddrInfo {
+ return nil
+}
+
+func mockContentRouting() *mockRouting {
+ r := mockRouting{}
+ r.provided = make(chan cid.Cid)
+ return &r
+}
+
+func TestAnnouncement(t *testing.T) {
+ ctx := context.Background()
+ defer ctx.Done()
+
+ ds := sync.MutexWrap(datastore.NewMapDatastore())
+ queue, err := q.NewQueue(ctx, "test", ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ r := mockContentRouting()
+
+ prov := NewProvider(ctx, queue, r)
+ prov.Run()
+
+ cids := cid.NewSet()
+
+ for i := 0; i < 100; i++ {
+ c := blockGenerator.Next().Cid()
+ cids.Add(c)
+ }
+
+ go func() {
+ for _, c := range cids.Keys() {
+ err = prov.Provide(c)
+ // A little goroutine stirring to exercise some different states
+ r := rand.Intn(10)
+ time.Sleep(time.Microsecond * time.Duration(r))
+ }
+ }()
+
+ for cids.Len() > 0 {
+ select {
+ case cp := <-r.provided:
+ if !cids.Has(cp) {
+ t.Fatal("Wrong CID provided")
+ }
+ cids.Remove(cp)
+ case <-time.After(time.Second * 5):
+ t.Fatal("Timeout waiting for cids to be provided.")
+ }
+ }
+ prov.Close()
+
+ select {
+ case cp := <-r.provided:
+ t.Fatal("did not expect to provide CID: ", cp)
+ case <-time.After(time.Second * 1):
+ }
+}
+
+func TestClose(t *testing.T) {
+ ctx := context.Background()
+ defer ctx.Done()
+
+ ds := sync.MutexWrap(datastore.NewMapDatastore())
+ queue, err := q.NewQueue(ctx, "test", ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ r := mockContentRouting()
+
+ prov := NewProvider(ctx, queue, r)
+ prov.Run()
+
+ prov.Close()
+
+ select {
+ case cp := <-r.provided:
+ t.Fatal("did not expect to provide anything, provided: ", cp)
+ case <-time.After(time.Second * 1):
+ }
+}
+
+func TestAnnouncementTimeout(t *testing.T) {
+ ctx := context.Background()
+ defer ctx.Done()
+
+ ds := sync.MutexWrap(datastore.NewMapDatastore())
+ queue, err := q.NewQueue(ctx, "test", ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ r := mockContentRouting()
+
+ prov := NewProvider(ctx, queue, r, WithTimeout(1*time.Second))
+ prov.Run()
+
+ cids := cid.NewSet()
+
+ for i := 0; i < 100; i++ {
+ c := blockGenerator.Next().Cid()
+ cids.Add(c)
+ }
+
+ go func() {
+ for _, c := range cids.Keys() {
+ err = prov.Provide(c)
+ // A little goroutine stirring to exercise some different states
+ r := rand.Intn(10)
+ time.Sleep(time.Microsecond * time.Duration(r))
+ }
+ }()
+
+ for cids.Len() > 0 {
+ select {
+ case cp := <-r.provided:
+ if !cids.Has(cp) {
+ t.Fatal("Wrong CID provided")
+ }
+ cids.Remove(cp)
+ case <-time.After(time.Second * 5):
+ t.Fatal("Timeout waiting for cids to be provided.")
+ }
+ }
+}
diff --git a/provider/simple/reprovide.go b/provider/simple/reprovide.go
new file mode 100644
index 0000000000..a29b484fc4
--- /dev/null
+++ b/provider/simple/reprovide.go
@@ -0,0 +1,255 @@
+package simple
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/cenkalti/backoff"
+ blocks "github.com/ipfs/boxo/blockstore"
+ "github.com/ipfs/boxo/fetcher"
+ fetcherhelpers "github.com/ipfs/boxo/fetcher/helpers"
+ "github.com/ipfs/boxo/verifcid"
+ "github.com/ipfs/go-cid"
+ "github.com/ipfs/go-cidutil"
+ logging "github.com/ipfs/go-log"
+ cidlink "github.com/ipld/go-ipld-prime/linking/cid"
+ "github.com/libp2p/go-libp2p/core/routing"
+)
+
+var logR = logging.Logger("reprovider.simple")
+
+// ErrClosed is returned by Trigger when operating on a closed reprovider.
+var ErrClosed = errors.New("reprovider service stopped")
+
+// KeyChanFunc is function streaming CIDs to pass to content routing
+type KeyChanFunc func(context.Context) (<-chan cid.Cid, error)
+
+// Reprovider reannounces blocks to the network
+type Reprovider struct {
+ // Reprovider context. Cancel to stop, then wait on closedCh.
+ ctx context.Context
+ cancel context.CancelFunc
+ closedCh chan struct{}
+
+ // Trigger triggers a reprovide.
+ trigger chan chan<- error
+
+ // The routing system to provide values through
+ rsys routing.ContentRouting
+
+ keyProvider KeyChanFunc
+
+ tick time.Duration
+}
+
+// NewReprovider creates new Reprovider instance.
+func NewReprovider(ctx context.Context, reprovideInterval time.Duration, rsys routing.ContentRouting, keyProvider KeyChanFunc) *Reprovider {
+ ctx, cancel := context.WithCancel(ctx)
+ return &Reprovider{
+ ctx: ctx,
+ cancel: cancel,
+ closedCh: make(chan struct{}),
+ trigger: make(chan chan<- error),
+
+ rsys: rsys,
+ keyProvider: keyProvider,
+ tick: reprovideInterval,
+ }
+}
+
+// Close the reprovider
+func (rp *Reprovider) Close() error {
+ rp.cancel()
+ <-rp.closedCh
+ return nil
+}
+
+// Run re-provides keys with 'tick' interval or when triggered
+func (rp *Reprovider) Run() {
+ defer close(rp.closedCh)
+
+ var initialReprovideCh, reprovideCh <-chan time.Time
+
+ // If reproviding is enabled (non-zero)
+ if rp.tick > 0 {
+ reprovideTicker := time.NewTicker(rp.tick)
+ defer reprovideTicker.Stop()
+ reprovideCh = reprovideTicker.C
+
+ // If the reprovide ticker is larger than a minute (likely),
+ // provide once after we've been up a minute.
+ //
+ // Don't provide _immediately_ as we might be just about to stop.
+ if rp.tick > time.Minute {
+ initialReprovideTimer := time.NewTimer(time.Minute)
+ defer initialReprovideTimer.Stop()
+
+ initialReprovideCh = initialReprovideTimer.C
+ }
+ }
+
+ var done chan<- error
+ for rp.ctx.Err() == nil {
+ select {
+ case <-initialReprovideCh:
+ case <-reprovideCh:
+ case done = <-rp.trigger:
+ case <-rp.ctx.Done():
+ return
+ }
+
+ err := rp.Reprovide()
+
+ // only log if we've hit an actual error, otherwise just tell the client we're shutting down
+ if rp.ctx.Err() != nil {
+ err = ErrClosed
+ } else if err != nil {
+ logR.Errorf("failed to reprovide: %s", err)
+ }
+
+ if done != nil {
+ if err != nil {
+ done <- err
+ }
+ close(done)
+ }
+ }
+}
+
+// Reprovide registers all keys given by rp.keyProvider to libp2p content routing
+func (rp *Reprovider) Reprovide() error {
+ keychan, err := rp.keyProvider(rp.ctx)
+ if err != nil {
+ return fmt.Errorf("failed to get key chan: %s", err)
+ }
+ for c := range keychan {
+ // hash security
+ if err := verifcid.ValidateCid(c); err != nil {
+ logR.Errorf("insecure hash in reprovider, %s (%s)", c, err)
+ continue
+ }
+ op := func() error {
+ err := rp.rsys.Provide(rp.ctx, c, true)
+ if err != nil {
+ logR.Debugf("Failed to provide key: %s", err)
+ }
+ return err
+ }
+
+ err := backoff.Retry(op, backoff.WithContext(backoff.NewExponentialBackOff(), rp.ctx))
+ if err != nil {
+ logR.Debugf("Providing failed after number of retries: %s", err)
+ return err
+ }
+ }
+ return nil
+}
+
+// Trigger starts the reprovision process in rp.Run and waits for it to finish.
+//
+// Returns an error if a reprovide is already in progress.
+func (rp *Reprovider) Trigger(ctx context.Context) error {
+ resultCh := make(chan error, 1)
+ select {
+ case rp.trigger <- resultCh:
+ default:
+ return fmt.Errorf("reprovider is already running")
+ }
+
+ select {
+ case err := <-resultCh:
+ return err
+ case <-rp.ctx.Done():
+ return ErrClosed
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+}
+
+// Strategies
+
+// NewBlockstoreProvider returns key provider using bstore.AllKeysChan
+func NewBlockstoreProvider(bstore blocks.Blockstore) KeyChanFunc {
+ return func(ctx context.Context) (<-chan cid.Cid, error) {
+ return bstore.AllKeysChan(ctx)
+ }
+}
+
+// Pinner interface defines how the simple.Reprovider wants to interact
+// with a Pinning service
+type Pinner interface {
+ DirectKeys(ctx context.Context) ([]cid.Cid, error)
+ RecursiveKeys(ctx context.Context) ([]cid.Cid, error)
+}
+
+// NewPinnedProvider returns provider supplying pinned keys
+func NewPinnedProvider(onlyRoots bool, pinning Pinner, fetchConfig fetcher.Factory) KeyChanFunc {
+ return func(ctx context.Context) (<-chan cid.Cid, error) {
+ set, err := pinSet(ctx, pinning, fetchConfig, onlyRoots)
+ if err != nil {
+ return nil, err
+ }
+
+ outCh := make(chan cid.Cid)
+ go func() {
+ defer close(outCh)
+ for c := range set.New {
+ select {
+ case <-ctx.Done():
+ return
+ case outCh <- c:
+ }
+ }
+
+ }()
+
+ return outCh, nil
+ }
+}
+
+func pinSet(ctx context.Context, pinning Pinner, fetchConfig fetcher.Factory, onlyRoots bool) (*cidutil.StreamingSet, error) {
+ set := cidutil.NewStreamingSet()
+
+ go func() {
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ defer close(set.New)
+
+ dkeys, err := pinning.DirectKeys(ctx)
+ if err != nil {
+ logR.Errorf("reprovide direct pins: %s", err)
+ return
+ }
+ for _, key := range dkeys {
+ set.Visitor(ctx)(key)
+ }
+
+ rkeys, err := pinning.RecursiveKeys(ctx)
+ if err != nil {
+ logR.Errorf("reprovide indirect pins: %s", err)
+ return
+ }
+
+ session := fetchConfig.NewSession(ctx)
+ for _, key := range rkeys {
+ set.Visitor(ctx)(key)
+ if !onlyRoots {
+ err := fetcherhelpers.BlockAll(ctx, session, cidlink.Link{Cid: key}, func(res fetcher.FetchResult) error {
+ clink, ok := res.LastBlockLink.(cidlink.Link)
+ if ok {
+ set.Visitor(ctx)(clink.Cid)
+ }
+ return nil
+ })
+ if err != nil {
+ logR.Errorf("reprovide indirect pins: %s", err)
+ return
+ }
+ }
+ }
+ }()
+
+ return set, nil
+}
diff --git a/provider/simple/reprovide_test.go b/provider/simple/reprovide_test.go
new file mode 100644
index 0000000000..99efe794f1
--- /dev/null
+++ b/provider/simple/reprovide_test.go
@@ -0,0 +1,284 @@
+package simple_test
+
+import (
+ "bytes"
+ "context"
+ "testing"
+ "time"
+
+ "github.com/ipfs/go-cid"
+ ds "github.com/ipfs/go-datastore"
+ dssync "github.com/ipfs/go-datastore/sync"
+ blocks "github.com/ipfs/boxo/blocks"
+ bsrv "github.com/ipfs/boxo/blockservice"
+ blockstore "github.com/ipfs/boxo/blockstore"
+ offline "github.com/ipfs/boxo/exchange/offline"
+ bsfetcher "github.com/ipfs/boxo/fetcher/impl/blockservice"
+ mock "github.com/ipfs/boxo/routing/mock"
+ "github.com/ipld/go-ipld-prime"
+ "github.com/ipld/go-ipld-prime/codec/dagcbor"
+ "github.com/ipld/go-ipld-prime/fluent/qp"
+ cidlink "github.com/ipld/go-ipld-prime/linking/cid"
+ basicnode "github.com/ipld/go-ipld-prime/node/basic"
+ testutil "github.com/libp2p/go-libp2p-testing/net"
+ "github.com/libp2p/go-libp2p/core/peer"
+ mh "github.com/multiformats/go-multihash"
+
+ . "github.com/ipfs/boxo/provider/simple"
+)
+
+func setupRouting(t *testing.T) (clA, clB mock.Client, idA, idB peer.ID) {
+ mrserv := mock.NewServer()
+
+ iidA := testutil.RandIdentityOrFatal(t)
+ iidB := testutil.RandIdentityOrFatal(t)
+
+ clA = mrserv.Client(iidA)
+ clB = mrserv.Client(iidB)
+
+ return clA, clB, iidA.ID(), iidB.ID()
+}
+
+func setupDag(t *testing.T) (nodes []cid.Cid, bstore blockstore.Blockstore) {
+ bstore = blockstore.NewBlockstore(dssync.MutexWrap(ds.NewMapDatastore()))
+ for _, data := range []string{"foo", "bar"} {
+ nb := basicnode.Prototype.Any.NewBuilder()
+ err := nb.AssignString(data)
+ if err != nil {
+ t.Fatal(err)
+ }
+ blk := toBlock(t, nb.Build())
+ err = bstore.Put(context.Background(), blk)
+ if err != nil {
+ t.Fatal(err)
+ }
+ nodes = append(nodes, blk.Cid())
+ nd, err := qp.BuildMap(basicnode.Prototype.Map, 1, func(ma ipld.MapAssembler) {
+ qp.MapEntry(ma, "child", qp.Link(cidlink.Link{Cid: blk.Cid()}))
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ blk = toBlock(t, nd)
+ err = bstore.Put(context.Background(), blk)
+ if err != nil {
+ t.Fatal(err)
+ }
+ nodes = append(nodes, blk.Cid())
+ }
+
+ return nodes, bstore
+}
+
+func toBlock(t *testing.T, nd ipld.Node) blocks.Block {
+ buf := new(bytes.Buffer)
+ err := dagcbor.Encode(nd, buf)
+ if err != nil {
+ t.Fatal(err)
+ }
+ c, err := cid.Prefix{
+ Version: 1,
+ Codec: cid.DagCBOR,
+ MhType: mh.SHA2_256,
+ MhLength: -1,
+ }.Sum(buf.Bytes())
+ if err != nil {
+ t.Fatal(err)
+ }
+ blk, err := blocks.NewBlockWithCid(buf.Bytes(), c)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return blk
+}
+
+func TestReprovide(t *testing.T) {
+ testReprovide(t, func(r *Reprovider, ctx context.Context) error {
+ return r.Reprovide()
+ })
+}
+
+func TestTrigger(t *testing.T) {
+ testReprovide(t, func(r *Reprovider, ctx context.Context) error {
+ go r.Run()
+ time.Sleep(1 * time.Second)
+ defer r.Close()
+ err := r.Trigger(ctx)
+ return err
+ })
+}
+
+func testReprovide(t *testing.T, trigger func(r *Reprovider, ctx context.Context) error) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ clA, clB, idA, _ := setupRouting(t)
+ nodes, bstore := setupDag(t)
+
+ keyProvider := NewBlockstoreProvider(bstore)
+ reprov := NewReprovider(ctx, time.Hour, clA, keyProvider)
+ reprov.Trigger(context.Background())
+ err := trigger(reprov, ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var providers []peer.AddrInfo
+ maxProvs := 100
+
+ for _, c := range nodes {
+ // We provide raw cids because of the multihash keying
+ // FIXME(@Jorropo): I think this change should be done in the DHT layer, probably an issue with our routing mock.
+ b := c.Bytes()
+ b[1] = 0x55 // rewrite the cid to raw
+ _, c, err := cid.CidFromBytes(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ provChan := clB.FindProvidersAsync(ctx, c, maxProvs)
+ for p := range provChan {
+ providers = append(providers, p)
+ }
+
+ if len(providers) == 0 {
+ t.Fatal("Should have gotten a provider")
+ }
+
+ if providers[0].ID != idA {
+ t.Fatal("Somehow got the wrong peer back as a provider.")
+ }
+ }
+}
+
+func TestTriggerTwice(t *testing.T) {
+ // Ensure we can only trigger once at a time.
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ clA, _, _, _ := setupRouting(t)
+
+ keyCh := make(chan cid.Cid)
+ startCh := make(chan struct{})
+ keyFunc := func(ctx context.Context) (<-chan cid.Cid, error) {
+ <-startCh
+ return keyCh, nil
+ }
+
+ reprov := NewReprovider(ctx, time.Hour, clA, keyFunc)
+ go reprov.Run()
+ defer reprov.Close()
+
+ // Wait for the reprovider to start, otherwise, the reprovider will
+ // think a concurrent reprovide is running.
+ //
+ // We _could_ fix this race... but that would be complexity for nothing.
+ // 1. We start a reprovide 1 minute after startup anyways.
+ // 2. The window is really narrow.
+ time.Sleep(1 * time.Second)
+
+ errCh := make(chan error, 2)
+
+ // Trigger in the background
+ go func() {
+ errCh <- reprov.Trigger(ctx)
+ }()
+
+ // Wait for the trigger to really start.
+ startCh <- struct{}{}
+
+ start := time.Now()
+ // Try to trigger again, this should fail immediately.
+ if err := reprov.Trigger(ctx); err == nil {
+ t.Fatal("expected an error")
+ }
+ if time.Since(start) > 10*time.Millisecond {
+ t.Fatal("expected reprovide to fail instantly")
+ }
+
+ // Let the trigger progress.
+ close(keyCh)
+
+ // Check the result.
+ err := <-errCh
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Try to trigger again, this should work.
+ go func() {
+ errCh <- reprov.Trigger(ctx)
+ }()
+ startCh <- struct{}{}
+ err = <-errCh
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+type mockPinner struct {
+ recursive []cid.Cid
+ direct []cid.Cid
+}
+
+func (mp *mockPinner) DirectKeys(ctx context.Context) ([]cid.Cid, error) {
+ return mp.direct, nil
+}
+
+func (mp *mockPinner) RecursiveKeys(ctx context.Context) ([]cid.Cid, error) {
+ return mp.recursive, nil
+}
+
+func TestReprovidePinned(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ nodes, bstore := setupDag(t)
+
+ fetchConfig := bsfetcher.NewFetcherConfig(bsrv.New(bstore, offline.Exchange(bstore)))
+
+ for i := 0; i < 2; i++ {
+ clA, clB, idA, _ := setupRouting(t)
+
+ onlyRoots := i == 0
+ t.Logf("only roots: %v", onlyRoots)
+
+ var provide, dont []cid.Cid
+ if onlyRoots {
+ provide = []cid.Cid{nodes[1], nodes[3]}
+ dont = []cid.Cid{nodes[0], nodes[2]}
+ } else {
+ provide = []cid.Cid{nodes[0], nodes[1], nodes[3]}
+ dont = []cid.Cid{nodes[2]}
+ }
+
+ keyProvider := NewPinnedProvider(onlyRoots, &mockPinner{
+ recursive: []cid.Cid{nodes[1]},
+ direct: []cid.Cid{nodes[3]},
+ }, fetchConfig)
+
+ reprov := NewReprovider(ctx, time.Hour, clA, keyProvider)
+ err := reprov.Reprovide()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for i, c := range provide {
+ prov, ok := <-clB.FindProvidersAsync(ctx, c, 1)
+ if !ok {
+ t.Errorf("Should have gotten a provider for %d", i)
+ continue
+ }
+
+ if prov.ID != idA {
+ t.Errorf("Somehow got the wrong peer back as a provider.")
+ continue
+ }
+ }
+ for i, c := range dont {
+ prov, ok := <-clB.FindProvidersAsync(ctx, c, 1)
+ if ok {
+ t.Fatalf("found provider %s for %d, expected none", prov.ID, i)
+ }
+ }
+ }
+}
diff --git a/provider/system.go b/provider/system.go
new file mode 100644
index 0000000000..9fc3e8879c
--- /dev/null
+++ b/provider/system.go
@@ -0,0 +1,60 @@
+package provider
+
+import (
+ "context"
+
+ "github.com/ipfs/go-cid"
+)
+
+// System defines the interface for interacting with the value
+// provider system
+type System interface {
+ Run()
+ Close() error
+ Provide(cid.Cid) error
+ Reprovide(context.Context) error
+}
+
+type system struct {
+ provider Provider
+ reprovider Reprovider
+}
+
+// NewSystem constructs a new provider system from a provider and reprovider
+func NewSystem(provider Provider, reprovider Reprovider) System {
+ return &system{provider, reprovider}
+}
+
+// Run the provider system by running the provider and reprovider
+func (s *system) Run() {
+ go s.provider.Run()
+ go s.reprovider.Run()
+}
+
+// Close the provider and reprovider
+func (s *system) Close() error {
+ var errs []error
+
+ if err := s.provider.Close(); err != nil {
+ errs = append(errs, err)
+ }
+
+ if err := s.reprovider.Close(); err != nil {
+ errs = append(errs, err)
+ }
+
+ if len(errs) > 0 {
+ return errs[0]
+ }
+ return nil
+}
+
+// Provide a value
+func (s *system) Provide(cid cid.Cid) error {
+ return s.provider.Provide(cid)
+}
+
+// Reprovide all the previously provided values
+func (s *system) Reprovide(ctx context.Context) error {
+ return s.reprovider.Trigger(ctx)
+}
diff --git a/routing/http/README.md b/routing/http/README.md
new file mode 100644
index 0000000000..65650ed509
--- /dev/null
+++ b/routing/http/README.md
@@ -0,0 +1,24 @@
+go-delegated-routing
+=======================
+
+> Delegated routing Client and Server over Reframe RPC
+
+This package provides delegated routing implementation in Go:
+- Client (for IPFS nodes like [Kubo](https://github.com/ipfs/kubo/blob/master/docs/config.md#routingrouters-parameters)),
+- Server (for public indexers such as https://cid.contact)
+
+## Documentation
+
+- Go docs: https://pkg.go.dev/github.com/ipfs/boxo/routing/http/
+
+## Lead Maintainer
+
+🦗🎶
+
+## Contributing
+
+Contributions are welcome! This repository is part of the IPFS project and therefore governed by our [contributing guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md).
+
+## License
+
+[SPDX-License-Identifier: Apache-2.0 OR MIT](LICENSE.md)
\ No newline at end of file
diff --git a/routing/http/client/client.go b/routing/http/client/client.go
new file mode 100644
index 0000000000..4e5cfb8f47
--- /dev/null
+++ b/routing/http/client/client.go
@@ -0,0 +1,247 @@
+package client
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/benbjohnson/clock"
+ ipns "github.com/ipfs/boxo/ipns"
+ "github.com/ipfs/boxo/routing/http/contentrouter"
+ "github.com/ipfs/boxo/routing/http/internal/drjson"
+ "github.com/ipfs/boxo/routing/http/server"
+ "github.com/ipfs/boxo/routing/http/types"
+ "github.com/ipfs/go-cid"
+ logging "github.com/ipfs/go-log/v2"
+ record "github.com/libp2p/go-libp2p-record"
+ "github.com/libp2p/go-libp2p/core/crypto"
+ "github.com/libp2p/go-libp2p/core/peer"
+ "github.com/multiformats/go-multiaddr"
+)
+
+var logger = logging.Logger("service/delegatedrouting")
+
+type client struct {
+ baseURL string
+ httpClient httpClient
+ validator record.Validator
+ clock clock.Clock
+
+ peerID peer.ID
+ addrs []types.Multiaddr
+ identity crypto.PrivKey
+
+ // called immeidately after signing a provide req
+ // used for testing, e.g. testing the server with a mangled signature
+ afterSignCallback func(req *types.WriteBitswapProviderRecord)
+}
+
+// defaultUserAgent is used as a fallback to inform HTTP server which library
+// version sent a request
+var defaultUserAgent = moduleVersion()
+
+var _ contentrouter.Client = &client{}
+
+type httpClient interface {
+ Do(req *http.Request) (*http.Response, error)
+}
+
+type option func(*client)
+
+func WithIdentity(identity crypto.PrivKey) option {
+ return func(c *client) {
+ c.identity = identity
+ }
+}
+
+func WithHTTPClient(h httpClient) option {
+ return func(c *client) {
+ c.httpClient = h
+ }
+}
+
+func WithUserAgent(ua string) option {
+ return func(c *client) {
+ if ua == "" {
+ return
+ }
+ httpClient, ok := c.httpClient.(*http.Client)
+ if !ok {
+ return
+ }
+ transport, ok := httpClient.Transport.(*ResponseBodyLimitedTransport)
+ if !ok {
+ return
+ }
+ transport.UserAgent = ua
+ }
+}
+
+func WithProviderInfo(peerID peer.ID, addrs []multiaddr.Multiaddr) option {
+ return func(c *client) {
+ c.peerID = peerID
+ for _, a := range addrs {
+ c.addrs = append(c.addrs, types.Multiaddr{Multiaddr: a})
+ }
+ }
+}
+
+// New creates a content routing API client.
+// The Provider and identity parameters are option. If they are nil, the `Provide` method will not function.
+func New(baseURL string, opts ...option) (*client, error) {
+ defaultHTTPClient := &http.Client{
+ Transport: &ResponseBodyLimitedTransport{
+ RoundTripper: http.DefaultTransport,
+ LimitBytes: 1 << 20,
+ UserAgent: defaultUserAgent,
+ },
+ }
+ client := &client{
+ baseURL: baseURL,
+ httpClient: defaultHTTPClient,
+ validator: ipns.Validator{},
+ clock: clock.New(),
+ }
+
+ for _, opt := range opts {
+ opt(client)
+ }
+
+ if client.identity != nil && client.peerID.Size() != 0 && !client.peerID.MatchesPublicKey(client.identity.GetPublic()) {
+ return nil, errors.New("identity does not match provider")
+ }
+
+ return client, nil
+}
+
+func (c *client) FindProviders(ctx context.Context, key cid.Cid) (provs []types.ProviderResponse, err error) {
+ measurement := newMeasurement("FindProviders")
+ defer func() {
+ measurement.length = len(provs)
+ measurement.record(ctx)
+ }()
+
+ url := c.baseURL + server.ProvidePath + key.String()
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
+ if err != nil {
+ return nil, err
+ }
+ measurement.host = req.Host
+
+ start := c.clock.Now()
+ resp, err := c.httpClient.Do(req)
+
+ measurement.err = err
+ measurement.latency = c.clock.Since(start)
+
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ measurement.statusCode = resp.StatusCode
+ if resp.StatusCode == http.StatusNotFound {
+ return nil, nil
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, httpError(resp.StatusCode, resp.Body)
+ }
+
+ parsedResp := &types.ReadProvidersResponse{}
+ err = json.NewDecoder(resp.Body).Decode(parsedResp)
+ return parsedResp.Providers, err
+}
+
+func (c *client) ProvideBitswap(ctx context.Context, keys []cid.Cid, ttl time.Duration) (time.Duration, error) {
+ if c.identity == nil {
+ return 0, errors.New("cannot provide Bitswap records without an identity")
+ }
+ if c.peerID.Size() == 0 {
+ return 0, errors.New("cannot provide Bitswap records without a peer ID")
+ }
+
+ ks := make([]types.CID, len(keys))
+ for i, c := range keys {
+ ks[i] = types.CID{Cid: c}
+ }
+
+ now := c.clock.Now()
+
+ req := types.WriteBitswapProviderRecord{
+ Protocol: "transport-bitswap",
+ Schema: types.SchemaBitswap,
+ Payload: types.BitswapPayload{
+ Keys: ks,
+ AdvisoryTTL: &types.Duration{Duration: ttl},
+ Timestamp: &types.Time{Time: now},
+ ID: &c.peerID,
+ Addrs: c.addrs,
+ },
+ }
+ err := req.Sign(c.peerID, c.identity)
+ if err != nil {
+ return 0, err
+ }
+
+ if c.afterSignCallback != nil {
+ c.afterSignCallback(&req)
+ }
+
+ advisoryTTL, err := c.provideSignedBitswapRecord(ctx, &req)
+ if err != nil {
+ return 0, err
+ }
+
+ return advisoryTTL, err
+}
+
+// ProvideAsync makes a provide request to a delegated router
+func (c *client) provideSignedBitswapRecord(ctx context.Context, bswp *types.WriteBitswapProviderRecord) (time.Duration, error) {
+ req := types.WriteProvidersRequest{Providers: []types.WriteProviderRecord{bswp}}
+
+ url := c.baseURL + server.ProvidePath
+
+ b, err := drjson.MarshalJSONBytes(req)
+ if err != nil {
+ return 0, err
+ }
+
+ httpReq, err := http.NewRequestWithContext(ctx, http.MethodPut, url, bytes.NewBuffer(b))
+ if err != nil {
+ return 0, err
+ }
+
+ resp, err := c.httpClient.Do(httpReq)
+ if err != nil {
+ return 0, fmt.Errorf("making HTTP req to provide a signed record: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return 0, httpError(resp.StatusCode, resp.Body)
+ }
+ var provideResult types.WriteProvidersResponse
+ err = json.NewDecoder(resp.Body).Decode(&provideResult)
+ if err != nil {
+ return 0, err
+ }
+ if len(provideResult.ProvideResults) != 1 {
+ return 0, fmt.Errorf("expected 1 result but got %d", len(provideResult.ProvideResults))
+ }
+
+ v, ok := provideResult.ProvideResults[0].(*types.WriteBitswapProviderRecordResponse)
+ if !ok {
+ return 0, fmt.Errorf("expected AdvisoryTTL field")
+ }
+
+ if v.AdvisoryTTL != nil {
+ return v.AdvisoryTTL.Duration, nil
+ }
+
+ return 0, nil
+}
diff --git a/routing/http/client/client_test.go b/routing/http/client/client_test.go
new file mode 100644
index 0000000000..d8fc4abac1
--- /dev/null
+++ b/routing/http/client/client_test.go
@@ -0,0 +1,342 @@
+package client
+
+import (
+ "context"
+ "crypto/rand"
+ "net/http"
+ "net/http/httptest"
+ "runtime"
+ "testing"
+ "time"
+
+ "github.com/benbjohnson/clock"
+ "github.com/ipfs/boxo/routing/http/server"
+ "github.com/ipfs/boxo/routing/http/types"
+ "github.com/ipfs/go-cid"
+ "github.com/libp2p/go-libp2p/core/crypto"
+ "github.com/libp2p/go-libp2p/core/peer"
+ "github.com/multiformats/go-multiaddr"
+ "github.com/multiformats/go-multibase"
+ "github.com/multiformats/go-multihash"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+)
+
+type mockContentRouter struct{ mock.Mock }
+
+func (m *mockContentRouter) FindProviders(ctx context.Context, key cid.Cid) ([]types.ProviderResponse, error) {
+ args := m.Called(ctx, key)
+ return args.Get(0).([]types.ProviderResponse), args.Error(1)
+}
+func (m *mockContentRouter) ProvideBitswap(ctx context.Context, req *server.BitswapWriteProvideRequest) (time.Duration, error) {
+ args := m.Called(ctx, req)
+ return args.Get(0).(time.Duration), args.Error(1)
+}
+
+func (m *mockContentRouter) Provide(ctx context.Context, req *server.WriteProvideRequest) (types.ProviderResponse, error) {
+ args := m.Called(ctx, req)
+ return args.Get(0).(types.ProviderResponse), args.Error(1)
+}
+
+type testDeps struct {
+ router *mockContentRouter
+ server *httptest.Server
+ peerID peer.ID
+ addrs []multiaddr.Multiaddr
+ client *client
+}
+
+func makeTestDeps(t *testing.T) testDeps {
+ const testUserAgent = "testUserAgent"
+ peerID, addrs, identity := makeProviderAndIdentity()
+ router := &mockContentRouter{}
+ server := httptest.NewServer(server.Handler(router))
+ t.Cleanup(server.Close)
+ serverAddr := "http://" + server.Listener.Addr().String()
+ c, err := New(serverAddr, WithProviderInfo(peerID, addrs), WithIdentity(identity), WithUserAgent(testUserAgent))
+ if err != nil {
+ panic(err)
+ }
+ assertUserAgentOverride(t, c, testUserAgent)
+ return testDeps{
+ router: router,
+ server: server,
+ peerID: peerID,
+ addrs: addrs,
+ client: c,
+ }
+}
+
+func assertUserAgentOverride(t *testing.T, c *client, expected string) {
+ httpClient, ok := c.httpClient.(*http.Client)
+ if !ok {
+ t.Error("invalid c.httpClient")
+ }
+ transport, ok := httpClient.Transport.(*ResponseBodyLimitedTransport)
+ if !ok {
+ t.Error("invalid httpClient.Transport")
+ }
+ if transport.UserAgent != expected {
+ t.Error("invalid httpClient.Transport.UserAgent")
+ }
+}
+
+func makeCID() cid.Cid {
+ buf := make([]byte, 63)
+ _, err := rand.Read(buf)
+ if err != nil {
+ panic(err)
+ }
+ mh, err := multihash.Encode(buf, multihash.SHA2_256)
+ if err != nil {
+ panic(err)
+ }
+ c := cid.NewCidV1(0, mh)
+ return c
+}
+
+func addrsToDRAddrs(addrs []multiaddr.Multiaddr) (drmas []types.Multiaddr) {
+ for _, a := range addrs {
+ drmas = append(drmas, types.Multiaddr{Multiaddr: a})
+ }
+ return
+}
+
+func drAddrsToAddrs(drmas []types.Multiaddr) (addrs []multiaddr.Multiaddr) {
+ for _, a := range drmas {
+ addrs = append(addrs, a.Multiaddr)
+ }
+ return
+}
+
+func makeBSReadProviderResp() types.ReadBitswapProviderRecord {
+ peerID, addrs, _ := makeProviderAndIdentity()
+ return types.ReadBitswapProviderRecord{
+ Protocol: "transport-bitswap",
+ Schema: types.SchemaBitswap,
+ ID: &peerID,
+ Addrs: addrsToDRAddrs(addrs),
+ }
+}
+
+func makeProviderAndIdentity() (peer.ID, []multiaddr.Multiaddr, crypto.PrivKey) {
+ priv, _, err := crypto.GenerateEd25519Key(rand.Reader)
+ if err != nil {
+ panic(err)
+ }
+ peerID, err := peer.IDFromPrivateKey(priv)
+ if err != nil {
+ panic(err)
+ }
+ ma1, err := multiaddr.NewMultiaddr("/ip4/0.0.0.0/tcp/4001")
+ if err != nil {
+ panic(err)
+ }
+
+ ma2, err := multiaddr.NewMultiaddr("/ip4/0.0.0.0/tcp/4002")
+ if err != nil {
+ panic(err)
+ }
+
+ return peerID, []multiaddr.Multiaddr{ma1, ma2}, priv
+}
+
+func TestClient_FindProviders(t *testing.T) {
+ bsReadProvResp := makeBSReadProviderResp()
+ bitswapProvs := []types.ProviderResponse{&bsReadProvResp}
+
+ cases := []struct {
+ name string
+ httpStatusCode int
+ stopServer bool
+ routerProvs []types.ProviderResponse
+ routerErr error
+
+ expProvs []types.ProviderResponse
+ expErrContains []string
+ expWinErrContains []string
+ }{
+ {
+ name: "happy case",
+ routerProvs: bitswapProvs,
+ expProvs: bitswapProvs,
+ },
+ {
+ name: "returns an error if there's a non-200 response",
+ httpStatusCode: 500,
+ expErrContains: []string{"HTTP error with StatusCode=500: "},
+ },
+ {
+ name: "returns an error if the HTTP client returns a non-HTTP error",
+ stopServer: true,
+ expErrContains: []string{"connect: connection refused"},
+ expWinErrContains: []string{"connectex: No connection could be made because the target machine actively refused it."},
+ },
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ deps := makeTestDeps(t)
+ client := deps.client
+ router := deps.router
+
+ if c.httpStatusCode != 0 {
+ deps.server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(c.httpStatusCode)
+ })
+ }
+
+ if c.stopServer {
+ deps.server.Close()
+ }
+ cid := makeCID()
+
+ router.On("FindProviders", mock.Anything, cid).
+ Return(c.routerProvs, c.routerErr)
+
+ provs, err := client.FindProviders(context.Background(), cid)
+
+ var errList []string
+ if runtime.GOOS == "windows" && len(c.expWinErrContains) != 0 {
+ errList = c.expWinErrContains
+ } else {
+ errList = c.expErrContains
+ }
+
+ for _, exp := range errList {
+ require.ErrorContains(t, err, exp)
+ }
+ if len(errList) == 0 {
+ require.NoError(t, err)
+ }
+
+ assert.Equal(t, c.expProvs, provs)
+ })
+ }
+}
+
+func TestClient_Provide(t *testing.T) {
+ cases := []struct {
+ name string
+ manglePath bool
+ mangleSignature bool
+ stopServer bool
+ noProviderInfo bool
+ noIdentity bool
+
+ cids []cid.Cid
+ ttl time.Duration
+
+ routerAdvisoryTTL time.Duration
+ routerErr error
+
+ expErrContains string
+ expWinErrContains string
+
+ expAdvisoryTTL time.Duration
+ }{
+ {
+ name: "happy case",
+ cids: []cid.Cid{makeCID()},
+ ttl: 1 * time.Hour,
+ routerAdvisoryTTL: 1 * time.Minute,
+
+ expAdvisoryTTL: 1 * time.Minute,
+ },
+ {
+ name: "should return a 403 if the payload signature verification fails",
+ cids: []cid.Cid{},
+ mangleSignature: true,
+
+ expErrContains: "HTTP error with StatusCode=403",
+ },
+ {
+ name: "should return error if identity is not provided",
+ noIdentity: true,
+ expErrContains: "cannot provide Bitswap records without an identity",
+ },
+ {
+ name: "should return error if provider is not provided",
+ noProviderInfo: true,
+ expErrContains: "cannot provide Bitswap records without a peer ID",
+ },
+ {
+ name: "returns an error if there's a non-200 response",
+ manglePath: true,
+ expErrContains: "HTTP error with StatusCode=404: 404 page not found",
+ },
+ {
+ name: "returns an error if the HTTP client returns a non-HTTP error",
+ stopServer: true,
+ expErrContains: "connect: connection refused",
+ expWinErrContains: "connectex: No connection could be made because the target machine actively refused it.",
+ },
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ // deps := makeTestDeps(t)
+ deps := makeTestDeps(t)
+ client := deps.client
+ router := deps.router
+
+ if c.noIdentity {
+ client.identity = nil
+ }
+ if c.noProviderInfo {
+ client.peerID = ""
+ client.addrs = nil
+ }
+
+ clock := clock.NewMock()
+ clock.Set(time.Now())
+ client.clock = clock
+
+ ctx := context.Background()
+
+ if c.manglePath {
+ client.baseURL += "/foo"
+ }
+ if c.stopServer {
+ deps.server.Close()
+ }
+ if c.mangleSignature {
+ client.afterSignCallback = func(req *types.WriteBitswapProviderRecord) {
+ mh, err := multihash.Encode([]byte("boom"), multihash.SHA2_256)
+ require.NoError(t, err)
+ mb, err := multibase.Encode(multibase.Base64, mh)
+ require.NoError(t, err)
+
+ req.Signature = mb
+ }
+ }
+
+ expectedProvReq := &server.BitswapWriteProvideRequest{
+ Keys: c.cids,
+ Timestamp: clock.Now().Truncate(time.Millisecond),
+ AdvisoryTTL: c.ttl,
+ Addrs: drAddrsToAddrs(client.addrs),
+ ID: client.peerID,
+ }
+
+ router.On("ProvideBitswap", mock.Anything, expectedProvReq).
+ Return(c.routerAdvisoryTTL, c.routerErr)
+
+ advisoryTTL, err := client.ProvideBitswap(ctx, c.cids, c.ttl)
+
+ var errorString string
+ if runtime.GOOS == "windows" && c.expWinErrContains != "" {
+ errorString = c.expWinErrContains
+ } else {
+ errorString = c.expErrContains
+ }
+
+ if errorString != "" {
+ require.ErrorContains(t, err, errorString)
+ } else {
+ require.NoError(t, err)
+ }
+
+ assert.Equal(t, c.expAdvisoryTTL, advisoryTTL)
+ })
+ }
+}
diff --git a/routing/http/client/error.go b/routing/http/client/error.go
new file mode 100644
index 0000000000..b3b7904d51
--- /dev/null
+++ b/routing/http/client/error.go
@@ -0,0 +1,27 @@
+package client
+
+import (
+ "fmt"
+ "io"
+)
+
+type HTTPError struct {
+ StatusCode int
+ Body string
+}
+
+func (e *HTTPError) Error() string {
+ return fmt.Sprintf("HTTP error with StatusCode=%d: %s", e.StatusCode, e.Body)
+}
+
+func httpError(statusCode int, body io.Reader) error {
+ bodyBytes, err := io.ReadAll(io.LimitReader(body, 1024))
+ if err != nil {
+ logger.Warnw("could not read body bytes from error response", "Error", err)
+ bodyBytes = []byte("unable to read body")
+ }
+ return &HTTPError{
+ StatusCode: statusCode,
+ Body: string(bodyBytes),
+ }
+}
diff --git a/routing/http/client/measures.go b/routing/http/client/measures.go
new file mode 100644
index 0000000000..942460518e
--- /dev/null
+++ b/routing/http/client/measures.go
@@ -0,0 +1,119 @@
+package client
+
+import (
+ "context"
+ "errors"
+ "net"
+ "strconv"
+ "time"
+
+ "go.opencensus.io/stats"
+ "go.opencensus.io/stats/view"
+ "go.opencensus.io/tag"
+)
+
+var (
+ distMS = view.Distribution(0, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000)
+ distLength = view.Distribution(0, 1, 2, 5, 10, 11, 12, 15, 20, 50, 100, 200, 500)
+
+ measureLatency = stats.Int64("routing_http_client_latency", "the latency of operations by the routing HTTP client", stats.UnitMilliseconds)
+ measureLength = stats.Int64("routing_http_client_length", "the number of elements in a response collection", stats.UnitDimensionless)
+
+ keyOperation = tag.MustNewKey("operation")
+ keyHost = tag.MustNewKey("host")
+ keyStatusCode = tag.MustNewKey("code")
+ keyError = tag.MustNewKey("error")
+
+ ViewLatency = &view.View{
+ Measure: measureLatency,
+ Aggregation: distMS,
+ TagKeys: []tag.Key{keyOperation, keyHost, keyStatusCode, keyError},
+ }
+ ViewLength = &view.View{
+ Measure: measureLength,
+ Aggregation: distLength,
+ TagKeys: []tag.Key{keyOperation, keyHost},
+ }
+
+ OpenCensusViews = []*view.View{
+ ViewLatency,
+ ViewLength,
+ }
+)
+
+type measurement struct {
+ operation string
+ err error
+ latency time.Duration
+ statusCode int
+ host string
+ length int
+}
+
+func (m measurement) record(ctx context.Context) {
+ stats.RecordWithTags(
+ ctx,
+ []tag.Mutator{
+ tag.Upsert(keyHost, m.host),
+ tag.Upsert(keyOperation, m.operation),
+ tag.Upsert(keyStatusCode, strconv.Itoa(m.statusCode)),
+ tag.Upsert(keyError, metricsErrStr(m.err)),
+ },
+ measureLatency.M(m.latency.Milliseconds()),
+ )
+ if m.err == nil {
+ stats.RecordWithTags(
+ ctx,
+ []tag.Mutator{
+ tag.Upsert(keyHost, m.host),
+ tag.Upsert(keyOperation, m.operation),
+ },
+ measureLength.M(int64(m.length)),
+ )
+ }
+}
+
+func newMeasurement(operation string) measurement {
+ return measurement{
+ operation: operation,
+ host: "None",
+ }
+}
+
+// metricsErrStr converts an error into a string that can be used as a metric label.
+// Errs are mapped to strings explicitly to avoid accidental high dimensionality.
+func metricsErrStr(err error) string {
+ if err == nil {
+ return "None"
+ }
+ var httpErr *HTTPError
+ if errors.As(err, &httpErr) {
+ return "HTTP"
+ }
+ if errors.Is(err, context.DeadlineExceeded) {
+ return "DeadlineExceeded"
+ }
+ if errors.Is(err, context.Canceled) {
+ return "Canceled"
+ }
+ var dnsErr *net.DNSError
+ if errors.As(err, &dnsErr) {
+ if dnsErr.IsNotFound {
+ return "DNSNotFound"
+ }
+ if dnsErr.IsTimeout {
+ return "DNSTimeout"
+ }
+ return "DNS"
+ }
+
+ var netErr net.Error
+ if errors.As(err, &netErr) {
+ if netErr.Timeout() {
+ return "NetTimeout"
+ }
+ return "Net"
+ }
+
+ return "Other"
+}
diff --git a/routing/http/client/transport.go b/routing/http/client/transport.go
new file mode 100644
index 0000000000..357d25cb2e
--- /dev/null
+++ b/routing/http/client/transport.go
@@ -0,0 +1,79 @@
+package client
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "reflect"
+ "runtime/debug"
+ "strings"
+)
+
+type ResponseBodyLimitedTransport struct {
+ http.RoundTripper
+ LimitBytes int64
+ UserAgent string
+}
+
+func (r *ResponseBodyLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+ if r.UserAgent != "" {
+ req.Header.Set("User-Agent", r.UserAgent)
+ }
+ resp, err := r.RoundTripper.RoundTrip(req)
+ if resp != nil && resp.Body != nil {
+ resp.Body = &limitReadCloser{
+ limit: r.LimitBytes,
+ ReadCloser: resp.Body,
+ }
+ }
+ return resp, err
+}
+
+type limitReadCloser struct {
+ limit int64
+ bytesRead int64
+ io.ReadCloser
+}
+
+func (l *limitReadCloser) Read(p []byte) (int, error) {
+ n, err := l.ReadCloser.Read(p)
+ l.bytesRead += int64(n)
+ if l.bytesRead > l.limit {
+ return 0, fmt.Errorf("reached read limit of %d bytes after reading %d bytes", l.limit, l.bytesRead)
+ }
+ return n, err
+}
+
+// ImportPath is the canonical import path that allows us to identify
+// official client builds vs modified forks, and use that info in User-Agent header.
+var ImportPath = importPath()
+
+// importPath returns the path that library consumers would have in go.mod
+func importPath() string {
+ p := reflect.ValueOf(ResponseBodyLimitedTransport{}).Type().PkgPath()
+ // we have monorepo, so stripping the remainder
+ return strings.TrimSuffix(p, "/routing/http/client")
+}
+
+// moduleVersion returns a useful user agent version string allowing us to
+// identify requests coming from official releases of this module vs forks.
+func moduleVersion() (ua string) {
+ ua = ImportPath
+ var module *debug.Module
+ if bi, ok := debug.ReadBuildInfo(); ok {
+ // If debug.ReadBuildInfo was successful, we can read Version by finding
+ // this client in the dependency list of the app that has it in go.mod
+ for _, dep := range bi.Deps {
+ if dep.Path == ImportPath {
+ module = dep
+ break
+ }
+ }
+ if module != nil {
+ ua += "@" + module.Version
+ return
+ }
+ ua += "@unknown"
+ }
+ return
+}
diff --git a/routing/http/client/transport_test.go b/routing/http/client/transport_test.go
new file mode 100644
index 0000000000..7da545062c
--- /dev/null
+++ b/routing/http/client/transport_test.go
@@ -0,0 +1,84 @@
+package client
+
+import (
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type testServer struct {
+ bytesToWrite int
+}
+
+func (s *testServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ bytes := make([]byte, s.bytesToWrite)
+ for i := 0; i < s.bytesToWrite; i++ {
+ bytes[i] = 'a'
+ }
+ _, err := w.Write(bytes)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func TestResponseBodyLimitedTransport(t *testing.T) {
+ for _, c := range []struct {
+ name string
+ limit int64
+ serverSend int
+
+ expErr string
+ }{
+ {
+ name: "under the limit should succeed",
+ limit: 1 << 20,
+ serverSend: 1 << 19,
+ },
+ {
+ name: "over the limit should fail",
+ limit: 1 << 20,
+ serverSend: 1 << 21,
+ expErr: "reached read limit of 1048576 bytes after reading",
+ },
+ {
+ name: "exactly on the limit should succeed",
+ limit: 1 << 20,
+ serverSend: 1 << 20,
+ },
+ } {
+ t.Run(c.name, func(t *testing.T) {
+ server := httptest.NewServer(&testServer{bytesToWrite: c.serverSend})
+ t.Cleanup(server.Close)
+
+ client := server.Client()
+ client.Transport = &ResponseBodyLimitedTransport{
+ LimitBytes: c.limit,
+ RoundTripper: client.Transport,
+ }
+
+ resp, err := client.Get(server.URL)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ _, err = io.ReadAll(resp.Body)
+
+ if c.expErr == "" {
+ assert.NoError(t, err)
+ } else {
+ assert.Contains(t, err.Error(), c.expErr)
+ }
+
+ })
+ }
+}
+
+func TestUserAgentVersionString(t *testing.T) {
+ // forks will have to update below lines to pass test
+ assert.Equal(t, importPath(), "github.com/ipfs/boxo")
+ // @unknown because we run in tests
+ assert.Equal(t, moduleVersion(), "github.com/ipfs/boxo@unknown")
+}
diff --git a/routing/http/contentrouter/contentrouter.go b/routing/http/contentrouter/contentrouter.go
new file mode 100644
index 0000000000..572ac26559
--- /dev/null
+++ b/routing/http/contentrouter/contentrouter.go
@@ -0,0 +1,140 @@
+package contentrouter
+
+import (
+ "context"
+ "reflect"
+ "time"
+
+ "github.com/ipfs/boxo/routing/http/internal"
+ "github.com/ipfs/boxo/routing/http/types"
+ "github.com/ipfs/go-cid"
+ logging "github.com/ipfs/go-log/v2"
+ "github.com/libp2p/go-libp2p/core/peer"
+ "github.com/libp2p/go-libp2p/core/routing"
+ "github.com/multiformats/go-multiaddr"
+ "github.com/multiformats/go-multihash"
+)
+
+var logger = logging.Logger("service/contentrouting")
+
+const ttl = 24 * time.Hour
+
+type Client interface {
+ ProvideBitswap(ctx context.Context, keys []cid.Cid, ttl time.Duration) (time.Duration, error)
+ FindProviders(ctx context.Context, key cid.Cid) ([]types.ProviderResponse, error)
+}
+
+type contentRouter struct {
+ client Client
+ maxProvideConcurrency int
+ maxProvideBatchSize int
+}
+
+var _ routing.ContentRouting = (*contentRouter)(nil)
+
+type option func(c *contentRouter)
+
+func WithMaxProvideConcurrency(max int) option {
+ return func(c *contentRouter) {
+ c.maxProvideConcurrency = max
+ }
+}
+
+func WithMaxProvideBatchSize(max int) option {
+ return func(c *contentRouter) {
+ c.maxProvideBatchSize = max
+ }
+}
+
+func NewContentRoutingClient(c Client, opts ...option) *contentRouter {
+ cr := &contentRouter{
+ client: c,
+ maxProvideConcurrency: 5,
+ maxProvideBatchSize: 100,
+ }
+ for _, opt := range opts {
+ opt(cr)
+ }
+ return cr
+}
+
+func (c *contentRouter) Provide(ctx context.Context, key cid.Cid, announce bool) error {
+ // If 'true' is
+ // passed, it also announces it, otherwise it is just kept in the local
+ // accounting of which objects are being provided.
+ if !announce {
+ return nil
+ }
+
+ _, err := c.client.ProvideBitswap(ctx, []cid.Cid{key}, ttl)
+ return err
+}
+
+// ProvideMany provides a set of keys to the remote delegate.
+// Large sets of keys are chunked into multiple requests and sent concurrently, according to the concurrency configuration.
+// TODO: implement retries through transient errors
+func (c *contentRouter) ProvideMany(ctx context.Context, mhKeys []multihash.Multihash) error {
+ keys := make([]cid.Cid, 0, len(mhKeys))
+ for _, m := range mhKeys {
+ keys = append(keys, cid.NewCidV1(cid.Raw, m))
+ }
+
+ if len(keys) <= c.maxProvideBatchSize {
+ _, err := c.client.ProvideBitswap(ctx, keys, ttl)
+ return err
+ }
+
+ return internal.DoBatch(
+ ctx,
+ c.maxProvideBatchSize,
+ c.maxProvideConcurrency,
+ keys,
+ func(ctx context.Context, batch []cid.Cid) error {
+ _, err := c.client.ProvideBitswap(ctx, batch, ttl)
+ return err
+ },
+ )
+}
+
+// Ready is part of the existing `ProvideMany` interface.
+func (c *contentRouter) Ready() bool {
+ return true
+}
+
+func (c *contentRouter) FindProvidersAsync(ctx context.Context, key cid.Cid, numResults int) <-chan peer.AddrInfo {
+ results, err := c.client.FindProviders(ctx, key)
+ if err != nil {
+ logger.Warnw("error finding providers", "CID", key, "Error", err)
+ ch := make(chan peer.AddrInfo)
+ close(ch)
+ return ch
+ }
+
+ ch := make(chan peer.AddrInfo, len(results))
+ for _, r := range results {
+ if r.GetSchema() == types.SchemaBitswap {
+ result, ok := r.(*types.ReadBitswapProviderRecord)
+ if !ok {
+ logger.Errorw(
+ "problem casting find providers result",
+ "Schema", r.GetSchema(),
+ "Type", reflect.TypeOf(r).String(),
+ )
+ continue
+ }
+
+ var addrs []multiaddr.Multiaddr
+ for _, a := range result.Addrs {
+ addrs = append(addrs, a.Multiaddr)
+ }
+
+ ch <- peer.AddrInfo{
+ ID: *result.ID,
+ Addrs: addrs,
+ }
+ }
+
+ }
+ close(ch)
+ return ch
+}
diff --git a/routing/http/contentrouter/contentrouter_test.go b/routing/http/contentrouter/contentrouter_test.go
new file mode 100644
index 0000000000..643ad301ff
--- /dev/null
+++ b/routing/http/contentrouter/contentrouter_test.go
@@ -0,0 +1,139 @@
+package contentrouter
+
+import (
+ "context"
+ "crypto/rand"
+ "testing"
+ "time"
+
+ "github.com/ipfs/boxo/routing/http/types"
+ "github.com/ipfs/go-cid"
+ "github.com/libp2p/go-libp2p/core/peer"
+ "github.com/multiformats/go-multihash"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+)
+
+type mockClient struct{ mock.Mock }
+
+func (m *mockClient) ProvideBitswap(ctx context.Context, keys []cid.Cid, ttl time.Duration) (time.Duration, error) {
+ args := m.Called(ctx, keys, ttl)
+ return args.Get(0).(time.Duration), args.Error(1)
+}
+func (m *mockClient) FindProviders(ctx context.Context, key cid.Cid) ([]types.ProviderResponse, error) {
+ args := m.Called(ctx, key)
+ return args.Get(0).([]types.ProviderResponse), args.Error(1)
+}
+func (m *mockClient) Ready(ctx context.Context) (bool, error) {
+ args := m.Called(ctx)
+ return args.Bool(0), args.Error(1)
+}
+func makeCID() cid.Cid {
+ buf := make([]byte, 63)
+ _, err := rand.Read(buf)
+ if err != nil {
+ panic(err)
+ }
+ mh, err := multihash.Encode(buf, multihash.SHA2_256)
+ if err != nil {
+ panic(err)
+ }
+ c := cid.NewCidV1(cid.Raw, mh)
+ return c
+}
+
+func TestProvide(t *testing.T) {
+ for _, c := range []struct {
+ name string
+ announce bool
+
+ expNotProvided bool
+ }{
+ {
+ name: "announce=false results in no client request",
+ announce: false,
+ expNotProvided: true,
+ },
+ {
+ name: "announce=true results in a client req",
+ announce: true,
+ },
+ } {
+ t.Run(c.name, func(t *testing.T) {
+ ctx := context.Background()
+ key := makeCID()
+ client := &mockClient{}
+ crc := NewContentRoutingClient(client)
+
+ if !c.expNotProvided {
+ client.On("ProvideBitswap", ctx, []cid.Cid{key}, ttl).Return(time.Minute, nil)
+ }
+
+ err := crc.Provide(ctx, key, c.announce)
+ assert.NoError(t, err)
+
+ if c.expNotProvided {
+ client.AssertNumberOfCalls(t, "ProvideBitswap", 0)
+ }
+
+ })
+ }
+}
+
+func TestProvideMany(t *testing.T) {
+ cids := []cid.Cid{makeCID(), makeCID()}
+ var mhs []multihash.Multihash
+ for _, c := range cids {
+ mhs = append(mhs, c.Hash())
+ }
+ ctx := context.Background()
+ client := &mockClient{}
+ crc := NewContentRoutingClient(client)
+
+ client.On("ProvideBitswap", ctx, cids, ttl).Return(time.Minute, nil)
+
+ err := crc.ProvideMany(ctx, mhs)
+ require.NoError(t, err)
+}
+
+func TestFindProvidersAsync(t *testing.T) {
+ key := makeCID()
+ ctx := context.Background()
+ client := &mockClient{}
+ crc := NewContentRoutingClient(client)
+
+ p1 := peer.ID("peer1")
+ p2 := peer.ID("peer2")
+ ais := []types.ProviderResponse{
+ &types.ReadBitswapProviderRecord{
+ Protocol: "transport-bitswap",
+ Schema: types.SchemaBitswap,
+ ID: &p1,
+ },
+ &types.ReadBitswapProviderRecord{
+ Protocol: "transport-bitswap",
+ Schema: types.SchemaBitswap,
+ ID: &p2,
+ },
+ &types.UnknownProviderRecord{
+ Protocol: "UNKNOWN",
+ },
+ }
+
+ client.On("FindProviders", ctx, key).Return(ais, nil)
+
+ aiChan := crc.FindProvidersAsync(ctx, key, 2)
+
+ var actualAIs []peer.AddrInfo
+ for ai := range aiChan {
+ actualAIs = append(actualAIs, ai)
+ }
+
+ expected := []peer.AddrInfo{
+ {ID: p1},
+ {ID: p2},
+ }
+
+ require.Equal(t, expected, actualAIs)
+}
diff --git a/routing/http/internal/drjson/json.go b/routing/http/internal/drjson/json.go
new file mode 100644
index 0000000000..3bc3ab9420
--- /dev/null
+++ b/routing/http/internal/drjson/json.go
@@ -0,0 +1,26 @@
+package drjson
+
+import (
+ "bytes"
+ "encoding/json"
+)
+
+func marshalJSON(val any) (*bytes.Buffer, error) {
+ buf := &bytes.Buffer{}
+ enc := json.NewEncoder(buf)
+ enc.SetEscapeHTML(false)
+ err := enc.Encode(val)
+ return buf, err
+}
+
+// MarshalJSONBytes is needed to avoid changes
+// on the original bytes due to HTML escapes.
+func MarshalJSONBytes(val any) ([]byte, error) {
+ buf, err := marshalJSON(val)
+ if err != nil {
+ return nil, err
+ }
+
+ // remove last \n added by Encode
+ return buf.Bytes()[:buf.Len()-1], nil
+}
diff --git a/routing/http/internal/drjson/json_test.go b/routing/http/internal/drjson/json_test.go
new file mode 100644
index 0000000000..0e3bae81b2
--- /dev/null
+++ b/routing/http/internal/drjson/json_test.go
@@ -0,0 +1,16 @@
+package drjson
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestMarshalJSON(t *testing.T) {
+ // ensure that < is not escaped, which is the default Go behavior
+ bytes, err := MarshalJSONBytes(map[string]string{"<": "<"})
+ if err != nil {
+ panic(err)
+ }
+ require.Equal(t, "{\"<\":\"<\"}", string(bytes))
+}
diff --git a/routing/http/internal/goroutines.go b/routing/http/internal/goroutines.go
new file mode 100644
index 0000000000..4c44428ab4
--- /dev/null
+++ b/routing/http/internal/goroutines.go
@@ -0,0 +1,71 @@
+package internal
+
+import (
+ "context"
+ "sync"
+
+ "github.com/samber/lo"
+)
+
+// DoBatch processes a slice of items with concurrency no higher than maxConcurrency by splitting it into batches no larger than maxBatchSize.
+// If an error is returned for any batch, the process is short-circuited and the error is immediately returned.
+func DoBatch[A any](ctx context.Context, maxBatchSize, maxConcurrency int, items []A, f func(context.Context, []A) error) error {
+ if len(items) == 0 {
+ return nil
+ }
+ batches := lo.Chunk(items, maxBatchSize)
+ workerCtx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ batchChan := make(chan []A)
+ errChan := make(chan error)
+ wg := sync.WaitGroup{}
+ for i := 0; i < maxConcurrency && i < len(batches); i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case batch := <-batchChan:
+ err := f(workerCtx, batch)
+ if err != nil {
+ select {
+ case errChan <- err:
+ case <-workerCtx.Done():
+ return
+ }
+ }
+ case <-workerCtx.Done():
+ return
+ }
+ }
+ }()
+ }
+
+ // work sender
+ go func() {
+ defer close(errChan)
+ defer wg.Wait()
+ for _, batch := range batches {
+ select {
+ case batchChan <- batch:
+ case <-workerCtx.Done():
+ return
+ }
+ }
+ cancel()
+ }()
+
+ // receive any errors
+ select {
+ case err, ok := <-errChan:
+ if !ok {
+ // we finished without any errors, congratulations
+ return nil
+ }
+ // short circuit on the first error we get
+ // canceling the worker ctx and thus all workers,
+ return err
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+}
diff --git a/routing/http/internal/goroutines_test.go b/routing/http/internal/goroutines_test.go
new file mode 100644
index 0000000000..a2ee487301
--- /dev/null
+++ b/routing/http/internal/goroutines_test.go
@@ -0,0 +1,107 @@
+package internal
+
+import (
+ "context"
+ "errors"
+ "reflect"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+func sequence(n int) (items []int) {
+ for i := 0; i < n; i++ {
+ items = append(items, i+1)
+ }
+ return
+}
+
+func singleItemBatches(items []int) (batches [][]int) {
+ for _, item := range items {
+ batches = append(batches, []int{item})
+ }
+ return
+}
+
+func TestDoBatch(t *testing.T) {
+ cases := []struct {
+ name string
+ items []int
+ maxBatchSize int
+ maxConcurrency int
+ shouldErrOnce bool
+
+ expBatches [][]int
+ expErrContains string
+ }{
+ {
+ name: "no items",
+ },
+ {
+ name: "batch size = 1",
+ items: sequence(3),
+ maxBatchSize: 1,
+ maxConcurrency: 1,
+ expBatches: [][]int{{1}, {2}, {3}},
+ },
+ {
+ name: "batch size > 1",
+ items: sequence(6),
+ maxBatchSize: 2,
+ maxConcurrency: 2,
+ expBatches: [][]int{{1, 2}, {3, 4}, {5, 6}},
+ },
+ {
+ name: "a lot of items and concurrency",
+ items: sequence(1000),
+ maxBatchSize: 1,
+ maxConcurrency: 100,
+ expBatches: singleItemBatches(sequence(1000)),
+ },
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
+ defer cancel()
+
+ var mut sync.Mutex
+ var batches [][]int
+
+ var onceMut sync.Mutex
+ var errored bool
+
+ DoBatch(ctx, c.maxBatchSize, c.maxConcurrency, c.items, func(ctx context.Context, batch []int) error {
+ if c.shouldErrOnce {
+ onceMut.Lock()
+ if !errored {
+ errored = true
+ defer onceMut.Unlock()
+ return errors.New("boom")
+ }
+ onceMut.Unlock()
+ }
+
+ mut.Lock()
+ batches = append(batches, batch)
+ mut.Unlock()
+ return nil
+ })
+
+ require.Equal(t, len(c.expBatches), len(batches), "expected equal len %v %v", c.expBatches, batches)
+ for _, expBatch := range c.expBatches {
+ requireContainsBatch(t, batches, expBatch)
+ }
+ })
+ }
+}
+
+func requireContainsBatch(t *testing.T, batches [][]int, batch []int) {
+ for _, b := range batches {
+ if reflect.DeepEqual(batch, b) {
+ return
+ }
+ }
+ t.Fatalf("expected batch %v, but not found in batches %v", batch, batches)
+}
diff --git a/routing/http/server/server.go b/routing/http/server/server.go
new file mode 100644
index 0000000000..ca650ff285
--- /dev/null
+++ b/routing/http/server/server.go
@@ -0,0 +1,186 @@
+package server
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+
+ "github.com/gorilla/mux"
+ "github.com/ipfs/boxo/routing/http/internal/drjson"
+ "github.com/ipfs/boxo/routing/http/types"
+ "github.com/ipfs/go-cid"
+ "github.com/libp2p/go-libp2p/core/peer"
+ "github.com/multiformats/go-multiaddr"
+
+ logging "github.com/ipfs/go-log/v2"
+)
+
+var logger = logging.Logger("service/server/delegatedrouting")
+
+const ProvidePath = "/routing/v1/providers/"
+const FindProvidersPath = "/routing/v1/providers/{cid}"
+
+type ContentRouter interface {
+ FindProviders(ctx context.Context, key cid.Cid) ([]types.ProviderResponse, error)
+ ProvideBitswap(ctx context.Context, req *BitswapWriteProvideRequest) (time.Duration, error)
+ Provide(ctx context.Context, req *WriteProvideRequest) (types.ProviderResponse, error)
+}
+
+type BitswapWriteProvideRequest struct {
+ Keys []cid.Cid
+ Timestamp time.Time
+ AdvisoryTTL time.Duration
+ ID peer.ID
+ Addrs []multiaddr.Multiaddr
+}
+
+type WriteProvideRequest struct {
+ Protocol string
+ Schema string
+ Bytes []byte
+}
+
+type serverOption func(s *server)
+
+func Handler(svc ContentRouter, opts ...serverOption) http.Handler {
+ server := &server{
+ svc: svc,
+ }
+
+ for _, opt := range opts {
+ opt(server)
+ }
+
+ r := mux.NewRouter()
+ r.HandleFunc(ProvidePath, server.provide).Methods(http.MethodPut)
+ r.HandleFunc(FindProvidersPath, server.findProviders).Methods(http.MethodGet)
+
+ return r
+}
+
+type server struct {
+ svc ContentRouter
+}
+
+func (s *server) provide(w http.ResponseWriter, httpReq *http.Request) {
+ req := types.WriteProvidersRequest{}
+ err := json.NewDecoder(httpReq.Body).Decode(&req)
+ _ = httpReq.Body.Close()
+ if err != nil {
+ writeErr(w, "Provide", http.StatusBadRequest, fmt.Errorf("invalid request: %w", err))
+ return
+ }
+
+ resp := types.WriteProvidersResponse{}
+
+ for i, prov := range req.Providers {
+ switch v := prov.(type) {
+ case *types.WriteBitswapProviderRecord:
+ err := v.Verify()
+ if err != nil {
+ logErr("Provide", "signature verification failed", err)
+ writeErr(w, "Provide", http.StatusForbidden, errors.New("signature verification failed"))
+ return
+ }
+
+ keys := make([]cid.Cid, len(v.Payload.Keys))
+ for i, k := range v.Payload.Keys {
+ keys[i] = k.Cid
+
+ }
+ addrs := make([]multiaddr.Multiaddr, len(v.Payload.Addrs))
+ for i, a := range v.Payload.Addrs {
+ addrs[i] = a.Multiaddr
+ }
+ advisoryTTL, err := s.svc.ProvideBitswap(httpReq.Context(), &BitswapWriteProvideRequest{
+ Keys: keys,
+ Timestamp: v.Payload.Timestamp.Time,
+ AdvisoryTTL: v.Payload.AdvisoryTTL.Duration,
+ ID: *v.Payload.ID,
+ Addrs: addrs,
+ })
+ if err != nil {
+ writeErr(w, "Provide", http.StatusInternalServerError, fmt.Errorf("delegate error: %w", err))
+ return
+ }
+ resp.ProvideResults = append(resp.ProvideResults,
+ &types.WriteBitswapProviderRecordResponse{
+ Protocol: v.Protocol,
+ Schema: v.Schema,
+ AdvisoryTTL: &types.Duration{Duration: advisoryTTL},
+ },
+ )
+ case *types.UnknownProviderRecord:
+ provResp, err := s.svc.Provide(httpReq.Context(), &WriteProvideRequest{
+ Protocol: v.Protocol,
+ Schema: v.Schema,
+ Bytes: v.Bytes,
+ })
+ if err != nil {
+ writeErr(w, "Provide", http.StatusInternalServerError, fmt.Errorf("delegate error: %w", err))
+ return
+ }
+ resp.ProvideResults = append(resp.ProvideResults, provResp)
+ default:
+ writeErr(w, "Provide", http.StatusBadRequest, fmt.Errorf("provider record %d does not contain a protocol", i))
+ return
+ }
+ }
+ writeResult(w, "Provide", resp)
+}
+
+func (s *server) findProviders(w http.ResponseWriter, httpReq *http.Request) {
+ vars := mux.Vars(httpReq)
+ cidStr := vars["cid"]
+ cid, err := cid.Decode(cidStr)
+ if err != nil {
+ writeErr(w, "FindProviders", http.StatusBadRequest, fmt.Errorf("unable to parse CID: %w", err))
+ return
+ }
+ providers, err := s.svc.FindProviders(httpReq.Context(), cid)
+ if err != nil {
+ writeErr(w, "FindProviders", http.StatusInternalServerError, fmt.Errorf("delegate error: %w", err))
+ return
+ }
+ response := types.ReadProvidersResponse{Providers: providers}
+ writeResult(w, "FindProviders", response)
+}
+
+func writeResult(w http.ResponseWriter, method string, val any) {
+ w.Header().Add("Content-Type", "application/json")
+
+ // keep the marshaling separate from the writing, so we can distinguish bugs (which surface as 500)
+ // from transient network issues (which surface as transport errors)
+ b, err := drjson.MarshalJSONBytes(val)
+ if err != nil {
+ writeErr(w, method, http.StatusInternalServerError, fmt.Errorf("marshaling response: %w", err))
+ return
+ }
+
+ _, err = io.Copy(w, bytes.NewBuffer(b))
+ if err != nil {
+ logErr("Provide", "writing response body", err)
+ }
+}
+
+func writeErr(w http.ResponseWriter, method string, statusCode int, cause error) {
+ w.WriteHeader(statusCode)
+ causeStr := cause.Error()
+ if len(causeStr) > 1024 {
+ causeStr = causeStr[:1024]
+ }
+ _, err := w.Write([]byte(causeStr))
+ if err != nil {
+ logErr(method, "error writing error cause", err)
+ return
+ }
+}
+
+func logErr(method, msg string, err error) {
+ logger.Infow(msg, "Method", method, "Error", err)
+}
diff --git a/routing/http/server/server_test.go b/routing/http/server/server_test.go
new file mode 100644
index 0000000000..5609b2ed38
--- /dev/null
+++ b/routing/http/server/server_test.go
@@ -0,0 +1,63 @@
+package server
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/ipfs/boxo/routing/http/types"
+ "github.com/ipfs/go-cid"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+)
+
+func TestHeaders(t *testing.T) {
+ router := &mockContentRouter{}
+ server := httptest.NewServer(Handler(router))
+ t.Cleanup(server.Close)
+ serverAddr := "http://" + server.Listener.Addr().String()
+
+ result := []types.ProviderResponse{
+ &types.ReadBitswapProviderRecord{
+ Protocol: "transport-bitswap",
+ Schema: types.SchemaBitswap,
+ },
+ }
+
+ c := "baeabep4vu3ceru7nerjjbk37sxb7wmftteve4hcosmyolsbsiubw2vr6pqzj6mw7kv6tbn6nqkkldnklbjgm5tzbi4hkpkled4xlcr7xz4bq"
+ cb, err := cid.Decode(c)
+ require.NoError(t, err)
+
+ router.On("FindProviders", mock.Anything, cb).
+ Return(result, nil)
+
+ resp, err := http.Get(serverAddr + ProvidePath + c)
+ require.NoError(t, err)
+ require.Equal(t, 200, resp.StatusCode)
+ header := resp.Header.Get("Content-Type")
+ require.Equal(t, "application/json", header)
+
+ resp, err = http.Get(serverAddr + ProvidePath + "BAD_CID")
+ require.NoError(t, err)
+ require.Equal(t, 400, resp.StatusCode)
+ header = resp.Header.Get("Content-Type")
+ require.Equal(t, "text/plain; charset=utf-8", header)
+}
+
+type mockContentRouter struct{ mock.Mock }
+
+func (m *mockContentRouter) FindProviders(ctx context.Context, key cid.Cid) ([]types.ProviderResponse, error) {
+ args := m.Called(ctx, key)
+ return args.Get(0).([]types.ProviderResponse), args.Error(1)
+}
+func (m *mockContentRouter) ProvideBitswap(ctx context.Context, req *BitswapWriteProvideRequest) (time.Duration, error) {
+ args := m.Called(ctx, req)
+ return args.Get(0).(time.Duration), args.Error(1)
+}
+
+func (m *mockContentRouter) Provide(ctx context.Context, req *WriteProvideRequest) (types.ProviderResponse, error) {
+ args := m.Called(ctx, req)
+ return args.Get(0).(types.ProviderResponse), args.Error(1)
+}
diff --git a/routing/http/types/ipfs.go b/routing/http/types/ipfs.go
new file mode 100644
index 0000000000..1e139fa226
--- /dev/null
+++ b/routing/http/types/ipfs.go
@@ -0,0 +1,42 @@
+package types
+
+import (
+ "encoding/json"
+
+ "github.com/ipfs/boxo/routing/http/internal/drjson"
+ "github.com/ipfs/go-cid"
+ "github.com/multiformats/go-multiaddr"
+)
+
+type CID struct{ cid.Cid }
+
+func (c *CID) MarshalJSON() ([]byte, error) { return drjson.MarshalJSONBytes(c.String()) }
+func (c *CID) UnmarshalJSON(b []byte) error {
+ var s string
+ err := json.Unmarshal(b, &s)
+ if err != nil {
+ return err
+ }
+ decodedCID, err := cid.Decode(s)
+ if err != nil {
+ return err
+ }
+ c.Cid = decodedCID
+ return nil
+}
+
+type Multiaddr struct{ multiaddr.Multiaddr }
+
+func (m *Multiaddr) UnmarshalJSON(b []byte) error {
+ var s string
+ err := json.Unmarshal(b, &s)
+ if err != nil {
+ return err
+ }
+ ma, err := multiaddr.NewMultiaddr(s)
+ if err != nil {
+ return err
+ }
+ m.Multiaddr = ma
+ return nil
+}
diff --git a/routing/http/types/provider.go b/routing/http/types/provider.go
new file mode 100644
index 0000000000..ef9e95ada7
--- /dev/null
+++ b/routing/http/types/provider.go
@@ -0,0 +1,142 @@
+package types
+
+import (
+ "encoding/json"
+)
+
+// WriteProviderRecord is a type that enforces structs to imlement it to avoid confusion
+type WriteProviderRecord interface {
+ IsWriteProviderRecord()
+}
+
+// ReadProviderRecord is a type that enforces structs to imlement it to avoid confusion
+type ReadProviderRecord interface {
+ IsReadProviderRecord()
+}
+
+type WriteProvidersRequest struct {
+ Providers []WriteProviderRecord
+}
+
+func (r *WriteProvidersRequest) UnmarshalJSON(b []byte) error {
+ type wpr struct {
+ Providers []json.RawMessage
+ }
+ var tempWPR wpr
+ err := json.Unmarshal(b, &tempWPR)
+ if err != nil {
+ return err
+ }
+
+ for _, provBytes := range tempWPR.Providers {
+ var rawProv UnknownProviderRecord
+ err := json.Unmarshal(provBytes, &rawProv)
+ if err != nil {
+ return err
+ }
+
+ switch rawProv.Schema {
+ case SchemaBitswap:
+ var prov WriteBitswapProviderRecord
+ err := json.Unmarshal(rawProv.Bytes, &prov)
+ if err != nil {
+ return err
+ }
+ r.Providers = append(r.Providers, &prov)
+ default:
+ var prov UnknownProviderRecord
+ err := json.Unmarshal(b, &prov)
+ if err != nil {
+ return err
+ }
+ r.Providers = append(r.Providers, &prov)
+ }
+ }
+ return nil
+}
+
+// ProviderResponse is implemented for any ProviderResponse. It needs to have a Protocol field.
+type ProviderResponse interface {
+ GetProtocol() string
+ GetSchema() string
+}
+
+// WriteProvidersResponse is the result of a Provide operation
+type WriteProvidersResponse struct {
+ ProvideResults []ProviderResponse
+}
+
+// rawWriteProvidersResponse is a helper struct to make possible to parse WriteProvidersResponse's
+type rawWriteProvidersResponse struct {
+ ProvideResults []json.RawMessage
+}
+
+func (r *WriteProvidersResponse) UnmarshalJSON(b []byte) error {
+ var tempWPR rawWriteProvidersResponse
+ err := json.Unmarshal(b, &tempWPR)
+ if err != nil {
+ return err
+ }
+
+ for _, provBytes := range tempWPR.ProvideResults {
+ var rawProv UnknownProviderRecord
+ err := json.Unmarshal(provBytes, &rawProv)
+ if err != nil {
+ return err
+ }
+
+ switch rawProv.Schema {
+ case SchemaBitswap:
+ var prov WriteBitswapProviderRecordResponse
+ err := json.Unmarshal(rawProv.Bytes, &prov)
+ if err != nil {
+ return err
+ }
+ r.ProvideResults = append(r.ProvideResults, &prov)
+ default:
+ r.ProvideResults = append(r.ProvideResults, &rawProv)
+ }
+ }
+
+ return nil
+}
+
+// ReadProvidersResponse is the result of a Provide request
+type ReadProvidersResponse struct {
+ Providers []ProviderResponse
+}
+
+// rawReadProvidersResponse is a helper struct to make possible to parse ReadProvidersResponse's
+type rawReadProvidersResponse struct {
+ Providers []json.RawMessage
+}
+
+func (r *ReadProvidersResponse) UnmarshalJSON(b []byte) error {
+ var tempFPR rawReadProvidersResponse
+ err := json.Unmarshal(b, &tempFPR)
+ if err != nil {
+ return err
+ }
+
+ for _, provBytes := range tempFPR.Providers {
+ var readProv UnknownProviderRecord
+ err := json.Unmarshal(provBytes, &readProv)
+ if err != nil {
+ return err
+ }
+
+ switch readProv.Schema {
+ case SchemaBitswap:
+ var prov ReadBitswapProviderRecord
+ err := json.Unmarshal(readProv.Bytes, &prov)
+ if err != nil {
+ return err
+ }
+ r.Providers = append(r.Providers, &prov)
+ default:
+ r.Providers = append(r.Providers, &readProv)
+ }
+
+ }
+ return nil
+}
diff --git a/routing/http/types/provider_bitswap.go b/routing/http/types/provider_bitswap.go
new file mode 100644
index 0000000000..66243dd5dc
--- /dev/null
+++ b/routing/http/types/provider_bitswap.go
@@ -0,0 +1,184 @@
+package types
+
+import (
+ "crypto/sha256"
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "github.com/ipfs/boxo/routing/http/internal/drjson"
+ "github.com/libp2p/go-libp2p/core/crypto"
+ "github.com/libp2p/go-libp2p/core/peer"
+ "github.com/multiformats/go-multibase"
+)
+
+const SchemaBitswap = "bitswap"
+
+var _ WriteProviderRecord = &WriteBitswapProviderRecord{}
+
+// WriteBitswapProviderRecord is used when we want to add a new provider record that is using bitswap.
+type WriteBitswapProviderRecord struct {
+ Protocol string
+ Schema string
+ Signature string
+
+ // this content must be untouched because it is signed and we need to verify it
+ RawPayload json.RawMessage `json:"Payload"`
+ Payload BitswapPayload `json:"-"`
+}
+
+type BitswapPayload struct {
+ Keys []CID
+ Timestamp *Time
+ AdvisoryTTL *Duration
+ ID *peer.ID
+ Addrs []Multiaddr
+}
+
+func (*WriteBitswapProviderRecord) IsWriteProviderRecord() {}
+
+type tmpBWPR WriteBitswapProviderRecord
+
+func (p *WriteBitswapProviderRecord) UnmarshalJSON(b []byte) error {
+ var bwp tmpBWPR
+ err := json.Unmarshal(b, &bwp)
+ if err != nil {
+ return err
+ }
+
+ p.Protocol = bwp.Protocol
+ p.Schema = bwp.Schema
+ p.Signature = bwp.Signature
+ p.RawPayload = bwp.RawPayload
+
+ return json.Unmarshal(bwp.RawPayload, &p.Payload)
+}
+
+func (p *WriteBitswapProviderRecord) IsSigned() bool {
+ return p.Signature != ""
+}
+
+func (p *WriteBitswapProviderRecord) setRawPayload() error {
+ payloadBytes, err := drjson.MarshalJSONBytes(p.Payload)
+ if err != nil {
+ return fmt.Errorf("marshaling bitswap write provider payload: %w", err)
+ }
+
+ p.RawPayload = payloadBytes
+
+ return nil
+}
+
+func (p *WriteBitswapProviderRecord) Sign(peerID peer.ID, key crypto.PrivKey) error {
+ if p.IsSigned() {
+ return errors.New("already signed")
+ }
+
+ if key == nil {
+ return errors.New("no key provided")
+ }
+
+ sid, err := peer.IDFromPrivateKey(key)
+ if err != nil {
+ return err
+ }
+ if sid != peerID {
+ return errors.New("not the correct signing key")
+ }
+
+ err = p.setRawPayload()
+ if err != nil {
+ return err
+ }
+ hash := sha256.Sum256([]byte(p.RawPayload))
+ sig, err := key.Sign(hash[:])
+ if err != nil {
+ return err
+ }
+
+ sigStr, err := multibase.Encode(multibase.Base64, sig)
+ if err != nil {
+ return fmt.Errorf("multibase-encoding signature: %w", err)
+ }
+
+ p.Signature = sigStr
+ return nil
+}
+
+func (p *WriteBitswapProviderRecord) Verify() error {
+ if !p.IsSigned() {
+ return errors.New("not signed")
+ }
+
+ if p.Payload.ID == nil {
+ return errors.New("peer ID must be specified")
+ }
+
+ // note that we only generate and set the payload if it hasn't already been set
+ // to allow for passing through the payload untouched if it is already provided
+ if p.RawPayload == nil {
+ err := p.setRawPayload()
+ if err != nil {
+ return err
+ }
+ }
+
+ pk, err := p.Payload.ID.ExtractPublicKey()
+ if err != nil {
+ return fmt.Errorf("extracing public key from peer ID: %w", err)
+ }
+
+ _, sigBytes, err := multibase.Decode(p.Signature)
+ if err != nil {
+ return fmt.Errorf("multibase-decoding signature to verify: %w", err)
+ }
+
+ hash := sha256.Sum256([]byte(p.RawPayload))
+ ok, err := pk.Verify(hash[:], sigBytes)
+ if err != nil {
+ return fmt.Errorf("verifying hash with signature: %w", err)
+ }
+ if !ok {
+ return errors.New("signature failed to verify")
+ }
+
+ return nil
+}
+
+var _ ProviderResponse = &WriteBitswapProviderRecordResponse{}
+
+// WriteBitswapProviderRecordResponse will be returned as a result of WriteBitswapProviderRecord
+type WriteBitswapProviderRecordResponse struct {
+ Protocol string
+ Schema string
+ AdvisoryTTL *Duration
+}
+
+func (wbprr *WriteBitswapProviderRecordResponse) GetProtocol() string {
+ return wbprr.Protocol
+}
+
+func (wbprr *WriteBitswapProviderRecordResponse) GetSchema() string {
+ return wbprr.Schema
+}
+
+var _ ReadProviderRecord = &ReadBitswapProviderRecord{}
+var _ ProviderResponse = &ReadBitswapProviderRecord{}
+
+// ReadBitswapProviderRecord is a provider result with parameters for bitswap providers
+type ReadBitswapProviderRecord struct {
+ Protocol string
+ Schema string
+ ID *peer.ID
+ Addrs []Multiaddr
+}
+
+func (rbpr *ReadBitswapProviderRecord) GetProtocol() string {
+ return rbpr.Protocol
+}
+
+func (rbpr *ReadBitswapProviderRecord) GetSchema() string {
+ return rbpr.Schema
+}
+
+func (*ReadBitswapProviderRecord) IsReadProviderRecord() {}
diff --git a/routing/http/types/provider_unknown.go b/routing/http/types/provider_unknown.go
new file mode 100644
index 0000000000..3dadc0e9b9
--- /dev/null
+++ b/routing/http/types/provider_unknown.go
@@ -0,0 +1,61 @@
+package types
+
+import (
+ "encoding/json"
+
+ "github.com/ipfs/boxo/routing/http/internal/drjson"
+)
+
+var _ ReadProviderRecord = &UnknownProviderRecord{}
+var _ WriteProviderRecord = &UnknownProviderRecord{}
+var _ ProviderResponse = &UnknownProviderRecord{}
+
+// UnknownProviderRecord is used when we cannot parse the provider record using `GetProtocol`
+type UnknownProviderRecord struct {
+ Protocol string
+ Schema string
+ Bytes []byte
+}
+
+func (u *UnknownProviderRecord) GetProtocol() string {
+ return u.Protocol
+}
+
+func (u *UnknownProviderRecord) GetSchema() string {
+ return u.Schema
+}
+
+func (u *UnknownProviderRecord) IsReadProviderRecord() {}
+func (u UnknownProviderRecord) IsWriteProviderRecord() {}
+
+func (u *UnknownProviderRecord) UnmarshalJSON(b []byte) error {
+ m := map[string]interface{}{}
+ if err := json.Unmarshal(b, &m); err != nil {
+ return err
+ }
+
+ ps, ok := m["Protocol"].(string)
+ if ok {
+ u.Protocol = ps
+ }
+ schema, ok := m["Schema"].(string)
+ if ok {
+ u.Schema = schema
+ }
+
+ u.Bytes = b
+
+ return nil
+}
+
+func (u UnknownProviderRecord) MarshalJSON() ([]byte, error) {
+ m := map[string]interface{}{}
+ err := json.Unmarshal(u.Bytes, &m)
+ if err != nil {
+ return nil, err
+ }
+ m["Protocol"] = u.Protocol
+ m["Schema"] = u.Schema
+
+ return drjson.MarshalJSONBytes(m)
+}
diff --git a/routing/http/types/time.go b/routing/http/types/time.go
new file mode 100644
index 0000000000..1d938807ed
--- /dev/null
+++ b/routing/http/types/time.go
@@ -0,0 +1,36 @@
+package types
+
+import (
+ "encoding/json"
+ "time"
+
+ "github.com/ipfs/boxo/routing/http/internal/drjson"
+)
+
+type Time struct{ time.Time }
+
+func (t *Time) MarshalJSON() ([]byte, error) {
+ return drjson.MarshalJSONBytes(t.Time.UnixMilli())
+}
+func (t *Time) UnmarshalJSON(b []byte) error {
+ var timestamp int64
+ err := json.Unmarshal(b, ×tamp)
+ if err != nil {
+ return err
+ }
+ t.Time = time.UnixMilli(timestamp)
+ return nil
+}
+
+type Duration struct{ time.Duration }
+
+func (d *Duration) MarshalJSON() ([]byte, error) { return drjson.MarshalJSONBytes(d.Duration) }
+func (d *Duration) UnmarshalJSON(b []byte) error {
+ var dur int64
+ err := json.Unmarshal(b, &dur)
+ if err != nil {
+ return err
+ }
+ d.Duration = time.Duration(dur)
+ return nil
+}
diff --git a/routing/mock/centralized_client.go b/routing/mock/centralized_client.go
new file mode 100644
index 0000000000..ac3f938ccd
--- /dev/null
+++ b/routing/mock/centralized_client.go
@@ -0,0 +1,88 @@
+package mockrouting
+
+import (
+ "context"
+ "time"
+
+ "github.com/ipfs/go-cid"
+ logging "github.com/ipfs/go-log"
+ tnet "github.com/libp2p/go-libp2p-testing/net"
+ "github.com/libp2p/go-libp2p/core/peer"
+ "github.com/libp2p/go-libp2p/core/routing"
+ ma "github.com/multiformats/go-multiaddr"
+)
+
+var log = logging.Logger("mockrouter")
+
+type client struct {
+ vs routing.ValueStore
+ server server
+ peer tnet.Identity
+}
+
+// PutValue FIXME(brian): is this method meant to simulate putting a value into the network?
+func (c *client) PutValue(ctx context.Context, key string, val []byte, opts ...routing.Option) error {
+ log.Debugf("PutValue: %s", key)
+ return c.vs.PutValue(ctx, key, val, opts...)
+}
+
+// GetValue FIXME(brian): is this method meant to simulate getting a value from the network?
+func (c *client) GetValue(ctx context.Context, key string, opts ...routing.Option) ([]byte, error) {
+ log.Debugf("GetValue: %s", key)
+ return c.vs.GetValue(ctx, key, opts...)
+}
+
+func (c *client) SearchValue(ctx context.Context, key string, opts ...routing.Option) (<-chan []byte, error) {
+ log.Debugf("SearchValue: %s", key)
+ return c.vs.SearchValue(ctx, key, opts...)
+}
+
+func (c *client) FindProviders(ctx context.Context, key cid.Cid) ([]peer.AddrInfo, error) {
+ return c.server.Providers(key), nil
+}
+
+func (c *client) FindPeer(ctx context.Context, pid peer.ID) (peer.AddrInfo, error) {
+ log.Debugf("FindPeer: %s", pid)
+ return peer.AddrInfo{}, nil
+}
+
+func (c *client) FindProvidersAsync(ctx context.Context, k cid.Cid, max int) <-chan peer.AddrInfo {
+ out := make(chan peer.AddrInfo)
+ go func() {
+ defer close(out)
+ for i, p := range c.server.Providers(k) {
+ if max <= i {
+ return
+ }
+ select {
+ case out <- p:
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+ return out
+}
+
+// Provide returns once the message is on the network. Value is not necessarily
+// visible yet.
+func (c *client) Provide(_ context.Context, key cid.Cid, brd bool) error {
+ if !brd {
+ return nil
+ }
+ info := peer.AddrInfo{
+ ID: c.peer.ID(),
+ Addrs: []ma.Multiaddr{c.peer.Address()},
+ }
+ return c.server.Announce(info, key)
+}
+
+func (c *client) Ping(ctx context.Context, p peer.ID) (time.Duration, error) {
+ return 0, nil
+}
+
+func (c *client) Bootstrap(context.Context) error {
+ return nil
+}
+
+var _ routing.Routing = &client{}
diff --git a/routing/mock/centralized_server.go b/routing/mock/centralized_server.go
new file mode 100644
index 0000000000..d55de70814
--- /dev/null
+++ b/routing/mock/centralized_server.go
@@ -0,0 +1,91 @@
+package mockrouting
+
+import (
+ "context"
+ "math/rand"
+ "sync"
+ "time"
+
+ "github.com/ipfs/boxo/routing/offline"
+ "github.com/ipfs/go-cid"
+ ds "github.com/ipfs/go-datastore"
+ dssync "github.com/ipfs/go-datastore/sync"
+ tnet "github.com/libp2p/go-libp2p-testing/net"
+ "github.com/libp2p/go-libp2p/core/peer"
+)
+
+// server is the mockrouting.Client's private interface to the routing server
+type server interface {
+ Announce(peer.AddrInfo, cid.Cid) error
+ Providers(cid.Cid) []peer.AddrInfo
+
+ Server
+}
+
+// s is an implementation of the private server interface
+type s struct {
+ delayConf DelayConfig
+
+ lock sync.RWMutex
+ providers map[string]map[peer.ID]providerRecord
+}
+
+type providerRecord struct {
+ Peer peer.AddrInfo
+ Created time.Time
+}
+
+func (rs *s) Announce(p peer.AddrInfo, c cid.Cid) error {
+ rs.lock.Lock()
+ defer rs.lock.Unlock()
+
+ k := c.KeyString()
+
+ _, ok := rs.providers[k]
+ if !ok {
+ rs.providers[k] = make(map[peer.ID]providerRecord)
+ }
+ rs.providers[k][p.ID] = providerRecord{
+ Created: time.Now(),
+ Peer: p,
+ }
+ return nil
+}
+
+func (rs *s) Providers(c cid.Cid) []peer.AddrInfo {
+ rs.delayConf.Query.Wait() // before locking
+
+ rs.lock.RLock()
+ defer rs.lock.RUnlock()
+ k := c.KeyString()
+
+ var ret []peer.AddrInfo
+ records, ok := rs.providers[k]
+ if !ok {
+ return ret
+ }
+ for _, r := range records {
+ if time.Since(r.Created) > rs.delayConf.ValueVisibility.Get() {
+ ret = append(ret, r.Peer)
+ }
+ }
+
+ for i := range ret {
+ j := rand.Intn(i + 1)
+ ret[i], ret[j] = ret[j], ret[i]
+ }
+
+ return ret
+}
+
+func (rs *s) Client(p tnet.Identity) Client {
+ return rs.ClientWithDatastore(context.Background(), p, dssync.MutexWrap(ds.NewMapDatastore()))
+}
+
+func (rs *s) ClientWithDatastore(_ context.Context, p tnet.Identity, datastore ds.Datastore) Client {
+ return &client{
+ peer: p,
+ vs: offline.NewOfflineRouter(datastore, MockValidator{}),
+ server: rs,
+ }
+}
diff --git a/routing/mock/centralized_test.go b/routing/mock/centralized_test.go
new file mode 100644
index 0000000000..403e21350f
--- /dev/null
+++ b/routing/mock/centralized_test.go
@@ -0,0 +1,176 @@
+package mockrouting
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ u "github.com/ipfs/boxo/util"
+ "github.com/ipfs/go-cid"
+ delay "github.com/ipfs/go-ipfs-delay"
+ tnet "github.com/libp2p/go-libp2p-testing/net"
+ "github.com/libp2p/go-libp2p/core/peer"
+)
+
+func TestKeyNotFound(t *testing.T) {
+
+ var pi = tnet.RandIdentityOrFatal(t)
+ var key = cid.NewCidV0(u.Hash([]byte("mock key")))
+ var ctx = context.Background()
+
+ rs := NewServer()
+ providers := rs.Client(pi).FindProvidersAsync(ctx, key, 10)
+ _, ok := <-providers
+ if ok {
+ t.Fatal("should be closed")
+ }
+}
+
+func TestClientFindProviders(t *testing.T) {
+ pi := tnet.RandIdentityOrFatal(t)
+ rs := NewServer()
+ client := rs.Client(pi)
+
+ k := cid.NewCidV0(u.Hash([]byte("hello")))
+ err := client.Provide(context.Background(), k, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // This is bad... but simulating networks is hard
+ time.Sleep(time.Millisecond * 300)
+ max := 100
+
+ providersFromClient := client.FindProvidersAsync(context.Background(), k, max)
+ isInClient := false
+ for p := range providersFromClient {
+ if p.ID == pi.ID() {
+ isInClient = true
+ }
+ }
+ if !isInClient {
+ t.Fatal("Despite client providing key, client didn't receive peer when finding providers")
+ }
+}
+
+func TestClientOverMax(t *testing.T) {
+ rs := NewServer()
+ k := cid.NewCidV0(u.Hash([]byte("hello")))
+ numProvidersForHelloKey := 100
+ for i := 0; i < numProvidersForHelloKey; i++ {
+ pi := tnet.RandIdentityOrFatal(t)
+ err := rs.Client(pi).Provide(context.Background(), k, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ max := 10
+ pi := tnet.RandIdentityOrFatal(t)
+ client := rs.Client(pi)
+
+ providersFromClient := client.FindProvidersAsync(context.Background(), k, max)
+ i := 0
+ for range providersFromClient {
+ i++
+ }
+ if i != max {
+ t.Fatal("Too many providers returned")
+ }
+}
+
+// TODO does dht ensure won't receive self as a provider? probably not.
+func TestCanceledContext(t *testing.T) {
+ rs := NewServer()
+ k := cid.NewCidV0(u.Hash([]byte("hello")))
+
+ // avoid leaking goroutine, without using the context to signal
+ // (we want the goroutine to keep trying to publish on a
+ // cancelled context until we've tested it doesnt do anything.)
+ done := make(chan struct{})
+ defer func() { done <- struct{}{} }()
+
+ t.Log("async'ly announce infinite stream of providers for key")
+ i := 0
+ go func() { // infinite stream
+ for {
+ select {
+ case <-done:
+ t.Log("exiting async worker")
+ return
+ default:
+ }
+
+ pi, err := tnet.RandIdentity()
+ if err != nil {
+ t.Error(err)
+ }
+ err = rs.Client(pi).Provide(context.Background(), k, true)
+ if err != nil {
+ t.Error(err)
+ }
+ i++
+ }
+ }()
+
+ local := tnet.RandIdentityOrFatal(t)
+ client := rs.Client(local)
+
+ t.Log("warning: max is finite so this test is non-deterministic")
+ t.Log("context cancellation could simply take lower priority")
+ t.Log("and result in receiving the max number of results")
+ max := 1000
+
+ t.Log("cancel the context before consuming")
+ ctx, cancelFunc := context.WithCancel(context.Background())
+ cancelFunc()
+ providers := client.FindProvidersAsync(ctx, k, max)
+
+ numProvidersReturned := 0
+ for range providers {
+ numProvidersReturned++
+ }
+ t.Log(numProvidersReturned)
+
+ if numProvidersReturned == max {
+ t.Fatal("Context cancel had no effect")
+ }
+}
+
+func TestValidAfter(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ pi := tnet.RandIdentityOrFatal(t)
+ key := cid.NewCidV0(u.Hash([]byte("mock key")))
+ conf := DelayConfig{
+ ValueVisibility: delay.Fixed(1 * time.Hour),
+ Query: delay.Fixed(0),
+ }
+
+ rs := NewServerWithDelay(conf)
+
+ rs.Client(pi).Provide(ctx, key, true)
+
+ var providers []peer.AddrInfo
+ max := 100
+ providersChan := rs.Client(pi).FindProvidersAsync(ctx, key, max)
+ for p := range providersChan {
+ providers = append(providers, p)
+ }
+ if len(providers) > 0 {
+ t.Fail()
+ }
+
+ conf.ValueVisibility.Set(0)
+ time.Sleep(100 * time.Millisecond)
+
+ providersChan = rs.Client(pi).FindProvidersAsync(ctx, key, max)
+ t.Log("providers", providers)
+ for p := range providersChan {
+ providers = append(providers, p)
+ }
+ if len(providers) != 1 {
+ t.Fail()
+ }
+}
diff --git a/routing/mock/interface.go b/routing/mock/interface.go
new file mode 100644
index 0000000000..35430a72c5
--- /dev/null
+++ b/routing/mock/interface.go
@@ -0,0 +1,59 @@
+// Package mockrouting provides a virtual routing server. To use it,
+// create a virtual routing server and use the Client() method to get a
+// routing client (Routing). The server quacks like a DHT but is
+// really a local in-memory hash table.
+package mockrouting
+
+import (
+ "context"
+
+ ds "github.com/ipfs/go-datastore"
+ delay "github.com/ipfs/go-ipfs-delay"
+ tnet "github.com/libp2p/go-libp2p-testing/net"
+ "github.com/libp2p/go-libp2p/core/peer"
+ "github.com/libp2p/go-libp2p/core/routing"
+)
+
+// MockValidator is a record validator that always returns success.
+type MockValidator struct{}
+
+func (MockValidator) Validate(_ string, _ []byte) error { return nil }
+func (MockValidator) Select(_ string, _ [][]byte) (int, error) { return 0, nil }
+
+// Server provides mockrouting Clients
+type Server interface {
+ Client(p tnet.Identity) Client
+ ClientWithDatastore(context.Context, tnet.Identity, ds.Datastore) Client
+}
+
+// Client implements Routing
+type Client interface {
+ routing.Routing
+}
+
+// NewServer returns a mockrouting Server
+func NewServer() Server {
+ return NewServerWithDelay(DelayConfig{
+ ValueVisibility: delay.Fixed(0),
+ Query: delay.Fixed(0),
+ })
+}
+
+// NewServerWithDelay returns a mockrouting Server with a delay!
+func NewServerWithDelay(conf DelayConfig) Server {
+ return &s{
+ providers: make(map[string]map[peer.ID]providerRecord),
+ delayConf: conf,
+ }
+}
+
+// DelayConfig can be used to configured the fake delays of a mock server.
+// Use with NewServerWithDelay().
+type DelayConfig struct {
+ // ValueVisibility is the time it takes for a value to be visible in the network
+ // FIXME there _must_ be a better term for this
+ ValueVisibility delay.D
+
+ // Query is the time it takes to receive a response from a routing query
+ Query delay.D
+}
diff --git a/routing/none/none_client.go b/routing/none/none_client.go
new file mode 100644
index 0000000000..6f400b54a1
--- /dev/null
+++ b/routing/none/none_client.go
@@ -0,0 +1,55 @@
+// Package nilrouting implements a routing client that does nothing.
+package nilrouting
+
+import (
+ "context"
+ "errors"
+
+ "github.com/ipfs/go-cid"
+ ds "github.com/ipfs/go-datastore"
+ record "github.com/libp2p/go-libp2p-record"
+ "github.com/libp2p/go-libp2p/core/host"
+ "github.com/libp2p/go-libp2p/core/peer"
+ "github.com/libp2p/go-libp2p/core/routing"
+)
+
+type nilclient struct {
+}
+
+func (c *nilclient) PutValue(_ context.Context, _ string, _ []byte, _ ...routing.Option) error {
+ return nil
+}
+
+func (c *nilclient) GetValue(_ context.Context, _ string, _ ...routing.Option) ([]byte, error) {
+ return nil, errors.New("tried GetValue from nil routing")
+}
+
+func (c *nilclient) SearchValue(_ context.Context, _ string, _ ...routing.Option) (<-chan []byte, error) {
+ return nil, errors.New("tried SearchValue from nil routing")
+}
+
+func (c *nilclient) FindPeer(_ context.Context, _ peer.ID) (peer.AddrInfo, error) {
+ return peer.AddrInfo{}, nil
+}
+
+func (c *nilclient) FindProvidersAsync(_ context.Context, _ cid.Cid, _ int) <-chan peer.AddrInfo {
+ out := make(chan peer.AddrInfo)
+ defer close(out)
+ return out
+}
+
+func (c *nilclient) Provide(_ context.Context, _ cid.Cid, _ bool) error {
+ return nil
+}
+
+func (c *nilclient) Bootstrap(_ context.Context) error {
+ return nil
+}
+
+// ConstructNilRouting creates an Routing client which does nothing.
+func ConstructNilRouting(_ context.Context, _ host.Host, _ ds.Batching, _ record.Validator) (routing.Routing, error) {
+ return &nilclient{}, nil
+}
+
+// ensure nilclient satisfies interface
+var _ routing.Routing = &nilclient{}
diff --git a/routing/offline/offline.go b/routing/offline/offline.go
new file mode 100644
index 0000000000..b9d5281ff0
--- /dev/null
+++ b/routing/offline/offline.go
@@ -0,0 +1,126 @@
+// Package offline implements Routing with a client which
+// is only able to perform offline operations.
+package offline
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "time"
+
+ "github.com/gogo/protobuf/proto"
+ dshelp "github.com/ipfs/boxo/datastore/dshelp"
+ "github.com/ipfs/go-cid"
+ ds "github.com/ipfs/go-datastore"
+ record "github.com/libp2p/go-libp2p-record"
+ pb "github.com/libp2p/go-libp2p-record/pb"
+ "github.com/libp2p/go-libp2p/core/peer"
+ "github.com/libp2p/go-libp2p/core/routing"
+)
+
+// ErrOffline is returned when trying to perform operations that
+// require connectivity.
+var ErrOffline = errors.New("routing system in offline mode")
+
+// NewOfflineRouter returns an Routing implementation which only performs
+// offline operations. It allows to Put and Get signed dht
+// records to and from the local datastore.
+func NewOfflineRouter(dstore ds.Datastore, validator record.Validator) routing.Routing {
+ return &offlineRouting{
+ datastore: dstore,
+ validator: validator,
+ }
+}
+
+// offlineRouting implements the Routing interface,
+// but only provides the capability to Put and Get signed dht
+// records to and from the local datastore.
+type offlineRouting struct {
+ datastore ds.Datastore
+ validator record.Validator
+}
+
+func (c *offlineRouting) PutValue(ctx context.Context, key string, val []byte, _ ...routing.Option) error {
+ if err := c.validator.Validate(key, val); err != nil {
+ return err
+ }
+ if old, err := c.GetValue(ctx, key); err == nil {
+ // be idempotent to be nice.
+ if bytes.Equal(old, val) {
+ return nil
+ }
+ // check to see if the older record is better
+ i, err := c.validator.Select(key, [][]byte{val, old})
+ if err != nil {
+ // this shouldn't happen for validated records.
+ return err
+ }
+ if i != 0 {
+ return errors.New("can't replace a newer record with an older one")
+ }
+ }
+ rec := record.MakePutRecord(key, val)
+ data, err := proto.Marshal(rec)
+ if err != nil {
+ return err
+ }
+
+ return c.datastore.Put(ctx, dshelp.NewKeyFromBinary([]byte(key)), data)
+}
+
+func (c *offlineRouting) GetValue(ctx context.Context, key string, _ ...routing.Option) ([]byte, error) {
+ buf, err := c.datastore.Get(ctx, dshelp.NewKeyFromBinary([]byte(key)))
+ if err != nil {
+ return nil, err
+ }
+
+ rec := new(pb.Record)
+ err = proto.Unmarshal(buf, rec)
+ if err != nil {
+ return nil, err
+ }
+ val := rec.GetValue()
+
+ err = c.validator.Validate(key, val)
+ if err != nil {
+ return nil, err
+ }
+ return val, nil
+}
+
+func (c *offlineRouting) SearchValue(ctx context.Context, key string, _ ...routing.Option) (<-chan []byte, error) {
+ out := make(chan []byte, 1)
+ go func() {
+ defer close(out)
+ v, err := c.GetValue(ctx, key)
+ if err == nil {
+ out <- v
+ }
+ }()
+ return out, nil
+}
+
+func (c *offlineRouting) FindPeer(ctx context.Context, pid peer.ID) (peer.AddrInfo, error) {
+ return peer.AddrInfo{}, ErrOffline
+}
+
+func (c *offlineRouting) FindProvidersAsync(ctx context.Context, k cid.Cid, max int) <-chan peer.AddrInfo {
+ out := make(chan peer.AddrInfo)
+ close(out)
+ return out
+}
+
+func (c *offlineRouting) Provide(_ context.Context, k cid.Cid, _ bool) error {
+ return ErrOffline
+}
+
+func (c *offlineRouting) Ping(ctx context.Context, p peer.ID) (time.Duration, error) {
+ return 0, ErrOffline
+}
+
+func (c *offlineRouting) Bootstrap(context.Context) error {
+ return nil
+}
+
+// ensure offlineRouting matches the Routing interface
+var _ routing.Routing = &offlineRouting{}
diff --git a/routing/offline/offline_test.go b/routing/offline/offline_test.go
new file mode 100644
index 0000000000..9a17e86898
--- /dev/null
+++ b/routing/offline/offline_test.go
@@ -0,0 +1,91 @@
+package offline
+
+import (
+ "bytes"
+ "context"
+ "testing"
+
+ cid "github.com/ipfs/go-cid"
+ ds "github.com/ipfs/go-datastore"
+
+ "github.com/libp2p/go-libp2p/core/routing"
+ "github.com/libp2p/go-libp2p/core/test"
+
+ mh "github.com/multiformats/go-multihash"
+)
+
+type blankValidator struct{}
+
+func (blankValidator) Validate(_ string, _ []byte) error { return nil }
+func (blankValidator) Select(_ string, _ [][]byte) (int, error) { return 0, nil }
+
+func TestOfflineRouterStorage(t *testing.T) {
+ ctx := context.Background()
+
+ nds := ds.NewMapDatastore()
+ offline := NewOfflineRouter(nds, blankValidator{})
+
+ if err := offline.PutValue(ctx, "key", []byte("testing 1 2 3")); err != nil {
+ t.Fatal(err)
+ }
+
+ val, err := offline.GetValue(ctx, "key")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal([]byte("testing 1 2 3"), val) {
+ t.Fatal("OfflineRouter does not properly store")
+ }
+
+ _, err = offline.GetValue(ctx, "notHere")
+ if err == nil {
+ t.Fatal("Router should throw errors for unfound records")
+ }
+
+ local, err := offline.GetValue(ctx, "key", routing.Offline)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = offline.GetValue(ctx, "notHere", routing.Offline)
+ if err == nil {
+ t.Fatal("Router should throw errors for unfound records")
+ }
+
+ if !bytes.Equal([]byte("testing 1 2 3"), local) {
+ t.Fatal("OfflineRouter does not properly store")
+ }
+}
+
+func TestOfflineRouterLocal(t *testing.T) {
+ ctx := context.Background()
+
+ nds := ds.NewMapDatastore()
+ offline := NewOfflineRouter(nds, blankValidator{})
+
+ id, _ := test.RandPeerID()
+ _, err := offline.FindPeer(ctx, id)
+ if err != ErrOffline {
+ t.Fatal("OfflineRouting should alert that its offline")
+ }
+
+ h, _ := mh.Sum([]byte("test data1"), mh.SHA2_256, -1)
+ c1 := cid.NewCidV0(h)
+ pChan := offline.FindProvidersAsync(ctx, c1, 1)
+ p, ok := <-pChan
+ if ok {
+ t.Fatalf("FindProvidersAsync did not return a closed channel. Instead we got %+v !", p)
+ }
+
+ h2, _ := mh.Sum([]byte("test data1"), mh.SHA2_256, -1)
+ c2 := cid.NewCidV0(h2)
+ err = offline.Provide(ctx, c2, true)
+ if err != ErrOffline {
+ t.Fatal("OfflineRouting should alert that its offline")
+ }
+
+ err = offline.Bootstrap(ctx)
+ if err != nil {
+ t.Fatal("You shouldn't be able to bootstrap offline routing.")
+ }
+}
diff --git a/tar/extractor.go b/tar/extractor.go
new file mode 100644
index 0000000000..b5377ddca5
--- /dev/null
+++ b/tar/extractor.go
@@ -0,0 +1,327 @@
+package tar
+
+import (
+ "archive/tar"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ fp "path/filepath"
+ "strings"
+)
+
+var errTraverseSymlink = errors.New("cannot traverse symlinks")
+var errInvalidRoot = errors.New("tar has invalid root")
+var errInvalidRootMultipleRoots = fmt.Errorf("contains more than one root or the root directory is not the first entry : %w", errInvalidRoot)
+
+// Extractor is used for extracting tar files to a filesystem.
+//
+// The Extractor can only extract tar files containing files, directories and symlinks. Additionally, the tar files must
+// either have a single file, or symlink in them, or must have all of its objects inside of a single root directory
+// object.
+//
+// If the tar file contains a single file/symlink then it will try and extract it with semantics similar to Linux's
+// `cp`. In particular, the name of the extracted file/symlink will match the extraction path. If the extraction path
+// is a directory then it will extract into the directory using its original name.
+//
+// Overwriting: Extraction of files and symlinks will result in overwriting the existing objects with the same name
+// when possible (i.e. other files, symlinks, and empty directories).
+type Extractor struct {
+ Path string
+ Progress func(int64) int64
+}
+
+// Extract extracts a tar file to the file system. See the Extractor for more information on the limitations on the
+// tar files that can be extracted.
+func (te *Extractor) Extract(reader io.Reader) error {
+ if isNullDevice(te.Path) {
+ return nil
+ }
+
+ tarReader := tar.NewReader(reader)
+
+ var firstObjectWasDir bool
+
+ header, err := tarReader.Next()
+ if err != nil && err != io.EOF {
+ return err
+ }
+ if header == nil || err == io.EOF {
+ return fmt.Errorf("empty tar file")
+ }
+
+ // Specially handle the first entry assuming it is a single root object (e.g. root directory, single file,
+ // or single symlink)
+
+ // track what the root tar path is so we can ensure that all other entries are below the root
+ if strings.Contains(header.Name, "/") {
+ return fmt.Errorf("root name contains multiple components : %q : %w", header.Name, errInvalidRoot)
+ }
+ switch header.Name {
+ case "", ".", "..":
+ return fmt.Errorf("invalid root path: %q : %w", header.Name, errInvalidRoot)
+ }
+ rootName := header.Name
+
+ // Get the platform-specific output path
+ rootOutputPath := fp.Clean(te.Path)
+ if err := validatePlatformPath(rootOutputPath); err != nil {
+ return err
+ }
+
+ // If the last element in the rootOutputPath (which is passed by the user) is a symlink do not follow it
+ // this makes it easier for users to reason about where files are getting extracted to even when the tar is not
+ // from a trusted source
+ //
+ // For example, if the user extracts a mutable link to a tar file (http://sometimesbad.tld/t.tar) and situationally
+ // it contains a folder, file, or symlink the outputs could hop around the user's file system. This is especially
+ // annoying since we allow symlinks to point anywhere a user might want them to.
+ switch header.Typeflag {
+ case tar.TypeDir:
+ // if this is the root directory, use it as the output path for remaining files
+ firstObjectWasDir = true
+ if err := te.extractDir(rootOutputPath); err != nil {
+ return err
+ }
+ case tar.TypeReg, tar.TypeSymlink:
+ // Check if the output path already exists, so we know whether we should
+ // create our output with that name, or if we should put the output inside
+ // a preexisting directory
+
+ rootIsExistingDirectory := false
+ // We do not follow links here
+ if stat, err := os.Lstat(rootOutputPath); err != nil {
+ if !os.IsNotExist(err) {
+ return err
+ }
+ } else if stat.IsDir() {
+ rootIsExistingDirectory = true
+ }
+
+ outputPath := rootOutputPath
+ // If the root is a directory which already exists then put the file/symlink in the directory
+ if rootIsExistingDirectory {
+ // make sure the root has a valid name
+ if err := validatePathComponent(rootName); err != nil {
+ return err
+ }
+
+ // If the output path directory exists then put the file/symlink into the directory.
+ outputPath = fp.Join(rootOutputPath, rootName)
+ }
+
+ // If an object with the target name already exists overwrite it
+ if header.Typeflag == tar.TypeReg {
+ if err := te.extractFile(outputPath, tarReader); err != nil {
+ return err
+ }
+ } else if err := te.extractSymlink(outputPath, header); err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("unrecognized tar header type: %d", header.Typeflag)
+ }
+
+ // files come recursively in order
+ for {
+ header, err := tarReader.Next()
+ if err != nil && err != io.EOF {
+ return err
+ }
+ if header == nil || err == io.EOF {
+ break
+ }
+
+ // Make sure that we only have a single root element
+ if !firstObjectWasDir {
+ return fmt.Errorf("the root was not a directory and the tar has multiple entries: %w", errInvalidRoot)
+ }
+
+ // validate the path to remove paths we refuse to work with and make it easier to reason about
+ if err := validateTarPath(header.Name); err != nil {
+ return err
+ }
+ cleanedPath := header.Name
+
+ relPath, err := getRelativePath(rootName, cleanedPath)
+ if err != nil {
+ return err
+ }
+
+ outputPath, err := te.outputPath(rootOutputPath, relPath)
+ if err != nil {
+ return err
+ }
+
+ // This check should already be covered by previous validation, but may catch bugs that slip through.
+ // Checks if the relative path matches or exceeds the root
+ // We check for matching because the outputPath function strips the original root
+ rel, err := fp.Rel(rootOutputPath, outputPath)
+ if err != nil || rel == "." {
+ return errInvalidRootMultipleRoots
+ }
+ for _, e := range strings.Split(fp.ToSlash(rel), "/") {
+ if e == ".." {
+ return fmt.Errorf("relative path contains '..'")
+ }
+ }
+
+ switch header.Typeflag {
+ case tar.TypeDir:
+ if err := te.extractDir(outputPath); err != nil {
+ return err
+ }
+ case tar.TypeReg:
+ if err := te.extractFile(outputPath, tarReader); err != nil {
+ return err
+ }
+ case tar.TypeSymlink:
+ if err := te.extractSymlink(outputPath, header); err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("unrecognized tar header type: %d", header.Typeflag)
+ }
+ }
+ return nil
+}
+
+// validateTarPath returns an error if the path has problematic characters
+func validateTarPath(tarPath string) error {
+ if len(tarPath) == 0 {
+ return fmt.Errorf("path is empty")
+ }
+
+ if tarPath[0] == '/' {
+ return fmt.Errorf("%q : path starts with '/'", tarPath)
+ }
+
+ elems := strings.Split(tarPath, "/") // break into elems
+ for _, e := range elems {
+ switch e {
+ case "", ".", "..":
+ return fmt.Errorf("%q : path contains %q", tarPath, e)
+ }
+ }
+ return nil
+}
+
+// getRelativePath returns the relative path between rootTarPath and tarPath. Assumes both paths have been cleaned.
+// Will error if the tarPath is not below the rootTarPath.
+func getRelativePath(rootName, tarPath string) (string, error) {
+ if !strings.HasPrefix(tarPath, rootName+"/") {
+ return "", errInvalidRootMultipleRoots
+ }
+ return tarPath[len(rootName)+1:], nil
+}
+
+// outputPath returns the directory path at which to place the file relativeTarPath. Assumes relativeTarPath is cleaned.
+func (te *Extractor) outputPath(basePlatformPath, relativeTarPath string) (string, error) {
+ elems := strings.Split(relativeTarPath, "/")
+
+ platformPath := basePlatformPath
+ for i, e := range elems {
+ if err := validatePathComponent(e); err != nil {
+ return "", err
+ }
+ platformPath = fp.Join(platformPath, e)
+
+ // Last element is not checked since it will be removed (if it exists) by any of the extraction functions.
+ // For more details see:
+ // https://github.com/libarchive/libarchive/blob/0fd2ed25d78e9f4505de5dcb6208c6c0ff8d2edb/libarchive/archive_write_disk_posix.c#L2810
+ if i == len(elems)-1 {
+ break
+ }
+
+ fi, err := os.Lstat(platformPath)
+ if err != nil {
+ return "", err
+ }
+
+ if fi.Mode()&os.ModeSymlink != 0 {
+ return "", errTraverseSymlink
+ }
+ if !fi.Mode().IsDir() {
+ return "", errors.New("cannot traverse non-directory objects")
+ }
+ }
+
+ return platformPath, nil
+}
+
+var errExtractedDirToSymlink = errors.New("cannot extract to symlink")
+
+func (te *Extractor) extractDir(path string) error {
+ err := os.MkdirAll(path, 0755)
+ if err != nil {
+ return err
+ }
+
+ if stat, err := os.Lstat(path); err != nil {
+ return err
+ } else if !stat.IsDir() {
+ return errExtractedDirToSymlink
+ }
+ return nil
+}
+
+func (te *Extractor) extractSymlink(path string, h *tar.Header) error {
+ if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+
+ return os.Symlink(h.Linkname, path)
+}
+
+func (te *Extractor) extractFile(path string, r *tar.Reader) error {
+ // Attempt removing the target so we can overwrite files, symlinks and empty directories
+ if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+
+ // Create a temporary file in the target directory and then rename the temporary file to the target to better deal
+ // with races on the file system.
+ base := fp.Dir(path)
+ tmpfile, err := os.CreateTemp(base, "")
+ if err != nil {
+ return err
+ }
+ if err := copyWithProgress(tmpfile, r, te.Progress); err != nil {
+ _ = tmpfile.Close()
+ _ = os.Remove(tmpfile.Name())
+ return err
+ }
+ if err := tmpfile.Close(); err != nil {
+ _ = os.Remove(tmpfile.Name())
+ return err
+ }
+
+ if err := os.Rename(tmpfile.Name(), path); err != nil {
+ _ = os.Remove(tmpfile.Name())
+ return err
+ }
+
+ return nil
+}
+
+func copyWithProgress(to io.Writer, from io.Reader, cb func(int64) int64) error {
+ buf := make([]byte, 4096)
+ for {
+ n, err := from.Read(buf)
+ if n != 0 {
+ if cb != nil {
+ cb(int64(n))
+ }
+ _, err2 := to.Write(buf[:n])
+ if err2 != nil {
+ return err2
+ }
+ }
+ if err != nil {
+ if err == io.EOF {
+ return nil
+ }
+ return err
+ }
+ }
+}
diff --git a/tar/extractor_test.go b/tar/extractor_test.go
new file mode 100644
index 0000000000..717c65d196
--- /dev/null
+++ b/tar/extractor_test.go
@@ -0,0 +1,437 @@
+package tar
+
+import (
+ "archive/tar"
+ "bytes"
+ "fmt"
+ "io"
+ "os"
+ fp "path/filepath"
+ "runtime"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var symlinksEnabled bool
+var symlinksEnabledErr error
+
+func init() {
+ // check if the platform supports symlinks
+ // inspired by https://github.com/golang/go/blob/770f1de8c54256d5b17447028e47b201ba8e62c8/src/internal/testenv/testenv_windows.go#L17
+
+ tmpdir, err := os.MkdirTemp("", "platformsymtest")
+ if err != nil {
+ panic("failed to create temp directory: " + err.Error())
+ }
+ defer os.RemoveAll(tmpdir)
+
+ symlinksEnabledErr = os.Symlink("target", fp.Join(tmpdir, "symlink"))
+ symlinksEnabled = symlinksEnabledErr == nil
+
+ if !symlinksEnabled {
+ // for now assume symlinks only fail on Windows, Android and Plan9
+ // taken from https://github.com/golang/go/blob/770f1de8c54256d5b17447028e47b201ba8e62c8/src/internal/testenv/testenv_notwin.go#L14
+ // and https://github.com/golang/go/blob/770f1de8c54256d5b17447028e47b201ba8e62c8/src/internal/testenv/testenv_windows.go#L34
+ switch runtime.GOOS {
+ case "windows", "android", "plan9":
+ default:
+ panic(fmt.Errorf("attempted symlink creation failed: %w", symlinksEnabledErr))
+ }
+ }
+}
+
+func TestSingleFile(t *testing.T) {
+ fileName := "file..ext"
+ fileData := "file data"
+
+ testTarExtraction(t, nil, []tarEntry{
+ &fileTarEntry{fileName, []byte(fileData)},
+ },
+ func(t *testing.T, extractDir string) {
+ f, err := os.Open(fp.Join(extractDir, fileName))
+ assert.NoError(t, err)
+ data, err := io.ReadAll(f)
+ assert.NoError(t, err)
+ assert.Equal(t, fileData, string(data))
+ assert.NoError(t, f.Close())
+ },
+ nil,
+ )
+}
+
+func TestSingleDirectory(t *testing.T) {
+ dirName := "dir..sfx"
+
+ testTarExtraction(t, nil, []tarEntry{
+ &dirTarEntry{dirName},
+ },
+ func(t *testing.T, extractDir string) {
+ f, err := os.Open(extractDir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ objs, err := f.Readdir(1)
+ if err == io.EOF && len(objs) == 0 {
+ return
+ }
+ t.Fatalf("expected an empty directory")
+ },
+ nil,
+ )
+}
+
+func TestDirectoryFollowSymlinkToNothing(t *testing.T) {
+ dirName := "dir"
+ childName := "child"
+
+ entries := []tarEntry{
+ &dirTarEntry{dirName},
+ &dirTarEntry{dirName + "/" + childName},
+ }
+
+ testTarExtraction(t, func(t *testing.T, rootDir string) {
+ target := fp.Join(rootDir, tarOutRoot)
+ if err := os.Symlink(fp.Join(target, "foo"), fp.Join(target, childName)); err != nil {
+ t.Fatal(err)
+ }
+ }, entries, nil,
+ os.ErrExist,
+ )
+}
+
+func TestDirectoryFollowSymlinkToFile(t *testing.T) {
+ dirName := "dir"
+ childName := "child"
+
+ entries := []tarEntry{
+ &dirTarEntry{dirName},
+ &dirTarEntry{dirName + "/" + childName},
+ }
+
+ testTarExtraction(t, func(t *testing.T, rootDir string) {
+ target := fp.Join(rootDir, tarOutRoot)
+ symlinkTarget := fp.Join(target, "foo")
+ if err := os.WriteFile(symlinkTarget, []byte("original data"), os.ModePerm); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(symlinkTarget, fp.Join(target, childName)); err != nil {
+ t.Fatal(err)
+ }
+ }, entries, nil,
+ syscall.ENOTDIR,
+ )
+}
+
+func TestDirectoryFollowSymlinkToDirectory(t *testing.T) {
+ dirName := "dir"
+ childName := "child"
+
+ entries := []tarEntry{
+ &dirTarEntry{dirName},
+ &dirTarEntry{dirName + "/" + childName},
+ }
+
+ testTarExtraction(t, func(t *testing.T, rootDir string) {
+ target := fp.Join(rootDir, tarOutRoot)
+ symlinkTarget := fp.Join(target, "foo")
+ if err := os.Mkdir(symlinkTarget, os.ModePerm); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(symlinkTarget, fp.Join(target, childName)); err != nil {
+ t.Fatal(err)
+ }
+ }, entries, nil,
+ errExtractedDirToSymlink,
+ )
+}
+
+func TestSingleSymlink(t *testing.T) {
+ if !symlinksEnabled {
+ t.Skip("symlinks disabled on this platform", symlinksEnabledErr)
+ }
+
+ targetName := "file"
+ symlinkName := "symlink"
+
+ testTarExtraction(t, nil, []tarEntry{
+ &symlinkTarEntry{targetName, symlinkName},
+ }, func(t *testing.T, extractDir string) {
+ symlinkPath := fp.Join(extractDir, symlinkName)
+ fi, err := os.Lstat(symlinkPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, fi.Mode()&os.ModeSymlink != 0, true, "expected to be a symlink")
+ targetPath, err := os.Readlink(symlinkPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, targetName, targetPath)
+ }, nil)
+}
+
+func TestMultipleRoots(t *testing.T) {
+ testTarExtraction(t, nil, []tarEntry{
+ &dirTarEntry{"root"},
+ &dirTarEntry{"sibling"},
+ }, nil, errInvalidRoot)
+}
+
+func TestMultipleRootsNested(t *testing.T) {
+ testTarExtraction(t, nil, []tarEntry{
+ &dirTarEntry{"root/child1"},
+ &dirTarEntry{"root/child2"},
+ }, nil, errInvalidRoot)
+}
+
+func TestOutOfOrderRoot(t *testing.T) {
+ testTarExtraction(t, nil, []tarEntry{
+ &dirTarEntry{"root/child"},
+ &dirTarEntry{"root"},
+ }, nil, errInvalidRoot)
+}
+
+func TestOutOfOrder(t *testing.T) {
+ testTarExtraction(t, nil, []tarEntry{
+ &dirTarEntry{"root/child/grandchild"},
+ &dirTarEntry{"root/child"},
+ }, nil, errInvalidRoot)
+}
+
+func TestNestedDirectories(t *testing.T) {
+ testTarExtraction(t, nil, []tarEntry{
+ &dirTarEntry{"root"},
+ &dirTarEntry{"root/child"},
+ &dirTarEntry{"root/child/grandchild"},
+ }, func(t *testing.T, extractDir string) {
+ walkIndex := 0
+ err := fp.Walk(extractDir,
+ func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ switch walkIndex {
+ case 0:
+ assert.Equal(t, info.Name(), tarOutRoot)
+ case 1:
+ assert.Equal(t, info.Name(), "child")
+ case 2:
+ assert.Equal(t, info.Name(), "grandchild")
+ default:
+ assert.Fail(t, "has more than 3 entries", path)
+ }
+ walkIndex++
+ return nil
+ })
+ assert.NoError(t, err)
+ }, nil)
+}
+
+func TestRootDirectoryHasSubpath(t *testing.T) {
+ testTarExtraction(t, nil, []tarEntry{
+ &dirTarEntry{"root/child"},
+ &dirTarEntry{"root/child/grandchild"},
+ }, nil, errInvalidRoot)
+}
+
+func TestFilesAndFolders(t *testing.T) {
+ testTarExtraction(t, nil, []tarEntry{
+ &dirTarEntry{"root"},
+ &dirTarEntry{"root/childdir"},
+ &fileTarEntry{"root/childdir/file1", []byte("some data")},
+ }, nil, nil)
+}
+
+func TestInternalSymlinkTraverse(t *testing.T) {
+ if !symlinksEnabled {
+ t.Skip("symlinks disabled on this platform", symlinksEnabledErr)
+ }
+ testTarExtraction(t, nil, []tarEntry{
+ // FIXME: We are ignoring the first element in the path check so
+ // we add a directory at the start to bypass this.
+ &dirTarEntry{"root"},
+ &dirTarEntry{"root/child"},
+ &symlinkTarEntry{"child", "root/symlink-dir"},
+ &fileTarEntry{"root/symlink-dir/file", []byte("file")},
+ },
+ nil,
+ errTraverseSymlink,
+ )
+}
+
+func TestExternalSymlinkTraverse(t *testing.T) {
+ if !symlinksEnabled {
+ t.Skip("symlinks disabled on this platform", symlinksEnabledErr)
+ }
+ testTarExtraction(t, nil, []tarEntry{
+ // FIXME: We are ignoring the first element in the path check so
+ // we add a directory at the start to bypass this.
+ &dirTarEntry{"inner"},
+ &symlinkTarEntry{"..", "inner/symlink-dir"},
+ &fileTarEntry{"inner/symlink-dir/file", []byte("overwrite content")},
+ },
+ nil,
+ errTraverseSymlink,
+ )
+}
+
+func TestLastElementOverwrite(t *testing.T) {
+ if !symlinksEnabled {
+ t.Skip("symlinks disabled on this platform", symlinksEnabledErr)
+ }
+ const originalData = "original"
+ testTarExtraction(t, func(t *testing.T, rootDir string) {
+ // Create an outside target that will try to be overwritten.
+ // This file will reside outside of the extraction directory root.
+ f, err := os.Create(fp.Join(rootDir, "outside-ref"))
+ assert.NoError(t, err)
+ n, err := f.WriteString(originalData)
+ assert.NoError(t, err)
+ assert.Equal(t, len(originalData), n)
+ },
+ []tarEntry{
+ &dirTarEntry{"root"},
+ &symlinkTarEntry{"../outside-ref", "root/symlink"},
+ &fileTarEntry{"root/symlink", []byte("overwrite content")},
+ },
+ func(t *testing.T, extractDir string) {
+ // Check that outside-ref still exists but has not been
+ // overwritten or truncated (still size the same).
+ info, err := os.Stat(fp.Join(extractDir, "..", "outside-ref"))
+ assert.NoError(t, err)
+
+ assert.Equal(t, len(originalData), int(info.Size()), "outside reference has been overwritten")
+ },
+ nil,
+ )
+}
+
+const tarOutRoot = "tar-out-root"
+
+func testTarExtraction(t *testing.T, setup func(t *testing.T, rootDir string), tarEntries []tarEntry, check func(t *testing.T, extractDir string), extractError error) {
+ var err error
+
+ // Directory structure.
+ // FIXME: We can't easily work on a MemFS since we would need to replace
+ // all the `os` calls in the extractor so using a temporary dir.
+ rootDir, err := os.MkdirTemp("", "tar-extraction-test")
+ assert.NoError(t, err)
+ extractDir := fp.Join(rootDir, tarOutRoot)
+ err = os.MkdirAll(extractDir, 0755)
+ assert.NoError(t, err)
+
+ // Generated TAR file.
+ tarFilename := fp.Join(rootDir, "generated.tar")
+ tarFile, err := os.Create(tarFilename)
+ assert.NoError(t, err)
+ defer tarFile.Close()
+ tw := tar.NewWriter(tarFile)
+ defer tw.Close()
+
+ if setup != nil {
+ setup(t, rootDir)
+ }
+
+ writeTarFile(t, tarFilename, tarEntries)
+
+ testExtract(t, tarFilename, extractDir, extractError)
+
+ if check != nil {
+ check(t, extractDir)
+ }
+}
+
+func testExtract(t *testing.T, tarFile string, extractDir string, expectedError error) {
+ var err error
+
+ tarReader, err := os.Open(tarFile)
+ assert.NoError(t, err)
+
+ extractor := &Extractor{Path: extractDir}
+ err = extractor.Extract(tarReader)
+
+ assert.ErrorIs(t, err, expectedError)
+}
+
+// Based on the `writeXXXHeader` family of functions in
+// github.com/ipfs/go-ipfs-files@v0.0.8/tarwriter.go.
+func writeTarFile(t *testing.T, path string, entries []tarEntry) {
+ tarFile, err := os.Create(path)
+ assert.NoError(t, err)
+ defer tarFile.Close()
+
+ tw := tar.NewWriter(tarFile)
+ defer tw.Close()
+
+ for _, e := range entries {
+ err = e.write(tw)
+ assert.NoError(t, err)
+ }
+}
+
+type tarEntry interface {
+ write(tw *tar.Writer) error
+}
+
+var _ tarEntry = (*fileTarEntry)(nil)
+var _ tarEntry = (*dirTarEntry)(nil)
+var _ tarEntry = (*symlinkTarEntry)(nil)
+
+type fileTarEntry struct {
+ path string
+ buf []byte
+}
+
+func (e *fileTarEntry) write(tw *tar.Writer) error {
+ if err := writeFileHeader(tw, e.path, uint64(len(e.buf))); err != nil {
+ return err
+ }
+
+ if _, err := io.Copy(tw, bytes.NewReader(e.buf)); err != nil {
+ return err
+ }
+
+ tw.Flush()
+ return nil
+}
+func writeFileHeader(w *tar.Writer, fpath string, size uint64) error {
+ return w.WriteHeader(&tar.Header{
+ Name: fpath,
+ Size: int64(size),
+ Typeflag: tar.TypeReg,
+ Mode: 0644,
+ ModTime: time.Now(),
+ // TODO: set mode, dates, etc. when added to unixFS
+ })
+}
+
+type dirTarEntry struct {
+ path string
+}
+
+func (e *dirTarEntry) write(tw *tar.Writer) error {
+ return tw.WriteHeader(&tar.Header{
+ Name: e.path,
+ Typeflag: tar.TypeDir,
+ Mode: 0777,
+ ModTime: time.Now(),
+ // TODO: set mode, dates, etc. when added to unixFS
+ })
+}
+
+type symlinkTarEntry struct {
+ target string
+ path string
+}
+
+func (e *symlinkTarEntry) write(w *tar.Writer) error {
+ return w.WriteHeader(&tar.Header{
+ Name: e.path,
+ Linkname: e.target,
+ Mode: 0777,
+ Typeflag: tar.TypeSymlink,
+ })
+}
diff --git a/tar/sanitize.go b/tar/sanitize.go
new file mode 100644
index 0000000000..86fba1991b
--- /dev/null
+++ b/tar/sanitize.go
@@ -0,0 +1,30 @@
+//go:build !windows
+
+package tar
+
+import (
+ "fmt"
+ "os"
+ "strings"
+)
+
+func isNullDevice(path string) bool {
+ return path == os.DevNull
+}
+
+func validatePlatformPath(platformPath string) error {
+ if strings.Contains(platformPath, "\x00") {
+ return fmt.Errorf("invalid platform path: path components cannot contain null: %q", platformPath)
+ }
+ return nil
+}
+
+func validatePathComponent(c string) error {
+ if c == ".." {
+ return fmt.Errorf("invalid platform path: path component cannot be '..'")
+ }
+ if strings.Contains(c, "\x00") {
+ return fmt.Errorf("invalid platform path: path components cannot contain null: %q", c)
+ }
+ return nil
+}
diff --git a/tar/sanitize_windows.go b/tar/sanitize_windows.go
new file mode 100644
index 0000000000..4a788a4844
--- /dev/null
+++ b/tar/sanitize_windows.go
@@ -0,0 +1,74 @@
+package tar
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+)
+
+// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
+var reservedNames = [...]string{"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}
+
+const reservedCharsStr = `[<>:"\|?*]` + "\x00" //NOTE: `/` is not included as it is our standard path separator
+
+func isNullDevice(path string) bool {
+ // This is a case insensitive comparison to NUL
+ if len(path) != 3 {
+ return false
+ }
+ if path[0]|0x20 != 'n' {
+ return false
+ }
+ if path[1]|0x20 != 'u' {
+ return false
+ }
+ if path[2]|0x20 != 'l' {
+ return false
+ }
+ return true
+}
+
+// validatePathComponent returns an error if the given path component is not allowed on the platform
+func validatePathComponent(c string) error {
+ //MSDN: Do not end a file or directory name with a space or a period
+ if strings.HasSuffix(c, ".") {
+ return fmt.Errorf("invalid platform path: path components cannot end with '.' : %q", c)
+ }
+ if strings.HasSuffix(c, " ") {
+ return fmt.Errorf("invalid platform path: path components cannot end with ' ' : %q", c)
+ }
+
+ if c == ".." {
+ return fmt.Errorf("invalid platform path: path component cannot be '..'")
+ }
+ // error on reserved characters
+ if strings.ContainsAny(c, reservedCharsStr) {
+ return fmt.Errorf("invalid platform path: path components cannot contain any of %s : %q", reservedCharsStr, c)
+ }
+
+ // error on reserved names
+ for _, rn := range reservedNames {
+ if c == rn {
+ return fmt.Errorf("invalid platform path: path component is a reserved name: %s", c)
+ }
+ }
+
+ return nil
+}
+
+func validatePlatformPath(platformPath string) error {
+ // remove the volume name
+ p := platformPath[len(filepath.VolumeName(platformPath)):]
+
+ // convert to cleaned slash-path
+ p = filepath.ToSlash(p)
+ p = strings.Trim(p, "/")
+
+ // make sure all components of the path are valid
+ for _, e := range strings.Split(p, "/") {
+ if err := validatePathComponent(e); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/unixfs/README.md b/unixfs/README.md
new file mode 100644
index 0000000000..6c80d07c8c
--- /dev/null
+++ b/unixfs/README.md
@@ -0,0 +1,28 @@
+## Package Directory
+This package contains many subpackages, each of which can be very large on its own.
+
+### Top Level
+The top level unixfs package defines the unixfs format datastructures, and some helper methods around it.
+
+### importers
+The `importer` subpackage is what you'll use when you want to turn a normal file into a unixfs file.
+
+### io
+The `io` subpackage provides helpers for reading files and manipulating directories. The `DagReader` takes a
+reference to a unixfs file and returns a file handle that can be read from and seeked through. The `Directory`
+interface allows you to easily read items in a directory, add items to a directory, and do lookups.
+
+### mod
+The `mod` subpackage implements a `DagModifier` type that can be used to write to an existing unixfs file, or
+create a new one. The logic for this is significantly more complicated than for the dagreader, so its a separate
+type. (TODO: maybe it still belongs in the `io` subpackage though?)
+
+### hamt
+The `hamt` subpackage implements a CHAMP hamt that is used in unixfs directory sharding.
+
+### archive
+The `archive` subpackage implements a `tar` importer and exporter. The objects created here are not officially unixfs,
+but in the future, this may be integrated more directly.
+
+### test
+The `test` subpackage provides several utilities to make testing unixfs related things easier.
diff --git a/unixfs/file/unixfile.go b/unixfs/file/unixfile.go
new file mode 100644
index 0000000000..75e2cb218e
--- /dev/null
+++ b/unixfs/file/unixfile.go
@@ -0,0 +1,183 @@
+package unixfile
+
+import (
+ "context"
+ "errors"
+
+ ft "github.com/ipfs/boxo/unixfs"
+ uio "github.com/ipfs/boxo/unixfs/io"
+
+ "github.com/ipfs/boxo/files"
+ ipld "github.com/ipfs/go-ipld-format"
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+)
+
+// Number to file to prefetch in directories
+// TODO: should we allow setting this via context hint?
+const prefetchFiles = 4
+
+type ufsDirectory struct {
+ ctx context.Context
+ dserv ipld.DAGService
+ dir uio.Directory
+ size int64
+}
+
+type ufsIterator struct {
+ ctx context.Context
+ files chan *ipld.Link
+ dserv ipld.DAGService
+
+ curName string
+ curFile files.Node
+
+ err error
+ errCh chan error
+}
+
+func (it *ufsIterator) Name() string {
+ return it.curName
+}
+
+func (it *ufsIterator) Node() files.Node {
+ return it.curFile
+}
+
+func (it *ufsIterator) Next() bool {
+ if it.err != nil {
+ return false
+ }
+
+ var l *ipld.Link
+ var ok bool
+ for !ok {
+ if it.files == nil && it.errCh == nil {
+ return false
+ }
+ select {
+ case l, ok = <-it.files:
+ if !ok {
+ it.files = nil
+ }
+ case err := <-it.errCh:
+ it.errCh = nil
+ it.err = err
+
+ if err != nil {
+ return false
+ }
+ }
+ }
+
+ it.curFile = nil
+
+ nd, err := l.GetNode(it.ctx, it.dserv)
+ if err != nil {
+ it.err = err
+ return false
+ }
+
+ it.curName = l.Name
+ it.curFile, it.err = NewUnixfsFile(it.ctx, it.dserv, nd)
+ return it.err == nil
+}
+
+func (it *ufsIterator) Err() error {
+ return it.err
+}
+
+func (d *ufsDirectory) Close() error {
+ return nil
+}
+
+func (d *ufsDirectory) Entries() files.DirIterator {
+ fileCh := make(chan *ipld.Link, prefetchFiles)
+ errCh := make(chan error, 1)
+ go func() {
+ errCh <- d.dir.ForEachLink(d.ctx, func(link *ipld.Link) error {
+ if d.ctx.Err() != nil {
+ return d.ctx.Err()
+ }
+ select {
+ case fileCh <- link:
+ case <-d.ctx.Done():
+ return d.ctx.Err()
+ }
+ return nil
+ })
+
+ close(errCh)
+ close(fileCh)
+ }()
+
+ return &ufsIterator{
+ ctx: d.ctx,
+ files: fileCh,
+ errCh: errCh,
+ dserv: d.dserv,
+ }
+}
+
+func (d *ufsDirectory) Size() (int64, error) {
+ return d.size, nil
+}
+
+type ufsFile struct {
+ uio.DagReader
+}
+
+func (f *ufsFile) Size() (int64, error) {
+ return int64(f.DagReader.Size()), nil
+}
+
+func newUnixfsDir(ctx context.Context, dserv ipld.DAGService, nd *dag.ProtoNode) (files.Directory, error) {
+ dir, err := uio.NewDirectoryFromNode(dserv, nd)
+ if err != nil {
+ return nil, err
+ }
+
+ size, err := nd.Size()
+ if err != nil {
+ return nil, err
+ }
+
+ return &ufsDirectory{
+ ctx: ctx,
+ dserv: dserv,
+
+ dir: dir,
+ size: int64(size),
+ }, nil
+}
+
+func NewUnixfsFile(ctx context.Context, dserv ipld.DAGService, nd ipld.Node) (files.Node, error) {
+ switch dn := nd.(type) {
+ case *dag.ProtoNode:
+ fsn, err := ft.FSNodeFromBytes(dn.Data())
+ if err != nil {
+ return nil, err
+ }
+ if fsn.IsDir() {
+ return newUnixfsDir(ctx, dserv, dn)
+ }
+ if fsn.Type() == ft.TSymlink {
+ return files.NewLinkFile(string(fsn.Data()), nil), nil
+ }
+
+ case *dag.RawNode:
+ default:
+ return nil, errors.New("unknown node type")
+ }
+
+ dr, err := uio.NewDagReader(ctx, nd, dserv)
+ if err != nil {
+ return nil, err
+ }
+
+ return &ufsFile{
+ DagReader: dr,
+ }, nil
+}
+
+var _ files.Directory = &ufsDirectory{}
+var _ files.File = &ufsFile{}
diff --git a/unixfs/hamt/hamt.go b/unixfs/hamt/hamt.go
new file mode 100644
index 0000000000..06d284eb4c
--- /dev/null
+++ b/unixfs/hamt/hamt.go
@@ -0,0 +1,946 @@
+// Package hamt implements a Hash Array Mapped Trie over ipfs merkledag nodes.
+// It is implemented mostly as described in the wikipedia article on HAMTs,
+// however the table size is variable (usually 256 in our usages) as opposed to
+// 32 as suggested in the article. The hash function used is currently
+// Murmur3, but this value is configurable (the datastructure reports which
+// hash function its using).
+//
+// The one algorithmic change we implement that is not mentioned in the
+// wikipedia article is the collapsing of empty shards.
+// Given the following tree: ( '[' = shards, '{' = values )
+// [ 'A' ] -> [ 'B' ] -> { "ABC" }
+//
+// | L-> { "ABD" }
+// L-> { "ASDF" }
+//
+// If we simply removed "ABC", we would end up with a tree where shard 'B' only
+// has a single child. This causes two issues, the first, is that now we have
+// an extra lookup required to get to "ABD". The second issue is that now we
+// have a tree that contains only "ABD", but is not the same tree that we would
+// get by simply inserting "ABD" into a new tree. To address this, we always
+// check for empty shard nodes upon deletion and prune them to maintain a
+// consistent tree, independent of insertion order.
+package hamt
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "sync"
+
+ "golang.org/x/sync/errgroup"
+
+ format "github.com/ipfs/boxo/unixfs"
+ "github.com/ipfs/boxo/unixfs/internal"
+
+ bitfield "github.com/ipfs/go-bitfield"
+ cid "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+)
+
+const (
+ // HashMurmur3 is the multiformats identifier for Murmur3
+ HashMurmur3 uint64 = 0x22
+)
+
+func init() {
+ internal.HAMTHashFunction = murmur3Hash
+}
+
+func (ds *Shard) isValueNode() bool {
+ return ds.key != "" && ds.val != nil
+}
+
+// A Shard represents the HAMT. It should be initialized with NewShard().
+type Shard struct {
+ childer *childer
+
+ // Entries per node (number of possible childs indexed by the partial key).
+ tableSize int
+ // Bits needed to encode child indexes (log2 of number of entries). This is
+ // the number of bits taken from the hash key on each level of the tree.
+ tableSizeLg2 int
+
+ builder cid.Builder
+ hashFunc uint64
+
+ // String format with number of zeros that will be present in the hexadecimal
+ // encoding of the child index to always reach the fixed maxpadlen chars.
+ // Example: maxpadlen = 4 => prefixPadStr: "%04X" (print number in hexadecimal
+ // format padding with zeros to always reach 4 characters).
+ prefixPadStr string
+ // Length in chars of string that encodes child indexes. We encode indexes
+ // as hexadecimal strings to this is log4 of number of entries.
+ maxpadlen int
+
+ dserv ipld.DAGService
+
+ // FIXME: Remove. We don't actually store "value nodes". This confusing
+ // abstraction just removes the maxpadlen from the link names to extract
+ // the actual value link the trie is storing.
+ // leaf node
+ key string
+ val *ipld.Link
+}
+
+// NewShard creates a new, empty HAMT shard with the given size.
+func NewShard(dserv ipld.DAGService, size int) (*Shard, error) {
+ return NewShardValue(dserv, size, "", nil)
+}
+
+// NewShardValue creates a new, empty HAMT shard with the given key, value and size.
+func NewShardValue(dserv ipld.DAGService, size int, key string, value *ipld.Link) (*Shard, error) {
+ ds, err := makeShard(dserv, size, key, value)
+ if err != nil {
+ return nil, err
+ }
+
+ // FIXME: Make this at least a static configuration for testing.
+ ds.hashFunc = HashMurmur3
+ return ds, nil
+}
+
+func makeShard(ds ipld.DAGService, size int, key string, val *ipld.Link) (*Shard, error) {
+ lg2s, err := Logtwo(size)
+ if err != nil {
+ return nil, err
+ }
+ childer, err := newChilder(ds, size)
+ if err != nil {
+ return nil, err
+ }
+ maxpadding := fmt.Sprintf("%X", size-1)
+ s := &Shard{
+ tableSizeLg2: lg2s,
+ prefixPadStr: fmt.Sprintf("%%0%dX", len(maxpadding)),
+ maxpadlen: len(maxpadding),
+ childer: childer,
+ tableSize: size,
+ dserv: ds,
+
+ key: key,
+ val: val,
+ }
+
+ s.childer.sd = s
+
+ return s, nil
+}
+
+// NewHamtFromDag creates new a HAMT shard from the given DAG.
+func NewHamtFromDag(dserv ipld.DAGService, nd ipld.Node) (*Shard, error) {
+ pbnd, ok := nd.(*dag.ProtoNode)
+ if !ok {
+ return nil, dag.ErrNotProtobuf
+ }
+
+ fsn, err := format.FSNodeFromBytes(pbnd.Data())
+ if err != nil {
+ return nil, err
+ }
+
+ if fsn.Type() != format.THAMTShard {
+ return nil, fmt.Errorf("node was not a dir shard")
+ }
+
+ if fsn.HashType() != HashMurmur3 {
+ return nil, fmt.Errorf("only murmur3 supported as hash function")
+ }
+
+ size := int(fsn.Fanout())
+
+ ds, err := makeShard(dserv, size, "", nil)
+ if err != nil {
+ return nil, err
+ }
+
+ ds.childer.makeChilder(fsn.Data(), pbnd.Links())
+
+ ds.hashFunc = fsn.HashType()
+ ds.builder = pbnd.CidBuilder()
+
+ return ds, nil
+}
+
+// SetCidBuilder sets the CID Builder
+func (ds *Shard) SetCidBuilder(builder cid.Builder) {
+ ds.builder = builder
+}
+
+// CidBuilder gets the CID Builder, may be nil if unset
+func (ds *Shard) CidBuilder() cid.Builder {
+ return ds.builder
+}
+
+// Node serializes the HAMT structure into a merkledag node with unixfs formatting
+func (ds *Shard) Node() (ipld.Node, error) {
+ out := new(dag.ProtoNode)
+ out.SetCidBuilder(ds.builder)
+
+ sliceIndex := 0
+ // TODO: optimized 'for each set bit'
+ for childIndex := 0; childIndex < ds.tableSize; childIndex++ {
+ if !ds.childer.has(childIndex) {
+ continue
+ }
+
+ ch := ds.childer.child(sliceIndex)
+ if ch != nil {
+ clnk, err := ch.Link()
+ if err != nil {
+ return nil, err
+ }
+
+ err = out.AddRawLink(ds.linkNamePrefix(childIndex)+ch.key, clnk)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ // child unloaded, just copy in link with updated name
+ lnk := ds.childer.link(sliceIndex)
+ label := lnk.Name[ds.maxpadlen:]
+
+ err := out.AddRawLink(ds.linkNamePrefix(childIndex)+label, lnk)
+ if err != nil {
+ return nil, err
+ }
+ }
+ sliceIndex++
+ }
+
+ data, err := format.HAMTShardData(ds.childer.bitfield.Bytes(), uint64(ds.tableSize), HashMurmur3)
+ if err != nil {
+ return nil, err
+ }
+
+ out.SetData(data)
+
+ err = ds.dserv.Add(context.TODO(), out)
+ if err != nil {
+ return nil, err
+ }
+
+ return out, nil
+}
+
+func (ds *Shard) makeShardValue(lnk *ipld.Link) (*Shard, error) {
+ lnk2 := *lnk
+ s, err := makeShard(ds.dserv, ds.tableSize, "", nil)
+ if err != nil {
+ return nil, err
+ }
+
+ s.key = lnk.Name[ds.maxpadlen:]
+ s.val = &lnk2
+
+ return s, nil
+}
+
+// Set sets 'name' = nd in the HAMT
+func (ds *Shard) Set(ctx context.Context, name string, nd ipld.Node) error {
+ _, err := ds.Swap(ctx, name, nd)
+ return err
+}
+
+// Set sets 'name' = nd in the HAMT, using directly the information in the
+// given link. This avoids writing the given node, then reading it to making a
+// link out of it.
+func (ds *Shard) SetLink(ctx context.Context, name string, lnk *ipld.Link) error {
+ hv := newHashBits(name)
+
+ newLink := ipld.Link{
+ Name: lnk.Name,
+ Size: lnk.Size,
+ Cid: lnk.Cid,
+ }
+
+ // FIXME: We don't need to set the name here, it will get overwritten.
+ // This is confusing, confirm and remove this line.
+ newLink.Name = ds.linkNamePrefix(0) + name
+
+ _, err := ds.swapValue(ctx, hv, name, &newLink)
+ return err
+}
+
+// Swap sets a link pointing to the passed node as the value under the
+// name key in this Shard or its children. It also returns the previous link
+// under that name key (if any).
+func (ds *Shard) Swap(ctx context.Context, name string, node ipld.Node) (*ipld.Link, error) {
+ hv := newHashBits(name)
+ err := ds.dserv.Add(ctx, node)
+ if err != nil {
+ return nil, err
+ }
+
+ lnk, err := ipld.MakeLink(node)
+ if err != nil {
+ return nil, err
+ }
+
+ // FIXME: We don't need to set the name here, it will get overwritten.
+ // This is confusing, confirm and remove this line.
+ lnk.Name = ds.linkNamePrefix(0) + name
+
+ return ds.swapValue(ctx, hv, name, lnk)
+}
+
+// Remove deletes the named entry if it exists. Otherwise, it returns
+// os.ErrNotExist.
+func (ds *Shard) Remove(ctx context.Context, name string) error {
+ _, err := ds.Take(ctx, name)
+ return err
+}
+
+// Take is similar to the public Remove but also returns the
+// old removed link (if it exists).
+func (ds *Shard) Take(ctx context.Context, name string) (*ipld.Link, error) {
+ hv := newHashBits(name)
+ return ds.swapValue(ctx, hv, name, nil)
+}
+
+// Find searches for a child node by 'name' within this hamt
+func (ds *Shard) Find(ctx context.Context, name string) (*ipld.Link, error) {
+ hv := newHashBits(name)
+
+ var out *ipld.Link
+ err := ds.getValue(ctx, hv, name, func(sv *Shard) error {
+ out = sv.val
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return out, nil
+}
+
+type linkType int
+
+const (
+ invalidLink linkType = iota
+ shardLink
+ shardValueLink
+)
+
+func (ds *Shard) childLinkType(lnk *ipld.Link) (linkType, error) {
+ if len(lnk.Name) < ds.maxpadlen {
+ return invalidLink, fmt.Errorf("invalid link name '%s'", lnk.Name)
+ }
+ if len(lnk.Name) == ds.maxpadlen {
+ return shardLink, nil
+ }
+ return shardValueLink, nil
+}
+
+// Link returns a merklelink to this shard node
+func (ds *Shard) Link() (*ipld.Link, error) {
+ if ds.isValueNode() {
+ return ds.val, nil
+ }
+
+ nd, err := ds.Node()
+ if err != nil {
+ return nil, err
+ }
+
+ err = ds.dserv.Add(context.TODO(), nd)
+ if err != nil {
+ return nil, err
+ }
+
+ return ipld.MakeLink(nd)
+}
+
+func (ds *Shard) getValue(ctx context.Context, hv *hashBits, key string, cb func(*Shard) error) error {
+ childIndex, err := hv.Next(ds.tableSizeLg2)
+ if err != nil {
+ return err
+ }
+
+ if ds.childer.has(childIndex) {
+ child, err := ds.childer.get(ctx, ds.childer.sliceIndex(childIndex))
+ if err != nil {
+ return err
+ }
+
+ if child.isValueNode() {
+ if child.key == key {
+ return cb(child)
+ }
+ } else {
+ return child.getValue(ctx, hv, key, cb)
+ }
+ }
+
+ return os.ErrNotExist
+}
+
+// EnumLinks collects all links in the Shard.
+func (ds *Shard) EnumLinks(ctx context.Context) ([]*ipld.Link, error) {
+ var links []*ipld.Link
+
+ linkResults := ds.EnumLinksAsync(ctx)
+
+ for linkResult := range linkResults {
+ if linkResult.Err != nil {
+ return links, linkResult.Err
+ }
+ links = append(links, linkResult.Link)
+ }
+ return links, nil
+}
+
+// ForEachLink walks the Shard and calls the given function.
+func (ds *Shard) ForEachLink(ctx context.Context, f func(*ipld.Link) error) error {
+ return ds.walkTrie(ctx, func(sv *Shard) error {
+ lnk := sv.val
+ lnk.Name = sv.key
+
+ return f(lnk)
+ })
+}
+
+// EnumLinksAsync returns a channel which will receive Links in the directory
+// as they are enumerated, where order is not guaranteed
+func (ds *Shard) EnumLinksAsync(ctx context.Context) <-chan format.LinkResult {
+ linkResults := make(chan format.LinkResult)
+ ctx, cancel := context.WithCancel(ctx)
+ go func() {
+ defer close(linkResults)
+ defer cancel()
+
+ err := parallelShardWalk(ctx, ds, ds.dserv, func(formattedLink *ipld.Link) error {
+ emitResult(ctx, linkResults, format.LinkResult{Link: formattedLink, Err: nil})
+ return nil
+ })
+ if err != nil {
+ emitResult(ctx, linkResults, format.LinkResult{Link: nil, Err: err})
+ }
+ }()
+ return linkResults
+}
+
+type listCidsAndShards struct {
+ cids []cid.Cid
+ shards []*Shard
+}
+
+func (ds *Shard) walkChildren(processLinkValues func(formattedLink *ipld.Link) error) (*listCidsAndShards, error) {
+ res := &listCidsAndShards{}
+
+ for idx, lnk := range ds.childer.links {
+ if nextShard := ds.childer.children[idx]; nextShard == nil {
+ lnkLinkType, err := ds.childLinkType(lnk)
+ if err != nil {
+ return nil, err
+ }
+
+ switch lnkLinkType {
+ case shardValueLink:
+ sv, err := ds.makeShardValue(lnk)
+ if err != nil {
+ return nil, err
+ }
+ formattedLink := sv.val
+ formattedLink.Name = sv.key
+
+ if err := processLinkValues(formattedLink); err != nil {
+ return nil, err
+ }
+ case shardLink:
+ res.cids = append(res.cids, lnk.Cid)
+ default:
+ return nil, fmt.Errorf("unsupported shard link type")
+ }
+
+ } else {
+ if nextShard.val != nil {
+ formattedLink := &ipld.Link{
+ Name: nextShard.key,
+ Size: nextShard.val.Size,
+ Cid: nextShard.val.Cid,
+ }
+ if err := processLinkValues(formattedLink); err != nil {
+ return nil, err
+ }
+ } else {
+ res.shards = append(res.shards, nextShard)
+ }
+ }
+ }
+ return res, nil
+}
+
+// parallelShardWalk is quite similar to the DAG walking algorithm from https://github.com/ipfs/boxo/ipld/merkledag/blob/594e515f162e764183243b72c2ba84f743424c8c/merkledag.go#L464
+// However, there are a few notable differences:
+// 1. Some children are actualized Shard structs and some are in the blockstore, this will leverage walking over the in memory Shards as well as the stored blocks
+// 2. Instead of just passing each child into the worker pool by itself we group them so that we can leverage optimizations from GetMany.
+// This optimization also makes the walk a little more biased towards depth (as opposed to BFS) in the earlier part of the DAG.
+// This is particularly helpful for operations like estimating the directory size which should complete quickly when possible.
+// 3. None of the extra options from that package are needed
+func parallelShardWalk(ctx context.Context, root *Shard, dserv ipld.DAGService, processShardValues func(formattedLink *ipld.Link) error) error {
+ const concurrency = 32
+
+ var visitlk sync.Mutex
+ visitSet := cid.NewSet()
+ visit := visitSet.Visit
+
+ // Setup synchronization
+ grp, errGrpCtx := errgroup.WithContext(ctx)
+
+ // Input and output queues for workers.
+ feed := make(chan *listCidsAndShards)
+ out := make(chan *listCidsAndShards)
+ done := make(chan struct{})
+
+ for i := 0; i < concurrency; i++ {
+ grp.Go(func() error {
+ for feedChildren := range feed {
+ for _, nextShard := range feedChildren.shards {
+ nextChildren, err := nextShard.walkChildren(processShardValues)
+ if err != nil {
+ return err
+ }
+
+ select {
+ case out <- nextChildren:
+ case <-errGrpCtx.Done():
+ return nil
+ }
+ }
+
+ var linksToVisit []cid.Cid
+ for _, nextCid := range feedChildren.cids {
+ var shouldVisit bool
+
+ visitlk.Lock()
+ shouldVisit = visit(nextCid)
+ visitlk.Unlock()
+
+ if shouldVisit {
+ linksToVisit = append(linksToVisit, nextCid)
+ }
+ }
+
+ chNodes := dserv.GetMany(errGrpCtx, linksToVisit)
+ for optNode := range chNodes {
+ if optNode.Err != nil {
+ return optNode.Err
+ }
+
+ nextShard, err := NewHamtFromDag(dserv, optNode.Node)
+ if err != nil {
+ return err
+ }
+
+ nextChildren, err := nextShard.walkChildren(processShardValues)
+ if err != nil {
+ return err
+ }
+
+ select {
+ case out <- nextChildren:
+ case <-errGrpCtx.Done():
+ return nil
+ }
+ }
+
+ select {
+ case done <- struct{}{}:
+ case <-errGrpCtx.Done():
+ }
+ }
+ return nil
+ })
+ }
+
+ send := feed
+ var todoQueue []*listCidsAndShards
+ var inProgress int
+
+ next := &listCidsAndShards{
+ shards: []*Shard{root},
+ }
+
+dispatcherLoop:
+ for {
+ select {
+ case send <- next:
+ inProgress++
+ if len(todoQueue) > 0 {
+ next = todoQueue[0]
+ todoQueue = todoQueue[1:]
+ } else {
+ next = nil
+ send = nil
+ }
+ case <-done:
+ inProgress--
+ if inProgress == 0 && next == nil {
+ break dispatcherLoop
+ }
+ case nextNodes := <-out:
+ if next == nil {
+ next = nextNodes
+ send = feed
+ } else {
+ todoQueue = append(todoQueue, nextNodes)
+ }
+ case <-errGrpCtx.Done():
+ break dispatcherLoop
+ }
+ }
+ close(feed)
+ return grp.Wait()
+}
+
+func emitResult(ctx context.Context, linkResults chan<- format.LinkResult, r format.LinkResult) {
+ // make sure that context cancel is processed first
+ // the reason is due to the concurrency of EnumerateChildrenAsync
+ // it's possible for EnumLinksAsync to complete and close the linkResults
+ // channel before this code runs
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ }
+ select {
+ case linkResults <- r:
+ case <-ctx.Done():
+ }
+}
+
+func (ds *Shard) walkTrie(ctx context.Context, cb func(*Shard) error) error {
+ return ds.childer.each(ctx, func(s *Shard) error {
+ if s.isValueNode() {
+ if err := cb(s); err != nil {
+ return err
+ }
+ } else {
+ if err := s.walkTrie(ctx, cb); err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+}
+
+// swapValue sets the link `value` in the given key, either creating the entry
+// if it didn't exist or overwriting the old one. It returns the old entry (if any).
+func (ds *Shard) swapValue(ctx context.Context, hv *hashBits, key string, value *ipld.Link) (*ipld.Link, error) {
+ idx, err := hv.Next(ds.tableSizeLg2)
+ if err != nil {
+ return nil, err
+ }
+
+ if !ds.childer.has(idx) {
+ // Entry does not exist, create a new one.
+ return nil, ds.childer.insert(key, value, idx)
+ }
+
+ i := ds.childer.sliceIndex(idx)
+ child, err := ds.childer.get(ctx, i)
+ if err != nil {
+ return nil, err
+ }
+
+ if child.isValueNode() {
+ // Leaf node. This is the base case of this recursive function.
+ if child.key == key {
+ // We are in the correct shard (tree level) so we modify this child
+ // and return.
+ oldValue := child.val
+
+ if value == nil { // Remove old entry.
+ return oldValue, ds.childer.rm(idx)
+ }
+
+ child.val = value // Overwrite entry.
+ return oldValue, nil
+ }
+
+ if value == nil {
+ return nil, os.ErrNotExist
+ }
+
+ // We are in the same slot with another entry with a different key
+ // so we need to fork this leaf node into a shard with two childs:
+ // the old entry and the new one being inserted here.
+ // We don't overwrite anything here so we keep:
+ // `oldValue = nil`
+
+ // The child of this shard will now be a new shard. The old child value
+ // will be a child of this new shard (along with the new value being
+ // inserted).
+ grandChild := child
+ child, err = NewShard(ds.dserv, ds.tableSize)
+ if err != nil {
+ return nil, err
+ }
+ child.builder = ds.builder
+ chhv := newConsumedHashBits(grandChild.key, hv.consumed)
+
+ // We explicitly ignore the oldValue returned by the next two insertions
+ // (which will be nil) to highlight there is no overwrite here: they are
+ // done with different keys to a new (empty) shard. (At best this shard
+ // will create new ones until we find different slots for both.)
+ _, err = child.swapValue(ctx, hv, key, value)
+ if err != nil {
+ return nil, err
+ }
+ _, err = child.swapValue(ctx, chhv, grandChild.key, grandChild.val)
+ if err != nil {
+ return nil, err
+ }
+
+ // Replace this leaf node with the new Shard node.
+ ds.childer.set(child, i)
+ return nil, nil
+ } else {
+ // We are in a Shard (internal node). We will recursively call this
+ // function until finding the leaf (the logic of the `if` case above).
+ oldValue, err := child.swapValue(ctx, hv, key, value)
+ if err != nil {
+ return nil, err
+ }
+
+ if value == nil {
+ // We have removed an entry, check if we should remove shards
+ // as well.
+ switch child.childer.length() {
+ case 0:
+ // empty sub-shard, prune it
+ // Note: this shouldnt normally ever happen
+ // in the event of another implementation creates flawed
+ // structures, this will help to normalize them.
+ return oldValue, ds.childer.rm(idx)
+ case 1:
+ // The single child _should_ be a value by
+ // induction. However, we allow for it to be a
+ // shard in case an implementation is broken.
+
+ // Have we loaded the child? Prefer that.
+ schild := child.childer.child(0)
+ if schild != nil {
+ if schild.isValueNode() {
+ ds.childer.set(schild, i)
+ }
+ return oldValue, nil
+ }
+
+ // Otherwise, work with the link.
+ slnk := child.childer.link(0)
+ var lnkType linkType
+ lnkType, err = child.childer.sd.childLinkType(slnk)
+ if err != nil {
+ return nil, err
+ }
+ if lnkType == shardValueLink {
+ // sub-shard with a single value element, collapse it
+ ds.childer.setLink(slnk, i)
+ }
+ return oldValue, nil
+ }
+ }
+
+ return oldValue, nil
+ }
+}
+
+// linkNamePrefix takes in the bitfield index of an entry and returns its hex prefix
+func (ds *Shard) linkNamePrefix(idx int) string {
+ return fmt.Sprintf(ds.prefixPadStr, idx)
+}
+
+// childer wraps the links, children and bitfield
+// and provides basic operation (get, rm, insert and set) of manipulating children.
+// The slices `links` and `children` are always coordinated to have the entries
+// in the same index. A `childIndex` belonging to one of the original `Shard.size`
+// entries corresponds to a `sliceIndex` in `links` and `children` (the conversion
+// is done through `bitfield`).
+type childer struct {
+ sd *Shard
+ dserv ipld.DAGService
+ bitfield bitfield.Bitfield
+
+ // Only one of links/children will be non-nil for every child/link.
+ links []*ipld.Link
+ children []*Shard
+}
+
+const maximumHamtWidth = 1 << 10 // FIXME: Spec this and decide of a correct value
+
+func newChilder(ds ipld.DAGService, size int) (*childer, error) {
+ if size > maximumHamtWidth {
+ return nil, fmt.Errorf("hamt witdh (%d) exceed maximum allowed (%d)", size, maximumHamtWidth)
+ }
+ bf, err := bitfield.NewBitfield(size)
+ if err != nil {
+ return nil, err
+ }
+
+ return &childer{
+ dserv: ds,
+ bitfield: bf,
+ }, nil
+}
+
+func (s *childer) makeChilder(data []byte, links []*ipld.Link) *childer {
+ s.children = make([]*Shard, len(links))
+ s.bitfield.SetBytes(data)
+ if len(links) > 0 {
+ s.links = make([]*ipld.Link, len(links))
+ copy(s.links, links)
+ }
+
+ return s
+}
+
+// Return the `sliceIndex` associated with a child.
+func (s *childer) sliceIndex(childIndex int) (sliceIndex int) {
+ return s.bitfield.OnesBefore(childIndex)
+}
+
+func (s *childer) child(sliceIndex int) *Shard {
+ return s.children[sliceIndex]
+}
+
+func (s *childer) link(sliceIndex int) *ipld.Link {
+ return s.links[sliceIndex]
+}
+
+func (s *childer) insert(key string, lnk *ipld.Link, idx int) error {
+ if lnk == nil {
+ return os.ErrNotExist
+ }
+
+ lnk.Name = s.sd.linkNamePrefix(idx) + key
+ i := s.sliceIndex(idx)
+
+ sd, err := NewShardValue(s.dserv, 256, key, lnk)
+ if err != nil {
+ return err
+ }
+
+ s.children = append(s.children[:i], append([]*Shard{sd}, s.children[i:]...)...)
+ s.links = append(s.links[:i], append([]*ipld.Link{nil}, s.links[i:]...)...)
+ // Add a `nil` placeholder in `links` so the rest of the entries keep the same
+ // index as `children`.
+ s.bitfield.SetBit(idx)
+
+ return nil
+}
+
+func (s *childer) set(sd *Shard, i int) {
+ s.children[i] = sd
+ s.links[i] = nil
+}
+
+func (s *childer) setLink(lnk *ipld.Link, i int) {
+ s.children[i] = nil
+ s.links[i] = lnk
+}
+
+func (s *childer) rm(childIndex int) error {
+ i := s.sliceIndex(childIndex)
+
+ if err := s.check(i); err != nil {
+ return err
+ }
+
+ copy(s.children[i:], s.children[i+1:])
+ s.children = s.children[:len(s.children)-1]
+
+ copy(s.links[i:], s.links[i+1:])
+ s.links = s.links[:len(s.links)-1]
+
+ s.bitfield.UnsetBit(childIndex)
+
+ return nil
+}
+
+// get returns the i'th child of this shard. If it is cached in the
+// children array, it will return it from there. Otherwise, it loads the child
+// node from disk.
+func (s *childer) get(ctx context.Context, sliceIndex int) (*Shard, error) {
+ if err := s.check(sliceIndex); err != nil {
+ return nil, err
+ }
+
+ c := s.child(sliceIndex)
+ if c != nil {
+ return c, nil
+ }
+
+ return s.loadChild(ctx, sliceIndex)
+}
+
+// loadChild reads the i'th child node of this shard from disk and returns it
+// as a 'child' interface
+func (s *childer) loadChild(ctx context.Context, sliceIndex int) (*Shard, error) {
+ lnk := s.link(sliceIndex)
+ lnkLinkType, err := s.sd.childLinkType(lnk)
+ if err != nil {
+ return nil, err
+ }
+
+ var c *Shard
+ if lnkLinkType == shardLink {
+ nd, err := lnk.GetNode(ctx, s.dserv)
+ if err != nil {
+ return nil, err
+ }
+ cds, err := NewHamtFromDag(s.dserv, nd)
+ if err != nil {
+ return nil, err
+ }
+
+ c = cds
+ } else {
+ s, err := s.sd.makeShardValue(lnk)
+ if err != nil {
+ return nil, err
+ }
+ c = s
+ }
+
+ s.set(c, sliceIndex)
+
+ return c, nil
+}
+
+func (s *childer) has(childIndex int) bool {
+ return s.bitfield.Bit(childIndex)
+}
+
+func (s *childer) length() int {
+ return len(s.children)
+}
+
+func (s *childer) each(ctx context.Context, cb func(*Shard) error) error {
+ for i := range s.children {
+ c, err := s.get(ctx, i)
+ if err != nil {
+ return err
+ }
+
+ if err := cb(c); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (s *childer) check(sliceIndex int) error {
+ if sliceIndex >= len(s.children) || sliceIndex < 0 {
+ return fmt.Errorf("invalid index passed to operate children (likely corrupt bitfield)")
+ }
+
+ if len(s.children) != len(s.links) {
+ return fmt.Errorf("inconsistent lengths between children array and Links array")
+ }
+
+ return nil
+}
diff --git a/unixfs/hamt/hamt_stress_test.go b/unixfs/hamt/hamt_stress_test.go
new file mode 100644
index 0000000000..07c38fbb42
--- /dev/null
+++ b/unixfs/hamt/hamt_stress_test.go
@@ -0,0 +1,291 @@
+package hamt
+
+import (
+ "context"
+ "fmt"
+ "math/rand"
+ "os"
+ "testing"
+ "time"
+
+ ft "github.com/ipfs/boxo/unixfs"
+ mdtest "github.com/ipfs/boxo/ipld/merkledag/test"
+
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+func getNames(prefix string, count int) []string {
+ out := make([]string, count)
+ for i := 0; i < count; i++ {
+ out[i] = fmt.Sprintf("%s%d", prefix, i)
+ }
+ return out
+}
+
+const (
+ opAdd = iota
+ opDel
+ opFind
+)
+
+type testOp struct {
+ Op int
+ Val string
+}
+
+func stringArrToSet(arr []string) map[string]bool {
+ out := make(map[string]bool)
+ for _, s := range arr {
+ out[s] = true
+ }
+ return out
+}
+
+// generate two different random sets of operations to result in the same
+// ending directory (same set of entries at the end) and execute each of them
+// in turn, then compare to ensure the output is the same on each.
+func TestOrderConsistency(t *testing.T) {
+ seed := time.Now().UnixNano()
+ t.Logf("using seed = %d", seed)
+ ds := mdtest.Mock()
+
+ shardWidth := 1024
+
+ keep := getNames("good", 4000)
+ temp := getNames("tempo", 6000)
+
+ ops := genOpSet(seed, keep, temp)
+ s, err := executeOpSet(t, ds, shardWidth, ops)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = validateOpSetCompletion(t, s, keep, temp)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ops2 := genOpSet(seed+1000, keep, temp)
+ s2, err := executeOpSet(t, ds, shardWidth, ops2)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = validateOpSetCompletion(t, s2, keep, temp)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nd, err := s.Node()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nd2, err := s2.Node()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ k := nd.Cid()
+ k2 := nd2.Cid()
+
+ if !k.Equals(k2) {
+ t.Fatal("got different results: ", k, k2)
+ }
+}
+
+func validateOpSetCompletion(t *testing.T, s *Shard, keep, temp []string) error {
+ ctx := context.TODO()
+ for _, n := range keep {
+ _, err := s.Find(ctx, n)
+ if err != nil {
+ return fmt.Errorf("couldnt find %s: %s", n, err)
+ }
+ }
+
+ for _, n := range temp {
+ _, err := s.Find(ctx, n)
+ if err != os.ErrNotExist {
+ return fmt.Errorf("expected not to find: %s", err)
+ }
+ }
+
+ return nil
+}
+
+func executeOpSet(t *testing.T, ds ipld.DAGService, width int, ops []testOp) (*Shard, error) {
+ ctx := context.TODO()
+ s, err := NewShard(ds, width)
+ if err != nil {
+ return nil, err
+ }
+
+ e := ft.EmptyDirNode()
+ ds.Add(ctx, e)
+
+ for _, o := range ops {
+ switch o.Op {
+ case opAdd:
+ err := s.Set(ctx, o.Val, e)
+ if err != nil {
+ return nil, fmt.Errorf("inserting %s: %s", o.Val, err)
+ }
+ case opDel:
+ err := s.Remove(ctx, o.Val)
+ if err != nil {
+ return nil, fmt.Errorf("deleting %s: %s", o.Val, err)
+ }
+ case opFind:
+ _, err := s.Find(ctx, o.Val)
+ if err != nil {
+ return nil, fmt.Errorf("finding %s: %s", o.Val, err)
+ }
+ }
+ }
+
+ return s, nil
+}
+
+func genOpSet(seed int64, keep, temp []string) []testOp {
+ tempset := stringArrToSet(temp)
+
+ allnames := append(keep, temp...)
+ shuffle(seed, allnames)
+
+ var todel []string
+
+ var ops []testOp
+
+ for {
+ n := len(allnames) + len(todel)
+ if n == 0 {
+ return ops
+ }
+
+ rn := rand.Intn(n)
+
+ if rn < len(allnames) {
+ next := allnames[0]
+ allnames = allnames[1:]
+ ops = append(ops, testOp{
+ Op: opAdd,
+ Val: next,
+ })
+
+ if tempset[next] {
+ todel = append(todel, next)
+ }
+ } else {
+ shuffle(seed+100, todel)
+ next := todel[0]
+ todel = todel[1:]
+
+ ops = append(ops, testOp{
+ Op: opDel,
+ Val: next,
+ })
+ }
+ }
+}
+
+// executes the given op set with a repl to allow easier debugging
+/*func debugExecuteOpSet(ds node.DAGService, width int, ops []testOp) (*Shard, error) {
+
+ s, err := NewShard(ds, width)
+ if err != nil {
+ return nil, err
+ }
+
+ e := ft.EmptyDirNode()
+ ds.Add(e)
+ ctx := context.TODO()
+
+ run := 0
+
+ opnames := map[int]string{
+ opAdd: "add",
+ opDel: "del",
+ }
+
+mainloop:
+ for i := 0; i < len(ops); i++ {
+ o := ops[i]
+
+ fmt.Printf("Op %d: %s %s\n", i, opnames[o.Op], o.Val)
+ for run == 0 {
+ cmd := readCommand()
+ parts := strings.Split(cmd, " ")
+ switch parts[0] {
+ case "":
+ run = 1
+ case "find":
+ _, err := s.Find(ctx, parts[1])
+ if err == nil {
+ fmt.Println("success")
+ } else {
+ fmt.Println(err)
+ }
+ case "run":
+ if len(parts) > 1 {
+ n, err := strconv.Atoi(parts[1])
+ if err != nil {
+ panic(err)
+ }
+
+ run = n
+ } else {
+ run = -1
+ }
+ case "lookop":
+ for k = 0; k < len(ops); k++ {
+ if ops[k].Val == parts[1] {
+ fmt.Printf(" Op %d: %s %s\n", k, opnames[ops[k].Op], parts[1])
+ }
+ }
+ case "restart":
+ var err error
+ s, err = NewShard(ds, width)
+ if err != nil {
+ panic(err)
+ }
+ i = -1
+ continue mainloop
+ case "print":
+ nd, err := s.Node()
+ if err != nil {
+ panic(err)
+ }
+ printDag(ds, nd.(*dag.ProtoNode), 0)
+ }
+ }
+ run--
+
+ switch o.Op {
+ case opAdd:
+ err := s.Set(ctx, o.Val, e)
+ if err != nil {
+ return nil, fmt.Errorf("inserting %s: %s", o.Val, err)
+ }
+ case opDel:
+ fmt.Println("deleting: ", o.Val)
+ err := s.Remove(ctx, o.Val)
+ if err != nil {
+ return nil, fmt.Errorf("deleting %s: %s", o.Val, err)
+ }
+ case opFind:
+ _, err := s.Find(ctx, o.Val)
+ if err != nil {
+ return nil, fmt.Errorf("finding %s: %s", o.Val, err)
+ }
+ }
+ }
+
+ return s, nil
+}
+
+func readCommand() string {
+ fmt.Print("> ")
+ scan := bufio.NewScanner(os.Stdin)
+ scan.Scan()
+ return scan.Text()
+}*/
diff --git a/unixfs/hamt/hamt_test.go b/unixfs/hamt/hamt_test.go
new file mode 100644
index 0000000000..b4f7704759
--- /dev/null
+++ b/unixfs/hamt/hamt_test.go
@@ -0,0 +1,752 @@
+package hamt
+
+import (
+ "context"
+ "fmt"
+ "math/rand"
+ "os"
+ "sort"
+ "testing"
+ "time"
+
+ ft "github.com/ipfs/boxo/unixfs"
+ ipld "github.com/ipfs/go-ipld-format"
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+ mdtest "github.com/ipfs/boxo/ipld/merkledag/test"
+)
+
+func shuffle(seed int64, arr []string) {
+ r := rand.New(rand.NewSource(seed))
+ for i := 0; i < len(arr); i++ {
+ a := r.Intn(len(arr))
+ b := r.Intn(len(arr))
+ arr[a], arr[b] = arr[b], arr[a]
+ }
+}
+
+func makeDir(ds ipld.DAGService, size int) ([]string, *Shard, error) {
+ return makeDirWidth(ds, size, 256)
+}
+
+func makeDirWidth(ds ipld.DAGService, size, width int) ([]string, *Shard, error) {
+ ctx := context.Background()
+
+ s, err := NewShard(ds, width)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ var dirs []string
+ for i := 0; i < size; i++ {
+ dirs = append(dirs, fmt.Sprintf("DIRNAME%d", i))
+ }
+
+ shuffle(time.Now().UnixNano(), dirs)
+
+ for i := 0; i < len(dirs); i++ {
+ nd := ft.EmptyDirNode()
+ err := ds.Add(ctx, nd)
+ if err != nil {
+ return nil, nil, err
+ }
+ err = s.Set(ctx, dirs[i], nd)
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+
+ return dirs, s, nil
+}
+
+func assertLink(s *Shard, name string, found bool) error {
+ _, err := s.Find(context.Background(), name)
+ switch err {
+ case os.ErrNotExist:
+ if found {
+ return err
+ }
+
+ return nil
+ case nil:
+ if found {
+ return nil
+ }
+
+ return fmt.Errorf("expected not to find link named %s", name)
+ default:
+ return err
+ }
+}
+
+func assertLinksEqual(linksA []*ipld.Link, linksB []*ipld.Link) error {
+
+ if len(linksA) != len(linksB) {
+ return fmt.Errorf("links arrays are different sizes")
+ }
+
+ sort.Stable(dag.LinkSlice(linksA))
+ sort.Stable(dag.LinkSlice(linksB))
+ for i, a := range linksA {
+ b := linksB[i]
+ if a.Name != b.Name {
+ return fmt.Errorf("links names mismatch")
+ }
+
+ if a.Cid.String() != b.Cid.String() {
+ return fmt.Errorf("link hashes dont match")
+ }
+
+ if a.Size != b.Size {
+ return fmt.Errorf("link sizes not the same")
+ }
+ }
+
+ return nil
+}
+
+func assertSerializationWorks(ds ipld.DAGService, s *Shard) error {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ nd, err := s.Node()
+ if err != nil {
+ return err
+ }
+
+ nds, err := NewHamtFromDag(ds, nd)
+ if err != nil {
+ return err
+ }
+
+ linksA, err := s.EnumLinks(ctx)
+ if err != nil {
+ return err
+ }
+
+ linksB, err := nds.EnumLinks(ctx)
+ if err != nil {
+ return err
+ }
+
+ return assertLinksEqual(linksA, linksB)
+}
+
+func TestBasicSet(t *testing.T) {
+ ds := mdtest.Mock()
+ for _, w := range []int{128, 256, 512, 1024} {
+ t.Run(fmt.Sprintf("BasicSet%d", w), func(t *testing.T) {
+ names, s, err := makeDirWidth(ds, 1000, w)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ctx := context.Background()
+
+ for _, d := range names {
+ _, err := s.Find(ctx, d)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ })
+ }
+}
+
+func TestDirBuilding(t *testing.T) {
+ ds := mdtest.Mock()
+ _, _ = NewShard(ds, 256)
+
+ _, s, err := makeDir(ds, 200)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nd, err := s.Node()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ //printDag(ds, nd, 0)
+
+ k := nd.Cid()
+
+ if k.String() != "QmY89TkSEVHykWMHDmyejSWFj9CYNtvzw4UwnT9xbc4Zjc" {
+ t.Fatalf("output didnt match what we expected (got %s)", k.String())
+ }
+}
+
+func TestShardReload(t *testing.T) {
+ ds := mdtest.Mock()
+ _, _ = NewShard(ds, 256)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ _, s, err := makeDir(ds, 200)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nd, err := s.Node()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nds, err := NewHamtFromDag(ds, nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ lnks, err := nds.EnumLinks(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(lnks) != 200 {
+ t.Fatal("not enough links back")
+ }
+
+ _, err = nds.Find(ctx, "DIRNAME50")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Now test roundtrip marshal with no operations
+
+ nds, err = NewHamtFromDag(ds, nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ond, err := nds.Node()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ outk := ond.Cid()
+ ndk := nd.Cid()
+
+ if !outk.Equals(ndk) {
+ t.Fatal("roundtrip serialization failed")
+ }
+}
+
+func TestRemoveElems(t *testing.T) {
+ ds := mdtest.Mock()
+ dirs, s, err := makeDir(ds, 500)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ctx := context.Background()
+
+ for i := 0; i < 100; i++ {
+ err := s.Remove(ctx, fmt.Sprintf("NOTEXIST%d", rand.Int()))
+ if err != os.ErrNotExist {
+ t.Fatal("shouldnt be able to remove things that don't exist")
+ }
+ }
+
+ for _, d := range dirs {
+ _, err := s.Find(ctx, d)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ shuffle(time.Now().UnixNano(), dirs)
+
+ for _, d := range dirs {
+ err := s.Remove(ctx, d)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ nd, err := s.Node()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(nd.Links()) > 0 {
+ t.Fatal("shouldnt have any links here")
+ }
+
+ err = s.Remove(ctx, "doesnt exist")
+ if err != os.ErrNotExist {
+ t.Fatal("expected error does not exist")
+ }
+}
+
+func TestRemoveAfterMarshal(t *testing.T) {
+ ds := mdtest.Mock()
+ dirs, s, err := makeDir(ds, 500)
+ if err != nil {
+ t.Fatal(err)
+ }
+ nd, err := s.Node()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ s, err = NewHamtFromDag(ds, nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ctx := context.Background()
+
+ shuffle(time.Now().UnixNano(), dirs)
+
+ for i, d := range dirs {
+ err := s.Remove(ctx, d)
+ if err != nil {
+ t.Fatalf("%d/%d: %s", i, len(dirs), err)
+ }
+ }
+
+ nd, err = s.Node()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(nd.Links()) > 0 {
+ t.Fatal("shouldnt have any links here")
+ }
+
+ err = s.Remove(ctx, "doesnt exist")
+ if err != os.ErrNotExist {
+ t.Fatal("expected error does not exist")
+ }
+}
+
+func TestSetAfterMarshal(t *testing.T) {
+ ds := mdtest.Mock()
+ _, s, err := makeDir(ds, 300)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ctx := context.Background()
+
+ nd, err := s.Node()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nds, err := NewHamtFromDag(ds, nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for i := 0; i < 100; i++ {
+ empty := ft.EmptyDirNode()
+ err := nds.Set(ctx, fmt.Sprintf("moredirs%d", i), empty)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ nd, err = nds.Node()
+ if err != nil {
+ t.Fatal(err)
+ }
+ nds, err = NewHamtFromDag(ds, nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ links, err := nds.EnumLinks(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(links) != 400 {
+ t.Fatal("expected 400 links")
+ }
+
+ err = assertSerializationWorks(ds, nds)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestEnumLinksAsync(t *testing.T) {
+ ds := mdtest.Mock()
+ _, s, err := makeDir(ds, 300)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ctx := context.Background()
+
+ nd, err := s.Node()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nds, err := NewHamtFromDag(ds, nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ linksA, err := nds.EnumLinks(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ linkResults := nds.EnumLinksAsync(ctx)
+
+ var linksB []*ipld.Link
+
+ for linkResult := range linkResults {
+ if linkResult.Err != nil {
+ t.Fatal(linkResult.Err)
+ }
+ linksB = append(linksB, linkResult.Link)
+ }
+
+ err = assertLinksEqual(linksA, linksB)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestDuplicateAddShard(t *testing.T) {
+ ds := mdtest.Mock()
+ dir, _ := NewShard(ds, 256)
+ nd := new(dag.ProtoNode)
+ ctx := context.Background()
+
+ err := dir.Set(ctx, "test", nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = dir.Set(ctx, "test", nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ node, err := dir.Node()
+ if err != nil {
+ t.Fatal(err)
+ }
+ dir, err = NewHamtFromDag(ds, node)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ lnks, err := dir.EnumLinks(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(lnks) != 1 {
+ t.Fatal("expected only one link")
+ }
+}
+
+// fix https://github.com/ipfs/kubo/issues/9063
+func TestSetLink(t *testing.T) {
+ ds := mdtest.Mock()
+ dir, _ := NewShard(ds, 256)
+ _, s, err := makeDir(ds, 300)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ lnk, err := s.Link()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ctx := context.Background()
+
+ err = dir.SetLink(ctx, "test", lnk)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(dir.childer.children) != 1 {
+ t.Fatal("no child")
+ }
+
+ for _, sh := range dir.childer.children {
+ if sh.childer == nil {
+ t.Fatal("no childer on shard")
+ }
+ }
+}
+
+func TestLoadFailsFromNonShard(t *testing.T) {
+ ds := mdtest.Mock()
+ nd := ft.EmptyDirNode()
+
+ _, err := NewHamtFromDag(ds, nd)
+ if err == nil {
+ t.Fatal("expected dir shard creation to fail when given normal directory")
+ }
+
+ nd = new(dag.ProtoNode)
+
+ _, err = NewHamtFromDag(ds, nd)
+ if err == nil {
+ t.Fatal("expected dir shard creation to fail when given normal directory")
+ }
+}
+
+func TestFindNonExisting(t *testing.T) {
+ ds := mdtest.Mock()
+ _, s, err := makeDir(ds, 100)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ctx := context.Background()
+
+ for i := 0; i < 200; i++ {
+ _, err := s.Find(ctx, fmt.Sprintf("notfound%d", i))
+ if err != os.ErrNotExist {
+ t.Fatal("expected ErrNotExist")
+ }
+ }
+}
+
+func TestRemoveElemsAfterMarshal(t *testing.T) {
+ ds := mdtest.Mock()
+ dirs, s, err := makeDir(ds, 30)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ctx := context.Background()
+
+ sort.Strings(dirs)
+
+ err = s.Remove(ctx, dirs[0])
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := s.Find(ctx, dirs[0])
+ if err == nil {
+ t.Fatal("expected error, got: ", out)
+ }
+
+ nd, err := s.Node()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nds, err := NewHamtFromDag(ds, nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = nds.Find(ctx, dirs[0])
+ if err == nil {
+ t.Fatal("expected not to find ", dirs[0])
+ }
+
+ for _, d := range dirs[1:] {
+ _, err := nds.Find(ctx, d)
+ if err != nil {
+ t.Fatal("could not find expected link after unmarshaling")
+ }
+ }
+
+ for _, d := range dirs[1:] {
+ err := nds.Remove(ctx, d)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ nd, err = nds.Node()
+ if err != nil {
+ t.Fatal(err)
+ }
+ nds, err = NewHamtFromDag(ds, nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ links, err := nds.EnumLinks(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(links) != 0 {
+ t.Fatal("expected all links to be removed")
+ }
+
+ err = assertSerializationWorks(ds, nds)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestBitfieldIndexing(t *testing.T) {
+ ds := mdtest.Mock()
+ s, _ := NewShard(ds, 256)
+
+ set := func(i int) {
+ s.childer.bitfield.SetBit(i)
+ }
+
+ assert := func(i int, val int) {
+ if s.childer.sliceIndex(i) != val {
+ t.Fatalf("expected index %d to be %d", i, val)
+ }
+ }
+
+ assert(50, 0)
+ set(4)
+ set(5)
+ set(60)
+
+ assert(10, 2)
+ set(3)
+ assert(10, 3)
+ assert(1, 0)
+
+ assert(100, 4)
+ set(50)
+ assert(45, 3)
+ set(100)
+ assert(100, 5)
+}
+
+// test adding a sharded directory node as the child of another directory node.
+// if improperly implemented, the parent hamt may assume the child is a part of
+// itself.
+func TestSetHamtChild(t *testing.T) {
+ ctx := context.Background()
+
+ ds := mdtest.Mock()
+ s, _ := NewShard(ds, 256)
+
+ e := ft.EmptyDirNode()
+ ds.Add(ctx, e)
+
+ err := s.Set(ctx, "bar", e)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ snd, err := s.Node()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, ns, err := makeDir(ds, 50)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = ns.Set(ctx, "foo", snd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nsnd, err := ns.Node()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ hs, err := NewHamtFromDag(ds, nsnd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = assertLink(hs, "bar", false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = assertLink(hs, "foo", true)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func BenchmarkHAMTWalk(b *testing.B) {
+ ctx := context.Background()
+
+ ds := mdtest.Mock()
+ sh, _ := NewShard(ds, 256)
+ nd, err := sh.Node()
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ err = ds.Add(ctx, nd)
+ if err != nil {
+ b.Fatal(err)
+ }
+ ds.Add(ctx, ft.EmptyDirNode())
+
+ s, err := NewHamtFromDag(ds, nd)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ for j := 0; j < 1000; j++ {
+ err = s.Set(ctx, fmt.Sprintf("%d", j), ft.EmptyDirNode())
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+
+ for i := 0; i < b.N; i++ {
+ cnt := 0
+ err = s.ForEachLink(ctx, func(l *ipld.Link) error {
+ cnt++
+ return nil
+ })
+ if err != nil {
+ b.Fatal(err)
+ }
+ if cnt < 1000 {
+ b.Fatal("expected 100 children")
+ }
+ }
+}
+
+func BenchmarkHAMTSet(b *testing.B) {
+ ctx := context.Background()
+
+ ds := mdtest.Mock()
+ sh, _ := NewShard(ds, 256)
+ nd, err := sh.Node()
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ err = ds.Add(ctx, nd)
+ if err != nil {
+ b.Fatal(err)
+ }
+ ds.Add(ctx, ft.EmptyDirNode())
+
+ for i := 0; i < b.N; i++ {
+ s, err := NewHamtFromDag(ds, nd)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ err = s.Set(context.TODO(), fmt.Sprint(i), ft.EmptyDirNode())
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ out, err := s.Node()
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ nd = out
+ }
+}
+
+func TestHamtBadSize(t *testing.T) {
+ for _, size := range [...]int{-8, 7, 2, 1337, 1024 + 8, -3} {
+ _, err := NewShard(nil, size)
+ if err == nil {
+ t.Errorf("should have failed to construct hamt with bad size: %d", size)
+ }
+ }
+}
diff --git a/unixfs/hamt/util.go b/unixfs/hamt/util.go
new file mode 100644
index 0000000000..2216a217f6
--- /dev/null
+++ b/unixfs/hamt/util.go
@@ -0,0 +1,80 @@
+package hamt
+
+import (
+ "fmt"
+ "math/bits"
+
+ "github.com/ipfs/boxo/unixfs/internal"
+
+ "github.com/spaolacci/murmur3"
+)
+
+// hashBits is a helper that allows the reading of the 'next n bits' as an integer.
+type hashBits struct {
+ b []byte
+ consumed int
+}
+
+func newHashBits(val string) *hashBits {
+ return &hashBits{b: internal.HAMTHashFunction([]byte(val))}
+}
+
+func newConsumedHashBits(val string, consumed int) *hashBits {
+ hv := &hashBits{b: internal.HAMTHashFunction([]byte(val))}
+ hv.consumed = consumed
+ return hv
+}
+
+func mkmask(n int) byte {
+ return (1 << uint(n)) - 1
+}
+
+// Next returns the next 'i' bits of the hashBits value as an integer, or an
+// error if there aren't enough bits.
+func (hb *hashBits) Next(i int) (int, error) {
+ if hb.consumed+i > len(hb.b)*8 {
+ return 0, fmt.Errorf("sharded directory too deep")
+ }
+ return hb.next(i), nil
+}
+
+func (hb *hashBits) next(i int) int {
+ curbi := hb.consumed / 8
+ leftb := 8 - (hb.consumed % 8)
+
+ curb := hb.b[curbi]
+ if i == leftb {
+ out := int(mkmask(i) & curb)
+ hb.consumed += i
+ return out
+ } else if i < leftb {
+ a := curb & mkmask(leftb) // mask out the high bits we don't want
+ b := a & ^mkmask(leftb-i) // mask out the low bits we don't want
+ c := b >> uint(leftb-i) // shift whats left down
+ hb.consumed += i
+ return int(c)
+ } else {
+ out := int(mkmask(leftb) & curb)
+ out <<= uint(i - leftb)
+ hb.consumed += leftb
+ out += hb.next(i - leftb)
+ return out
+ }
+}
+
+func Logtwo(v int) (int, error) {
+ if v <= 0 {
+ return 0, fmt.Errorf("hamt size should be a power of two")
+ }
+ lg2 := bits.TrailingZeros(uint(v))
+ if 1<= 0; coff -= 4096 {
+ t.Log(coff)
+ n, err := rs.Seek(coff, io.SeekStart)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n != coff {
+ t.Fatal("wasnt able to seek to the right position")
+ }
+ nread, err := rs.Read(out[coff : coff+4096])
+ if err != nil {
+ t.Fatal(err)
+ }
+ if nread != 4096 {
+ t.Fatal("didnt read the correct number of bytes")
+ }
+ }
+
+ err = arrComp(out, should)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/unixfs/importer/balanced/builder.go b/unixfs/importer/balanced/builder.go
new file mode 100644
index 0000000000..6b5a7e074d
--- /dev/null
+++ b/unixfs/importer/balanced/builder.go
@@ -0,0 +1,267 @@
+// Package balanced provides methods to build balanced DAGs, which are generalistic
+// DAGs in which all leaves (nodes representing chunks of data) are at the same
+// distance from the root. Nodes can have only a maximum number of children; to be
+// able to store more leaf data nodes balanced DAGs are extended by increasing its
+// depth (and having more intermediary nodes).
+//
+// Internal nodes are always represented by UnixFS nodes (of type `File`) encoded
+// inside DAG nodes (see the `go-unixfs` package for details of UnixFS). In
+// contrast, leaf nodes with data have multiple possible representations: UnixFS
+// nodes as above, raw nodes with just the file data (no format) and Filestore
+// nodes (that directly link to the file on disk using a format stored on a raw
+// node, see the `go-ipfs/filestore` package for details of Filestore.)
+//
+// In the case the entire file fits into just one node it will be formatted as a
+// (single) leaf node (without parent) with the possible representations already
+// mentioned. This is the only scenario where the root can be of a type different
+// that the UnixFS node.
+//
+// Notes:
+//
+// 1. In the implementation. `FSNodeOverDag` structure is used for representing
+// the UnixFS node encoded inside the DAG node.
+// (see https://github.com/ipfs/go-ipfs/pull/5118.)
+//
+// 2. `TFile` is used for backwards-compatibility. It was a bug causing the leaf
+// nodes to be generated with this type instead of `TRaw`. The former one
+// should be used (like the trickle builder does).
+// (See https://github.com/ipfs/go-ipfs/pull/5120.)
+//
+// +-------------+
+// | Root 4 |
+// +-------------+
+// |
+// +--------------------------+----------------------------+
+// | |
+// +-------------+ +-------------+
+// | Node 2 | | Node 5 |
+// +-------------+ +-------------+
+// | |
+// +-------------+-------------+ +-------------+
+// | | |
+// +-------------+ +-------------+ +-------------+
+// | Node 1 | | Node 3 | | Node 6 |
+// +-------------+ +-------------+ +-------------+
+// | | |
+// +------+------+ +------+------+ +------+
+// | | | | |
+// +=========+ +=========+ +=========+ +=========+ +=========+
+// | Chunk 1 | | Chunk 2 | | Chunk 3 | | Chunk 4 | | Chunk 5 |
+// +=========+ +=========+ +=========+ +=========+ +=========+
+package balanced
+
+import (
+ "errors"
+
+ ft "github.com/ipfs/boxo/unixfs"
+ h "github.com/ipfs/boxo/unixfs/importer/helpers"
+
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+// Layout builds a balanced DAG layout. In a balanced DAG of depth 1, leaf nodes
+// with data are added to a single `root` until the maximum number of links is
+// reached. Then, to continue adding more data leaf nodes, a `newRoot` is created
+// pointing to the old `root` (which will now become and intermediary node),
+// increasing the depth of the DAG to 2. This will increase the maximum number of
+// data leaf nodes the DAG can have (`Maxlinks() ^ depth`). The `fillNodeRec`
+// function will add more intermediary child nodes to `newRoot` (which already has
+// `root` as child) that in turn will have leaf nodes with data added to them.
+// After that process is completed (the maximum number of links is reached),
+// `fillNodeRec` will return and the loop will be repeated: the `newRoot` created
+// will become the old `root` and a new root will be created again to increase the
+// depth of the DAG. The process is repeated until there is no more data to add
+// (i.e. the DagBuilderHelper’s Done() function returns true).
+//
+// The nodes are filled recursively, so the DAG is built from the bottom up. Leaf
+// nodes are created first using the chunked file data and its size. The size is
+// then bubbled up to the parent (internal) node, which aggregates all the sizes of
+// its children and bubbles that combined size up to its parent, and so on up to
+// the root. This way, a balanced DAG acts like a B-tree when seeking to a byte
+// offset in the file the graph represents: each internal node uses the file size
+// of its children as an index when seeking.
+//
+// `Layout` creates a root and hands it off to be filled:
+//
+// +-------------+
+// | Root 1 |
+// +-------------+
+// |
+// ( fillNodeRec fills in the )
+// ( chunks on the root. )
+// |
+// +------+------+
+// | |
+// + - - - - + + - - - - +
+// | Chunk 1 | | Chunk 2 |
+// + - - - - + + - - - - +
+//
+// ↓
+// When the root is full but there's more data...
+// ↓
+//
+// +-------------+
+// | Root 1 |
+// +-------------+
+// |
+// +------+------+
+// | |
+// +=========+ +=========+ + - - - - +
+// | Chunk 1 | | Chunk 2 | | Chunk 3 |
+// +=========+ +=========+ + - - - - +
+//
+// ↓
+// ...Layout's job is to create a new root.
+// ↓
+//
+// +-------------+
+// | Root 2 |
+// +-------------+
+// |
+// +-------------+ - - - - - - - - +
+// | |
+// +-------------+ ( fillNodeRec creates the )
+// | Node 1 | ( branch that connects )
+// +-------------+ ( "Root 2" to "Chunk 3." )
+// | |
+// +------+------+ + - - - - -+
+// | | |
+// +=========+ +=========+ + - - - - +
+// | Chunk 1 | | Chunk 2 | | Chunk 3 |
+// +=========+ +=========+ + - - - - +
+func Layout(db *h.DagBuilderHelper) (ipld.Node, error) {
+ if db.Done() {
+ // No data, return just an empty node.
+ root, err := db.NewLeafNode(nil, ft.TFile)
+ if err != nil {
+ return nil, err
+ }
+ // This works without Filestore support (`ProcessFileStore`).
+ // TODO: Why? Is there a test case missing?
+
+ return root, db.Add(root)
+ }
+
+ // The first `root` will be a single leaf node with data
+ // (corner case), after that subsequent `root` nodes will
+ // always be internal nodes (with a depth > 0) that can
+ // be handled by the loop.
+ root, fileSize, err := db.NewLeafDataNode(ft.TFile)
+ if err != nil {
+ return nil, err
+ }
+
+ // Each time a DAG of a certain `depth` is filled (because it
+ // has reached its maximum capacity of `db.Maxlinks()` per node)
+ // extend it by making it a sub-DAG of a bigger DAG with `depth+1`.
+ for depth := 1; !db.Done(); depth++ {
+
+ // Add the old `root` as a child of the `newRoot`.
+ newRoot := db.NewFSNodeOverDag(ft.TFile)
+ err = newRoot.AddChild(root, fileSize, db)
+ if err != nil {
+ return nil, err
+ }
+
+ // Fill the `newRoot` (that has the old `root` already as child)
+ // and make it the current `root` for the next iteration (when
+ // it will become "old").
+ root, fileSize, err = fillNodeRec(db, newRoot, depth)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return root, db.Add(root)
+}
+
+// fillNodeRec will "fill" the given internal (non-leaf) `node` with data by
+// adding child nodes to it, either leaf data nodes (if `depth` is 1) or more
+// internal nodes with higher depth (and calling itself recursively on them
+// until *they* are filled with data). The data to fill the node with is
+// provided by DagBuilderHelper.
+//
+// `node` represents a (sub-)DAG root that is being filled. If called recursively,
+// it is `nil`, a new node is created. If it has been called from `Layout` (see
+// diagram below) it points to the new root (that increases the depth of the DAG),
+// it already has a child (the old root). New children will be added to this new
+// root, and those children will in turn be filled (calling `fillNodeRec`
+// recursively).
+//
+// +-------------+
+// | `node` |
+// | (new root) |
+// +-------------+
+// |
+// +-------------+ - - - - - - + - - - - - - - - - - - +
+// | | |
+// +--------------+ + - - - - - + + - - - - - +
+// | (old root) | | new child | | |
+// +--------------+ + - - - - - + + - - - - - +
+// | | |
+// +------+------+ + - - + - - - +
+// | | | |
+// +=========+ +=========+ + - - - - + + - - - - +
+// | Chunk 1 | | Chunk 2 | | Chunk 3 | | Chunk 4 |
+// +=========+ +=========+ + - - - - + + - - - - +
+//
+// The `node` to be filled uses the `FSNodeOverDag` abstraction that allows adding
+// child nodes without packing/unpacking the UnixFS layer node (having an internal
+// `ft.FSNode` cache).
+//
+// It returns the `ipld.Node` representation of the passed `node` filled with
+// children and the `nodeFileSize` with the total size of the file chunk (leaf)
+// nodes stored under this node (parent nodes store this to enable efficient
+// seeking through the DAG when reading data later).
+//
+// warning: **children** pinned indirectly, but input node IS NOT pinned.
+func fillNodeRec(db *h.DagBuilderHelper, node *h.FSNodeOverDag, depth int) (filledNode ipld.Node, nodeFileSize uint64, err error) {
+ if depth < 1 {
+ return nil, 0, errors.New("attempt to fillNode at depth < 1")
+ }
+
+ if node == nil {
+ node = db.NewFSNodeOverDag(ft.TFile)
+ }
+
+ // Child node created on every iteration to add to parent `node`.
+ // It can be a leaf node or another internal node.
+ var childNode ipld.Node
+ // File size from the child node needed to update the `FSNode`
+ // in `node` when adding the child.
+ var childFileSize uint64
+
+ // While we have room and there is data available to be added.
+ for node.NumChildren() < db.Maxlinks() && !db.Done() {
+
+ if depth == 1 {
+ // Base case: add leaf node with data.
+ childNode, childFileSize, err = db.NewLeafDataNode(ft.TFile)
+ if err != nil {
+ return nil, 0, err
+ }
+ } else {
+ // Recursion case: create an internal node to in turn keep
+ // descending in the DAG and adding child nodes to it.
+ childNode, childFileSize, err = fillNodeRec(db, nil, depth-1)
+ if err != nil {
+ return nil, 0, err
+ }
+ }
+
+ err = node.AddChild(childNode, childFileSize, db)
+ if err != nil {
+ return nil, 0, err
+ }
+ }
+
+ nodeFileSize = node.FileSize()
+
+ // Get the final `dag.ProtoNode` with the `FSNode` data encoded inside.
+ filledNode, err = node.Commit()
+ if err != nil {
+ return nil, 0, err
+ }
+
+ return filledNode, nodeFileSize, nil
+}
diff --git a/unixfs/importer/helpers/dagbuilder.go b/unixfs/importer/helpers/dagbuilder.go
new file mode 100644
index 0000000000..920c053daa
--- /dev/null
+++ b/unixfs/importer/helpers/dagbuilder.go
@@ -0,0 +1,399 @@
+package helpers
+
+import (
+ "context"
+ "errors"
+ "io"
+ "os"
+
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+
+ ft "github.com/ipfs/boxo/unixfs"
+ pb "github.com/ipfs/boxo/unixfs/pb"
+
+ chunker "github.com/ipfs/boxo/chunker"
+ "github.com/ipfs/boxo/files"
+ pi "github.com/ipfs/boxo/filestore/posinfo"
+ cid "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+var ErrMissingFsRef = errors.New("missing file path or URL, can't create filestore reference")
+
+// DagBuilderHelper wraps together a bunch of objects needed to
+// efficiently create unixfs dag trees
+type DagBuilderHelper struct {
+ dserv ipld.DAGService
+ spl chunker.Splitter
+ recvdErr error
+ rawLeaves bool
+ nextData []byte // the next item to return.
+ maxlinks int
+ cidBuilder cid.Builder
+
+ // Filestore support variables.
+ // ----------------------------
+ // TODO: Encapsulate in `FilestoreNode` (which is basically what they are).
+ //
+ // Besides having the path this variable (if set) is used as a flag
+ // to indicate that Filestore should be used.
+ fullPath string
+ stat os.FileInfo
+ // Keeps track of the current file size added to the DAG (used in
+ // the balanced builder). It is assumed that the `DagBuilderHelper`
+ // is not reused to construct another DAG, but a new one (with a
+ // zero `offset`) is created.
+ offset uint64
+}
+
+// DagBuilderParams wraps configuration options to create a DagBuilderHelper
+// from a chunker.Splitter.
+type DagBuilderParams struct {
+ // Maximum number of links per intermediate node
+ Maxlinks int
+
+ // RawLeaves signifies that the importer should use raw ipld nodes as leaves
+ // instead of using the unixfs TRaw type
+ RawLeaves bool
+
+ // CID Builder to use if set
+ CidBuilder cid.Builder
+
+ // DAGService to write blocks to (required)
+ Dagserv ipld.DAGService
+
+ // NoCopy signals to the chunker that it should track fileinfo for
+ // filestore adds
+ NoCopy bool
+}
+
+// New generates a new DagBuilderHelper from the given params and a given
+// chunker.Splitter as data source.
+func (dbp *DagBuilderParams) New(spl chunker.Splitter) (*DagBuilderHelper, error) {
+ db := &DagBuilderHelper{
+ dserv: dbp.Dagserv,
+ spl: spl,
+ rawLeaves: dbp.RawLeaves,
+ cidBuilder: dbp.CidBuilder,
+ maxlinks: dbp.Maxlinks,
+ }
+ if fi, ok := spl.Reader().(files.FileInfo); dbp.NoCopy && ok {
+ db.fullPath = fi.AbsPath()
+ db.stat = fi.Stat()
+ }
+
+ if dbp.NoCopy && db.fullPath == "" { // Enforce NoCopy
+ return nil, ErrMissingFsRef
+ }
+
+ return db, nil
+}
+
+// prepareNext consumes the next item from the splitter and puts it
+// in the nextData field. it is idempotent-- if nextData is full
+// it will do nothing.
+func (db *DagBuilderHelper) prepareNext() {
+ // if we already have data waiting to be consumed, we're ready
+ if db.nextData != nil || db.recvdErr != nil {
+ return
+ }
+
+ db.nextData, db.recvdErr = db.spl.NextBytes()
+ if db.recvdErr == io.EOF {
+ db.recvdErr = nil
+ }
+}
+
+// Done returns whether or not we're done consuming the incoming data.
+func (db *DagBuilderHelper) Done() bool {
+ // ensure we have an accurate perspective on data
+ // as `done` this may be called before `next`.
+ db.prepareNext() // idempotent
+ if db.recvdErr != nil {
+ return false
+ }
+ return db.nextData == nil
+}
+
+// Next returns the next chunk of data to be inserted into the dag
+// if it returns nil, that signifies that the stream is at an end, and
+// that the current building operation should finish.
+func (db *DagBuilderHelper) Next() ([]byte, error) {
+ db.prepareNext() // idempotent
+ d := db.nextData
+ db.nextData = nil // signal we've consumed it
+ if db.recvdErr != nil {
+ return nil, db.recvdErr
+ }
+ return d, nil
+}
+
+// GetDagServ returns the dagservice object this Helper is using
+func (db *DagBuilderHelper) GetDagServ() ipld.DAGService {
+ return db.dserv
+}
+
+// GetCidBuilder returns the internal `cid.CidBuilder` set in the builder.
+func (db *DagBuilderHelper) GetCidBuilder() cid.Builder {
+ return db.cidBuilder
+}
+
+// NewLeafNode creates a leaf node filled with data. If rawLeaves is
+// defined then a raw leaf will be returned. Otherwise, it will create
+// and return `FSNodeOverDag` with `fsNodeType`.
+func (db *DagBuilderHelper) NewLeafNode(data []byte, fsNodeType pb.Data_DataType) (ipld.Node, error) {
+ if len(data) > BlockSizeLimit {
+ return nil, ErrSizeLimitExceeded
+ }
+
+ if db.rawLeaves {
+ // Encapsulate the data in a raw node.
+ if db.cidBuilder == nil {
+ return dag.NewRawNode(data), nil
+ }
+ rawnode, err := dag.NewRawNodeWPrefix(data, db.cidBuilder)
+ if err != nil {
+ return nil, err
+ }
+ return rawnode, nil
+ }
+
+ // Encapsulate the data in UnixFS node (instead of a raw node).
+ fsNodeOverDag := db.NewFSNodeOverDag(fsNodeType)
+ fsNodeOverDag.SetFileData(data)
+ node, err := fsNodeOverDag.Commit()
+ if err != nil {
+ return nil, err
+ }
+ // TODO: Encapsulate this sequence of calls into a function that
+ // just returns the final `ipld.Node` avoiding going through
+ // `FSNodeOverDag`.
+
+ return node, nil
+}
+
+// FillNodeLayer will add datanodes as children to the give node until
+// it is full in this layer or no more data.
+// NOTE: This function creates raw data nodes so it only works
+// for the `trickle.Layout`.
+func (db *DagBuilderHelper) FillNodeLayer(node *FSNodeOverDag) error {
+
+ // while we have room AND we're not done
+ for node.NumChildren() < db.maxlinks && !db.Done() {
+ child, childFileSize, err := db.NewLeafDataNode(ft.TRaw)
+ if err != nil {
+ return err
+ }
+
+ if err := node.AddChild(child, childFileSize, db); err != nil {
+ return err
+ }
+ }
+ // TODO: Do we need to commit here? The caller who created the
+ // `FSNodeOverDag` should be in charge of that.
+ _, err := node.Commit()
+ return err
+}
+
+// NewLeafDataNode builds the `node` with the data obtained from the
+// Splitter with the given constraints (BlockSizeLimit, RawLeaves)
+// specified when creating the DagBuilderHelper. It returns
+// `ipld.Node` with the `dataSize` (that will be used to keep track of
+// the DAG file size). The size of the data is computed here because
+// after that it will be hidden by `NewLeafNode` inside a generic
+// `ipld.Node` representation.
+func (db *DagBuilderHelper) NewLeafDataNode(fsNodeType pb.Data_DataType) (node ipld.Node, dataSize uint64, err error) {
+ fileData, err := db.Next()
+ if err != nil {
+ return nil, 0, err
+ }
+ dataSize = uint64(len(fileData))
+
+ // Create a new leaf node containing the file chunk data.
+ node, err = db.NewLeafNode(fileData, fsNodeType)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ // Convert this leaf to a `FilestoreNode` if needed.
+ node = db.ProcessFileStore(node, dataSize)
+
+ return node, dataSize, nil
+}
+
+// ProcessFileStore generates, if Filestore is being used, the
+// `FilestoreNode` representation of the `ipld.Node` that
+// contains the file data. If Filestore is not being used just
+// return the same node to continue with its addition to the DAG.
+//
+// The `db.offset` is updated at this point (instead of when
+// `NewLeafDataNode` is called, both work in tandem but the
+// offset is more related to this function).
+func (db *DagBuilderHelper) ProcessFileStore(node ipld.Node, dataSize uint64) ipld.Node {
+ // Check if Filestore is being used.
+ if db.fullPath != "" {
+ // Check if the node is actually a raw node (needed for
+ // Filestore support).
+ if _, ok := node.(*dag.RawNode); ok {
+ fn := &pi.FilestoreNode{
+ Node: node,
+ PosInfo: &pi.PosInfo{
+ Offset: db.offset,
+ FullPath: db.fullPath,
+ Stat: db.stat,
+ },
+ }
+
+ // Update `offset` with the size of the data generated by `db.Next`.
+ db.offset += dataSize
+
+ return fn
+ }
+ }
+
+ // Filestore is not used, return the same `node` argument.
+ return node
+}
+
+// Add inserts the given node in the DAGService.
+func (db *DagBuilderHelper) Add(node ipld.Node) error {
+ return db.dserv.Add(context.TODO(), node)
+}
+
+// Maxlinks returns the configured maximum number for links
+// for nodes built with this helper.
+func (db *DagBuilderHelper) Maxlinks() int {
+ return db.maxlinks
+}
+
+// FSNodeOverDag encapsulates an `unixfs.FSNode` that will be stored in a
+// `dag.ProtoNode`. Instead of just having a single `ipld.Node` that
+// would need to be constantly (un)packed to access and modify its
+// internal `FSNode` in the process of creating a UnixFS DAG, this
+// structure stores an `FSNode` cache to manipulate it (add child nodes)
+// directly , and only when the node has reached its final (immutable) state
+// (signaled by calling `Commit()`) is it committed to a single (indivisible)
+// `ipld.Node`.
+//
+// It is used mainly for internal (non-leaf) nodes, and for some
+// representations of data leaf nodes (that don't use raw nodes or
+// Filestore).
+//
+// It aims to replace the `UnixfsNode` structure which encapsulated too
+// many possible node state combinations.
+//
+// TODO: Revisit the name.
+type FSNodeOverDag struct {
+ dag *dag.ProtoNode
+ file *ft.FSNode
+}
+
+// NewFSNodeOverDag creates a new `dag.ProtoNode` and `ft.FSNode`
+// decoupled from one onther (and will continue in that way until
+// `Commit` is called), with `fsNodeType` specifying the type of
+// the UnixFS layer node (either `File` or `Raw`).
+func (db *DagBuilderHelper) NewFSNodeOverDag(fsNodeType pb.Data_DataType) *FSNodeOverDag {
+ node := new(FSNodeOverDag)
+ node.dag = new(dag.ProtoNode)
+ node.dag.SetCidBuilder(db.GetCidBuilder())
+
+ node.file = ft.NewFSNode(fsNodeType)
+
+ return node
+}
+
+// NewFSNFromDag reconstructs a FSNodeOverDag node from a given dag node
+func (db *DagBuilderHelper) NewFSNFromDag(nd *dag.ProtoNode) (*FSNodeOverDag, error) {
+ return NewFSNFromDag(nd)
+}
+
+// NewFSNFromDag reconstructs a FSNodeOverDag node from a given dag node
+func NewFSNFromDag(nd *dag.ProtoNode) (*FSNodeOverDag, error) {
+ mb, err := ft.FSNodeFromBytes(nd.Data())
+ if err != nil {
+ return nil, err
+ }
+
+ return &FSNodeOverDag{
+ dag: nd,
+ file: mb,
+ }, nil
+}
+
+// AddChild adds a `child` `ipld.Node` to both node layers. The
+// `dag.ProtoNode` creates a link to the child node while the
+// `ft.FSNode` stores its file size (that is, not the size of the
+// node but the size of the file data that it is storing at the
+// UnixFS layer). The child is also stored in the `DAGService`.
+func (n *FSNodeOverDag) AddChild(child ipld.Node, fileSize uint64, db *DagBuilderHelper) error {
+ err := n.dag.AddNodeLink("", child)
+ if err != nil {
+ return err
+ }
+
+ n.file.AddBlockSize(fileSize)
+
+ return db.Add(child)
+}
+
+// RemoveChild deletes the child node at the given index.
+func (n *FSNodeOverDag) RemoveChild(index int, dbh *DagBuilderHelper) {
+ n.file.RemoveBlockSize(index)
+ n.dag.SetLinks(append(n.dag.Links()[:index], n.dag.Links()[index+1:]...))
+}
+
+// Commit unifies (resolves) the cache nodes into a single `ipld.Node`
+// that represents them: the `ft.FSNode` is encoded inside the
+// `dag.ProtoNode`.
+//
+// TODO: Make it read-only after committing, allow to commit only once.
+func (n *FSNodeOverDag) Commit() (ipld.Node, error) {
+ fileData, err := n.file.GetBytes()
+ if err != nil {
+ return nil, err
+ }
+ n.dag.SetData(fileData)
+
+ return n.dag, nil
+}
+
+// NumChildren returns the number of children of the `ft.FSNode`.
+func (n *FSNodeOverDag) NumChildren() int {
+ return n.file.NumChildren()
+}
+
+// FileSize returns the `Filesize` attribute from the underlying
+// representation of the `ft.FSNode`.
+func (n *FSNodeOverDag) FileSize() uint64 {
+ return n.file.FileSize()
+}
+
+// SetFileData stores the `fileData` in the `ft.FSNode`. It
+// should be used only when `FSNodeOverDag` represents a leaf
+// node (internal nodes don't carry data, just file sizes).
+func (n *FSNodeOverDag) SetFileData(fileData []byte) {
+ n.file.SetData(fileData)
+}
+
+// GetDagNode fills out the proper formatting for the FSNodeOverDag node
+// inside of a DAG node and returns the dag node.
+// TODO: Check if we have committed (passed the UnixFS information
+// to the DAG layer) before returning this.
+func (n *FSNodeOverDag) GetDagNode() (ipld.Node, error) {
+ return n.dag, nil
+}
+
+// GetChild gets the ith child of this node from the given DAGService.
+func (n *FSNodeOverDag) GetChild(ctx context.Context, i int, ds ipld.DAGService) (*FSNodeOverDag, error) {
+ nd, err := n.dag.Links()[i].GetNode(ctx, ds)
+ if err != nil {
+ return nil, err
+ }
+
+ pbn, ok := nd.(*dag.ProtoNode)
+ if !ok {
+ return nil, dag.ErrNotProtobuf
+ }
+
+ return NewFSNFromDag(pbn)
+}
diff --git a/unixfs/importer/helpers/helpers.go b/unixfs/importer/helpers/helpers.go
new file mode 100644
index 0000000000..20cb598e62
--- /dev/null
+++ b/unixfs/importer/helpers/helpers.go
@@ -0,0 +1,31 @@
+package helpers
+
+import (
+ "fmt"
+)
+
+// BlockSizeLimit specifies the maximum size an imported block can have.
+var BlockSizeLimit = 1048576 // 1 MB
+
+// rough estimates on expected sizes
+var roughLinkBlockSize = 1 << 13 // 8KB
+var roughLinkSize = 34 + 8 + 5 // sha256 multihash + size + no name + protobuf framing
+
+// DefaultLinksPerBlock governs how the importer decides how many links there
+// will be per block. This calculation is based on expected distributions of:
+// - the expected distribution of block sizes
+// - the expected distribution of link sizes
+// - desired access speed
+//
+// For now, we use:
+//
+// var roughLinkBlockSize = 1 << 13 // 8KB
+// var roughLinkSize = 34 + 8 + 5 // sha256 multihash + size + no name
+// // + protobuf framing
+// var DefaultLinksPerBlock = (roughLinkBlockSize / roughLinkSize)
+// = ( 8192 / 47 )
+// = (approximately) 174
+var DefaultLinksPerBlock = roughLinkBlockSize / roughLinkSize
+
+// ErrSizeLimitExceeded signals that a block is larger than BlockSizeLimit.
+var ErrSizeLimitExceeded = fmt.Errorf("object size limit exceeded")
diff --git a/unixfs/importer/importer.go b/unixfs/importer/importer.go
new file mode 100644
index 0000000000..64fb96a63d
--- /dev/null
+++ b/unixfs/importer/importer.go
@@ -0,0 +1,41 @@
+// Package importer implements utilities used to create IPFS DAGs from files
+// and readers.
+package importer
+
+import (
+ bal "github.com/ipfs/boxo/unixfs/importer/balanced"
+ h "github.com/ipfs/boxo/unixfs/importer/helpers"
+ trickle "github.com/ipfs/boxo/unixfs/importer/trickle"
+
+ chunker "github.com/ipfs/boxo/chunker"
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+// BuildDagFromReader creates a DAG given a DAGService and a Splitter
+// implementation (Splitters are io.Readers), using a Balanced layout.
+func BuildDagFromReader(ds ipld.DAGService, spl chunker.Splitter) (ipld.Node, error) {
+ dbp := h.DagBuilderParams{
+ Dagserv: ds,
+ Maxlinks: h.DefaultLinksPerBlock,
+ }
+ db, err := dbp.New(spl)
+ if err != nil {
+ return nil, err
+ }
+ return bal.Layout(db)
+}
+
+// BuildTrickleDagFromReader creates a DAG given a DAGService and a Splitter
+// implementation (Splitters are io.Readers), using a Trickle Layout.
+func BuildTrickleDagFromReader(ds ipld.DAGService, spl chunker.Splitter) (ipld.Node, error) {
+ dbp := h.DagBuilderParams{
+ Dagserv: ds,
+ Maxlinks: h.DefaultLinksPerBlock,
+ }
+
+ db, err := dbp.New(spl)
+ if err != nil {
+ return nil, err
+ }
+ return trickle.Layout(db)
+}
diff --git a/unixfs/importer/importer_test.go b/unixfs/importer/importer_test.go
new file mode 100644
index 0000000000..3d608e4cd9
--- /dev/null
+++ b/unixfs/importer/importer_test.go
@@ -0,0 +1,152 @@
+package importer
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "testing"
+
+ uio "github.com/ipfs/boxo/unixfs/io"
+
+ chunker "github.com/ipfs/boxo/chunker"
+ u "github.com/ipfs/boxo/util"
+ cid "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+ mdtest "github.com/ipfs/boxo/ipld/merkledag/test"
+)
+
+func getBalancedDag(t testing.TB, size int64, blksize int64) (ipld.Node, ipld.DAGService) {
+ ds := mdtest.Mock()
+ r := io.LimitReader(u.NewTimeSeededRand(), size)
+ nd, err := BuildDagFromReader(ds, chunker.NewSizeSplitter(r, blksize))
+ if err != nil {
+ t.Fatal(err)
+ }
+ return nd, ds
+}
+
+func getTrickleDag(t testing.TB, size int64, blksize int64) (ipld.Node, ipld.DAGService) {
+ ds := mdtest.Mock()
+ r := io.LimitReader(u.NewTimeSeededRand(), size)
+ nd, err := BuildTrickleDagFromReader(ds, chunker.NewSizeSplitter(r, blksize))
+ if err != nil {
+ t.Fatal(err)
+ }
+ return nd, ds
+}
+
+func TestStableCid(t *testing.T) {
+ ds := mdtest.Mock()
+ buf := make([]byte, 10*1024*1024)
+ u.NewSeededRand(0xdeadbeef).Read(buf)
+ r := bytes.NewReader(buf)
+
+ nd, err := BuildDagFromReader(ds, chunker.DefaultSplitter(r))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expected, err := cid.Decode("QmZN1qquw84zhV4j6vT56tCcmFxaDaySL1ezTXFvMdNmrK")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !expected.Equals(nd.Cid()) {
+ t.Fatalf("expected CID %s, got CID %s", expected, nd)
+ }
+
+ dr, err := uio.NewDagReader(context.Background(), nd, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := io.ReadAll(dr)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !bytes.Equal(out, buf) {
+ t.Fatal("bad read")
+ }
+}
+
+func TestBalancedDag(t *testing.T) {
+ ds := mdtest.Mock()
+ buf := make([]byte, 10000)
+ u.NewTimeSeededRand().Read(buf)
+ r := bytes.NewReader(buf)
+
+ nd, err := BuildDagFromReader(ds, chunker.DefaultSplitter(r))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ dr, err := uio.NewDagReader(context.Background(), nd, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := io.ReadAll(dr)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !bytes.Equal(out, buf) {
+ t.Fatal("bad read")
+ }
+}
+
+func BenchmarkBalancedReadSmallBlock(b *testing.B) {
+ b.StopTimer()
+ nbytes := int64(10000000)
+ nd, ds := getBalancedDag(b, nbytes, 4096)
+
+ b.SetBytes(nbytes)
+ b.StartTimer()
+ runReadBench(b, nd, ds)
+}
+
+func BenchmarkTrickleReadSmallBlock(b *testing.B) {
+ b.StopTimer()
+ nbytes := int64(10000000)
+ nd, ds := getTrickleDag(b, nbytes, 4096)
+
+ b.SetBytes(nbytes)
+ b.StartTimer()
+ runReadBench(b, nd, ds)
+}
+
+func BenchmarkBalancedReadFull(b *testing.B) {
+ b.StopTimer()
+ nbytes := int64(10000000)
+ nd, ds := getBalancedDag(b, nbytes, chunker.DefaultBlockSize)
+
+ b.SetBytes(nbytes)
+ b.StartTimer()
+ runReadBench(b, nd, ds)
+}
+
+func BenchmarkTrickleReadFull(b *testing.B) {
+ b.StopTimer()
+ nbytes := int64(10000000)
+ nd, ds := getTrickleDag(b, nbytes, chunker.DefaultBlockSize)
+
+ b.SetBytes(nbytes)
+ b.StartTimer()
+ runReadBench(b, nd, ds)
+}
+
+func runReadBench(b *testing.B, nd ipld.Node, ds ipld.DAGService) {
+ for i := 0; i < b.N; i++ {
+ ctx, cancel := context.WithCancel(context.Background())
+ read, err := uio.NewDagReader(ctx, nd, ds)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ _, err = read.WriteTo(io.Discard)
+ if err != nil && err != io.EOF {
+ b.Fatal(err)
+ }
+ cancel()
+ }
+}
diff --git a/unixfs/importer/trickle/trickle_test.go b/unixfs/importer/trickle/trickle_test.go
new file mode 100644
index 0000000000..9c34a39f23
--- /dev/null
+++ b/unixfs/importer/trickle/trickle_test.go
@@ -0,0 +1,666 @@
+package trickle
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ mrand "math/rand"
+ "testing"
+
+ ft "github.com/ipfs/boxo/unixfs"
+ h "github.com/ipfs/boxo/unixfs/importer/helpers"
+ uio "github.com/ipfs/boxo/unixfs/io"
+
+ chunker "github.com/ipfs/boxo/chunker"
+ u "github.com/ipfs/boxo/util"
+ ipld "github.com/ipfs/go-ipld-format"
+ merkledag "github.com/ipfs/boxo/ipld/merkledag"
+ mdtest "github.com/ipfs/boxo/ipld/merkledag/test"
+)
+
+type UseRawLeaves bool
+
+const (
+ ProtoBufLeaves UseRawLeaves = false
+ RawLeaves UseRawLeaves = true
+)
+
+func runBothSubtests(t *testing.T, tfunc func(*testing.T, UseRawLeaves)) {
+ t.Run("leaves=ProtoBuf", func(t *testing.T) { tfunc(t, ProtoBufLeaves) })
+ t.Run("leaves=Raw", func(t *testing.T) { tfunc(t, RawLeaves) })
+}
+
+func buildTestDag(ds ipld.DAGService, spl chunker.Splitter, rawLeaves UseRawLeaves) (*merkledag.ProtoNode, error) {
+ dbp := h.DagBuilderParams{
+ Dagserv: ds,
+ Maxlinks: h.DefaultLinksPerBlock,
+ RawLeaves: bool(rawLeaves),
+ }
+
+ db, err := dbp.New(spl)
+ if err != nil {
+ return nil, err
+ }
+
+ nd, err := Layout(db)
+ if err != nil {
+ return nil, err
+ }
+
+ pbnd, ok := nd.(*merkledag.ProtoNode)
+ if !ok {
+ return nil, merkledag.ErrNotProtobuf
+ }
+
+ return pbnd, VerifyTrickleDagStructure(pbnd, VerifyParams{
+ Getter: ds,
+ Direct: dbp.Maxlinks,
+ LayerRepeat: depthRepeat,
+ RawLeaves: bool(rawLeaves),
+ })
+}
+
+// Test where calls to read are smaller than the chunk size
+func TestSizeBasedSplit(t *testing.T) {
+ runBothSubtests(t, testSizeBasedSplit)
+}
+
+func testSizeBasedSplit(t *testing.T, rawLeaves UseRawLeaves) {
+ if testing.Short() {
+ t.SkipNow()
+ }
+ bs := chunker.SizeSplitterGen(512)
+ testFileConsistency(t, bs, 32*512, rawLeaves)
+
+ bs = chunker.SizeSplitterGen(4096)
+ testFileConsistency(t, bs, 32*4096, rawLeaves)
+
+ // Uneven offset
+ testFileConsistency(t, bs, 31*4095, rawLeaves)
+}
+
+func dup(b []byte) []byte {
+ o := make([]byte, len(b))
+ copy(o, b)
+ return o
+}
+
+func testFileConsistency(t *testing.T, bs chunker.SplitterGen, nbytes int, rawLeaves UseRawLeaves) {
+ should := make([]byte, nbytes)
+ u.NewTimeSeededRand().Read(should)
+
+ read := bytes.NewReader(should)
+ ds := mdtest.Mock()
+ nd, err := buildTestDag(ds, bs(read), rawLeaves)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ r, err := uio.NewDagReader(context.Background(), nd, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := io.ReadAll(r)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = arrComp(out, should)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestBuilderConsistency(t *testing.T) {
+ runBothSubtests(t, testBuilderConsistency)
+}
+
+func testBuilderConsistency(t *testing.T, rawLeaves UseRawLeaves) {
+ nbytes := 100000
+ buf := new(bytes.Buffer)
+ io.CopyN(buf, u.NewTimeSeededRand(), int64(nbytes))
+ should := dup(buf.Bytes())
+ dagserv := mdtest.Mock()
+ nd, err := buildTestDag(dagserv, chunker.DefaultSplitter(buf), rawLeaves)
+ if err != nil {
+ t.Fatal(err)
+ }
+ r, err := uio.NewDagReader(context.Background(), nd, dagserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := io.ReadAll(r)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = arrComp(out, should)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func arrComp(a, b []byte) error {
+ if len(a) != len(b) {
+ return fmt.Errorf("arrays differ in length. %d != %d", len(a), len(b))
+ }
+ for i, v := range a {
+ if v != b[i] {
+ return fmt.Errorf("arrays differ at index: %d", i)
+ }
+ }
+ return nil
+}
+
+func TestIndirectBlocks(t *testing.T) {
+ runBothSubtests(t, testIndirectBlocks)
+}
+
+func testIndirectBlocks(t *testing.T, rawLeaves UseRawLeaves) {
+ splitter := chunker.SizeSplitterGen(512)
+ nbytes := 1024 * 1024
+ buf := make([]byte, nbytes)
+ u.NewTimeSeededRand().Read(buf)
+
+ read := bytes.NewReader(buf)
+
+ ds := mdtest.Mock()
+ dag, err := buildTestDag(ds, splitter(read), rawLeaves)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ reader, err := uio.NewDagReader(context.Background(), dag, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := io.ReadAll(reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !bytes.Equal(out, buf) {
+ t.Fatal("Not equal!")
+ }
+}
+
+func TestSeekingBasic(t *testing.T) {
+ runBothSubtests(t, testSeekingBasic)
+}
+
+func testSeekingBasic(t *testing.T, rawLeaves UseRawLeaves) {
+ nbytes := int64(10 * 1024)
+ should := make([]byte, nbytes)
+ u.NewTimeSeededRand().Read(should)
+
+ read := bytes.NewReader(should)
+ ds := mdtest.Mock()
+ nd, err := buildTestDag(ds, chunker.NewSizeSplitter(read, 512), rawLeaves)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rs, err := uio.NewDagReader(context.Background(), nd, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ start := int64(4000)
+ n, err := rs.Seek(start, io.SeekStart)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n != start {
+ t.Fatal("Failed to seek to correct offset")
+ }
+
+ out, err := io.ReadAll(rs)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = arrComp(out, should[start:])
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSeekToBegin(t *testing.T) {
+ runBothSubtests(t, testSeekToBegin)
+}
+
+func testSeekToBegin(t *testing.T, rawLeaves UseRawLeaves) {
+ nbytes := int64(10 * 1024)
+ should := make([]byte, nbytes)
+ u.NewTimeSeededRand().Read(should)
+
+ read := bytes.NewReader(should)
+ ds := mdtest.Mock()
+ nd, err := buildTestDag(ds, chunker.NewSizeSplitter(read, 500), rawLeaves)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rs, err := uio.NewDagReader(context.Background(), nd, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ n, err := io.CopyN(io.Discard, rs, 1024*4)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n != 4096 {
+ t.Fatal("Copy didnt copy enough bytes")
+ }
+
+ seeked, err := rs.Seek(0, io.SeekStart)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if seeked != 0 {
+ t.Fatal("Failed to seek to beginning")
+ }
+
+ out, err := io.ReadAll(rs)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = arrComp(out, should)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSeekToAlmostBegin(t *testing.T) {
+ runBothSubtests(t, testSeekToAlmostBegin)
+}
+
+func testSeekToAlmostBegin(t *testing.T, rawLeaves UseRawLeaves) {
+ nbytes := int64(10 * 1024)
+ should := make([]byte, nbytes)
+ u.NewTimeSeededRand().Read(should)
+
+ read := bytes.NewReader(should)
+ ds := mdtest.Mock()
+ nd, err := buildTestDag(ds, chunker.NewSizeSplitter(read, 500), rawLeaves)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rs, err := uio.NewDagReader(context.Background(), nd, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ n, err := io.CopyN(io.Discard, rs, 1024*4)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n != 4096 {
+ t.Fatal("Copy didnt copy enough bytes")
+ }
+
+ seeked, err := rs.Seek(1, io.SeekStart)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if seeked != 1 {
+ t.Fatal("Failed to seek to almost beginning")
+ }
+
+ out, err := io.ReadAll(rs)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = arrComp(out, should[1:])
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSeekEnd(t *testing.T) {
+ runBothSubtests(t, testSeekEnd)
+}
+
+func testSeekEnd(t *testing.T, rawLeaves UseRawLeaves) {
+ nbytes := int64(50 * 1024)
+ should := make([]byte, nbytes)
+ u.NewTimeSeededRand().Read(should)
+
+ read := bytes.NewReader(should)
+ ds := mdtest.Mock()
+ nd, err := buildTestDag(ds, chunker.NewSizeSplitter(read, 500), rawLeaves)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rs, err := uio.NewDagReader(context.Background(), nd, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ seeked, err := rs.Seek(0, io.SeekEnd)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if seeked != nbytes {
+ t.Fatal("Failed to seek to end")
+ }
+}
+
+func TestSeekEndSingleBlockFile(t *testing.T) {
+ runBothSubtests(t, testSeekEndSingleBlockFile)
+}
+
+func testSeekEndSingleBlockFile(t *testing.T, rawLeaves UseRawLeaves) {
+ nbytes := int64(100)
+ should := make([]byte, nbytes)
+ u.NewTimeSeededRand().Read(should)
+
+ read := bytes.NewReader(should)
+ ds := mdtest.Mock()
+ nd, err := buildTestDag(ds, chunker.NewSizeSplitter(read, 5000), rawLeaves)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rs, err := uio.NewDagReader(context.Background(), nd, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ seeked, err := rs.Seek(0, io.SeekEnd)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if seeked != nbytes {
+ t.Fatal("Failed to seek to end")
+ }
+}
+
+func TestSeekingStress(t *testing.T) {
+ runBothSubtests(t, testSeekingStress)
+}
+
+func testSeekingStress(t *testing.T, rawLeaves UseRawLeaves) {
+ nbytes := int64(1024 * 1024)
+ should := make([]byte, nbytes)
+ u.NewTimeSeededRand().Read(should)
+
+ read := bytes.NewReader(should)
+ ds := mdtest.Mock()
+ nd, err := buildTestDag(ds, chunker.NewSizeSplitter(read, 1000), rawLeaves)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rs, err := uio.NewDagReader(context.Background(), nd, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ testbuf := make([]byte, nbytes)
+ for i := 0; i < 50; i++ {
+ offset := mrand.Intn(int(nbytes))
+ l := int(nbytes) - offset
+ n, err := rs.Seek(int64(offset), io.SeekStart)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n != int64(offset) {
+ t.Fatal("Seek failed to move to correct position")
+ }
+
+ nread, err := rs.Read(testbuf[:l])
+ if err != nil {
+ t.Fatal(err)
+ }
+ if nread != l {
+ t.Fatal("Failed to read enough bytes")
+ }
+
+ err = arrComp(testbuf[:l], should[offset:offset+l])
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+}
+
+func TestSeekingConsistency(t *testing.T) {
+ runBothSubtests(t, testSeekingConsistency)
+}
+
+func testSeekingConsistency(t *testing.T, rawLeaves UseRawLeaves) {
+ nbytes := int64(128 * 1024)
+ should := make([]byte, nbytes)
+ u.NewTimeSeededRand().Read(should)
+
+ read := bytes.NewReader(should)
+ ds := mdtest.Mock()
+ nd, err := buildTestDag(ds, chunker.NewSizeSplitter(read, 500), rawLeaves)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rs, err := uio.NewDagReader(context.Background(), nd, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ out := make([]byte, nbytes)
+
+ for coff := nbytes - 4096; coff >= 0; coff -= 4096 {
+ t.Log(coff)
+ n, err := rs.Seek(coff, io.SeekStart)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n != coff {
+ t.Fatal("wasnt able to seek to the right position")
+ }
+ nread, err := rs.Read(out[coff : coff+4096])
+ if err != nil {
+ t.Fatal(err)
+ }
+ if nread != 4096 {
+ t.Fatal("didnt read the correct number of bytes")
+ }
+ }
+
+ err = arrComp(out, should)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestAppend(t *testing.T) {
+ runBothSubtests(t, testAppend)
+}
+
+func testAppend(t *testing.T, rawLeaves UseRawLeaves) {
+ nbytes := int64(128 * 1024)
+ should := make([]byte, nbytes)
+ u.NewTimeSeededRand().Read(should)
+
+ // Reader for half the bytes
+ read := bytes.NewReader(should[:nbytes/2])
+ ds := mdtest.Mock()
+ nd, err := buildTestDag(ds, chunker.NewSizeSplitter(read, 500), rawLeaves)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ dbp := &h.DagBuilderParams{
+ Dagserv: ds,
+ Maxlinks: h.DefaultLinksPerBlock,
+ RawLeaves: bool(rawLeaves),
+ }
+
+ r := bytes.NewReader(should[nbytes/2:])
+
+ ctx := context.Background()
+
+ db, err := dbp.New(chunker.NewSizeSplitter(r, 500))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nnode, err := Append(ctx, nd, db)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = VerifyTrickleDagStructure(nnode, VerifyParams{
+ Getter: ds,
+ Direct: dbp.Maxlinks,
+ LayerRepeat: depthRepeat,
+ RawLeaves: bool(rawLeaves),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fread, err := uio.NewDagReader(ctx, nnode, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := io.ReadAll(fread)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = arrComp(out, should)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+// This test appends one byte at a time to an empty file
+func TestMultipleAppends(t *testing.T) {
+ runBothSubtests(t, testMultipleAppends)
+}
+
+func testMultipleAppends(t *testing.T, rawLeaves UseRawLeaves) {
+ ds := mdtest.Mock()
+
+ // TODO: fix small size appends and make this number bigger
+ nbytes := int64(1000)
+ should := make([]byte, nbytes)
+ u.NewTimeSeededRand().Read(should)
+
+ read := bytes.NewReader(nil)
+ nd, err := buildTestDag(ds, chunker.NewSizeSplitter(read, 500), rawLeaves)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ dbp := &h.DagBuilderParams{
+ Dagserv: ds,
+ Maxlinks: 4,
+ RawLeaves: bool(rawLeaves),
+ }
+
+ spl := chunker.SizeSplitterGen(500)
+
+ ctx := context.Background()
+ for i := 0; i < len(should); i++ {
+
+ db, err := dbp.New(spl(bytes.NewReader(should[i : i+1])))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nnode, err := Append(ctx, nd, db)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = VerifyTrickleDagStructure(nnode, VerifyParams{
+ Getter: ds,
+ Direct: dbp.Maxlinks,
+ LayerRepeat: depthRepeat,
+ RawLeaves: bool(rawLeaves),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fread, err := uio.NewDagReader(ctx, nnode, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := io.ReadAll(fread)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = arrComp(out, should[:i+1])
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+}
+
+func TestAppendSingleBytesToEmpty(t *testing.T) {
+ ds := mdtest.Mock()
+
+ data := []byte("AB")
+
+ nd := new(merkledag.ProtoNode)
+ nd.SetData(ft.FilePBData(nil, 0))
+
+ dbp := &h.DagBuilderParams{
+ Dagserv: ds,
+ Maxlinks: 4,
+ }
+
+ spl := chunker.SizeSplitterGen(500)
+
+ ctx := context.Background()
+
+ db, err := dbp.New(spl(bytes.NewReader(data[:1])))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nnode, err := Append(ctx, nd, db)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ db, err = dbp.New(spl(bytes.NewReader(data[1:])))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ nnode, err = Append(ctx, nnode, db)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fread, err := uio.NewDagReader(ctx, nnode, ds)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := io.ReadAll(fread)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fmt.Println(out, data)
+ err = arrComp(out, data)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/unixfs/importer/trickle/trickledag.go b/unixfs/importer/trickle/trickledag.go
new file mode 100644
index 0000000000..4dff07eac2
--- /dev/null
+++ b/unixfs/importer/trickle/trickledag.go
@@ -0,0 +1,389 @@
+// Package trickle allows to build trickle DAGs.
+// In this type of DAG, non-leave nodes are first filled
+// with data leaves, and then incorporate "layers" of subtrees
+// as additional links.
+//
+// Each layer is a trickle sub-tree and is limited by an increasing
+// maximum depth. Thus, the nodes first layer
+// can only hold leaves (depth 1) but subsequent layers can grow deeper.
+// By default, this module places 4 nodes per layer (that is, 4 subtrees
+// of the same maximum depth before increasing it).
+//
+// Trickle DAGs are very good for sequentially reading data, as the
+// first data leaves are directly reachable from the root and those
+// coming next are always nearby. They are
+// suited for things like streaming applications.
+package trickle
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ ft "github.com/ipfs/boxo/unixfs"
+ h "github.com/ipfs/boxo/unixfs/importer/helpers"
+
+ cid "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+)
+
+// depthRepeat specifies how many times to append a child tree of a
+// given depth. Higher values increase the width of a given node, which
+// improves seek speeds.
+const depthRepeat = 4
+
+// Layout builds a new DAG with the trickle format using the provided
+// DagBuilderHelper. See the module's description for a more detailed
+// explanation.
+func Layout(db *h.DagBuilderHelper) (ipld.Node, error) {
+ newRoot := db.NewFSNodeOverDag(ft.TFile)
+ root, _, err := fillTrickleRec(db, newRoot, -1)
+ if err != nil {
+ return nil, err
+ }
+
+ return root, db.Add(root)
+}
+
+// fillTrickleRec creates a trickle (sub-)tree with an optional maximum specified depth
+// in the case maxDepth is greater than zero, or with unlimited depth otherwise
+// (where the DAG builder will signal the end of data to end the function).
+func fillTrickleRec(db *h.DagBuilderHelper, node *h.FSNodeOverDag, maxDepth int) (filledNode ipld.Node, nodeFileSize uint64, err error) {
+ // Always do this, even in the base case
+ if err := db.FillNodeLayer(node); err != nil {
+ return nil, 0, err
+ }
+
+ // For each depth in [1, `maxDepth`) (or without limit if `maxDepth` is -1,
+ // initial call from `Layout`) add `depthRepeat` sub-graphs of that depth.
+ for depth := 1; maxDepth == -1 || depth < maxDepth; depth++ {
+ if db.Done() {
+ break
+ // No more data, stop here, posterior append calls will figure out
+ // where we left off.
+ }
+
+ for repeatIndex := 0; repeatIndex < depthRepeat && !db.Done(); repeatIndex++ {
+
+ childNode, childFileSize, err := fillTrickleRec(db, db.NewFSNodeOverDag(ft.TFile), depth)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ if err := node.AddChild(childNode, childFileSize, db); err != nil {
+ return nil, 0, err
+ }
+ }
+ }
+
+ // Get the final `dag.ProtoNode` with the `FSNode` data encoded inside.
+ filledNode, err = node.Commit()
+ if err != nil {
+ return nil, 0, err
+ }
+
+ return filledNode, node.FileSize(), nil
+}
+
+// Append appends the data in `db` to the dag, using the Trickledag format
+func Append(ctx context.Context, basen ipld.Node, db *h.DagBuilderHelper) (out ipld.Node, errOut error) {
+ base, ok := basen.(*dag.ProtoNode)
+ if !ok {
+ return nil, dag.ErrNotProtobuf
+ }
+
+ // Convert to unixfs node for working with easily
+
+ fsn, err := h.NewFSNFromDag(base)
+ if err != nil {
+ return nil, err
+ }
+
+ // Get depth of this 'tree'
+ depth, repeatNumber := trickleDepthInfo(fsn, db.Maxlinks())
+ if depth == 0 {
+ // If direct blocks not filled...
+ if err := db.FillNodeLayer(fsn); err != nil {
+ return nil, err
+ }
+
+ if db.Done() {
+ // TODO: If `FillNodeLayer` stop `Commit`ing this should be
+ // the place (besides the function end) to call it.
+ return fsn.GetDagNode()
+ }
+
+ // If continuing, our depth has increased by one
+ depth++
+ }
+
+ // Last child in this node may not be a full tree, lets fill it up.
+ if err := appendFillLastChild(ctx, fsn, depth-1, repeatNumber, db); err != nil {
+ return nil, err
+ }
+
+ // after appendFillLastChild, our depth is now increased by one
+ if !db.Done() {
+ depth++
+ }
+
+ // Now, continue filling out tree like normal
+ for i := depth; !db.Done(); i++ {
+ for j := 0; j < depthRepeat && !db.Done(); j++ {
+ nextChild := db.NewFSNodeOverDag(ft.TFile)
+ childNode, childFileSize, err := fillTrickleRec(db, nextChild, i)
+ if err != nil {
+ return nil, err
+ }
+ err = fsn.AddChild(childNode, childFileSize, db)
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+ _, err = fsn.Commit()
+ if err != nil {
+ return nil, err
+ }
+ return fsn.GetDagNode()
+}
+
+func appendFillLastChild(ctx context.Context, fsn *h.FSNodeOverDag, depth int, repeatNumber int, db *h.DagBuilderHelper) error {
+ if fsn.NumChildren() <= db.Maxlinks() {
+ return nil
+ }
+ // TODO: Why do we need this check, didn't the caller already take
+ // care of this?
+
+ // Recursive step, grab last child
+ last := fsn.NumChildren() - 1
+ lastChild, err := fsn.GetChild(ctx, last, db.GetDagServ())
+ if err != nil {
+ return err
+ }
+
+ // Fill out last child (may not be full tree)
+ newChild, nchildSize, err := appendRec(ctx, lastChild, db, depth-1)
+ if err != nil {
+ return err
+ }
+
+ // Update changed child in parent node
+ fsn.RemoveChild(last, db)
+ filledNode, err := newChild.Commit()
+ if err != nil {
+ return err
+ }
+ err = fsn.AddChild(filledNode, nchildSize, db)
+ if err != nil {
+ return err
+ }
+
+ // Partially filled depth layer
+ if repeatNumber != 0 {
+ for ; repeatNumber < depthRepeat && !db.Done(); repeatNumber++ {
+ nextChild := db.NewFSNodeOverDag(ft.TFile)
+ childNode, childFileSize, err := fillTrickleRec(db, nextChild, depth)
+ if err != nil {
+ return err
+ }
+
+ if err := fsn.AddChild(childNode, childFileSize, db); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+// recursive call for Append
+func appendRec(ctx context.Context, fsn *h.FSNodeOverDag, db *h.DagBuilderHelper, maxDepth int) (*h.FSNodeOverDag, uint64, error) {
+ if maxDepth == 0 || db.Done() {
+ return fsn, fsn.FileSize(), nil
+ }
+
+ // Get depth of this 'tree'
+ depth, repeatNumber := trickleDepthInfo(fsn, db.Maxlinks())
+ if depth == 0 {
+ // If direct blocks not filled...
+ if err := db.FillNodeLayer(fsn); err != nil {
+ return nil, 0, err
+ }
+ depth++
+ }
+ // TODO: Same as `appendFillLastChild`, when is this case possible?
+
+ // If at correct depth, no need to continue
+ if depth == maxDepth {
+ return fsn, fsn.FileSize(), nil
+ }
+
+ if err := appendFillLastChild(ctx, fsn, depth, repeatNumber, db); err != nil {
+ return nil, 0, err
+ }
+
+ // after appendFillLastChild, our depth is now increased by one
+ if !db.Done() {
+ depth++
+ }
+
+ // Now, continue filling out tree like normal
+ for i := depth; i < maxDepth && !db.Done(); i++ {
+ for j := 0; j < depthRepeat && !db.Done(); j++ {
+ nextChild := db.NewFSNodeOverDag(ft.TFile)
+ childNode, childFileSize, err := fillTrickleRec(db, nextChild, i)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ if err := fsn.AddChild(childNode, childFileSize, db); err != nil {
+ return nil, 0, err
+ }
+ }
+ }
+
+ return fsn, fsn.FileSize(), nil
+}
+
+// Deduce where we left off in `fillTrickleRec`, returns the `depth`
+// with which new sub-graphs were being added and, within that depth,
+// in which `repeatNumber` of the total `depthRepeat` we should add.
+func trickleDepthInfo(node *h.FSNodeOverDag, maxlinks int) (depth int, repeatNumber int) {
+ n := node.NumChildren()
+
+ if n < maxlinks {
+ // We didn't even added the initial `maxlinks` leaf nodes (`FillNodeLayer`).
+ return 0, 0
+ }
+
+ nonLeafChildren := n - maxlinks
+ // The number of non-leaf child nodes added in `fillTrickleRec` (after
+ // the `FillNodeLayer` call).
+
+ depth = nonLeafChildren/depthRepeat + 1
+ // "Deduplicate" the added `depthRepeat` sub-graphs at each depth
+ // (rounding it up since we may be on an unfinished depth with less
+ // than `depthRepeat` sub-graphs).
+
+ repeatNumber = nonLeafChildren % depthRepeat
+ // What's left after taking full depths of `depthRepeat` sub-graphs
+ // is the current `repeatNumber` we're at (this fractional part is
+ // what we rounded up before).
+
+ return
+}
+
+// VerifyParams is used by VerifyTrickleDagStructure
+type VerifyParams struct {
+ Getter ipld.NodeGetter
+ Direct int
+ LayerRepeat int
+ Prefix *cid.Prefix
+ RawLeaves bool
+}
+
+// VerifyTrickleDagStructure checks that the given dag matches exactly the trickle dag datastructure
+// layout
+func VerifyTrickleDagStructure(nd ipld.Node, p VerifyParams) error {
+ return verifyTDagRec(nd, -1, p)
+}
+
+// Recursive call for verifying the structure of a trickledag
+func verifyTDagRec(n ipld.Node, depth int, p VerifyParams) error {
+ codec := cid.DagProtobuf
+ if depth == 0 {
+ if len(n.Links()) > 0 {
+ return errors.New("expected direct block")
+ }
+ // zero depth dag is raw data block
+ switch nd := n.(type) {
+ case *dag.ProtoNode:
+ fsn, err := ft.FSNodeFromBytes(nd.Data())
+ if err != nil {
+ return err
+ }
+
+ if fsn.Type() != ft.TRaw {
+ return errors.New("expected raw block")
+ }
+
+ if p.RawLeaves {
+ return errors.New("expected raw leaf, got a protobuf node")
+ }
+ case *dag.RawNode:
+ if !p.RawLeaves {
+ return errors.New("expected protobuf node as leaf")
+ }
+ codec = cid.Raw
+ default:
+ return errors.New("expected ProtoNode or RawNode")
+ }
+ }
+
+ // verify prefix
+ if p.Prefix != nil {
+ prefix := n.Cid().Prefix()
+ expect := *p.Prefix // make a copy
+ expect.Codec = uint64(codec)
+ if codec == cid.Raw && expect.Version == 0 {
+ expect.Version = 1
+ }
+ if expect.MhLength == -1 {
+ expect.MhLength = prefix.MhLength
+ }
+ if prefix != expect {
+ return fmt.Errorf("unexpected cid prefix: expected: %v; got %v", expect, prefix)
+ }
+ }
+
+ if depth == 0 {
+ return nil
+ }
+
+ nd, ok := n.(*dag.ProtoNode)
+ if !ok {
+ return errors.New("expected ProtoNode")
+ }
+
+ // Verify this is a branch node
+ fsn, err := ft.FSNodeFromBytes(nd.Data())
+ if err != nil {
+ return err
+ }
+
+ if fsn.Type() != ft.TFile {
+ return fmt.Errorf("expected file as branch node, got: %s", fsn.Type())
+ }
+
+ if len(fsn.Data()) > 0 {
+ return errors.New("branch node should not have data")
+ }
+
+ for i := 0; i < len(nd.Links()); i++ {
+ child, err := nd.Links()[i].GetNode(context.TODO(), p.Getter)
+ if err != nil {
+ return err
+ }
+
+ if i < p.Direct {
+ // Direct blocks
+ err := verifyTDagRec(child, 0, p)
+ if err != nil {
+ return err
+ }
+ } else {
+ // Recursive trickle dags
+ rdepth := ((i - p.Direct) / p.LayerRepeat) + 1
+ if rdepth >= depth && depth > 0 {
+ return errors.New("child dag was too deep")
+ }
+ err := verifyTDagRec(child, rdepth, p)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
diff --git a/unixfs/internal/config.go b/unixfs/internal/config.go
new file mode 100644
index 0000000000..9250ae2ae3
--- /dev/null
+++ b/unixfs/internal/config.go
@@ -0,0 +1,3 @@
+package internal
+
+var HAMTHashFunction func(val []byte) []byte
diff --git a/unixfs/io/completehamt_test.go b/unixfs/io/completehamt_test.go
new file mode 100644
index 0000000000..231765de10
--- /dev/null
+++ b/unixfs/io/completehamt_test.go
@@ -0,0 +1,101 @@
+package io
+
+import (
+ "context"
+ "encoding/binary"
+ "fmt"
+ "math"
+ "testing"
+
+ "github.com/ipfs/boxo/unixfs/internal"
+
+ mdtest "github.com/ipfs/boxo/ipld/merkledag/test"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/ipfs/boxo/unixfs"
+ "github.com/ipfs/boxo/unixfs/hamt"
+
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+// CreateCompleteHAMT creates a HAMT the following properties:
+// - its height (distance/edges from root to deepest node) is specified by treeHeight.
+// - all leaf Shard nodes have the same depth (and have only 'value' links).
+// - all internal Shard nodes point only to other Shards (and hence have zero 'value' links).
+// - the total number of 'value' links (directory entries) is:
+// childsPerNode ^ (treeHeight).
+// treeHeight: The number of layers of non-value HAMT nodes (e.g. height = 1 is a single shard pointing to some values)
+//
+// FIXME: HAMTHashFunction needs to be set to idHash by the caller. We depend on
+//
+// this simplification for the current logic to work.
+func CreateCompleteHAMT(ds ipld.DAGService, treeHeight int, childsPerNode int) (ipld.Node, error) {
+ if treeHeight < 1 {
+ panic("treeHeight < 1")
+ }
+ if treeHeight > 8 {
+ panic("treeHeight > 8: we don't allow a key larger than what can be encoded in a 64-bit word")
+ }
+
+ rootShard, err := hamt.NewShard(ds, childsPerNode)
+ if err != nil {
+ return nil, err
+ }
+
+ // Assuming we are using the ID hash function we can just insert all
+ // the combinations of a byte slice that will reach the desired height.
+ totalChildren := int(math.Pow(float64(childsPerNode), float64(treeHeight)))
+ log2ofChilds, err := hamt.Logtwo(childsPerNode)
+ if err != nil {
+ return nil, err
+ }
+ if log2ofChilds*treeHeight%8 != 0 {
+ return nil, fmt.Errorf("childsPerNode * treeHeight should be multiple of 8")
+ }
+ bytesInKey := log2ofChilds * treeHeight / 8
+ for i := 0; i < totalChildren; i++ {
+ var hashbuf [8]byte
+ binary.LittleEndian.PutUint64(hashbuf[:], uint64(i))
+ var oldLink *ipld.Link
+ oldLink, err = rootShard.Swap(context.Background(), string(hashbuf[:bytesInKey]), unixfs.EmptyFileNode())
+ if err != nil {
+ return nil, err
+ }
+ if oldLink != nil {
+ // We shouldn't be overwriting any value, otherwise the tree
+ // won't be complete.
+ return nil, fmt.Errorf("we have overwritten entry %s",
+ oldLink.Cid)
+ }
+ }
+
+ return rootShard.Node()
+}
+
+// Return the same value as the hash.
+func idHash(val []byte) []byte {
+ return val
+}
+
+// FIXME: This is not checking the exact height of the tree but just making
+//
+// sure there are as many children as we would have with a complete HAMT.
+func TestCreateCompleteShard(t *testing.T) {
+ oldHashFunc := internal.HAMTHashFunction
+ defer func() { internal.HAMTHashFunction = oldHashFunc }()
+ internal.HAMTHashFunction = idHash
+
+ ds := mdtest.Mock()
+ childsPerNode := 16
+ treeHeight := 2
+ node, err := CreateCompleteHAMT(ds, treeHeight, childsPerNode)
+ assert.NoError(t, err)
+
+ shard, err := hamt.NewHamtFromDag(ds, node)
+ assert.NoError(t, err)
+ links, err := shard.EnumLinks(context.Background())
+ assert.NoError(t, err)
+
+ childNodes := int(math.Pow(float64(childsPerNode), float64(treeHeight)))
+ assert.Equal(t, childNodes, len(links))
+}
diff --git a/unixfs/io/dagreader.go b/unixfs/io/dagreader.go
new file mode 100644
index 0000000000..d0500d31bf
--- /dev/null
+++ b/unixfs/io/dagreader.go
@@ -0,0 +1,488 @@
+package io
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "io"
+
+ unixfs "github.com/ipfs/boxo/unixfs"
+ ipld "github.com/ipfs/go-ipld-format"
+ mdag "github.com/ipfs/boxo/ipld/merkledag"
+)
+
+// Common errors
+var (
+ ErrIsDir = errors.New("this dag node is a directory")
+ ErrCantReadSymlinks = errors.New("cannot currently read symlinks")
+ ErrUnkownNodeType = errors.New("unknown node type")
+ ErrSeekNotSupported = errors.New("file does not support seeking")
+)
+
+// TODO: Rename the `DagReader` interface, this doesn't read *any* DAG, just
+// DAGs with UnixFS node (and it *belongs* to the `unixfs` package). Some
+// alternatives: `FileReader`, `UnixFSFileReader`, `UnixFSReader`.
+
+// A DagReader provides read-only read and seek acess to a unixfs file.
+// Different implementations of readers are used for the different
+// types of unixfs/protobuf-encoded nodes.
+type DagReader interface {
+ ReadSeekCloser
+ Size() uint64
+ CtxReadFull(context.Context, []byte) (int, error)
+}
+
+// A ReadSeekCloser implements interfaces to read, copy, seek and close.
+type ReadSeekCloser interface {
+ io.Reader
+ io.Seeker
+ io.Closer
+ io.WriterTo
+}
+
+// NewDagReader creates a new reader object that reads the data represented by
+// the given node, using the passed in DAGService for data retrieval.
+func NewDagReader(ctx context.Context, n ipld.Node, serv ipld.NodeGetter) (DagReader, error) {
+ var size uint64
+
+ switch n := n.(type) {
+ case *mdag.RawNode:
+ size = uint64(len(n.RawData()))
+
+ case *mdag.ProtoNode:
+ fsNode, err := unixfs.FSNodeFromBytes(n.Data())
+ if err != nil {
+ return nil, err
+ }
+
+ switch fsNode.Type() {
+ case unixfs.TFile, unixfs.TRaw:
+ size = fsNode.FileSize()
+
+ case unixfs.TDirectory, unixfs.THAMTShard:
+ // Dont allow reading directories
+ return nil, ErrIsDir
+
+ case unixfs.TMetadata:
+ if len(n.Links()) == 0 {
+ return nil, errors.New("incorrectly formatted metadata object")
+ }
+ child, err := n.Links()[0].GetNode(ctx, serv)
+ if err != nil {
+ return nil, err
+ }
+
+ childpb, ok := child.(*mdag.ProtoNode)
+ if !ok {
+ return nil, mdag.ErrNotProtobuf
+ }
+ return NewDagReader(ctx, childpb, serv)
+ case unixfs.TSymlink:
+ return nil, ErrCantReadSymlinks
+ default:
+ return nil, unixfs.ErrUnrecognizedType
+ }
+ default:
+ return nil, ErrUnkownNodeType
+ }
+
+ ctxWithCancel, cancel := context.WithCancel(ctx)
+
+ return &dagReader{
+ ctx: ctxWithCancel,
+ cancel: cancel,
+ serv: serv,
+ size: size,
+ rootNode: n,
+ dagWalker: ipld.NewWalker(ctxWithCancel, ipld.NewNavigableIPLDNode(n, serv)),
+ }, nil
+}
+
+// dagReader provides a way to easily read the data contained in a dag.
+type dagReader struct {
+
+ // Structure to perform the DAG iteration and search, the reader
+ // just needs to add logic to the `Visitor` callback passed to
+ // `Iterate` and `Seek`.
+ dagWalker *ipld.Walker
+
+ // Buffer with the data extracted from the current node being visited.
+ // To avoid revisiting a node to complete a (potential) partial read
+ // (or read after seek) the node's data is fully extracted in a single
+ // `readNodeDataBuffer` operation.
+ currentNodeData *bytes.Reader
+
+ // Implements the `Size()` API.
+ size uint64
+
+ // Current offset for the read head within the DAG file.
+ offset int64
+
+ // Root node of the DAG, stored to re-create the `dagWalker` (effectively
+ // re-setting the position of the reader, used during `Seek`).
+ rootNode ipld.Node
+
+ // Context passed to the `dagWalker`, the `cancel` function is used to
+ // cancel read operations (cancelling requested child node promises,
+ // see `ipld.NavigableIPLDNode.FetchChild` for details).
+ ctx context.Context
+ cancel func()
+
+ // Passed to the `dagWalker` that will use it to request nodes.
+ // TODO: Revisit name.
+ serv ipld.NodeGetter
+}
+
+// Size returns the total size of the data from the DAG structured file.
+func (dr *dagReader) Size() uint64 {
+ return dr.size
+}
+
+// Read implements the `io.Reader` interface through the `CtxReadFull`
+// method using the DAG reader's internal context.
+func (dr *dagReader) Read(b []byte) (int, error) {
+ return dr.CtxReadFull(dr.ctx, b)
+}
+
+// CtxReadFull reads data from the DAG structured file. It always
+// attempts a full read of the DAG until the `out` buffer is full.
+// It uses the `Walker` structure to iterate the file DAG and read
+// every node's data into the `out` buffer.
+func (dr *dagReader) CtxReadFull(ctx context.Context, out []byte) (n int, err error) {
+ // Set the `dagWalker`'s context to the `ctx` argument, it will be used
+ // to fetch the child node promises (see
+ // `ipld.NavigableIPLDNode.FetchChild` for details).
+ dr.dagWalker.SetContext(ctx)
+
+ // If there was a partially read buffer from the last visited
+ // node read it before visiting a new one.
+ if dr.currentNodeData != nil {
+ // TODO: Move this check inside `readNodeDataBuffer`?
+ n = dr.readNodeDataBuffer(out)
+
+ if n == len(out) {
+ return n, nil
+ // Output buffer full, no need to traverse the DAG.
+ }
+ }
+
+ // Iterate the DAG calling the passed `Visitor` function on every node
+ // to read its data into the `out` buffer, stop if there is an error or
+ // if the entire DAG is traversed (`EndOfDag`).
+ err = dr.dagWalker.Iterate(func(visitedNode ipld.NavigableNode) error {
+ node := ipld.ExtractIPLDNode(visitedNode)
+
+ // Skip internal nodes, they shouldn't have any file data
+ // (see the `balanced` package for more details).
+ if len(node.Links()) > 0 {
+ return nil
+ }
+
+ err = dr.saveNodeData(node)
+ if err != nil {
+ return err
+ }
+ // Save the leaf node file data in a buffer in case it is only
+ // partially read now and future `CtxReadFull` calls reclaim the
+ // rest (as each node is visited only once during `Iterate`).
+ //
+ // TODO: We could check if the entire node's data can fit in the
+ // remaining `out` buffer free space to skip this intermediary step.
+
+ n += dr.readNodeDataBuffer(out[n:])
+
+ if n == len(out) {
+ // Output buffer full, no need to keep traversing the DAG,
+ // signal the `Walker` to pause the iteration.
+ dr.dagWalker.Pause()
+ }
+
+ return nil
+ })
+
+ if err == ipld.EndOfDag {
+ return n, io.EOF
+ // Reached the end of the (DAG) file, no more data to read.
+ } else if err != nil {
+ return n, err
+ // Pass along any other errors from the `Visitor`.
+ }
+
+ return n, nil
+}
+
+// Save the UnixFS `node`'s data into the internal `currentNodeData` buffer to
+// later move it to the output buffer (`Read`) or seek into it (`Seek`).
+func (dr *dagReader) saveNodeData(node ipld.Node) error {
+ extractedNodeData, err := unixfs.ReadUnixFSNodeData(node)
+ if err != nil {
+ return err
+ }
+
+ dr.currentNodeData = bytes.NewReader(extractedNodeData)
+ return nil
+}
+
+// Read the `currentNodeData` buffer into `out`. This function can't have
+// any errors as it's always reading from a `bytes.Reader` and asking only
+// the available data in it.
+func (dr *dagReader) readNodeDataBuffer(out []byte) int {
+
+ n, _ := dr.currentNodeData.Read(out)
+ // Ignore the error as the EOF may not be returned in the first
+ // `Read` call, explicitly ask for an empty buffer below to check
+ // if we've reached the end.
+
+ if dr.currentNodeData.Len() == 0 {
+ dr.currentNodeData = nil
+ // Signal that the buffer was consumed (for later `Read` calls).
+ // This shouldn't return an EOF error as it's just the end of a
+ // single node's data, not the entire DAG.
+ }
+
+ dr.offset += int64(n)
+ // TODO: Should `offset` be incremented here or in the calling function?
+ // (Doing it here saves LoC but may be confusing as it's more hidden).
+
+ return n
+}
+
+// Similar to `readNodeDataBuffer` but it writes the contents to
+// an `io.Writer` argument.
+//
+// TODO: Check what part of the logic between the two functions
+// can be extracted away.
+func (dr *dagReader) writeNodeDataBuffer(w io.Writer) (int64, error) {
+
+ n, err := dr.currentNodeData.WriteTo(w)
+ if err != nil {
+ return n, err
+ }
+
+ if dr.currentNodeData.Len() == 0 {
+ dr.currentNodeData = nil
+ // Signal that the buffer was consumed (for later `Read` calls).
+ // This shouldn't return an EOF error as it's just the end of a
+ // single node's data, not the entire DAG.
+ }
+
+ dr.offset += int64(n)
+ return n, nil
+}
+
+// WriteTo writes to the given writer.
+// This follows the `bytes.Reader.WriteTo` implementation
+// where it starts from the internal index that may have
+// been modified by other `Read` calls.
+//
+// TODO: This implementation is very similar to `CtxReadFull`,
+// the common parts should be abstracted away.
+func (dr *dagReader) WriteTo(w io.Writer) (n int64, err error) {
+ // Use the internal reader's context to fetch the child node promises
+ // (see `ipld.NavigableIPLDNode.FetchChild` for details).
+ dr.dagWalker.SetContext(dr.ctx)
+
+ // If there was a partially read buffer from the last visited
+ // node read it before visiting a new one.
+ if dr.currentNodeData != nil {
+ n, err = dr.writeNodeDataBuffer(w)
+ if err != nil {
+ return n, err
+ }
+ }
+
+ // Iterate the DAG calling the passed `Visitor` function on every node
+ // to read its data into the `out` buffer, stop if there is an error or
+ // if the entire DAG is traversed (`EndOfDag`).
+ err = dr.dagWalker.Iterate(func(visitedNode ipld.NavigableNode) error {
+ node := ipld.ExtractIPLDNode(visitedNode)
+
+ // Skip internal nodes, they shouldn't have any file data
+ // (see the `balanced` package for more details).
+ if len(node.Links()) > 0 {
+ return nil
+ }
+
+ err = dr.saveNodeData(node)
+ if err != nil {
+ return err
+ }
+ // Save the leaf node file data in a buffer in case it is only
+ // partially read now and future `CtxReadFull` calls reclaim the
+ // rest (as each node is visited only once during `Iterate`).
+
+ written, err := dr.writeNodeDataBuffer(w)
+ n += written
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+
+ if err == ipld.EndOfDag {
+ return n, nil
+ }
+
+ return n, err
+}
+
+// Close the reader (cancelling fetch node operations requested with
+// the internal context, that is, `Read` calls but not `CtxReadFull`
+// with user-supplied contexts).
+func (dr *dagReader) Close() error {
+ dr.cancel()
+ return nil
+}
+
+// Seek implements `io.Seeker` seeking to a given offset in the DAG file,
+// it matches the standard unix `seek`. It moves the position of the internal
+// `dagWalker` and may also leave a `currentNodeData` buffer loaded in case
+// the seek is performed to the middle of the data in a node.
+//
+// TODO: Support seeking from the current position (relative seek)
+// through the `dagWalker` in `io.SeekCurrent`.
+func (dr *dagReader) Seek(offset int64, whence int) (int64, error) {
+ switch whence {
+ case io.SeekStart:
+ if offset < 0 {
+ return dr.offset, errors.New("invalid offset")
+ }
+
+ if offset == dr.offset {
+ return offset, nil
+ // Already at the requested `offset`, nothing to do.
+ }
+
+ left := offset
+ // Amount left to seek.
+
+ // Seek from the beginning of the DAG.
+ dr.resetPosition()
+
+ // Shortcut seeking to the beginning, we're already there.
+ if offset == 0 {
+ return 0, nil
+ }
+
+ // Use the internal reader's context to fetch the child node promises
+ // (see `ipld.NavigableIPLDNode.FetchChild` for details).
+ dr.dagWalker.SetContext(dr.ctx)
+ // TODO: Performance: we could adjust here `preloadSize` of
+ // `ipld.NavigableIPLDNode` also, when seeking we only want
+ // to fetch one child at a time.
+
+ // Seek the DAG by calling the provided `Visitor` function on every
+ // node the `dagWalker` descends to while searching which can be
+ // either an internal or leaf node. In the internal node case, check
+ // the child node sizes and set the corresponding child index to go
+ // down to next. In the leaf case (last visit of the search), if there
+ // is still an amount `left` to seek do it inside the node's data
+ // saved in the `currentNodeData` buffer, leaving it ready for a `Read`
+ // call.
+ err := dr.dagWalker.Seek(func(visitedNode ipld.NavigableNode) error {
+ node := ipld.ExtractIPLDNode(visitedNode)
+
+ if len(node.Links()) > 0 {
+ // Internal node, should be a `mdag.ProtoNode` containing a
+ // `unixfs.FSNode` (see the `balanced` package for more details).
+ fsNode, err := unixfs.ExtractFSNode(node)
+ if err != nil {
+ return err
+ }
+
+ // If there aren't enough size hints don't seek
+ // (see the `io.EOF` handling error comment below).
+ if fsNode.NumChildren() != len(node.Links()) {
+ return ErrSeekNotSupported
+ }
+
+ // Internal nodes have no data, so just iterate through the
+ // sizes of its children (advancing the child index of the
+ // `dagWalker`) to find where we need to go down to next in
+ // the search.
+ for {
+ childSize := fsNode.BlockSize(int(dr.dagWalker.ActiveChildIndex()))
+
+ if childSize > uint64(left) {
+ // This child's data contains the position requested
+ // in `offset`, go down this child.
+ return nil
+ }
+
+ // Else, skip this child.
+ left -= int64(childSize)
+ err := dr.dagWalker.NextChild()
+ if err == ipld.ErrNextNoChild {
+ // No more child nodes available, nothing to do,
+ // the `Seek` will stop on its own.
+ return nil
+ } else if err != nil {
+ return err
+ // Pass along any other errors (that may in future
+ // implementations be returned by `Next`) to stop
+ // the search.
+ }
+ }
+
+ } else {
+ // Leaf node, seek inside its data.
+ err := dr.saveNodeData(node)
+ if err != nil {
+ return err
+ }
+
+ _, err = dr.currentNodeData.Seek(left, io.SeekStart)
+ if err != nil {
+ return err
+ }
+ // The corner case of a DAG consisting only of a single (leaf)
+ // node should make no difference here. In that case, where the
+ // node doesn't have a parent UnixFS node with size hints, this
+ // implementation would allow this `Seek` to be called with an
+ // argument larger than the buffer size which normally wouldn't
+ // happen (because we would skip the node based on the size
+ // hint) but that would just mean that a future `CtxReadFull`
+ // call would read no data from the `currentNodeData` buffer.
+ // TODO: Re-check this reasoning.
+
+ return nil
+ // In the leaf node case the search will stop here.
+ }
+ })
+
+ if err != nil {
+ return 0, err
+ }
+
+ dr.offset = offset
+ return dr.offset, nil
+
+ case io.SeekCurrent:
+ if offset == 0 {
+ return dr.offset, nil
+ }
+
+ return dr.Seek(dr.offset+offset, io.SeekStart)
+ // TODO: Performance. This can be improved supporting relative
+ // searches in the `Walker` (see `Walker.Seek`).
+
+ case io.SeekEnd:
+ return dr.Seek(int64(dr.Size())+offset, io.SeekStart)
+
+ default:
+ return 0, errors.New("invalid whence")
+ }
+}
+
+// Reset the reader position by resetting the `dagWalker` and discarding
+// any partially used node's data in the `currentNodeData` buffer, used
+// in the `SeekStart` case.
+func (dr *dagReader) resetPosition() {
+ dr.currentNodeData = nil
+ dr.offset = 0
+
+ dr.dagWalker = ipld.NewWalker(dr.ctx, ipld.NewNavigableIPLDNode(dr.rootNode, dr.serv))
+ // TODO: This could be avoided (along with storing the `dr.rootNode` and
+ // `dr.serv` just for this call) if `Reset` is supported in the `Walker`.
+}
diff --git a/unixfs/io/dagreader_test.go b/unixfs/io/dagreader_test.go
new file mode 100644
index 0000000000..aacf74fe1a
--- /dev/null
+++ b/unixfs/io/dagreader_test.go
@@ -0,0 +1,326 @@
+package io
+
+import (
+ "bytes"
+ "io"
+ "strings"
+ "testing"
+
+ "github.com/ipfs/boxo/unixfs"
+ mdag "github.com/ipfs/boxo/ipld/merkledag"
+
+ context "context"
+
+ testu "github.com/ipfs/boxo/unixfs/test"
+)
+
+func TestBasicRead(t *testing.T) {
+ dserv := testu.GetDAGServ()
+ inbuf, node := testu.GetRandomNode(t, dserv, 1024, testu.UseProtoBufLeaves)
+ ctx, closer := context.WithCancel(context.Background())
+ defer closer()
+
+ reader, err := NewDagReader(ctx, node, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ outbuf, err := io.ReadAll(reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = testu.ArrComp(inbuf, outbuf)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSeekAndRead(t *testing.T) {
+ dserv := testu.GetDAGServ()
+ inbuf := make([]byte, 256)
+ for i := 0; i <= 255; i++ {
+ inbuf[i] = byte(i)
+ }
+
+ node := testu.GetNode(t, dserv, inbuf, testu.UseProtoBufLeaves)
+ ctx, closer := context.WithCancel(context.Background())
+ defer closer()
+
+ reader, err := NewDagReader(ctx, node, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for i := 255; i >= 0; i-- {
+ reader.Seek(int64(i), io.SeekStart)
+
+ if getOffset(reader) != int64(i) {
+ t.Fatal("expected offset to be increased by one after read")
+ }
+
+ out := readByte(t, reader)
+
+ if int(out) != i {
+ t.Fatalf("read %d at index %d, expected %d", out, i, i)
+ }
+
+ if getOffset(reader) != int64(i+1) {
+ t.Fatal("expected offset to be increased by one after read")
+ }
+ }
+}
+
+func TestSeekWithoutBlocksizes(t *testing.T) {
+ dserv := testu.GetDAGServ()
+ ctx, closer := context.WithCancel(context.Background())
+ defer closer()
+
+ inbuf := make([]byte, 1024)
+
+ for i := 0; i < 256; i++ {
+ inbuf[i*4] = byte(i)
+ }
+
+ inbuf[1023] = 1 // force the reader to be 1024 bytes
+ node := testu.GetNode(t, dserv, inbuf, testu.UseProtoBufLeaves)
+
+ // remove the blocksizes
+ pbnode := node.Copy().(*mdag.ProtoNode)
+ fsnode, err := unixfs.FSNodeFromBytes(pbnode.Data())
+ if err != nil {
+ t.Fatal(err)
+ }
+ fsnode.RemoveAllBlockSizes()
+ newData, err := fsnode.GetBytes()
+ if err != nil {
+ t.Fatal(err)
+ }
+ pbnode.SetData(newData)
+ err = dserv.Add(ctx, pbnode)
+ if err != nil {
+ t.Fatal(err)
+ }
+ node = pbnode
+
+ reader, err := NewDagReader(ctx, node, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = reader.Seek(-4, io.SeekEnd)
+ if err == nil {
+ t.Fatal("seeking shouldn't work without blocksizes")
+ }
+
+ _, err = reader.Seek(4, io.SeekStart)
+ if err == nil {
+ t.Fatal("seeking shouldn't work without blocksizes")
+ }
+
+ _, err = reader.Seek(4, io.SeekCurrent)
+ if err == nil {
+ t.Fatal("seeking shouldn't work without blocksizes")
+ }
+
+ // Seeking to the current position or the end should still work.
+
+ _, err = reader.Seek(0, io.SeekCurrent)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = reader.Seek(0, io.SeekStart)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestRelativeSeek(t *testing.T) {
+ dserv := testu.GetDAGServ()
+ ctx, closer := context.WithCancel(context.Background())
+ defer closer()
+
+ inbuf := make([]byte, 1024)
+
+ for i := 0; i < 256; i++ {
+ inbuf[i*4] = byte(i)
+ }
+
+ inbuf[1023] = 1 // force the reader to be 1024 bytes
+ node := testu.GetNode(t, dserv, inbuf, testu.UseProtoBufLeaves)
+
+ reader, err := NewDagReader(ctx, node, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for i := 0; i < 256; i++ {
+ if getOffset(reader) != int64(i*4) {
+ t.Fatalf("offset should be %d, was %d", i*4, getOffset(reader))
+ }
+ out := readByte(t, reader)
+ if int(out) != i {
+ t.Fatalf("expected to read: %d at %d, read %d", i, getOffset(reader)-1, out)
+ }
+ if i != 255 {
+ _, err := reader.Seek(3, io.SeekCurrent)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ }
+
+ _, err = reader.Seek(-4, io.SeekEnd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for i := 0; i < 256; i++ {
+ if getOffset(reader) != int64(1020-i*4) {
+ t.Fatalf("offset should be %d, was %d", 1020-i*4, getOffset(reader))
+ }
+ out := readByte(t, reader)
+ if int(out) != 255-i {
+ t.Fatalf("expected to read: %d at %d, read %d", 255-i, getOffset(reader)-1, out)
+ }
+ reader.Seek(-5, io.SeekCurrent) // seek 4 bytes but we read one byte every time so 5 bytes
+ }
+
+}
+
+func TestTypeFailures(t *testing.T) {
+ dserv := testu.GetDAGServ()
+ ctx, closer := context.WithCancel(context.Background())
+ defer closer()
+
+ node := unixfs.EmptyDirNode()
+ if _, err := NewDagReader(ctx, node, dserv); err != ErrIsDir {
+ t.Fatalf("excepted to get %v, got %v", ErrIsDir, err)
+ }
+
+ data, err := unixfs.SymlinkData("/somelink")
+ if err != nil {
+ t.Fatal(err)
+ }
+ node = mdag.NodeWithData(data)
+
+ if _, err := NewDagReader(ctx, node, dserv); err != ErrCantReadSymlinks {
+ t.Fatalf("excepted to get %v, got %v", ErrCantReadSymlinks, err)
+ }
+}
+
+func TestBadPBData(t *testing.T) {
+ dserv := testu.GetDAGServ()
+ ctx, closer := context.WithCancel(context.Background())
+ defer closer()
+
+ node := mdag.NodeWithData([]byte{42})
+ _, err := NewDagReader(ctx, node, dserv)
+ if err == nil {
+ t.Fatal("excepted error, got nil")
+ }
+}
+
+func TestMetadataNode(t *testing.T) {
+ ctx, closer := context.WithCancel(context.Background())
+ defer closer()
+
+ dserv := testu.GetDAGServ()
+ rdata, rnode := testu.GetRandomNode(t, dserv, 512, testu.UseProtoBufLeaves)
+ err := dserv.Add(ctx, rnode)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ data, err := unixfs.BytesForMetadata(&unixfs.Metadata{
+ MimeType: "text",
+ Size: 125,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ node := mdag.NodeWithData(data)
+
+ _, err = NewDagReader(ctx, node, dserv)
+ if err == nil {
+ t.Fatal("expected an error")
+ }
+ if !strings.Contains(err.Error(), "incorrectly formatted") {
+ t.Fatal("expected different error")
+ }
+
+ node.AddNodeLink("", rnode)
+
+ reader, err := NewDagReader(ctx, node, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+ readdata, err := io.ReadAll(reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := testu.ArrComp(rdata, readdata); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestWriteTo(t *testing.T) {
+ dserv := testu.GetDAGServ()
+ inbuf, node := testu.GetRandomNode(t, dserv, 1024, testu.UseProtoBufLeaves)
+ ctx, closer := context.WithCancel(context.Background())
+ defer closer()
+
+ reader, err := NewDagReader(ctx, node, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ outbuf := new(bytes.Buffer)
+ reader.WriteTo(outbuf)
+
+ err = testu.ArrComp(inbuf, outbuf.Bytes())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+}
+
+func TestReaderSzie(t *testing.T) {
+ dserv := testu.GetDAGServ()
+ size := int64(1024)
+ _, node := testu.GetRandomNode(t, dserv, size, testu.UseProtoBufLeaves)
+ ctx, closer := context.WithCancel(context.Background())
+ defer closer()
+
+ reader, err := NewDagReader(ctx, node, dserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if reader.Size() != uint64(size) {
+ t.Fatal("wrong reader size")
+ }
+}
+
+func readByte(t testing.TB, reader DagReader) byte {
+ out := make([]byte, 1)
+ c, err := reader.Read(out)
+
+ if c != 1 {
+ t.Fatal("reader should have read just one byte")
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ return out[0]
+}
+
+func getOffset(reader DagReader) int64 {
+ offset, err := reader.Seek(0, io.SeekCurrent)
+ if err != nil {
+ panic("failed to retrieve offset: " + err.Error())
+ }
+ return offset
+}
diff --git a/unixfs/io/directory.go b/unixfs/io/directory.go
new file mode 100644
index 0000000000..0679559a05
--- /dev/null
+++ b/unixfs/io/directory.go
@@ -0,0 +1,610 @@
+package io
+
+import (
+ "context"
+ "fmt"
+ "os"
+
+ "github.com/ipfs/boxo/unixfs/hamt"
+ "github.com/ipfs/boxo/unixfs/private/linksize"
+
+ "github.com/alecthomas/units"
+ format "github.com/ipfs/boxo/unixfs"
+ "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+ mdag "github.com/ipfs/boxo/ipld/merkledag"
+ logging "github.com/ipfs/go-log"
+)
+
+var log = logging.Logger("unixfs")
+
+// HAMTShardingSize is a global option that allows switching to a HAMTDirectory
+// when the BasicDirectory grows above the size (in bytes) signalled by this
+// flag. The default size of 0 disables the option.
+// The size is not the *exact* block size of the encoded BasicDirectory but just
+// the estimated size based byte length of links name and CID (BasicDirectory's
+// ProtoNode doesn't use the Data field so this estimate is pretty accurate).
+var HAMTShardingSize = int(256 * units.KiB)
+
+// DefaultShardWidth is the default value used for hamt sharding width.
+// Needs to be a power of two (shard entry size) and multiple of 8 (bitfield size).
+var DefaultShardWidth = 256
+
+// Directory defines a UnixFS directory. It is used for creating, reading and
+// editing directories. It allows to work with different directory schemes,
+// like the basic or the HAMT implementation.
+//
+// It just allows to perform explicit edits on a single directory, working with
+// directory trees is out of its scope, they are managed by the MFS layer
+// (which is the main consumer of this interface).
+type Directory interface {
+
+ // SetCidBuilder sets the CID Builder of the root node.
+ SetCidBuilder(cid.Builder)
+
+ // AddChild adds a (name, key) pair to the root node.
+ AddChild(context.Context, string, ipld.Node) error
+
+ // ForEachLink applies the given function to Links in the directory.
+ ForEachLink(context.Context, func(*ipld.Link) error) error
+
+ // EnumLinksAsync returns a channel which will receive Links in the directory
+ // as they are enumerated, where order is not gauranteed
+ EnumLinksAsync(context.Context) <-chan format.LinkResult
+
+ // Links returns the all the links in the directory node.
+ Links(context.Context) ([]*ipld.Link, error)
+
+ // Find returns the root node of the file named 'name' within this directory.
+ // In the case of HAMT-directories, it will traverse the tree.
+ //
+ // Returns os.ErrNotExist if the child does not exist.
+ Find(context.Context, string) (ipld.Node, error)
+
+ // RemoveChild removes the child with the given name.
+ //
+ // Returns os.ErrNotExist if the child doesn't exist.
+ RemoveChild(context.Context, string) error
+
+ // GetNode returns the root of this directory.
+ GetNode() (ipld.Node, error)
+
+ // GetCidBuilder returns the CID Builder used.
+ GetCidBuilder() cid.Builder
+}
+
+// TODO: Evaluate removing `dserv` from this layer and providing it in MFS.
+// (The functions should in that case add a `DAGService` argument.)
+
+// Link size estimation function. For production it's usually the one here
+// but during test we may mock it to get fixed sizes.
+func productionLinkSize(linkName string, linkCid cid.Cid) int {
+ return len(linkName) + linkCid.ByteLen()
+}
+
+func init() {
+ linksize.LinkSizeFunction = productionLinkSize
+}
+
+// BasicDirectory is the basic implementation of `Directory`. All the entries
+// are stored in a single node.
+type BasicDirectory struct {
+ node *mdag.ProtoNode
+ dserv ipld.DAGService
+
+ // Internal variable used to cache the estimated size of the basic directory:
+ // for each link, aggregate link name + link CID. DO NOT CHANGE THIS
+ // as it will affect the HAMT transition behavior in HAMTShardingSize.
+ // (We maintain this value up to date even if the HAMTShardingSize is off
+ // since potentially the option could be activated on the fly.)
+ estimatedSize int
+}
+
+// HAMTDirectory is the HAMT implementation of `Directory`.
+// (See package `hamt` for more information.)
+type HAMTDirectory struct {
+ shard *hamt.Shard
+ dserv ipld.DAGService
+
+ // Track the changes in size by the AddChild and RemoveChild calls
+ // for the HAMTShardingSize option.
+ sizeChange int
+}
+
+func newEmptyBasicDirectory(dserv ipld.DAGService) *BasicDirectory {
+ return newBasicDirectoryFromNode(dserv, format.EmptyDirNode())
+}
+
+func newBasicDirectoryFromNode(dserv ipld.DAGService, node *mdag.ProtoNode) *BasicDirectory {
+ basicDir := new(BasicDirectory)
+ basicDir.node = node
+ basicDir.dserv = dserv
+
+ // Scan node links (if any) to restore estimated size.
+ basicDir.computeEstimatedSize()
+
+ return basicDir
+}
+
+// NewDirectory returns a Directory implemented by DynamicDirectory
+// containing a BasicDirectory that can be converted to a HAMTDirectory.
+func NewDirectory(dserv ipld.DAGService) Directory {
+ return &DynamicDirectory{newEmptyBasicDirectory(dserv)}
+}
+
+// ErrNotADir implies that the given node was not a unixfs directory
+var ErrNotADir = fmt.Errorf("merkledag node was not a directory or shard")
+
+// NewDirectoryFromNode loads a unixfs directory from the given IPLD node and
+// DAGService.
+func NewDirectoryFromNode(dserv ipld.DAGService, node ipld.Node) (Directory, error) {
+ protoBufNode, ok := node.(*mdag.ProtoNode)
+ if !ok {
+ return nil, ErrNotADir
+ }
+
+ fsNode, err := format.FSNodeFromBytes(protoBufNode.Data())
+ if err != nil {
+ return nil, err
+ }
+
+ switch fsNode.Type() {
+ case format.TDirectory:
+ return &DynamicDirectory{newBasicDirectoryFromNode(dserv, protoBufNode.Copy().(*mdag.ProtoNode))}, nil
+ case format.THAMTShard:
+ shard, err := hamt.NewHamtFromDag(dserv, node)
+ if err != nil {
+ return nil, err
+ }
+ return &DynamicDirectory{&HAMTDirectory{shard, dserv, 0}}, nil
+ }
+
+ return nil, ErrNotADir
+}
+
+func (d *BasicDirectory) computeEstimatedSize() {
+ d.estimatedSize = 0
+ // err is just breaking the iteration and we always return nil
+ _ = d.ForEachLink(context.TODO(), func(l *ipld.Link) error {
+ d.addToEstimatedSize(l.Name, l.Cid)
+ return nil
+ })
+ // ForEachLink will never fail traversing the BasicDirectory
+ // and neither the inner callback `addToEstimatedSize`.
+}
+
+func (d *BasicDirectory) addToEstimatedSize(name string, linkCid cid.Cid) {
+ d.estimatedSize += linksize.LinkSizeFunction(name, linkCid)
+}
+
+func (d *BasicDirectory) removeFromEstimatedSize(name string, linkCid cid.Cid) {
+ d.estimatedSize -= linksize.LinkSizeFunction(name, linkCid)
+ if d.estimatedSize < 0 {
+ // Something has gone very wrong. Log an error and recompute the
+ // size from scratch.
+ log.Error("BasicDirectory's estimatedSize went below 0")
+ d.computeEstimatedSize()
+ }
+}
+
+// SetCidBuilder implements the `Directory` interface.
+func (d *BasicDirectory) SetCidBuilder(builder cid.Builder) {
+ d.node.SetCidBuilder(builder)
+}
+
+// AddChild implements the `Directory` interface. It adds (or replaces)
+// a link to the given `node` under `name`.
+func (d *BasicDirectory) AddChild(ctx context.Context, name string, node ipld.Node) error {
+ link, err := ipld.MakeLink(node)
+ if err != nil {
+ return err
+ }
+
+ return d.addLinkChild(ctx, name, link)
+}
+
+func (d *BasicDirectory) needsToSwitchToHAMTDir(name string, nodeToAdd ipld.Node) (bool, error) {
+ if HAMTShardingSize == 0 { // Option disabled.
+ return false, nil
+ }
+
+ operationSizeChange := 0
+ // Find if there is an old entry under that name that will be overwritten.
+ entryToRemove, err := d.node.GetNodeLink(name)
+ if err != mdag.ErrLinkNotFound {
+ if err != nil {
+ return false, err
+ }
+ operationSizeChange -= linksize.LinkSizeFunction(name, entryToRemove.Cid)
+ }
+ if nodeToAdd != nil {
+ operationSizeChange += linksize.LinkSizeFunction(name, nodeToAdd.Cid())
+ }
+
+ return d.estimatedSize+operationSizeChange >= HAMTShardingSize, nil
+}
+
+// addLinkChild adds the link as an entry to this directory under the given
+// name. Plumbing function for the AddChild API.
+func (d *BasicDirectory) addLinkChild(ctx context.Context, name string, link *ipld.Link) error {
+ // Remove old link and account for size change (if it existed; ignore
+ // `ErrNotExist` otherwise).
+ err := d.RemoveChild(ctx, name)
+ if err != nil && err != os.ErrNotExist {
+ return err
+ }
+
+ err = d.node.AddRawLink(name, link)
+ if err != nil {
+ return err
+ }
+ d.addToEstimatedSize(name, link.Cid)
+ return nil
+}
+
+// EnumLinksAsync returns a channel which will receive Links in the directory
+// as they are enumerated, where order is not gauranteed
+func (d *BasicDirectory) EnumLinksAsync(ctx context.Context) <-chan format.LinkResult {
+ linkResults := make(chan format.LinkResult)
+ go func() {
+ defer close(linkResults)
+ for _, l := range d.node.Links() {
+ select {
+ case linkResults <- format.LinkResult{
+ Link: l,
+ Err: nil,
+ }:
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+ return linkResults
+}
+
+// ForEachLink implements the `Directory` interface.
+func (d *BasicDirectory) ForEachLink(_ context.Context, f func(*ipld.Link) error) error {
+ for _, l := range d.node.Links() {
+ if err := f(l); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// Links implements the `Directory` interface.
+func (d *BasicDirectory) Links(ctx context.Context) ([]*ipld.Link, error) {
+ return d.node.Links(), nil
+}
+
+// Find implements the `Directory` interface.
+func (d *BasicDirectory) Find(ctx context.Context, name string) (ipld.Node, error) {
+ lnk, err := d.node.GetNodeLink(name)
+ if err == mdag.ErrLinkNotFound {
+ err = os.ErrNotExist
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ return d.dserv.Get(ctx, lnk.Cid)
+}
+
+// RemoveChild implements the `Directory` interface.
+func (d *BasicDirectory) RemoveChild(ctx context.Context, name string) error {
+ // We need to *retrieve* the link before removing it to update the estimated
+ // size. This means we may iterate the links slice twice: if traversing this
+ // becomes a problem, a factor of 2 isn't going to make much of a difference.
+ // We'd likely need to cache a link resolution map in that case.
+ link, err := d.node.GetNodeLink(name)
+ if err == mdag.ErrLinkNotFound {
+ return os.ErrNotExist
+ }
+ if err != nil {
+ return err // at the moment there is no other error besides ErrLinkNotFound
+ }
+
+ // The name actually existed so we should update the estimated size.
+ d.removeFromEstimatedSize(link.Name, link.Cid)
+
+ return d.node.RemoveNodeLink(name)
+ // GetNodeLink didn't return ErrLinkNotFound so this won't fail with that
+ // and we don't need to convert the error again.
+}
+
+// GetNode implements the `Directory` interface.
+func (d *BasicDirectory) GetNode() (ipld.Node, error) {
+ return d.node, nil
+}
+
+// GetCidBuilder implements the `Directory` interface.
+func (d *BasicDirectory) GetCidBuilder() cid.Builder {
+ return d.node.CidBuilder()
+}
+
+// switchToSharding returns a HAMT implementation of this directory.
+func (d *BasicDirectory) switchToSharding(ctx context.Context) (*HAMTDirectory, error) {
+ hamtDir := new(HAMTDirectory)
+ hamtDir.dserv = d.dserv
+
+ shard, err := hamt.NewShard(d.dserv, DefaultShardWidth)
+ if err != nil {
+ return nil, err
+ }
+ shard.SetCidBuilder(d.node.CidBuilder())
+ hamtDir.shard = shard
+
+ for _, lnk := range d.node.Links() {
+ err = hamtDir.shard.SetLink(ctx, lnk.Name, lnk)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return hamtDir, nil
+}
+
+// SetCidBuilder implements the `Directory` interface.
+func (d *HAMTDirectory) SetCidBuilder(builder cid.Builder) {
+ d.shard.SetCidBuilder(builder)
+}
+
+// AddChild implements the `Directory` interface.
+func (d *HAMTDirectory) AddChild(ctx context.Context, name string, nd ipld.Node) error {
+ oldChild, err := d.shard.Swap(ctx, name, nd)
+ if err != nil {
+ return err
+ }
+
+ if oldChild != nil {
+ d.removeFromSizeChange(oldChild.Name, oldChild.Cid)
+ }
+ d.addToSizeChange(name, nd.Cid())
+ return nil
+}
+
+// ForEachLink implements the `Directory` interface.
+func (d *HAMTDirectory) ForEachLink(ctx context.Context, f func(*ipld.Link) error) error {
+ return d.shard.ForEachLink(ctx, f)
+}
+
+// EnumLinksAsync returns a channel which will receive Links in the directory
+// as they are enumerated, where order is not gauranteed
+func (d *HAMTDirectory) EnumLinksAsync(ctx context.Context) <-chan format.LinkResult {
+ return d.shard.EnumLinksAsync(ctx)
+}
+
+// Links implements the `Directory` interface.
+func (d *HAMTDirectory) Links(ctx context.Context) ([]*ipld.Link, error) {
+ return d.shard.EnumLinks(ctx)
+}
+
+// Find implements the `Directory` interface. It will traverse the tree.
+func (d *HAMTDirectory) Find(ctx context.Context, name string) (ipld.Node, error) {
+ lnk, err := d.shard.Find(ctx, name)
+ if err != nil {
+ return nil, err
+ }
+
+ return lnk.GetNode(ctx, d.dserv)
+}
+
+// RemoveChild implements the `Directory` interface.
+func (d *HAMTDirectory) RemoveChild(ctx context.Context, name string) error {
+ oldChild, err := d.shard.Take(ctx, name)
+ if err != nil {
+ return err
+ }
+
+ if oldChild != nil {
+ d.removeFromSizeChange(oldChild.Name, oldChild.Cid)
+ }
+
+ return nil
+}
+
+// GetNode implements the `Directory` interface.
+func (d *HAMTDirectory) GetNode() (ipld.Node, error) {
+ return d.shard.Node()
+}
+
+// GetCidBuilder implements the `Directory` interface.
+func (d *HAMTDirectory) GetCidBuilder() cid.Builder {
+ return d.shard.CidBuilder()
+}
+
+// switchToBasic returns a BasicDirectory implementation of this directory.
+func (d *HAMTDirectory) switchToBasic(ctx context.Context) (*BasicDirectory, error) {
+ basicDir := newEmptyBasicDirectory(d.dserv)
+ basicDir.SetCidBuilder(d.GetCidBuilder())
+
+ err := d.ForEachLink(ctx, func(lnk *ipld.Link) error {
+ err := basicDir.addLinkChild(ctx, lnk.Name, lnk)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ // This function enumerates all the links in the Directory requiring all
+ // shards to be accessible but it is only called *after* sizeBelowThreshold
+ // returns true, which means we have already enumerated and fetched *all*
+ // shards in the first place (that's the only way we can be really sure
+ // we are actually below the threshold).
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return basicDir, nil
+}
+
+func (d *HAMTDirectory) addToSizeChange(name string, linkCid cid.Cid) {
+ d.sizeChange += linksize.LinkSizeFunction(name, linkCid)
+}
+
+func (d *HAMTDirectory) removeFromSizeChange(name string, linkCid cid.Cid) {
+ d.sizeChange -= linksize.LinkSizeFunction(name, linkCid)
+}
+
+// Evaluate a switch from HAMTDirectory to BasicDirectory in case the size will
+// go above the threshold when we are adding or removing an entry.
+// In both the add/remove operations any old name will be removed, and for the
+// add operation in particular a new entry will be added under that name (otherwise
+// nodeToAdd is nil). We compute both (potential) future subtraction and
+// addition to the size change.
+func (d *HAMTDirectory) needsToSwitchToBasicDir(ctx context.Context, name string, nodeToAdd ipld.Node) (switchToBasic bool, err error) {
+ if HAMTShardingSize == 0 { // Option disabled.
+ return false, nil
+ }
+
+ operationSizeChange := 0
+
+ // Find if there is an old entry under that name that will be overwritten
+ // (AddEntry) or flat out removed (RemoveEntry).
+ entryToRemove, err := d.shard.Find(ctx, name)
+ if err != os.ErrNotExist {
+ if err != nil {
+ return false, err
+ }
+ operationSizeChange -= linksize.LinkSizeFunction(name, entryToRemove.Cid)
+ }
+
+ // For the AddEntry case compute the size addition of the new entry.
+ if nodeToAdd != nil {
+ operationSizeChange += linksize.LinkSizeFunction(name, nodeToAdd.Cid())
+ }
+
+ if d.sizeChange+operationSizeChange >= 0 {
+ // We won't have reduced the HAMT net size.
+ return false, nil
+ }
+
+ // We have reduced the directory size, check if went below the
+ // HAMTShardingSize threshold to trigger a switch.
+ return d.sizeBelowThreshold(ctx, operationSizeChange)
+}
+
+// Evaluate directory size and a future sizeChange and check if it will be below
+// HAMTShardingSize threshold (to trigger a transition to a BasicDirectory).
+// Instead of enumerating the entire tree we eagerly call EnumLinksAsync
+// until we either reach a value above the threshold (in that case no need
+// to keep counting) or an error occurs (like the context being canceled
+// if we take too much time fetching the necessary shards).
+func (d *HAMTDirectory) sizeBelowThreshold(ctx context.Context, sizeChange int) (below bool, err error) {
+ if HAMTShardingSize == 0 {
+ panic("asked to compute HAMT size with HAMTShardingSize option off (0)")
+ }
+
+ // We don't necessarily compute the full size of *all* shards as we might
+ // end early if we already know we're above the threshold or run out of time.
+ partialSize := 0
+
+ // We stop the enumeration once we have enough information and exit this function.
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ for linkResult := range d.EnumLinksAsync(ctx) {
+ if linkResult.Err != nil {
+ return false, linkResult.Err
+ }
+
+ partialSize += linksize.LinkSizeFunction(linkResult.Link.Name, linkResult.Link.Cid)
+ if partialSize+sizeChange >= HAMTShardingSize {
+ // We have already fetched enough shards to assert we are
+ // above the threshold, so no need to keep fetching.
+ return false, nil
+ }
+ }
+
+ // We enumerated *all* links in all shards and didn't reach the threshold.
+ return true, nil
+}
+
+// DynamicDirectory wraps a Directory interface and provides extra logic
+// to switch from BasicDirectory to HAMTDirectory and backwards based on
+// size.
+type DynamicDirectory struct {
+ Directory
+}
+
+var _ Directory = (*DynamicDirectory)(nil)
+
+// AddChild implements the `Directory` interface. We check when adding new entries
+// if we should switch to HAMTDirectory according to global option(s).
+func (d *DynamicDirectory) AddChild(ctx context.Context, name string, nd ipld.Node) error {
+ hamtDir, ok := d.Directory.(*HAMTDirectory)
+ if ok {
+ // We evaluate a switch in the HAMTDirectory case even for an AddChild
+ // as it may overwrite an existing entry and end up actually reducing
+ // the directory size.
+ switchToBasic, err := hamtDir.needsToSwitchToBasicDir(ctx, name, nd)
+ if err != nil {
+ return err
+ }
+
+ if switchToBasic {
+ basicDir, err := hamtDir.switchToBasic(ctx)
+ if err != nil {
+ return err
+ }
+ err = basicDir.AddChild(ctx, name, nd)
+ if err != nil {
+ return err
+ }
+ d.Directory = basicDir
+ return nil
+ }
+
+ return d.Directory.AddChild(ctx, name, nd)
+ }
+
+ // BasicDirectory
+ basicDir := d.Directory.(*BasicDirectory)
+ switchToHAMT, err := basicDir.needsToSwitchToHAMTDir(name, nd)
+ if err != nil {
+ return err
+ }
+ if !switchToHAMT {
+ return basicDir.AddChild(ctx, name, nd)
+ }
+ hamtDir, err = basicDir.switchToSharding(ctx)
+ if err != nil {
+ return err
+ }
+ err = hamtDir.AddChild(ctx, name, nd)
+ if err != nil {
+ return err
+ }
+ d.Directory = hamtDir
+ return nil
+}
+
+// RemoveChild implements the `Directory` interface. Used in the case where we wrap
+// a HAMTDirectory that might need to be downgraded to a BasicDirectory. The
+// upgrade path is in AddChild.
+func (d *DynamicDirectory) RemoveChild(ctx context.Context, name string) error {
+ hamtDir, ok := d.Directory.(*HAMTDirectory)
+ if !ok {
+ return d.Directory.RemoveChild(ctx, name)
+ }
+
+ switchToBasic, err := hamtDir.needsToSwitchToBasicDir(ctx, name, nil)
+ if err != nil {
+ return err
+ }
+
+ if !switchToBasic {
+ return hamtDir.RemoveChild(ctx, name)
+ }
+
+ basicDir, err := hamtDir.switchToBasic(ctx)
+ if err != nil {
+ return err
+ }
+ err = basicDir.RemoveChild(ctx, name)
+ if err != nil {
+ return err
+ }
+ d.Directory = basicDir
+ return nil
+}
diff --git a/unixfs/io/directory_test.go b/unixfs/io/directory_test.go
new file mode 100644
index 0000000000..518afd0742
--- /dev/null
+++ b/unixfs/io/directory_test.go
@@ -0,0 +1,638 @@
+package io
+
+import (
+ "context"
+ "fmt"
+ "math"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ blocks "github.com/ipfs/boxo/blocks"
+ bsrv "github.com/ipfs/boxo/blockservice"
+ blockstore "github.com/ipfs/boxo/blockstore"
+ offline "github.com/ipfs/boxo/exchange/offline"
+ cid "github.com/ipfs/go-cid"
+ ds "github.com/ipfs/go-datastore"
+ dssync "github.com/ipfs/go-datastore/sync"
+ ipld "github.com/ipfs/go-ipld-format"
+ mdag "github.com/ipfs/boxo/ipld/merkledag"
+ mdtest "github.com/ipfs/boxo/ipld/merkledag/test"
+
+ ft "github.com/ipfs/boxo/unixfs"
+ "github.com/ipfs/boxo/unixfs/hamt"
+ "github.com/ipfs/boxo/unixfs/internal"
+ "github.com/ipfs/boxo/unixfs/private/linksize"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestEmptyNode(t *testing.T) {
+ n := ft.EmptyDirNode()
+ if len(n.Links()) != 0 {
+ t.Fatal("empty node should have 0 links")
+ }
+}
+
+func TestDirectoryGrowth(t *testing.T) {
+ ds := mdtest.Mock()
+ dir := NewDirectory(ds)
+ ctx := context.Background()
+
+ d := ft.EmptyDirNode()
+ ds.Add(ctx, d)
+
+ nelems := 10000
+
+ for i := 0; i < nelems; i++ {
+ err := dir.AddChild(ctx, fmt.Sprintf("dir%d", i), d)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ _, err := dir.GetNode()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ links, err := dir.Links(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(links) != nelems {
+ t.Fatal("didnt get right number of elements")
+ }
+
+ dirc := d.Cid()
+
+ names := make(map[string]bool)
+ for _, l := range links {
+ names[l.Name] = true
+ if !l.Cid.Equals(dirc) {
+ t.Fatal("link wasnt correct")
+ }
+ }
+
+ for i := 0; i < nelems; i++ {
+ dn := fmt.Sprintf("dir%d", i)
+ if !names[dn] {
+ t.Fatal("didnt find directory: ", dn)
+ }
+
+ _, err := dir.Find(context.Background(), dn)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+}
+
+func TestDuplicateAddDir(t *testing.T) {
+ ds := mdtest.Mock()
+ dir := NewDirectory(ds)
+ ctx := context.Background()
+ nd := ft.EmptyDirNode()
+
+ err := dir.AddChild(ctx, "test", nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = dir.AddChild(ctx, "test", nd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ lnks, err := dir.Links(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(lnks) != 1 {
+ t.Fatal("expected only one link")
+ }
+}
+
+func TestBasicDirectory_estimatedSize(t *testing.T) {
+ ds := mdtest.Mock()
+ basicDir := newEmptyBasicDirectory(ds)
+
+ testDirectorySizeEstimation(t, basicDir, ds, func(dir Directory) int {
+ return dir.(*BasicDirectory).estimatedSize
+ })
+}
+
+func TestHAMTDirectory_sizeChange(t *testing.T) {
+ ds := mdtest.Mock()
+ hamtDir, err := newEmptyHAMTDirectory(ds, DefaultShardWidth)
+ assert.NoError(t, err)
+
+ testDirectorySizeEstimation(t, hamtDir, ds, func(dir Directory) int {
+ // Since we created a HAMTDirectory from scratch with size 0 its
+ // internal sizeChange delta will in fact track the directory size
+ // throughout this run.
+ return dir.(*HAMTDirectory).sizeChange
+ })
+}
+
+func fullSizeEnumeration(dir Directory) int {
+ size := 0
+ dir.ForEachLink(context.Background(), func(l *ipld.Link) error {
+ size += linksize.LinkSizeFunction(l.Name, l.Cid)
+ return nil
+ })
+ return size
+}
+
+func testDirectorySizeEstimation(t *testing.T, dir Directory, ds ipld.DAGService, size func(Directory) int) {
+ linksize.LinkSizeFunction = mockLinkSizeFunc(1)
+ defer func() { linksize.LinkSizeFunction = productionLinkSize }()
+
+ ctx := context.Background()
+ child := ft.EmptyFileNode()
+ assert.NoError(t, ds.Add(ctx, child))
+
+ // Several overwrites should not corrupt the size estimation.
+ assert.NoError(t, dir.AddChild(ctx, "child", child))
+ assert.NoError(t, dir.AddChild(ctx, "child", child))
+ assert.NoError(t, dir.AddChild(ctx, "child", child))
+ assert.NoError(t, dir.RemoveChild(ctx, "child"))
+ assert.NoError(t, dir.AddChild(ctx, "child", child))
+ assert.NoError(t, dir.RemoveChild(ctx, "child"))
+ assert.Equal(t, 0, size(dir), "estimated size is not zero after removing all entries")
+
+ dirEntries := 100
+ for i := 0; i < dirEntries; i++ {
+ assert.NoError(t, dir.AddChild(ctx, fmt.Sprintf("child-%03d", i), child))
+ }
+ assert.Equal(t, dirEntries, size(dir), "estimated size inaccurate after adding many entries")
+
+ assert.NoError(t, dir.RemoveChild(ctx, "child-045")) // just random values
+ assert.NoError(t, dir.RemoveChild(ctx, "child-063"))
+ assert.NoError(t, dir.RemoveChild(ctx, "child-011"))
+ assert.NoError(t, dir.RemoveChild(ctx, "child-000"))
+ assert.NoError(t, dir.RemoveChild(ctx, "child-099"))
+ dirEntries -= 5
+ assert.Equal(t, dirEntries, size(dir), "estimated size inaccurate after removing some entries")
+
+ // All of the following remove operations will fail (won't impact dirEntries):
+ assert.Error(t, dir.RemoveChild(ctx, "nonexistent-name"))
+ assert.Error(t, dir.RemoveChild(ctx, "child-045")) // already removed
+ assert.Error(t, dir.RemoveChild(ctx, "child-100"))
+ assert.Equal(t, dirEntries, size(dir), "estimated size inaccurate after failed remove attempts")
+
+ // Restore a directory from original's node and check estimated size consistency.
+ dirNode, err := dir.GetNode()
+ assert.NoError(t, err)
+ restoredDir, err := NewDirectoryFromNode(ds, dirNode.(*mdag.ProtoNode))
+ assert.NoError(t, err)
+ assert.Equal(t, size(dir), fullSizeEnumeration(restoredDir), "restored directory's size doesn't match original's")
+ // We don't use the estimation size function for the restored directory
+ // because in the HAMT case this function depends on the sizeChange variable
+ // that will be cleared when loading the directory from the node.
+ // This also covers the case of comparing the size estimation `size()` with
+ // the full enumeration function `fullSizeEnumeration()` to make sure it's
+ // correct.
+}
+
+// Any entry link size will have the fixedSize passed.
+func mockLinkSizeFunc(fixedSize int) func(linkName string, linkCid cid.Cid) int {
+ return func(_ string, _ cid.Cid) int {
+ return fixedSize
+ }
+}
+
+func checkBasicDirectory(t *testing.T, dir Directory, errorMessage string) {
+ if _, ok := dir.(*DynamicDirectory).Directory.(*BasicDirectory); !ok {
+ t.Fatal(errorMessage)
+ }
+}
+
+func checkHAMTDirectory(t *testing.T, dir Directory, errorMessage string) {
+ if _, ok := dir.(*DynamicDirectory).Directory.(*HAMTDirectory); !ok {
+ t.Fatal(errorMessage)
+ }
+}
+
+func TestProductionLinkSize(t *testing.T) {
+ link, err := ipld.MakeLink(ft.EmptyDirNode())
+ assert.NoError(t, err)
+ link.Name = "directory_link_name"
+ assert.Equal(t, 53, productionLinkSize(link.Name, link.Cid))
+
+ link, err = ipld.MakeLink(ft.EmptyFileNode())
+ assert.NoError(t, err)
+ link.Name = "file_link_name"
+ assert.Equal(t, 48, productionLinkSize(link.Name, link.Cid))
+
+ ds := mdtest.Mock()
+ basicDir := newEmptyBasicDirectory(ds)
+ assert.NoError(t, err)
+ for i := 0; i < 10; i++ {
+ basicDir.AddChild(context.Background(), strconv.FormatUint(uint64(i), 10), ft.EmptyFileNode())
+ }
+ basicDirNode, err := basicDir.GetNode()
+ assert.NoError(t, err)
+ link, err = ipld.MakeLink(basicDirNode)
+ assert.NoError(t, err)
+ link.Name = "basic_dir"
+ assert.Equal(t, 43, productionLinkSize(link.Name, link.Cid))
+}
+
+// Test HAMTDirectory <-> BasicDirectory switch based on directory size. The
+// switch is managed by the DynamicDirectory abstraction.
+func TestDynamicDirectorySwitch(t *testing.T) {
+ oldHamtOption := HAMTShardingSize
+ defer func() { HAMTShardingSize = oldHamtOption }()
+ HAMTShardingSize = 0 // Disable automatic switch at the start.
+ linksize.LinkSizeFunction = mockLinkSizeFunc(1)
+ defer func() { linksize.LinkSizeFunction = productionLinkSize }()
+
+ ds := mdtest.Mock()
+ dir := NewDirectory(ds)
+ checkBasicDirectory(t, dir, "new dir is not BasicDirectory")
+
+ ctx := context.Background()
+ child := ft.EmptyDirNode()
+ err := ds.Add(ctx, child)
+ assert.NoError(t, err)
+
+ err = dir.AddChild(ctx, "1", child)
+ assert.NoError(t, err)
+ checkBasicDirectory(t, dir, "added child, option still disabled")
+
+ // Set a threshold so big a new entry won't trigger the change.
+ HAMTShardingSize = math.MaxInt32
+
+ err = dir.AddChild(ctx, "2", child)
+ assert.NoError(t, err)
+ checkBasicDirectory(t, dir, "added child, option now enabled but at max")
+
+ // Now set it so low to make sure any new entry will trigger the upgrade.
+ HAMTShardingSize = 1
+
+ // We are already above the threshold, we trigger the switch with an overwrite
+ // (any AddChild() should reevaluate the size).
+ err = dir.AddChild(ctx, "2", child)
+ assert.NoError(t, err)
+ checkHAMTDirectory(t, dir, "added child, option at min, should switch up")
+
+ // Set threshold at the number of current entries and delete the last one
+ // to trigger a switch and evaluate if the rest of the entries are conserved.
+ HAMTShardingSize = 2
+ err = dir.RemoveChild(ctx, "2")
+ assert.NoError(t, err)
+ checkBasicDirectory(t, dir, "removed threshold entry, option at min, should switch down")
+}
+
+func TestIntegrityOfDirectorySwitch(t *testing.T) {
+ ds := mdtest.Mock()
+ dir := NewDirectory(ds)
+ checkBasicDirectory(t, dir, "new dir is not BasicDirectory")
+
+ ctx := context.Background()
+ child := ft.EmptyDirNode()
+ err := ds.Add(ctx, child)
+ assert.NoError(t, err)
+
+ basicDir := newEmptyBasicDirectory(ds)
+ hamtDir, err := newEmptyHAMTDirectory(ds, DefaultShardWidth)
+ assert.NoError(t, err)
+ for i := 0; i < 1000; i++ {
+ basicDir.AddChild(ctx, strconv.FormatUint(uint64(i), 10), child)
+ hamtDir.AddChild(ctx, strconv.FormatUint(uint64(i), 10), child)
+ }
+ compareDirectoryEntries(t, basicDir, hamtDir)
+
+ hamtDirFromSwitch, err := basicDir.switchToSharding(ctx)
+ assert.NoError(t, err)
+ basicDirFromSwitch, err := hamtDir.switchToBasic(ctx)
+ assert.NoError(t, err)
+ compareDirectoryEntries(t, basicDir, basicDirFromSwitch)
+ compareDirectoryEntries(t, hamtDir, hamtDirFromSwitch)
+}
+
+// This is the value of concurrent fetches during dag.Walk. Used in
+// test to better predict how many nodes will be fetched.
+var defaultConcurrentFetch = 32
+
+// FIXME: Taken from private github.com/ipfs/boxo/ipld/merkledag@v0.2.3/merkledag.go.
+// (We can also pass an explicit concurrency value in `(*Shard).EnumLinksAsync()`
+// and take ownership of this configuration, but departing from the more
+// standard and reliable one in `go-merkledag`.
+
+// Test that we fetch as little nodes as needed to reach the HAMTShardingSize
+// during the sizeBelowThreshold computation.
+func TestHAMTEnumerationWhenComputingSize(t *testing.T) {
+ // Adjust HAMT global/static options for the test to simplify its logic.
+ // FIXME: These variables weren't designed to be modified and we should
+ // review in depth side effects.
+
+ // Set all link sizes to a uniform 1 so the estimated directory size
+ // is just the count of its entry links (in HAMT/Shard terminology these
+ // are the "value" links pointing to anything that is *not* another Shard).
+ linksize.LinkSizeFunction = mockLinkSizeFunc(1)
+ defer func() { linksize.LinkSizeFunction = productionLinkSize }()
+
+ // Use an identity hash function to ease the construction of "complete" HAMTs
+ // (see CreateCompleteHAMT below for more details). (Ideally this should be
+ // a parameter we pass and not a global option we modify in the caller.)
+ oldHashFunc := internal.HAMTHashFunction
+ defer func() { internal.HAMTHashFunction = oldHashFunc }()
+ internal.HAMTHashFunction = idHash
+
+ oldHamtOption := HAMTShardingSize
+ defer func() { HAMTShardingSize = oldHamtOption }()
+
+ // --- End of test static configuration adjustments. ---
+
+ // Some arbitrary values below that make this test not that expensive.
+ treeHeight := 4
+ // How many leaf shards nodes (with value links,
+ // i.e., directory entries) do we need to reach the threshold.
+ thresholdToWidthRatio := 4
+ // Departing from DefaultShardWidth of 256 to reduce HAMT size in
+ // CreateCompleteHAMT.
+ shardWidth := 16
+ HAMTShardingSize = shardWidth * thresholdToWidthRatio
+
+ // We create a "complete" HAMT (see CreateCompleteHAMT for more details)
+ // with a regular structure to be able to predict how many Shard nodes we
+ // will need to fetch in order to reach the HAMTShardingSize threshold in
+ // sizeBelowThreshold (assuming a sequential DAG walk function).
+
+ bstore := blockstore.NewBlockstore(dssync.MutexWrap(ds.NewMapDatastore()))
+ countGetsDS := newCountGetsDS(bstore)
+ dsrv := mdag.NewDAGService(bsrv.New(countGetsDS, offline.Exchange(countGetsDS)))
+ completeHAMTRoot, err := CreateCompleteHAMT(dsrv, treeHeight, shardWidth)
+ assert.NoError(t, err)
+
+ // Calculate the optimal number of nodes to traverse
+ optimalNodesToFetch := 0
+ nodesToProcess := HAMTShardingSize
+ for i := 0; i < treeHeight-1; i++ {
+ // divide by the shard width to get the parents and continue up the tree
+ parentNodes := int(math.Ceil(float64(nodesToProcess) / float64(shardWidth)))
+ optimalNodesToFetch += parentNodes
+ nodesToProcess = parentNodes
+ }
+
+ // With this structure and a BFS traversal (from `parallelWalkDepth`) then
+ // we would roughly fetch the following nodes:
+ nodesToFetch := 0
+ // * all layers up to (but not including) the last one with leaf nodes
+ // (because it's a BFS)
+ for i := 0; i < treeHeight-1; i++ {
+ nodesToFetch += int(math.Pow(float64(shardWidth), float64(i)))
+ }
+ // * `thresholdToWidthRatio` leaf Shards with enough value links to reach
+ // the HAMTShardingSize threshold.
+ nodesToFetch += thresholdToWidthRatio
+
+ hamtDir, err := newHAMTDirectoryFromNode(dsrv, completeHAMTRoot)
+ assert.NoError(t, err)
+
+ countGetsDS.resetCounter()
+ countGetsDS.setRequestDelay(10 * time.Millisecond)
+ // (Without the `setRequestDelay` above the number of nodes fetched
+ // drops dramatically and unpredictably as the BFS starts to behave
+ // more like a DFS because some search paths are fetched faster than
+ // others.)
+ below, err := hamtDir.sizeBelowThreshold(context.TODO(), 0)
+ assert.NoError(t, err)
+ assert.False(t, below)
+ t.Logf("fetched %d nodes (predicted range: %d-%d)",
+ countGetsDS.uniqueCidsFetched(), optimalNodesToFetch, nodesToFetch+defaultConcurrentFetch)
+ // Check that the actual number of nodes fetched is within the margin of the
+ // estimated `nodesToFetch` plus an extra of `defaultConcurrentFetch` since
+ // we are fetching in parallel.
+ assert.True(t, countGetsDS.uniqueCidsFetched() <= nodesToFetch+defaultConcurrentFetch)
+ assert.True(t, countGetsDS.uniqueCidsFetched() >= optimalNodesToFetch)
+}
+
+// Compare entries in the leftDir against the rightDir and possibly
+// missingEntries in the second.
+func compareDirectoryEntries(t *testing.T, leftDir Directory, rightDir Directory) {
+ leftLinks, err := getAllLinksSortedByName(leftDir)
+ assert.NoError(t, err)
+ rightLinks, err := getAllLinksSortedByName(rightDir)
+ assert.NoError(t, err)
+
+ assert.Equal(t, len(leftLinks), len(rightLinks))
+
+ for i, leftLink := range leftLinks {
+ assert.Equal(t, leftLink, rightLinks[i]) // FIXME: Can we just compare the entire struct?
+ }
+}
+
+func getAllLinksSortedByName(d Directory) ([]*ipld.Link, error) {
+ entries, err := d.Links(context.Background())
+ if err != nil {
+ return nil, err
+ }
+ sortLinksByName(entries)
+ return entries, nil
+}
+
+func sortLinksByName(l []*ipld.Link) {
+ sort.SliceStable(l, func(i, j int) bool {
+ return strings.Compare(l[i].Name, l[j].Name) == -1 // FIXME: Is this correct?
+ })
+}
+
+func TestDirBuilder(t *testing.T) {
+ ds := mdtest.Mock()
+ dir := NewDirectory(ds)
+ ctx := context.Background()
+
+ child := ft.EmptyDirNode()
+ err := ds.Add(ctx, child)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ count := 5000
+
+ for i := 0; i < count; i++ {
+ err := dir.AddChild(ctx, fmt.Sprintf("entry %d", i), child)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ dirnd, err := dir.GetNode()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ links, err := dir.Links(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(links) != count {
+ t.Fatal("not enough links dawg", len(links), count)
+ }
+
+ adir, err := NewDirectoryFromNode(ds, dirnd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ links, err = adir.Links(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ names := make(map[string]bool)
+ for _, lnk := range links {
+ names[lnk.Name] = true
+ }
+
+ for i := 0; i < count; i++ {
+ n := fmt.Sprintf("entry %d", i)
+ if !names[n] {
+ t.Fatal("COULDNT FIND: ", n)
+ }
+ }
+
+ if len(links) != count {
+ t.Fatal("wrong number of links", len(links), count)
+ }
+
+ linkResults := dir.EnumLinksAsync(ctx)
+
+ asyncNames := make(map[string]bool)
+ var asyncLinks []*ipld.Link
+
+ for linkResult := range linkResults {
+ if linkResult.Err != nil {
+ t.Fatal(linkResult.Err)
+ }
+ asyncNames[linkResult.Link.Name] = true
+ asyncLinks = append(asyncLinks, linkResult.Link)
+ }
+
+ for i := 0; i < count; i++ {
+ n := fmt.Sprintf("entry %d", i)
+ if !asyncNames[n] {
+ t.Fatal("COULDNT FIND: ", n)
+ }
+ }
+
+ if len(asyncLinks) != count {
+ t.Fatal("wrong number of links", len(asyncLinks), count)
+ }
+}
+
+func newHAMTDirectoryFromNode(dserv ipld.DAGService, node ipld.Node) (*HAMTDirectory, error) {
+ shard, err := hamt.NewHamtFromDag(dserv, node)
+ if err != nil {
+ return nil, err
+ }
+ return &HAMTDirectory{
+ dserv: dserv,
+ shard: shard,
+ }, nil
+}
+
+func newEmptyHAMTDirectory(dserv ipld.DAGService, shardWidth int) (*HAMTDirectory, error) {
+ shard, err := hamt.NewShard(dserv, shardWidth)
+ if err != nil {
+ return nil, err
+ }
+
+ return &HAMTDirectory{
+ dserv: dserv,
+ shard: shard,
+ }, nil
+}
+
+// countGetsDS is a DAG service that keeps track of the number of
+// unique CIDs fetched.
+type countGetsDS struct {
+ blockstore.Blockstore
+
+ cidsFetched map[cid.Cid]struct{}
+ mapLock sync.Mutex
+ started bool
+
+ getRequestDelay time.Duration
+}
+
+var _ blockstore.Blockstore = (*countGetsDS)(nil)
+
+func newCountGetsDS(bs blockstore.Blockstore) *countGetsDS {
+ return &countGetsDS{
+ bs,
+ make(map[cid.Cid]struct{}),
+ sync.Mutex{},
+ false,
+ 0,
+ }
+}
+
+func (d *countGetsDS) resetCounter() {
+ d.mapLock.Lock()
+ defer d.mapLock.Unlock()
+ d.cidsFetched = make(map[cid.Cid]struct{})
+ d.started = true
+}
+
+func (d *countGetsDS) uniqueCidsFetched() int {
+ d.mapLock.Lock()
+ defer d.mapLock.Unlock()
+ return len(d.cidsFetched)
+}
+
+func (d *countGetsDS) setRequestDelay(timeout time.Duration) {
+ d.getRequestDelay = timeout
+}
+
+func (d *countGetsDS) maybeSleep(c cid.Cid) {
+ d.mapLock.Lock()
+ _, cidRequestedBefore := d.cidsFetched[c]
+ d.cidsFetched[c] = struct{}{}
+ d.mapLock.Unlock()
+
+ if d.getRequestDelay != 0 && !cidRequestedBefore {
+ // First request gets a timeout to simulate a network fetch.
+ // Subsequent requests get no timeout simulating an in-disk cache.
+ time.Sleep(d.getRequestDelay)
+ }
+}
+
+func (d *countGetsDS) Has(ctx context.Context, c cid.Cid) (bool, error) {
+ if d.started {
+ panic("implement me")
+ }
+ return d.Blockstore.Has(ctx, c)
+}
+
+func (d *countGetsDS) Get(ctx context.Context, c cid.Cid) (blocks.Block, error) {
+ blk, err := d.Blockstore.Get(ctx, c)
+ if err != nil {
+ return nil, err
+ }
+
+ d.maybeSleep(c)
+ return blk, nil
+}
+
+func (d *countGetsDS) GetSize(ctx context.Context, c cid.Cid) (int, error) {
+ if d.started {
+ panic("implement me")
+ }
+ return d.Blockstore.GetSize(ctx, c)
+}
+
+func (d *countGetsDS) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) {
+ if d.started {
+ panic("implement me")
+ }
+ return d.Blockstore.AllKeysChan(ctx)
+}
diff --git a/unixfs/io/doc.go b/unixfs/io/doc.go
new file mode 100644
index 0000000000..cf844bd23a
--- /dev/null
+++ b/unixfs/io/doc.go
@@ -0,0 +1,3 @@
+// Package io implements convenience objects for working with the ipfs
+// unixfs data format.
+package io
diff --git a/unixfs/io/resolve.go b/unixfs/io/resolve.go
new file mode 100644
index 0000000000..cefe6753dc
--- /dev/null
+++ b/unixfs/io/resolve.go
@@ -0,0 +1,41 @@
+package io
+
+import (
+ "context"
+
+ ft "github.com/ipfs/boxo/unixfs"
+ hamt "github.com/ipfs/boxo/unixfs/hamt"
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+// ResolveUnixfsOnce resolves a single hop of a path through a graph in a
+// unixfs context. This includes handling traversing sharded directories.
+func ResolveUnixfsOnce(ctx context.Context, ds ipld.NodeGetter, nd ipld.Node, names []string) (*ipld.Link, []string, error) {
+ pn, ok := nd.(*dag.ProtoNode)
+ if ok {
+ fsn, err := ft.FSNodeFromBytes(pn.Data())
+ if err != nil {
+ // Not a unixfs node, use standard object traversal code
+ return nd.ResolveLink(names)
+ }
+
+ if fsn.Type() == ft.THAMTShard {
+ rods := dag.NewReadOnlyDagService(ds)
+ s, err := hamt.NewHamtFromDag(rods, nd)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ out, err := s.Find(ctx, names[0])
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return out, names[1:], nil
+ }
+ }
+
+ return nd.ResolveLink(names)
+}
diff --git a/unixfs/mod/dagmodifier.go b/unixfs/mod/dagmodifier.go
new file mode 100644
index 0000000000..e89b5f5f92
--- /dev/null
+++ b/unixfs/mod/dagmodifier.go
@@ -0,0 +1,605 @@
+// Package mod provides DAG modification utilities to, for example,
+// insert additional nodes in a unixfs DAG or truncate them.
+package mod
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "io"
+
+ ft "github.com/ipfs/boxo/unixfs"
+ help "github.com/ipfs/boxo/unixfs/importer/helpers"
+ trickle "github.com/ipfs/boxo/unixfs/importer/trickle"
+ uio "github.com/ipfs/boxo/unixfs/io"
+
+ chunker "github.com/ipfs/boxo/chunker"
+ cid "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+ mdag "github.com/ipfs/boxo/ipld/merkledag"
+)
+
+// Common errors
+var (
+ ErrSeekFail = errors.New("failed to seek properly")
+ ErrUnrecognizedWhence = errors.New("unrecognized whence")
+ ErrNotUnixfs = errors.New("dagmodifier only supports unixfs nodes (proto or raw)")
+)
+
+// 2MB
+var writebufferSize = 1 << 21
+
+// DagModifier is the only struct licensed and able to correctly
+// perform surgery on a DAG 'file'
+// Dear god, please rename this to something more pleasant
+type DagModifier struct {
+ dagserv ipld.DAGService
+ curNode ipld.Node
+
+ splitter chunker.SplitterGen
+ ctx context.Context
+ readCancel func()
+
+ writeStart uint64
+ curWrOff uint64
+ wrBuf *bytes.Buffer
+
+ Prefix cid.Prefix
+ RawLeaves bool
+
+ read uio.DagReader
+}
+
+// NewDagModifier returns a new DagModifier, the Cid prefix for newly
+// created nodes will be inhered from the passed in node. If the Cid
+// version if not 0 raw leaves will also be enabled. The Prefix and
+// RawLeaves options can be overridden by changing them after the call.
+func NewDagModifier(ctx context.Context, from ipld.Node, serv ipld.DAGService, spl chunker.SplitterGen) (*DagModifier, error) {
+ switch from.(type) {
+ case *mdag.ProtoNode, *mdag.RawNode:
+ // ok
+ default:
+ return nil, ErrNotUnixfs
+ }
+
+ prefix := from.Cid().Prefix()
+ prefix.Codec = cid.DagProtobuf
+ rawLeaves := false
+ if prefix.Version > 0 {
+ rawLeaves = true
+ }
+
+ return &DagModifier{
+ curNode: from.Copy(),
+ dagserv: serv,
+ splitter: spl,
+ ctx: ctx,
+ Prefix: prefix,
+ RawLeaves: rawLeaves,
+ }, nil
+}
+
+// WriteAt will modify a dag file in place
+func (dm *DagModifier) WriteAt(b []byte, offset int64) (int, error) {
+ // TODO: this is currently VERY inefficient
+ // each write that happens at an offset other than the current one causes a
+ // flush to disk, and dag rewrite
+ if offset == int64(dm.writeStart) && dm.wrBuf != nil {
+ // If we would overwrite the previous write
+ if len(b) >= dm.wrBuf.Len() {
+ dm.wrBuf.Reset()
+ }
+ } else if uint64(offset) != dm.curWrOff {
+ size, err := dm.Size()
+ if err != nil {
+ return 0, err
+ }
+ if offset > size {
+ err := dm.expandSparse(offset - size)
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ err = dm.Sync()
+ if err != nil {
+ return 0, err
+ }
+ dm.writeStart = uint64(offset)
+ }
+
+ return dm.Write(b)
+}
+
+// A reader that just returns zeros
+type zeroReader struct{}
+
+func (zr zeroReader) Read(b []byte) (int, error) {
+ for i := range b {
+ b[i] = 0
+ }
+ return len(b), nil
+}
+
+// expandSparse grows the file with zero blocks of 4096
+// A small blocksize is chosen to aid in deduplication
+func (dm *DagModifier) expandSparse(size int64) error {
+ r := io.LimitReader(zeroReader{}, size)
+ spl := chunker.NewSizeSplitter(r, 4096)
+ nnode, err := dm.appendData(dm.curNode, spl)
+ if err != nil {
+ return err
+ }
+ err = dm.dagserv.Add(dm.ctx, nnode)
+ return err
+}
+
+// Write continues writing to the dag at the current offset
+func (dm *DagModifier) Write(b []byte) (int, error) {
+ if dm.read != nil {
+ dm.read = nil
+ }
+ if dm.wrBuf == nil {
+ dm.wrBuf = new(bytes.Buffer)
+ }
+
+ n, err := dm.wrBuf.Write(b)
+ if err != nil {
+ return n, err
+ }
+ dm.curWrOff += uint64(n)
+ if dm.wrBuf.Len() > writebufferSize {
+ err := dm.Sync()
+ if err != nil {
+ return n, err
+ }
+ }
+ return n, nil
+}
+
+// Size returns the Filesize of the node
+func (dm *DagModifier) Size() (int64, error) {
+ fileSize, err := fileSize(dm.curNode)
+ if err != nil {
+ return 0, err
+ }
+ if dm.wrBuf != nil && int64(dm.wrBuf.Len())+int64(dm.writeStart) > int64(fileSize) {
+ return int64(dm.wrBuf.Len()) + int64(dm.writeStart), nil
+ }
+ return int64(fileSize), nil
+}
+
+func fileSize(n ipld.Node) (uint64, error) {
+ switch nd := n.(type) {
+ case *mdag.ProtoNode:
+ fsn, err := ft.FSNodeFromBytes(nd.Data())
+ if err != nil {
+ return 0, err
+ }
+ return fsn.FileSize(), nil
+ case *mdag.RawNode:
+ return uint64(len(nd.RawData())), nil
+ default:
+ return 0, ErrNotUnixfs
+ }
+}
+
+// Sync writes changes to this dag to disk
+func (dm *DagModifier) Sync() error {
+ // No buffer? Nothing to do
+ if dm.wrBuf == nil {
+ return nil
+ }
+
+ // If we have an active reader, kill it
+ if dm.read != nil {
+ dm.read = nil
+ dm.readCancel()
+ }
+
+ // Number of bytes we're going to write
+ buflen := dm.wrBuf.Len()
+
+ fs, err := fileSize(dm.curNode)
+ if err != nil {
+ return err
+ }
+ if fs < dm.writeStart {
+ if err := dm.expandSparse(int64(dm.writeStart - fs)); err != nil {
+ return err
+ }
+ }
+
+ // overwrite existing dag nodes
+ thisc, err := dm.modifyDag(dm.curNode, dm.writeStart)
+ if err != nil {
+ return err
+ }
+
+ dm.curNode, err = dm.dagserv.Get(dm.ctx, thisc)
+ if err != nil {
+ return err
+ }
+
+ // need to write past end of current dag
+ if dm.wrBuf.Len() > 0 {
+ dm.curNode, err = dm.appendData(dm.curNode, dm.splitter(dm.wrBuf))
+ if err != nil {
+ return err
+ }
+
+ err = dm.dagserv.Add(dm.ctx, dm.curNode)
+ if err != nil {
+ return err
+ }
+ }
+
+ dm.writeStart += uint64(buflen)
+ dm.wrBuf = nil
+
+ return nil
+}
+
+// modifyDag writes the data in 'dm.wrBuf' over the data in 'node' starting at 'offset'
+// returns the new key of the passed in node.
+func (dm *DagModifier) modifyDag(n ipld.Node, offset uint64) (cid.Cid, error) {
+ // If we've reached a leaf node.
+ if len(n.Links()) == 0 {
+ switch nd0 := n.(type) {
+ case *mdag.ProtoNode:
+ fsn, err := ft.FSNodeFromBytes(nd0.Data())
+ if err != nil {
+ return cid.Cid{}, err
+ }
+
+ _, err = dm.wrBuf.Read(fsn.Data()[offset:])
+ if err != nil && err != io.EOF {
+ return cid.Cid{}, err
+ }
+
+ // Update newly written node..
+ b, err := fsn.GetBytes()
+ if err != nil {
+ return cid.Cid{}, err
+ }
+
+ nd := new(mdag.ProtoNode)
+ nd.SetData(b)
+ nd.SetCidBuilder(nd0.CidBuilder())
+ err = dm.dagserv.Add(dm.ctx, nd)
+ if err != nil {
+ return cid.Cid{}, err
+ }
+
+ return nd.Cid(), nil
+ case *mdag.RawNode:
+ origData := nd0.RawData()
+ bytes := make([]byte, len(origData))
+
+ // copy orig data up to offset
+ copy(bytes, origData[:offset])
+
+ // copy in new data
+ n, err := dm.wrBuf.Read(bytes[offset:])
+ if err != nil && err != io.EOF {
+ return cid.Cid{}, err
+ }
+
+ // copy remaining data
+ offsetPlusN := int(offset) + n
+ if offsetPlusN < len(origData) {
+ copy(bytes[offsetPlusN:], origData[offsetPlusN:])
+ }
+
+ nd, err := mdag.NewRawNodeWPrefix(bytes, nd0.Cid().Prefix())
+ if err != nil {
+ return cid.Cid{}, err
+ }
+ err = dm.dagserv.Add(dm.ctx, nd)
+ if err != nil {
+ return cid.Cid{}, err
+ }
+
+ return nd.Cid(), nil
+ }
+ }
+
+ node, ok := n.(*mdag.ProtoNode)
+ if !ok {
+ return cid.Cid{}, ErrNotUnixfs
+ }
+
+ fsn, err := ft.FSNodeFromBytes(node.Data())
+ if err != nil {
+ return cid.Cid{}, err
+ }
+
+ var cur uint64
+ for i, bs := range fsn.BlockSizes() {
+ // We found the correct child to write into
+ if cur+bs > offset {
+ child, err := node.Links()[i].GetNode(dm.ctx, dm.dagserv)
+ if err != nil {
+ return cid.Cid{}, err
+ }
+
+ k, err := dm.modifyDag(child, offset-cur)
+ if err != nil {
+ return cid.Cid{}, err
+ }
+
+ node.Links()[i].Cid = k
+
+ // Recache serialized node
+ _, err = node.EncodeProtobuf(true)
+ if err != nil {
+ return cid.Cid{}, err
+ }
+
+ if dm.wrBuf.Len() == 0 {
+ // No more bytes to write!
+ break
+ }
+ offset = cur + bs
+ }
+ cur += bs
+ }
+
+ err = dm.dagserv.Add(dm.ctx, node)
+ return node.Cid(), err
+}
+
+// appendData appends the blocks from the given chan to the end of this dag
+func (dm *DagModifier) appendData(nd ipld.Node, spl chunker.Splitter) (ipld.Node, error) {
+ switch nd := nd.(type) {
+ case *mdag.ProtoNode, *mdag.RawNode:
+ dbp := &help.DagBuilderParams{
+ Dagserv: dm.dagserv,
+ Maxlinks: help.DefaultLinksPerBlock,
+ CidBuilder: dm.Prefix,
+ RawLeaves: dm.RawLeaves,
+ }
+ db, err := dbp.New(spl)
+ if err != nil {
+ return nil, err
+ }
+ return trickle.Append(dm.ctx, nd, db)
+ default:
+ return nil, ErrNotUnixfs
+ }
+}
+
+// Read data from this dag starting at the current offset
+func (dm *DagModifier) Read(b []byte) (int, error) {
+ err := dm.readPrep()
+ if err != nil {
+ return 0, err
+ }
+
+ n, err := dm.read.Read(b)
+ dm.curWrOff += uint64(n)
+ return n, err
+}
+
+func (dm *DagModifier) readPrep() error {
+ err := dm.Sync()
+ if err != nil {
+ return err
+ }
+
+ if dm.read == nil {
+ ctx, cancel := context.WithCancel(dm.ctx)
+ dr, err := uio.NewDagReader(ctx, dm.curNode, dm.dagserv)
+ if err != nil {
+ cancel()
+ return err
+ }
+
+ i, err := dr.Seek(int64(dm.curWrOff), io.SeekStart)
+ if err != nil {
+ cancel()
+ return err
+ }
+
+ if i != int64(dm.curWrOff) {
+ cancel()
+ return ErrSeekFail
+ }
+
+ dm.readCancel = cancel
+ dm.read = dr
+ }
+
+ return nil
+}
+
+// CtxReadFull reads data from this dag starting at the current offset
+func (dm *DagModifier) CtxReadFull(ctx context.Context, b []byte) (int, error) {
+ err := dm.readPrep()
+ if err != nil {
+ return 0, err
+ }
+
+ n, err := dm.read.CtxReadFull(ctx, b)
+ dm.curWrOff += uint64(n)
+ return n, err
+}
+
+// GetNode gets the modified DAG Node
+func (dm *DagModifier) GetNode() (ipld.Node, error) {
+ err := dm.Sync()
+ if err != nil {
+ return nil, err
+ }
+ return dm.curNode.Copy(), nil
+}
+
+// HasChanges returned whether or not there are unflushed changes to this dag
+func (dm *DagModifier) HasChanges() bool {
+ return dm.wrBuf != nil
+}
+
+// Seek modifies the offset according to whence. See unixfs/io for valid whence
+// values.
+func (dm *DagModifier) Seek(offset int64, whence int) (int64, error) {
+ err := dm.Sync()
+ if err != nil {
+ return 0, err
+ }
+
+ fisize, err := dm.Size()
+ if err != nil {
+ return 0, err
+ }
+
+ var newoffset uint64
+ switch whence {
+ case io.SeekCurrent:
+ newoffset = dm.curWrOff + uint64(offset)
+ case io.SeekStart:
+ newoffset = uint64(offset)
+ case io.SeekEnd:
+ newoffset = uint64(fisize) - uint64(offset)
+ default:
+ return 0, ErrUnrecognizedWhence
+ }
+
+ if int64(newoffset) > fisize {
+ if err := dm.expandSparse(int64(newoffset) - fisize); err != nil {
+ return 0, err
+ }
+ }
+ dm.curWrOff = newoffset
+ dm.writeStart = newoffset
+
+ if dm.read != nil {
+ _, err = dm.read.Seek(offset, whence)
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ return int64(dm.curWrOff), nil
+}
+
+// Truncate truncates the current Node to 'size' and replaces it with the
+// new one.
+func (dm *DagModifier) Truncate(size int64) error {
+ err := dm.Sync()
+ if err != nil {
+ return err
+ }
+
+ realSize, err := dm.Size()
+ if err != nil {
+ return err
+ }
+ if size == int64(realSize) {
+ return nil
+ }
+
+ // Truncate can also be used to expand the file
+ if size > int64(realSize) {
+ return dm.expandSparse(int64(size) - realSize)
+ }
+
+ nnode, err := dm.dagTruncate(dm.ctx, dm.curNode, uint64(size))
+ if err != nil {
+ return err
+ }
+
+ err = dm.dagserv.Add(dm.ctx, nnode)
+ if err != nil {
+ return err
+ }
+
+ dm.curNode = nnode
+ return nil
+}
+
+// dagTruncate truncates the given node to 'size' and returns the modified Node
+func (dm *DagModifier) dagTruncate(ctx context.Context, n ipld.Node, size uint64) (ipld.Node, error) {
+ if len(n.Links()) == 0 {
+ switch nd := n.(type) {
+ case *mdag.ProtoNode:
+ // TODO: this can likely be done without marshaling and remarshaling
+ fsn, err := ft.FSNodeFromBytes(nd.Data())
+ if err != nil {
+ return nil, err
+ }
+ nd.SetData(ft.WrapData(fsn.Data()[:size]))
+ return nd, nil
+ case *mdag.RawNode:
+ return mdag.NewRawNodeWPrefix(nd.RawData()[:size], nd.Cid().Prefix())
+ }
+ }
+
+ nd, ok := n.(*mdag.ProtoNode)
+ if !ok {
+ return nil, ErrNotUnixfs
+ }
+
+ var cur uint64
+ end := 0
+ var modified ipld.Node
+ ndata, err := ft.FSNodeFromBytes(nd.Data())
+ if err != nil {
+ return nil, err
+ }
+ // Reset the block sizes of the node to adjust them
+ // with the new values of the truncated children.
+ ndata.RemoveAllBlockSizes()
+ for i, lnk := range nd.Links() {
+ child, err := lnk.GetNode(ctx, dm.dagserv)
+ if err != nil {
+ return nil, err
+ }
+
+ childsize, err := fileSize(child)
+ if err != nil {
+ return nil, err
+ }
+
+ // found the child we want to cut
+ if size < cur+childsize {
+ nchild, err := dm.dagTruncate(ctx, child, size-cur)
+ if err != nil {
+ return nil, err
+ }
+
+ ndata.AddBlockSize(size - cur)
+
+ modified = nchild
+ end = i
+ break
+ }
+ cur += childsize
+ ndata.AddBlockSize(childsize)
+ }
+
+ err = dm.dagserv.Add(ctx, modified)
+ if err != nil {
+ return nil, err
+ }
+
+ nd.SetLinks(nd.Links()[:end])
+ err = nd.AddNodeLink("", modified)
+ if err != nil {
+ return nil, err
+ }
+
+ d, err := ndata.GetBytes()
+ if err != nil {
+ return nil, err
+ }
+ // Save the new block sizes to the original node.
+ nd.SetData(d)
+
+ // invalidate cache and recompute serialized data
+ _, err = nd.EncodeProtobuf(true)
+ if err != nil {
+ return nil, err
+ }
+
+ return nd, nil
+}
diff --git a/unixfs/mod/dagmodifier_test.go b/unixfs/mod/dagmodifier_test.go
new file mode 100644
index 0000000000..04972edf98
--- /dev/null
+++ b/unixfs/mod/dagmodifier_test.go
@@ -0,0 +1,859 @@
+package mod
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "testing"
+
+ h "github.com/ipfs/boxo/unixfs/importer/helpers"
+ trickle "github.com/ipfs/boxo/unixfs/importer/trickle"
+ uio "github.com/ipfs/boxo/unixfs/io"
+ testu "github.com/ipfs/boxo/unixfs/test"
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+
+ "github.com/ipfs/boxo/unixfs"
+ u "github.com/ipfs/boxo/util"
+)
+
+func testModWrite(t *testing.T, beg, size uint64, orig []byte, dm *DagModifier, opts testu.NodeOpts) []byte {
+ newdata := make([]byte, size)
+ r := u.NewTimeSeededRand()
+ r.Read(newdata)
+
+ if size+beg > uint64(len(orig)) {
+ orig = append(orig, make([]byte, (size+beg)-uint64(len(orig)))...)
+ }
+ copy(orig[beg:], newdata)
+
+ nmod, err := dm.WriteAt(newdata, int64(beg))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if nmod != int(size) {
+ t.Fatalf("Mod length not correct! %d != %d", nmod, size)
+ }
+
+ verifyNode(t, orig, dm, opts)
+
+ return orig
+}
+
+func verifyNode(t *testing.T, orig []byte, dm *DagModifier, opts testu.NodeOpts) {
+ nd, err := dm.GetNode()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = trickle.VerifyTrickleDagStructure(nd, trickle.VerifyParams{
+ Getter: dm.dagserv,
+ Direct: h.DefaultLinksPerBlock,
+ LayerRepeat: 4,
+ Prefix: &opts.Prefix,
+ RawLeaves: opts.RawLeavesUsed,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rd, err := uio.NewDagReader(context.Background(), nd, dm.dagserv)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ after, err := io.ReadAll(rd)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = testu.ArrComp(after, orig)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func runAllSubtests(t *testing.T, tfunc func(*testing.T, testu.NodeOpts)) {
+ t.Run("opts=ProtoBufLeaves", func(t *testing.T) { tfunc(t, testu.UseProtoBufLeaves) })
+ t.Run("opts=RawLeaves", func(t *testing.T) { tfunc(t, testu.UseRawLeaves) })
+ t.Run("opts=CidV1", func(t *testing.T) { tfunc(t, testu.UseCidV1) })
+ t.Run("opts=Blake2b256", func(t *testing.T) { tfunc(t, testu.UseBlake2b256) })
+}
+
+func TestDagModifierBasic(t *testing.T) {
+ runAllSubtests(t, testDagModifierBasic)
+}
+func testDagModifierBasic(t *testing.T, opts testu.NodeOpts) {
+ dserv := testu.GetDAGServ()
+ b, n := testu.GetRandomNode(t, dserv, 50000, opts)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dagmod, err := NewDagModifier(ctx, n, dserv, testu.SizeSplitterGen(512))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if opts.ForceRawLeaves {
+ dagmod.RawLeaves = true
+ }
+
+ // Within zero block
+ beg := uint64(15)
+ length := uint64(60)
+
+ t.Log("Testing mod within zero block")
+ b = testModWrite(t, beg, length, b, dagmod, opts)
+
+ // Within bounds of existing file
+ beg = 1000
+ length = 4000
+ t.Log("Testing mod within bounds of existing multiblock file.")
+ b = testModWrite(t, beg, length, b, dagmod, opts)
+
+ // Extend bounds
+ beg = 49500
+ length = 4000
+
+ t.Log("Testing mod that extends file.")
+ b = testModWrite(t, beg, length, b, dagmod, opts)
+
+ // "Append"
+ beg = uint64(len(b))
+ length = 3000
+ t.Log("Testing pure append")
+ _ = testModWrite(t, beg, length, b, dagmod, opts)
+
+ // Verify reported length
+ node, err := dagmod.GetNode()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ size, err := fileSize(node)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expected := uint64(50000 + 3500 + 3000)
+ if size != expected {
+ t.Fatalf("Final reported size is incorrect [%d != %d]", size, expected)
+ }
+}
+
+func TestMultiWrite(t *testing.T) {
+ runAllSubtests(t, testMultiWrite)
+}
+func testMultiWrite(t *testing.T, opts testu.NodeOpts) {
+ dserv := testu.GetDAGServ()
+ n := testu.GetEmptyNode(t, dserv, opts)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dagmod, err := NewDagModifier(ctx, n, dserv, testu.SizeSplitterGen(512))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if opts.ForceRawLeaves {
+ dagmod.RawLeaves = true
+ }
+
+ data := make([]byte, 4000)
+ u.NewTimeSeededRand().Read(data)
+
+ for i := 0; i < len(data); i++ {
+ n, err := dagmod.WriteAt(data[i:i+1], int64(i))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n != 1 {
+ t.Fatal("Somehow wrote the wrong number of bytes! (n != 1)")
+ }
+
+ size, err := dagmod.Size()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if size != int64(i+1) {
+ t.Fatal("Size was reported incorrectly")
+ }
+ }
+
+ verifyNode(t, data, dagmod, opts)
+}
+
+func TestMultiWriteAndFlush(t *testing.T) {
+ runAllSubtests(t, testMultiWriteAndFlush)
+}
+func testMultiWriteAndFlush(t *testing.T, opts testu.NodeOpts) {
+ dserv := testu.GetDAGServ()
+ n := testu.GetEmptyNode(t, dserv, opts)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dagmod, err := NewDagModifier(ctx, n, dserv, testu.SizeSplitterGen(512))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if opts.ForceRawLeaves {
+ dagmod.RawLeaves = true
+ }
+
+ data := make([]byte, 20)
+ u.NewTimeSeededRand().Read(data)
+
+ for i := 0; i < len(data); i++ {
+ n, err := dagmod.WriteAt(data[i:i+1], int64(i))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n != 1 {
+ t.Fatal("Somehow wrote the wrong number of bytes! (n != 1)")
+ }
+ err = dagmod.Sync()
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ verifyNode(t, data, dagmod, opts)
+}
+
+func TestWriteNewFile(t *testing.T) {
+ runAllSubtests(t, testWriteNewFile)
+}
+func testWriteNewFile(t *testing.T, opts testu.NodeOpts) {
+ dserv := testu.GetDAGServ()
+ n := testu.GetEmptyNode(t, dserv, opts)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dagmod, err := NewDagModifier(ctx, n, dserv, testu.SizeSplitterGen(512))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if opts.ForceRawLeaves {
+ dagmod.RawLeaves = true
+ }
+
+ towrite := make([]byte, 2000)
+ u.NewTimeSeededRand().Read(towrite)
+
+ nw, err := dagmod.Write(towrite)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if nw != len(towrite) {
+ t.Fatal("Wrote wrong amount")
+ }
+
+ verifyNode(t, towrite, dagmod, opts)
+}
+
+func TestMultiWriteCoal(t *testing.T) {
+ runAllSubtests(t, testMultiWriteCoal)
+}
+func testMultiWriteCoal(t *testing.T, opts testu.NodeOpts) {
+ dserv := testu.GetDAGServ()
+ n := testu.GetEmptyNode(t, dserv, opts)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dagmod, err := NewDagModifier(ctx, n, dserv, testu.SizeSplitterGen(512))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if opts.ForceRawLeaves {
+ dagmod.RawLeaves = true
+ }
+
+ data := make([]byte, 1000)
+ u.NewTimeSeededRand().Read(data)
+
+ for i := 0; i < len(data); i++ {
+ n, err := dagmod.WriteAt(data[:i+1], 0)
+ if err != nil {
+ fmt.Println("FAIL AT ", i)
+ t.Fatal(err)
+ }
+ if n != i+1 {
+ t.Fatal("Somehow wrote the wrong number of bytes! (n != 1)")
+ }
+
+ }
+
+ verifyNode(t, data, dagmod, opts)
+}
+
+func TestLargeWriteChunks(t *testing.T) {
+ runAllSubtests(t, testLargeWriteChunks)
+}
+func testLargeWriteChunks(t *testing.T, opts testu.NodeOpts) {
+ dserv := testu.GetDAGServ()
+ n := testu.GetEmptyNode(t, dserv, opts)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dagmod, err := NewDagModifier(ctx, n, dserv, testu.SizeSplitterGen(512))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if opts.ForceRawLeaves {
+ dagmod.RawLeaves = true
+ }
+
+ wrsize := 1000
+ datasize := 10000000
+ data := make([]byte, datasize)
+
+ u.NewTimeSeededRand().Read(data)
+
+ for i := 0; i < datasize/wrsize; i++ {
+ n, err := dagmod.WriteAt(data[i*wrsize:(i+1)*wrsize], int64(i*wrsize))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n != wrsize {
+ t.Fatal("failed to write buffer")
+ }
+ }
+
+ _, err = dagmod.Seek(0, io.SeekStart)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := io.ReadAll(dagmod)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err = testu.ArrComp(out, data); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestDagTruncate(t *testing.T) {
+ runAllSubtests(t, testDagTruncate)
+}
+func testDagTruncate(t *testing.T, opts testu.NodeOpts) {
+ dserv := testu.GetDAGServ()
+ b, n := testu.GetRandomNode(t, dserv, 50000, opts)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dagmod, err := NewDagModifier(ctx, n, dserv, testu.SizeSplitterGen(512))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if opts.ForceRawLeaves {
+ dagmod.RawLeaves = true
+ }
+
+ err = dagmod.Truncate(12345)
+ if err != nil {
+ t.Fatal(err)
+ }
+ size, err := dagmod.Size()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if size != 12345 {
+ t.Fatal("size was incorrect!")
+ }
+
+ _, err = dagmod.Seek(0, io.SeekStart)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := io.ReadAll(dagmod)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err = testu.ArrComp(out, b[:12345]); err != nil {
+ t.Fatal(err)
+ }
+
+ err = dagmod.Truncate(10)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ size, err = dagmod.Size()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if size != 10 {
+ t.Fatal("size was incorrect!")
+ }
+
+ err = dagmod.Truncate(0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ size, err = dagmod.Size()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if size != 0 {
+ t.Fatal("size was incorrect!")
+ }
+}
+
+// TestDagSync tests that a DAG will expand sparse during sync
+// if offset > curNode's size.
+func TestDagSync(t *testing.T) {
+ dserv := testu.GetDAGServ()
+ nd := dag.NodeWithData(unixfs.FilePBData(nil, 0))
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dagmod, err := NewDagModifier(ctx, nd, dserv, testu.SizeSplitterGen(512))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = dagmod.Write([]byte("test1"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = dagmod.Sync()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Truncate leave the offset at 5 and filesize at 0
+ err = dagmod.Truncate(0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = dagmod.Write([]byte("test2"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // When Offset > filesize , Sync will call enpandSparse
+ err = dagmod.Sync()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = dagmod.Seek(0, io.SeekStart)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := io.ReadAll(dagmod)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err = testu.ArrComp(out[5:], []byte("test2")); err != nil {
+ t.Fatal(err)
+ }
+}
+
+// TestDagTruncateSameSize tests that a DAG truncated
+// to the same size (i.e., doing nothing) doesn't modify
+// the DAG (its hash).
+func TestDagTruncateSameSize(t *testing.T) {
+ runAllSubtests(t, testDagTruncateSameSize)
+}
+func testDagTruncateSameSize(t *testing.T, opts testu.NodeOpts) {
+ dserv := testu.GetDAGServ()
+ _, n := testu.GetRandomNode(t, dserv, 50000, opts)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dagmod, err := NewDagModifier(ctx, n, dserv, testu.SizeSplitterGen(512))
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Copied from `TestDagTruncate`.
+
+ size, err := dagmod.Size()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = dagmod.Truncate(size)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ modifiedNode, err := dagmod.GetNode()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if modifiedNode.Cid().Equals(n.Cid()) == false {
+ t.Fatal("the node has been modified!")
+ }
+}
+
+func TestSparseWrite(t *testing.T) {
+ runAllSubtests(t, testSparseWrite)
+}
+func testSparseWrite(t *testing.T, opts testu.NodeOpts) {
+ dserv := testu.GetDAGServ()
+ n := testu.GetEmptyNode(t, dserv, opts)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dagmod, err := NewDagModifier(ctx, n, dserv, testu.SizeSplitterGen(512))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if opts.ForceRawLeaves {
+ dagmod.RawLeaves = true
+ }
+
+ buf := make([]byte, 5000)
+ u.NewTimeSeededRand().Read(buf[2500:])
+
+ wrote, err := dagmod.WriteAt(buf[2500:], 2500)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if wrote != 2500 {
+ t.Fatal("incorrect write amount")
+ }
+
+ _, err = dagmod.Seek(0, io.SeekStart)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := io.ReadAll(dagmod)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err = testu.ArrComp(out, buf); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSeekPastEndWrite(t *testing.T) {
+ runAllSubtests(t, testSeekPastEndWrite)
+}
+func testSeekPastEndWrite(t *testing.T, opts testu.NodeOpts) {
+ dserv := testu.GetDAGServ()
+ n := testu.GetEmptyNode(t, dserv, opts)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dagmod, err := NewDagModifier(ctx, n, dserv, testu.SizeSplitterGen(512))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if opts.ForceRawLeaves {
+ dagmod.RawLeaves = true
+ }
+
+ buf := make([]byte, 5000)
+ u.NewTimeSeededRand().Read(buf[2500:])
+
+ nseek, err := dagmod.Seek(2500, io.SeekStart)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if nseek != 2500 {
+ t.Fatal("failed to seek")
+ }
+
+ wrote, err := dagmod.Write(buf[2500:])
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if wrote != 2500 {
+ t.Fatal("incorrect write amount")
+ }
+
+ _, err = dagmod.Seek(0, io.SeekStart)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ out, err := io.ReadAll(dagmod)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err = testu.ArrComp(out, buf); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestRelativeSeek(t *testing.T) {
+ runAllSubtests(t, testRelativeSeek)
+}
+func testRelativeSeek(t *testing.T, opts testu.NodeOpts) {
+ dserv := testu.GetDAGServ()
+ n := testu.GetEmptyNode(t, dserv, opts)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dagmod, err := NewDagModifier(ctx, n, dserv, testu.SizeSplitterGen(512))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if opts.ForceRawLeaves {
+ dagmod.RawLeaves = true
+ }
+
+ for i := 0; i < 64; i++ {
+ dagmod.Write([]byte{byte(i)})
+ if _, err := dagmod.Seek(1, io.SeekCurrent); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ out, err := io.ReadAll(dagmod)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for i, v := range out {
+ if v != 0 && i/2 != int(v) {
+ t.Errorf("expected %d, at index %d, got %d", i/2, i, v)
+ }
+ }
+}
+
+func TestInvalidSeek(t *testing.T) {
+ runAllSubtests(t, testInvalidSeek)
+}
+func testInvalidSeek(t *testing.T, opts testu.NodeOpts) {
+ dserv := testu.GetDAGServ()
+ n := testu.GetEmptyNode(t, dserv, opts)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dagmod, err := NewDagModifier(ctx, n, dserv, testu.SizeSplitterGen(512))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if opts.ForceRawLeaves {
+ dagmod.RawLeaves = true
+ }
+
+ _, err = dagmod.Seek(10, -10)
+
+ if err != ErrUnrecognizedWhence {
+ t.Fatal(err)
+ }
+}
+
+func TestEndSeek(t *testing.T) {
+ runAllSubtests(t, testEndSeek)
+}
+func testEndSeek(t *testing.T, opts testu.NodeOpts) {
+ dserv := testu.GetDAGServ()
+
+ n := testu.GetEmptyNode(t, dserv, opts)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dagmod, err := NewDagModifier(ctx, n, dserv, testu.SizeSplitterGen(512))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if opts.ForceRawLeaves {
+ dagmod.RawLeaves = true
+ }
+
+ _, err = dagmod.Write(make([]byte, 100))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ offset, err := dagmod.Seek(0, io.SeekCurrent)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if offset != 100 {
+ t.Fatal("expected the relative seek 0 to return current location")
+ }
+
+ offset, err = dagmod.Seek(0, io.SeekStart)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if offset != 0 {
+ t.Fatal("expected the absolute seek to set offset at 0")
+ }
+
+ offset, err = dagmod.Seek(0, io.SeekEnd)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if offset != 100 {
+ t.Fatal("expected the end seek to set offset at end")
+ }
+}
+
+func TestReadAndSeek(t *testing.T) {
+ runAllSubtests(t, testReadAndSeek)
+}
+func testReadAndSeek(t *testing.T, opts testu.NodeOpts) {
+ dserv := testu.GetDAGServ()
+
+ n := testu.GetEmptyNode(t, dserv, opts)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dagmod, err := NewDagModifier(ctx, n, dserv, testu.SizeSplitterGen(512))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if opts.ForceRawLeaves {
+ dagmod.RawLeaves = true
+ }
+
+ writeBuf := []byte{0, 1, 2, 3, 4, 5, 6, 7}
+ dagmod.Write(writeBuf)
+
+ if !dagmod.HasChanges() {
+ t.Fatal("there are changes, this should be true")
+ }
+
+ readBuf := make([]byte, 4)
+ offset, err := dagmod.Seek(0, io.SeekStart)
+ if offset != 0 {
+ t.Fatal("expected offset to be 0")
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // read 0,1,2,3
+ c, err := dagmod.Read(readBuf)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if c != 4 {
+ t.Fatalf("expected length of 4 got %d", c)
+ }
+
+ for i := byte(0); i < 4; i++ {
+ if readBuf[i] != i {
+ t.Fatalf("wrong value %d [at index %d]", readBuf[i], i)
+ }
+ }
+
+ // skip 4
+ _, err = dagmod.Seek(1, io.SeekCurrent)
+ if err != nil {
+ t.Fatalf("error: %s, offset %d, reader offset %d", err, dagmod.curWrOff, getOffset(dagmod.read))
+ }
+
+ //read 5,6,7
+ readBuf = make([]byte, 3)
+ c, err = dagmod.Read(readBuf)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if c != 3 {
+ t.Fatalf("expected length of 3 got %d", c)
+ }
+
+ for i := byte(0); i < 3; i++ {
+ if readBuf[i] != i+5 {
+ t.Fatalf("wrong value %d [at index %d]", readBuf[i], i)
+ }
+
+ }
+
+}
+
+func TestCtxRead(t *testing.T) {
+ runAllSubtests(t, testCtxRead)
+}
+func testCtxRead(t *testing.T, opts testu.NodeOpts) {
+ dserv := testu.GetDAGServ()
+
+ n := testu.GetEmptyNode(t, dserv, opts)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ dagmod, err := NewDagModifier(ctx, n, dserv, testu.SizeSplitterGen(512))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if opts.ForceRawLeaves {
+ dagmod.RawLeaves = true
+ }
+
+ _, err = dagmod.Write([]byte{0, 1, 2, 3, 4, 5, 6, 7})
+ if err != nil {
+ t.Fatal(err)
+ }
+ dagmod.Seek(0, io.SeekStart)
+
+ readBuf := make([]byte, 4)
+ _, err = dagmod.CtxReadFull(ctx, readBuf)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = testu.ArrComp(readBuf, []byte{0, 1, 2, 3})
+ if err != nil {
+ t.Fatal(err)
+ }
+ // TODO(Kubuxu): context cancel case, I will do it after I figure out dagreader tests,
+ // because this is exacelly the same.
+}
+
+func BenchmarkDagmodWrite(b *testing.B) {
+ b.StopTimer()
+ dserv := testu.GetDAGServ()
+ n := testu.GetEmptyNode(b, dserv, testu.UseProtoBufLeaves)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ wrsize := 4096
+
+ dagmod, err := NewDagModifier(ctx, n, dserv, testu.SizeSplitterGen(512))
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ buf := make([]byte, b.N*wrsize)
+ u.NewTimeSeededRand().Read(buf)
+ b.StartTimer()
+ b.SetBytes(int64(wrsize))
+ for i := 0; i < b.N; i++ {
+ n, err := dagmod.Write(buf[i*wrsize : (i+1)*wrsize])
+ if err != nil {
+ b.Fatal(err)
+ }
+ if n != wrsize {
+ b.Fatal("Wrote bad size")
+ }
+ }
+}
+
+func getOffset(reader uio.DagReader) int64 {
+ offset, err := reader.Seek(0, io.SeekCurrent)
+ if err != nil {
+ panic("failed to retrieve offset: " + err.Error())
+ }
+ return offset
+}
diff --git a/unixfs/pb/Makefile b/unixfs/pb/Makefile
new file mode 100644
index 0000000000..51552a0961
--- /dev/null
+++ b/unixfs/pb/Makefile
@@ -0,0 +1,11 @@
+PB = $(wildcard *.proto)
+GO = $(PB:.proto=.pb.go)
+
+all: $(GO)
+
+%.pb.go: %.proto
+ protoc --proto_path=$(GOPATH)/src:. --gogo_out=. $<
+
+clean:
+ rm -f *.pb.go
+ rm -f *.go
diff --git a/unixfs/pb/unixfs.pb.go b/unixfs/pb/unixfs.pb.go
new file mode 100644
index 0000000000..e523140075
--- /dev/null
+++ b/unixfs/pb/unixfs.pb.go
@@ -0,0 +1,218 @@
+// Code generated by protoc-gen-gogo. DO NOT EDIT.
+// source: unixfs.proto
+
+package unixfs_pb
+
+import (
+ fmt "fmt"
+ math "math"
+
+ proto "github.com/gogo/protobuf/proto"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
+
+type Data_DataType int32
+
+const (
+ Data_Raw Data_DataType = 0
+ Data_Directory Data_DataType = 1
+ Data_File Data_DataType = 2
+ Data_Metadata Data_DataType = 3
+ Data_Symlink Data_DataType = 4
+ Data_HAMTShard Data_DataType = 5
+)
+
+var Data_DataType_name = map[int32]string{
+ 0: "Raw",
+ 1: "Directory",
+ 2: "File",
+ 3: "Metadata",
+ 4: "Symlink",
+ 5: "HAMTShard",
+}
+
+var Data_DataType_value = map[string]int32{
+ "Raw": 0,
+ "Directory": 1,
+ "File": 2,
+ "Metadata": 3,
+ "Symlink": 4,
+ "HAMTShard": 5,
+}
+
+func (x Data_DataType) Enum() *Data_DataType {
+ p := new(Data_DataType)
+ *p = x
+ return p
+}
+
+func (x Data_DataType) String() string {
+ return proto.EnumName(Data_DataType_name, int32(x))
+}
+
+func (x *Data_DataType) UnmarshalJSON(data []byte) error {
+ value, err := proto.UnmarshalJSONEnum(Data_DataType_value, data, "Data_DataType")
+ if err != nil {
+ return err
+ }
+ *x = Data_DataType(value)
+ return nil
+}
+
+func (Data_DataType) EnumDescriptor() ([]byte, []int) {
+ return fileDescriptor_e2fd76cc44dfc7c3, []int{0, 0}
+}
+
+type Data struct {
+ Type *Data_DataType `protobuf:"varint,1,req,name=Type,enum=unixfs.pb.Data_DataType" json:"Type,omitempty"`
+ Data []byte `protobuf:"bytes,2,opt,name=Data" json:"Data,omitempty"`
+ Filesize *uint64 `protobuf:"varint,3,opt,name=filesize" json:"filesize,omitempty"`
+ Blocksizes []uint64 `protobuf:"varint,4,rep,name=blocksizes" json:"blocksizes,omitempty"`
+ HashType *uint64 `protobuf:"varint,5,opt,name=hashType" json:"hashType,omitempty"`
+ Fanout *uint64 `protobuf:"varint,6,opt,name=fanout" json:"fanout,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *Data) Reset() { *m = Data{} }
+func (m *Data) String() string { return proto.CompactTextString(m) }
+func (*Data) ProtoMessage() {}
+func (*Data) Descriptor() ([]byte, []int) {
+ return fileDescriptor_e2fd76cc44dfc7c3, []int{0}
+}
+func (m *Data) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_Data.Unmarshal(m, b)
+}
+func (m *Data) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_Data.Marshal(b, m, deterministic)
+}
+func (m *Data) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_Data.Merge(m, src)
+}
+func (m *Data) XXX_Size() int {
+ return xxx_messageInfo_Data.Size(m)
+}
+func (m *Data) XXX_DiscardUnknown() {
+ xxx_messageInfo_Data.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Data proto.InternalMessageInfo
+
+func (m *Data) GetType() Data_DataType {
+ if m != nil && m.Type != nil {
+ return *m.Type
+ }
+ return Data_Raw
+}
+
+func (m *Data) GetData() []byte {
+ if m != nil {
+ return m.Data
+ }
+ return nil
+}
+
+func (m *Data) GetFilesize() uint64 {
+ if m != nil && m.Filesize != nil {
+ return *m.Filesize
+ }
+ return 0
+}
+
+func (m *Data) GetBlocksizes() []uint64 {
+ if m != nil {
+ return m.Blocksizes
+ }
+ return nil
+}
+
+func (m *Data) GetHashType() uint64 {
+ if m != nil && m.HashType != nil {
+ return *m.HashType
+ }
+ return 0
+}
+
+func (m *Data) GetFanout() uint64 {
+ if m != nil && m.Fanout != nil {
+ return *m.Fanout
+ }
+ return 0
+}
+
+type Metadata struct {
+ MimeType *string `protobuf:"bytes,1,opt,name=MimeType" json:"MimeType,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *Metadata) Reset() { *m = Metadata{} }
+func (m *Metadata) String() string { return proto.CompactTextString(m) }
+func (*Metadata) ProtoMessage() {}
+func (*Metadata) Descriptor() ([]byte, []int) {
+ return fileDescriptor_e2fd76cc44dfc7c3, []int{1}
+}
+func (m *Metadata) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_Metadata.Unmarshal(m, b)
+}
+func (m *Metadata) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_Metadata.Marshal(b, m, deterministic)
+}
+func (m *Metadata) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_Metadata.Merge(m, src)
+}
+func (m *Metadata) XXX_Size() int {
+ return xxx_messageInfo_Metadata.Size(m)
+}
+func (m *Metadata) XXX_DiscardUnknown() {
+ xxx_messageInfo_Metadata.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Metadata proto.InternalMessageInfo
+
+func (m *Metadata) GetMimeType() string {
+ if m != nil && m.MimeType != nil {
+ return *m.MimeType
+ }
+ return ""
+}
+
+func init() {
+ proto.RegisterEnum("unixfs.pb.Data_DataType", Data_DataType_name, Data_DataType_value)
+ proto.RegisterType((*Data)(nil), "unixfs.pb.Data")
+ proto.RegisterType((*Metadata)(nil), "unixfs.pb.Metadata")
+}
+
+func init() { proto.RegisterFile("unixfs.proto", fileDescriptor_e2fd76cc44dfc7c3) }
+
+var fileDescriptor_e2fd76cc44dfc7c3 = []byte{
+ // 254 bytes of a gzipped FileDescriptorProto
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x90, 0xb1, 0x6a, 0xeb, 0x30,
+ 0x18, 0x85, 0xaf, 0x6c, 0x25, 0xb1, 0xff, 0xeb, 0x16, 0xf1, 0x0f, 0x45, 0x74, 0x28, 0xc6, 0x43,
+ 0xd1, 0x50, 0x3c, 0xf4, 0x0d, 0x0a, 0xa1, 0x74, 0xf1, 0xa2, 0x84, 0xee, 0x4a, 0x22, 0x63, 0x11,
+ 0xc7, 0x0a, 0xb6, 0x42, 0xeb, 0x3e, 0x45, 0x1f, 0xb9, 0xc8, 0x8e, 0xdd, 0x2e, 0x82, 0x4f, 0xe7,
+ 0x7c, 0xe2, 0x20, 0x48, 0x2e, 0x8d, 0xf9, 0x2c, 0xbb, 0xfc, 0xdc, 0x5a, 0x67, 0x31, 0x9e, 0x68,
+ 0x97, 0x7d, 0x07, 0x40, 0xd7, 0xca, 0x29, 0x7c, 0x02, 0xba, 0xed, 0xcf, 0x9a, 0x93, 0x34, 0x10,
+ 0xb7, 0xcf, 0x3c, 0x9f, 0x2b, 0xb9, 0x8f, 0x87, 0xc3, 0xe7, 0x72, 0x68, 0x21, 0x8e, 0x16, 0x0f,
+ 0x52, 0x22, 0x12, 0x39, 0xbe, 0x70, 0x0f, 0x51, 0x69, 0x6a, 0xdd, 0x99, 0x2f, 0xcd, 0xc3, 0x94,
+ 0x08, 0x2a, 0x67, 0xc6, 0x07, 0x80, 0x5d, 0x6d, 0xf7, 0x47, 0x0f, 0x1d, 0xa7, 0x69, 0x28, 0xa8,
+ 0xfc, 0x73, 0xe3, 0xdd, 0x4a, 0x75, 0xd5, 0xb0, 0x60, 0x31, 0xba, 0x13, 0xe3, 0x1d, 0x2c, 0x4b,
+ 0xd5, 0xd8, 0x8b, 0xe3, 0xcb, 0x21, 0xb9, 0x52, 0xf6, 0x0e, 0xd1, 0xb4, 0x0a, 0x57, 0x10, 0x4a,
+ 0xf5, 0xc1, 0xfe, 0xe1, 0x0d, 0xc4, 0x6b, 0xd3, 0xea, 0xbd, 0xb3, 0x6d, 0xcf, 0x08, 0x46, 0x40,
+ 0x5f, 0x4d, 0xad, 0x59, 0x80, 0x09, 0x44, 0x85, 0x76, 0xea, 0xa0, 0x9c, 0x62, 0x21, 0xfe, 0x87,
+ 0xd5, 0xa6, 0x3f, 0xd5, 0xa6, 0x39, 0x32, 0xea, 0x9d, 0xb7, 0x97, 0x62, 0xbb, 0xa9, 0x54, 0x7b,
+ 0x60, 0x8b, 0xec, 0xf1, 0xb7, 0xe9, 0x77, 0x15, 0xe6, 0xa4, 0xaf, 0x3f, 0x43, 0x44, 0x2c, 0x67,
+ 0xfe, 0x09, 0x00, 0x00, 0xff, 0xff, 0xe9, 0xa0, 0x51, 0x10, 0x54, 0x01, 0x00, 0x00,
+}
diff --git a/unixfs/pb/unixfs.proto b/unixfs/pb/unixfs.proto
new file mode 100644
index 0000000000..ffc059e8be
--- /dev/null
+++ b/unixfs/pb/unixfs.proto
@@ -0,0 +1,26 @@
+syntax = "proto2";
+
+package unixfs.pb;
+
+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;
+}
+
+message Metadata {
+ optional string MimeType = 1;
+}
diff --git a/unixfs/private/linksize/linksize.go b/unixfs/private/linksize/linksize.go
new file mode 100644
index 0000000000..e7ae098b61
--- /dev/null
+++ b/unixfs/private/linksize/linksize.go
@@ -0,0 +1,5 @@
+package linksize
+
+import "github.com/ipfs/go-cid"
+
+var LinkSizeFunction func(linkName string, linkCid cid.Cid) int
diff --git a/unixfs/test/utils.go b/unixfs/test/utils.go
new file mode 100644
index 0000000000..e28ff57460
--- /dev/null
+++ b/unixfs/test/utils.go
@@ -0,0 +1,138 @@
+package testu
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "testing"
+
+ ft "github.com/ipfs/boxo/unixfs"
+ h "github.com/ipfs/boxo/unixfs/importer/helpers"
+ trickle "github.com/ipfs/boxo/unixfs/importer/trickle"
+
+ chunker "github.com/ipfs/boxo/chunker"
+ u "github.com/ipfs/boxo/util"
+ cid "github.com/ipfs/go-cid"
+ ipld "github.com/ipfs/go-ipld-format"
+ mdag "github.com/ipfs/boxo/ipld/merkledag"
+ mdagmock "github.com/ipfs/boxo/ipld/merkledag/test"
+ mh "github.com/multiformats/go-multihash"
+)
+
+// SizeSplitterGen creates a generator.
+func SizeSplitterGen(size int64) chunker.SplitterGen {
+ return func(r io.Reader) chunker.Splitter {
+ return chunker.NewSizeSplitter(r, size)
+ }
+}
+
+// GetDAGServ returns a mock DAGService.
+func GetDAGServ() ipld.DAGService {
+ return mdagmock.Mock()
+}
+
+// NodeOpts is used by GetNode, GetEmptyNode and GetRandomNode
+type NodeOpts struct {
+ Prefix cid.Prefix
+ // ForceRawLeaves if true will force the use of raw leaves
+ ForceRawLeaves bool
+ // RawLeavesUsed is true if raw leaves or either implicitly or explicitly enabled
+ RawLeavesUsed bool
+}
+
+// Some shorthands for NodeOpts.
+var (
+ UseProtoBufLeaves = NodeOpts{Prefix: mdag.V0CidPrefix()}
+ UseRawLeaves = NodeOpts{Prefix: mdag.V0CidPrefix(), ForceRawLeaves: true, RawLeavesUsed: true}
+ UseCidV1 = NodeOpts{Prefix: mdag.V1CidPrefix(), RawLeavesUsed: true}
+ UseBlake2b256 NodeOpts
+)
+
+func init() {
+ UseBlake2b256 = UseCidV1
+ UseBlake2b256.Prefix.MhType = mh.Names["blake2b-256"]
+ UseBlake2b256.Prefix.MhLength = -1
+}
+
+// GetNode returns a unixfs file node with the specified data.
+func GetNode(t testing.TB, dserv ipld.DAGService, data []byte, opts NodeOpts) ipld.Node {
+ in := bytes.NewReader(data)
+
+ dbp := h.DagBuilderParams{
+ Dagserv: dserv,
+ Maxlinks: h.DefaultLinksPerBlock,
+ CidBuilder: opts.Prefix,
+ RawLeaves: opts.RawLeavesUsed,
+ }
+
+ db, err := dbp.New(SizeSplitterGen(500)(in))
+ if err != nil {
+ t.Fatal(err)
+ }
+ node, err := trickle.Layout(db)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ return node
+}
+
+// GetEmptyNode returns an empty unixfs file node.
+func GetEmptyNode(t testing.TB, dserv ipld.DAGService, opts NodeOpts) ipld.Node {
+ return GetNode(t, dserv, []byte{}, opts)
+}
+
+// GetRandomNode returns a random unixfs file node.
+func GetRandomNode(t testing.TB, dserv ipld.DAGService, size int64, opts NodeOpts) ([]byte, ipld.Node) {
+ in := io.LimitReader(u.NewTimeSeededRand(), size)
+ buf, err := io.ReadAll(in)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ node := GetNode(t, dserv, buf, opts)
+ return buf, node
+}
+
+// ArrComp checks if two byte slices are the same.
+func ArrComp(a, b []byte) error {
+ if len(a) != len(b) {
+ return fmt.Errorf("arrays differ in length. %d != %d", len(a), len(b))
+ }
+ for i, v := range a {
+ if v != b[i] {
+ return fmt.Errorf("arrays differ at index: %d", i)
+ }
+ }
+ return nil
+}
+
+// PrintDag pretty-prints the given dag to stdout.
+func PrintDag(nd *mdag.ProtoNode, ds ipld.DAGService, indent int) {
+ fsn, err := ft.FSNodeFromBytes(nd.Data())
+ if err != nil {
+ panic(err)
+ }
+
+ for i := 0; i < indent; i++ {
+ fmt.Print(" ")
+ }
+ fmt.Printf("{size = %d, type = %s, children = %d", fsn.FileSize(), fsn.Type().String(), fsn.NumChildren())
+ if len(nd.Links()) > 0 {
+ fmt.Println()
+ }
+ for _, lnk := range nd.Links() {
+ child, err := lnk.GetNode(context.Background(), ds)
+ if err != nil {
+ panic(err)
+ }
+ PrintDag(child.(*mdag.ProtoNode), ds, indent+1)
+ }
+ if len(nd.Links()) > 0 {
+ for i := 0; i < indent; i++ {
+ fmt.Print(" ")
+ }
+ }
+ fmt.Println("}")
+}
diff --git a/unixfs/unixfs.go b/unixfs/unixfs.go
new file mode 100644
index 0000000000..6f68bdfb69
--- /dev/null
+++ b/unixfs/unixfs.go
@@ -0,0 +1,418 @@
+// Package unixfs implements a data format for files in the IPFS filesystem It
+// is not the only format in ipfs, but it is the one that the filesystem
+// assumes
+package unixfs
+
+import (
+ "errors"
+ "fmt"
+
+ proto "github.com/gogo/protobuf/proto"
+ dag "github.com/ipfs/boxo/ipld/merkledag"
+
+ pb "github.com/ipfs/boxo/unixfs/pb"
+ ipld "github.com/ipfs/go-ipld-format"
+)
+
+// A LinkResult for any parallel enumeration of links
+// TODO: Should this live in go-ipld-format?
+type LinkResult struct {
+ Link *ipld.Link
+ Err error
+}
+
+// Shorthands for protobuffer types
+const (
+ TRaw = pb.Data_Raw
+ TFile = pb.Data_File
+ TDirectory = pb.Data_Directory
+ TMetadata = pb.Data_Metadata
+ TSymlink = pb.Data_Symlink
+ THAMTShard = pb.Data_HAMTShard
+)
+
+// Common errors
+var (
+ ErrMalformedFileFormat = errors.New("malformed data in file format")
+ ErrUnrecognizedType = errors.New("unrecognized node type")
+)
+
+// FromBytes unmarshals a byte slice as protobuf Data.
+// Deprecated: Use `FSNodeFromBytes` instead to avoid direct manipulation of `pb.Data`.
+func FromBytes(data []byte) (*pb.Data, error) {
+ pbdata := new(pb.Data)
+ err := proto.Unmarshal(data, pbdata)
+ if err != nil {
+ return nil, err
+ }
+ return pbdata, nil
+}
+
+// FilePBData creates a protobuf File with the given
+// byte slice and returns the marshaled protobuf bytes representing it.
+func FilePBData(data []byte, totalsize uint64) []byte {
+ pbfile := new(pb.Data)
+ typ := pb.Data_File
+ pbfile.Type = &typ
+ pbfile.Data = data
+ pbfile.Filesize = proto.Uint64(totalsize)
+
+ data, err := proto.Marshal(pbfile)
+ if err != nil {
+ // This really shouldnt happen, i promise
+ // The only failure case for marshal is if required fields
+ // are not filled out, and they all are. If the proto object
+ // gets changed and nobody updates this function, the code
+ // should panic due to programmer error
+ panic(err)
+ }
+ return data
+}
+
+// FolderPBData returns Bytes that represent a Directory.
+func FolderPBData() []byte {
+ pbfile := new(pb.Data)
+ typ := pb.Data_Directory
+ pbfile.Type = &typ
+
+ data, err := proto.Marshal(pbfile)
+ if err != nil {
+ //this really shouldnt happen, i promise
+ panic(err)
+ }
+ return data
+}
+
+// WrapData marshals raw bytes into a `Data_Raw` type protobuf message.
+func WrapData(b []byte) []byte {
+ pbdata := new(pb.Data)
+ typ := pb.Data_Raw
+ pbdata.Data = b
+ pbdata.Type = &typ
+ pbdata.Filesize = proto.Uint64(uint64(len(b)))
+
+ out, err := proto.Marshal(pbdata)
+ if err != nil {
+ // This shouldnt happen. seriously.
+ panic(err)
+ }
+
+ return out
+}
+
+// SymlinkData returns a `Data_Symlink` protobuf message for the path you specify.
+func SymlinkData(path string) ([]byte, error) {
+ pbdata := new(pb.Data)
+ typ := pb.Data_Symlink
+ pbdata.Data = []byte(path)
+ pbdata.Type = &typ
+
+ out, err := proto.Marshal(pbdata)
+ if err != nil {
+ return nil, err
+ }
+
+ return out, nil
+}
+
+// HAMTShardData return a `Data_HAMTShard` protobuf message
+func HAMTShardData(data []byte, fanout uint64, hashType uint64) ([]byte, error) {
+ pbdata := new(pb.Data)
+ typ := pb.Data_HAMTShard
+ pbdata.Type = &typ
+ pbdata.HashType = proto.Uint64(hashType)
+ pbdata.Data = data
+ pbdata.Fanout = proto.Uint64(fanout)
+
+ out, err := proto.Marshal(pbdata)
+ if err != nil {
+ return nil, err
+ }
+
+ return out, nil
+}
+
+// UnwrapData unmarshals a protobuf messages and returns the contents.
+func UnwrapData(data []byte) ([]byte, error) {
+ pbdata := new(pb.Data)
+ err := proto.Unmarshal(data, pbdata)
+ if err != nil {
+ return nil, err
+ }
+ return pbdata.GetData(), nil
+}
+
+// DataSize returns the size of the contents in protobuf wrapped slice.
+// For raw data it simply provides the length of it. For Data_Files, it
+// will return the associated filesize. Note that Data_Directories will
+// return an error.
+func DataSize(data []byte) (uint64, error) {
+ pbdata := new(pb.Data)
+ err := proto.Unmarshal(data, pbdata)
+ if err != nil {
+ return 0, err
+ }
+ return size(pbdata)
+}
+
+func size(pbdata *pb.Data) (uint64, error) {
+ switch pbdata.GetType() {
+ case pb.Data_Directory, pb.Data_HAMTShard:
+ return 0, errors.New("can't get data size of directory")
+ case pb.Data_File, pb.Data_Raw:
+ return pbdata.GetFilesize(), nil
+ case pb.Data_Symlink:
+ return uint64(len(pbdata.GetData())), nil
+ default:
+ return 0, errors.New("unrecognized node data type")
+ }
+}
+
+// An FSNode represents a filesystem object using the UnixFS specification.
+//
+// The `NewFSNode` constructor should be used instead of just calling `new(FSNode)`
+// to guarantee that the required (`Type` and `Filesize`) fields in the `format`
+// structure are initialized before marshaling (in `GetBytes()`).
+type FSNode struct {
+
+ // UnixFS format defined as a protocol buffers message.
+ format pb.Data
+}
+
+// FSNodeFromBytes unmarshal a protobuf message onto an FSNode.
+func FSNodeFromBytes(b []byte) (*FSNode, error) {
+ n := new(FSNode)
+ err := proto.Unmarshal(b, &n.format)
+ if err != nil {
+ return nil, err
+ }
+
+ return n, nil
+}
+
+// NewFSNode creates a new FSNode structure with the given `dataType`.
+//
+// It initializes the (required) `Type` field (that doesn't have a `Set()`
+// accessor so it must be specified at creation), otherwise the `Marshal()`
+// method in `GetBytes()` would fail (`required field "Type" not set`).
+//
+// It also initializes the `Filesize` pointer field to ensure its value
+// is never nil before marshaling, this is not a required field but it is
+// done to be backwards compatible with previous `go-ipfs` versions hash.
+// (If it wasn't initialized there could be cases where `Filesize` could
+// have been left at nil, when the `FSNode` was created but no data or
+// child nodes were set to adjust it, as is the case in `NewLeaf()`.)
+func NewFSNode(dataType pb.Data_DataType) *FSNode {
+ n := new(FSNode)
+ n.format.Type = &dataType
+
+ // Initialize by `Filesize` by updating it with a dummy (zero) value.
+ n.UpdateFilesize(0)
+
+ return n
+}
+
+// HashType gets hash type of format
+func (n *FSNode) HashType() uint64 {
+ return n.format.GetHashType()
+}
+
+// Fanout gets fanout of format
+func (n *FSNode) Fanout() uint64 {
+ return n.format.GetFanout()
+}
+
+// AddBlockSize adds the size of the next child block of this node
+func (n *FSNode) AddBlockSize(s uint64) {
+ n.UpdateFilesize(int64(s))
+ n.format.Blocksizes = append(n.format.Blocksizes, s)
+}
+
+// RemoveBlockSize removes the given child block's size.
+func (n *FSNode) RemoveBlockSize(i int) {
+ n.UpdateFilesize(-int64(n.format.Blocksizes[i]))
+ n.format.Blocksizes = append(n.format.Blocksizes[:i], n.format.Blocksizes[i+1:]...)
+}
+
+// BlockSize returns the block size indexed by `i`.
+// TODO: Evaluate if this function should be bounds checking.
+func (n *FSNode) BlockSize(i int) uint64 {
+ return n.format.Blocksizes[i]
+}
+
+// BlockSizes gets blocksizes of format
+func (n *FSNode) BlockSizes() []uint64 {
+ return n.format.GetBlocksizes()
+}
+
+// RemoveAllBlockSizes removes all the child block sizes of this node.
+func (n *FSNode) RemoveAllBlockSizes() {
+ n.format.Blocksizes = []uint64{}
+ n.format.Filesize = proto.Uint64(uint64(len(n.Data())))
+}
+
+// GetBytes marshals this node as a protobuf message.
+func (n *FSNode) GetBytes() ([]byte, error) {
+ return proto.Marshal(&n.format)
+}
+
+// FileSize returns the size of the file.
+func (n *FSNode) FileSize() uint64 {
+ // XXX: This needs to be able to return an error when we don't know the
+ // size.
+ size, _ := size(&n.format)
+ return size
+}
+
+// NumChildren returns the number of child blocks of this node
+func (n *FSNode) NumChildren() int {
+ return len(n.format.Blocksizes)
+}
+
+// Data retrieves the `Data` field from the internal `format`.
+func (n *FSNode) Data() []byte {
+ return n.format.GetData()
+}
+
+// SetData sets the `Data` field from the internal `format`
+// updating its `Filesize`.
+func (n *FSNode) SetData(newData []byte) {
+ n.UpdateFilesize(int64(len(newData) - len(n.Data())))
+ n.format.Data = newData
+}
+
+// UpdateFilesize updates the `Filesize` field from the internal `format`
+// by a signed difference (`filesizeDiff`).
+// TODO: Add assert to check for `Filesize` > 0?
+func (n *FSNode) UpdateFilesize(filesizeDiff int64) {
+ n.format.Filesize = proto.Uint64(uint64(
+ int64(n.format.GetFilesize()) + filesizeDiff))
+}
+
+// Type retrieves the `Type` field from the internal `format`.
+func (n *FSNode) Type() pb.Data_DataType {
+ return n.format.GetType()
+}
+
+// IsDir checks whether the node represents a directory
+func (n *FSNode) IsDir() bool {
+ switch n.Type() {
+ case pb.Data_Directory, pb.Data_HAMTShard:
+ return true
+ default:
+ return false
+ }
+}
+
+// Metadata is used to store additional FSNode information.
+type Metadata struct {
+ MimeType string
+ Size uint64
+}
+
+// MetadataFromBytes Unmarshals a protobuf Data message into Metadata.
+// The provided slice should have been encoded with BytesForMetadata().
+func MetadataFromBytes(b []byte) (*Metadata, error) {
+ pbd := new(pb.Data)
+ err := proto.Unmarshal(b, pbd)
+ if err != nil {
+ return nil, err
+ }
+ if pbd.GetType() != pb.Data_Metadata {
+ return nil, errors.New("incorrect node type")
+ }
+
+ pbm := new(pb.Metadata)
+ err = proto.Unmarshal(pbd.Data, pbm)
+ if err != nil {
+ return nil, err
+ }
+ md := new(Metadata)
+ md.MimeType = pbm.GetMimeType()
+ return md, nil
+}
+
+// Bytes marshals Metadata as a protobuf message of Metadata type.
+func (m *Metadata) Bytes() ([]byte, error) {
+ pbm := new(pb.Metadata)
+ pbm.MimeType = &m.MimeType
+ return proto.Marshal(pbm)
+}
+
+// BytesForMetadata wraps the given Metadata as a profobuf message of Data type,
+// setting the DataType to Metadata. The wrapped bytes are itself the
+// result of calling m.Bytes().
+func BytesForMetadata(m *Metadata) ([]byte, error) {
+ pbd := new(pb.Data)
+ pbd.Filesize = proto.Uint64(m.Size)
+ typ := pb.Data_Metadata
+ pbd.Type = &typ
+ mdd, err := m.Bytes()
+ if err != nil {
+ return nil, err
+ }
+
+ pbd.Data = mdd
+ return proto.Marshal(pbd)
+}
+
+// EmptyDirNode creates an empty folder Protonode.
+func EmptyDirNode() *dag.ProtoNode {
+ return dag.NodeWithData(FolderPBData())
+}
+
+// EmptyFileNode creates an empty file Protonode.
+func EmptyFileNode() *dag.ProtoNode {
+ return dag.NodeWithData(FilePBData(nil, 0))
+}
+
+// ReadUnixFSNodeData extracts the UnixFS data from an IPLD node.
+// Raw nodes are (also) processed because they are used as leaf
+// nodes containing (only) UnixFS data.
+func ReadUnixFSNodeData(node ipld.Node) (data []byte, err error) {
+ switch node := node.(type) {
+
+ case *dag.ProtoNode:
+ fsNode, err := FSNodeFromBytes(node.Data())
+ if err != nil {
+ return nil, fmt.Errorf("incorrectly formatted protobuf: %s", err)
+ }
+
+ switch fsNode.Type() {
+ case pb.Data_File, pb.Data_Raw:
+ return fsNode.Data(), nil
+ // Only leaf nodes (of type `Data_Raw`) contain data but due to a
+ // bug the `Data_File` type (normally used for internal nodes) is
+ // also used for leaf nodes, so both types are accepted here
+ // (see the `balanced` package for more details).
+ default:
+ return nil, fmt.Errorf("found %s node in unexpected place",
+ fsNode.Type().String())
+ }
+
+ case *dag.RawNode:
+ return node.RawData(), nil
+
+ default:
+ return nil, ErrUnrecognizedType
+ // TODO: To avoid rewriting the error message, but a different error from
+ // `unixfs.ErrUnrecognizedType` should be used (defining it in the
+ // `merkledag` or `go-ipld-format` packages).
+ }
+}
+
+// Extract the `unixfs.FSNode` from the `ipld.Node` (assuming this
+// was implemented by a `mdag.ProtoNode`).
+func ExtractFSNode(node ipld.Node) (*FSNode, error) {
+ protoNode, ok := node.(*dag.ProtoNode)
+ if !ok {
+ return nil, errors.New("expected a ProtoNode as internal node")
+ }
+
+ fsNode, err := FSNodeFromBytes(protoNode.Data())
+ if err != nil {
+ return nil, err
+ }
+
+ return fsNode, nil
+}
diff --git a/unixfs/unixfs_test.go b/unixfs/unixfs_test.go
new file mode 100644
index 0000000000..bbd2bd3b36
--- /dev/null
+++ b/unixfs/unixfs_test.go
@@ -0,0 +1,186 @@
+package unixfs
+
+import (
+ "bytes"
+ "testing"
+
+ proto "github.com/gogo/protobuf/proto"
+
+ pb "github.com/ipfs/boxo/unixfs/pb"
+)
+
+func TestFSNode(t *testing.T) {
+ fsn := NewFSNode(TFile)
+ for i := 0; i < 16; i++ {
+ fsn.AddBlockSize(100)
+ }
+ fsn.RemoveBlockSize(15)
+
+ fsn.SetData(make([]byte, 128))
+
+ b, err := fsn.GetBytes()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ pbn := new(pb.Data)
+ err = proto.Unmarshal(b, pbn)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ds, err := DataSize(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ nKids := fsn.NumChildren()
+ if nKids != 15 {
+ t.Fatal("Wrong number of child nodes")
+ }
+
+ if ds != (100*15)+128 {
+ t.Fatal("Datasize calculations incorrect!")
+ }
+
+ nfsn, err := FSNodeFromBytes(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if nfsn.FileSize() != (100*15)+128 {
+ t.Fatal("fsNode FileSize calculations incorrect")
+ }
+}
+
+func TestPBdataTools(t *testing.T) {
+ raw := []byte{0x00, 0x01, 0x02, 0x17, 0xA1}
+ rawPB := WrapData(raw)
+
+ pbDataSize, err := DataSize(rawPB)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ same := len(raw) == int(pbDataSize)
+ if !same {
+ t.Fatal("WrapData changes the size of data.")
+ }
+
+ rawPBBytes, err := UnwrapData(rawPB)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ same = bytes.Equal(raw, rawPBBytes)
+ if !same {
+ t.Fatal("Unwrap failed to produce the correct wrapped data.")
+ }
+
+ rawPBdata, err := FSNodeFromBytes(rawPB)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ isRaw := rawPBdata.Type() == TRaw
+ if !isRaw {
+ t.Fatal("WrapData does not create pb.Data_Raw!")
+ }
+
+ catFile := []byte("Mr_Meowgie.gif")
+ catPBfile := FilePBData(catFile, 17)
+ catSize, err := DataSize(catPBfile)
+ if catSize != 17 {
+ t.Fatal("FilePBData is the wrong size.")
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ dirPB := FolderPBData()
+ dir, err := FSNodeFromBytes(dirPB)
+ isDir := dir.Type() == TDirectory
+ if !isDir {
+ t.Fatal("FolderPBData does not create a directory!")
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, dirErr := DataSize(dirPB)
+ if dirErr == nil {
+ t.Fatal("DataSize didn't throw an error when taking the size of a directory.")
+ }
+
+ catSym, err := SymlinkData("/ipfs/adad123123/meowgie.gif")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ catSymPB, err := FSNodeFromBytes(catSym)
+ isSym := catSymPB.Type() == TSymlink
+ if !isSym {
+ t.Fatal("Failed to make a Symlink.")
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSymlinkFilesize(t *testing.T) {
+ path := "/ipfs/adad123123/meowgie.gif"
+ sym, err := SymlinkData(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ size, err := DataSize(sym)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if int(size) != len(path) {
+ t.Fatalf("size mismatch: %d != %d", size, len(path))
+ }
+}
+
+func TestMetadata(t *testing.T) {
+ meta := &Metadata{
+ MimeType: "audio/aiff",
+ Size: 12345,
+ }
+
+ _, err := meta.Bytes()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ metaPB, err := BytesForMetadata(meta)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ meta, err = MetadataFromBytes(metaPB)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ mimeAiff := meta.MimeType == "audio/aiff"
+ if !mimeAiff {
+ t.Fatal("Metadata does not Marshal and Unmarshal properly!")
+ }
+
+}
+
+func TestIsDir(t *testing.T) {
+ prepares := map[pb.Data_DataType]bool{
+ TDirectory: true,
+ THAMTShard: true,
+ TFile: false,
+ TMetadata: false,
+ TRaw: false,
+ TSymlink: false,
+ }
+ for typ, v := range prepares {
+ fsn := NewFSNode(typ)
+ if fsn.IsDir() != v {
+ t.Fatalf("type %v, IsDir() should be %v, but %v", typ, v, fsn.IsDir())
+ }
+ }
+}
diff --git a/util/.gitignore b/util/.gitignore
new file mode 100644
index 0000000000..1377554ebe
--- /dev/null
+++ b/util/.gitignore
@@ -0,0 +1 @@
+*.swp
diff --git a/util/file.go b/util/file.go
new file mode 100644
index 0000000000..e6e30df4d3
--- /dev/null
+++ b/util/file.go
@@ -0,0 +1,12 @@
+package util
+
+import "os"
+
+// FileExists check if the file with the given path exits.
+func FileExists(filename string) bool {
+ fi, err := os.Lstat(filename)
+ if fi != nil || (err != nil && !os.IsNotExist(err)) {
+ return true
+ }
+ return false
+}
diff --git a/util/file_test.go b/util/file_test.go
new file mode 100644
index 0000000000..040b229270
--- /dev/null
+++ b/util/file_test.go
@@ -0,0 +1,10 @@
+package util
+
+import "testing"
+
+func TestFileDoesNotExist(t *testing.T) {
+ t.Parallel()
+ if FileExists("i would be surprised to discover that this file exists") {
+ t.Fail()
+ }
+}
diff --git a/util/time.go b/util/time.go
new file mode 100644
index 0000000000..37d720fb1b
--- /dev/null
+++ b/util/time.go
@@ -0,0 +1,22 @@
+package util
+
+import "time"
+
+// TimeFormatIpfs is the format ipfs uses to represent time in string form.
+var TimeFormatIpfs = time.RFC3339Nano
+
+// ParseRFC3339 parses an RFC3339Nano-formatted time stamp and
+// returns the UTC time.
+func ParseRFC3339(s string) (time.Time, error) {
+ t, err := time.Parse(TimeFormatIpfs, s)
+ if err != nil {
+ return time.Time{}, err
+ }
+ return t.UTC(), nil
+}
+
+// FormatRFC3339 returns the string representation of the
+// UTC value of the given time in RFC3339Nano format.
+func FormatRFC3339(t time.Time) string {
+ return t.UTC().Format(TimeFormatIpfs)
+}
diff --git a/util/time_test.go b/util/time_test.go
new file mode 100644
index 0000000000..b5a98caa62
--- /dev/null
+++ b/util/time_test.go
@@ -0,0 +1,16 @@
+package util
+
+import (
+ "testing"
+ "time"
+)
+
+func TestTimeFormatParseInversion(t *testing.T) {
+ v, err := ParseRFC3339(FormatRFC3339(time.Now()))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if v.Location() != time.UTC {
+ t.Fatal("Time should be UTC")
+ }
+}
diff --git a/util/util.go b/util/util.go
new file mode 100644
index 0000000000..ffcab2f33d
--- /dev/null
+++ b/util/util.go
@@ -0,0 +1,158 @@
+// Package util implements various utility functions used within ipfs
+// that do not currently have a better place to live.
+package util
+
+import (
+ "errors"
+ "io"
+ "math/rand"
+ "os"
+ "path/filepath"
+ "runtime/debug"
+ "strings"
+ "time"
+
+ b58 "github.com/mr-tron/base58/base58"
+ mh "github.com/multiformats/go-multihash"
+)
+
+// DefaultIpfsHash is the current default hash function used by IPFS.
+const DefaultIpfsHash = mh.SHA2_256
+
+// Debug is a global flag for debugging.
+var Debug bool
+
+// ErrNotImplemented signifies a function has not been implemented yet.
+var ErrNotImplemented = errors.New("error: not implemented yet")
+
+// ErrTimeout implies that a timeout has been triggered
+var ErrTimeout = errors.New("error: call timed out")
+
+// ErrSearchIncomplete implies that a search type operation didn't
+// find the expected node, but did find 'a' node.
+var ErrSearchIncomplete = errors.New("error: search incomplete")
+
+// ErrCast is returned when a cast fails AND the program should not panic.
+func ErrCast() error {
+ debug.PrintStack()
+ return errCast
+}
+
+var errCast = errors.New("cast error")
+
+// ExpandPathnames takes a set of paths and turns them into absolute paths
+func ExpandPathnames(paths []string) ([]string, error) {
+ var out []string
+ for _, p := range paths {
+ abspath, err := filepath.Abs(p)
+ if err != nil {
+ return nil, err
+ }
+ out = append(out, abspath)
+ }
+ return out, nil
+}
+
+type randGen struct {
+ rand.Rand
+}
+
+// NewTimeSeededRand returns a random bytes reader
+// which has been initialized with the current time.
+func NewTimeSeededRand() io.Reader {
+ src := rand.NewSource(time.Now().UnixNano())
+ return &randGen{
+ Rand: *rand.New(src),
+ }
+}
+
+// NewSeededRand returns a random bytes reader
+// initialized with the given seed.
+func NewSeededRand(seed int64) io.Reader {
+ src := rand.NewSource(seed)
+ return &randGen{
+ Rand: *rand.New(src),
+ }
+}
+
+func (r *randGen) Read(p []byte) (n int, err error) {
+ for i := 0; i < len(p); i++ {
+ p[i] = byte(r.Rand.Intn(255))
+ }
+ return len(p), nil
+}
+
+// GetenvBool is the way to check an env var as a boolean
+func GetenvBool(name string) bool {
+ v := strings.ToLower(os.Getenv(name))
+ return v == "true" || v == "t" || v == "1"
+}
+
+// MultiErr is a util to return multiple errors
+type MultiErr []error
+
+func (m MultiErr) Error() string {
+ if len(m) == 0 {
+ return "no errors"
+ }
+
+ s := "Multiple errors: "
+ for i, e := range m {
+ if i != 0 {
+ s += ", "
+ }
+ s += e.Error()
+ }
+ return s
+}
+
+// Partition splits a subject 3 parts: prefix, separator, suffix.
+// The first occurrence of the separator will be matched.
+// ie. Partition("Ready, steady, go!", ", ") -> ["Ready", ", ", "steady, go!"]
+func Partition(subject string, sep string) (string, string, string) {
+ if i := strings.Index(subject, sep); i != -1 {
+ return subject[:i], subject[i : i+len(sep)], subject[i+len(sep):]
+ }
+ return subject, "", ""
+}
+
+// RPartition splits a subject 3 parts: prefix, separator, suffix.
+// The last occurrence of the separator will be matched.
+// ie. RPartition("Ready, steady, go!", ", ") -> ["Ready, steady", ", ", "go!"]
+func RPartition(subject string, sep string) (string, string, string) {
+ if i := strings.LastIndex(subject, sep); i != -1 {
+ return subject[:i], subject[i : i+len(sep)], subject[i+len(sep):]
+ }
+ return subject, "", ""
+}
+
+// Hash is the global IPFS hash function. uses multihash SHA2_256, 256 bits
+func Hash(data []byte) mh.Multihash {
+ h, err := mh.Sum(data, DefaultIpfsHash, -1)
+ if err != nil {
+ // this error can be safely ignored (panic) because multihash only fails
+ // from the selection of hash function. If the fn + length are valid, it
+ // won't error.
+ panic("multihash failed to hash using SHA2_256.")
+ }
+ return h
+}
+
+// IsValidHash checks whether a given hash is valid (b58 decodable, len > 0)
+func IsValidHash(s string) bool {
+ out, err := b58.Decode(s)
+ if err != nil {
+ return false
+ }
+ _, err = mh.Cast(out)
+ return err == nil
+}
+
+// XOR takes two byte slices, XORs them together, returns the resulting slice.
+func XOR(a, b []byte) []byte {
+ c := make([]byte, len(a))
+ for i := 0; i < len(a); i++ {
+ c[i] = a[i] ^ b[i]
+ }
+ return c
+}
diff --git a/util/util_test.go b/util/util_test.go
new file mode 100644
index 0000000000..70747ad902
--- /dev/null
+++ b/util/util_test.go
@@ -0,0 +1,63 @@
+package util
+
+import (
+ "bytes"
+ "testing"
+)
+
+func TestXOR(t *testing.T) {
+ cases := [][3][]byte{
+ {
+ {0xFF, 0xFF, 0xFF},
+ {0xFF, 0xFF, 0xFF},
+ {0x00, 0x00, 0x00},
+ },
+ {
+ {0x00, 0xFF, 0x00},
+ {0xFF, 0xFF, 0xFF},
+ {0xFF, 0x00, 0xFF},
+ },
+ {
+ {0x55, 0x55, 0x55},
+ {0x55, 0xFF, 0xAA},
+ {0x00, 0xAA, 0xFF},
+ },
+ }
+
+ for _, c := range cases {
+ r := XOR(c[0], c[1])
+ if !bytes.Equal(r, c[2]) {
+ t.Error("XOR failed")
+ }
+ }
+}
+
+func BenchmarkHash256K(b *testing.B) {
+ buf := make([]byte, 256*1024)
+ NewTimeSeededRand().Read(buf)
+ b.SetBytes(int64(256 * 1024))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ Hash(buf)
+ }
+}
+
+func BenchmarkHash512K(b *testing.B) {
+ buf := make([]byte, 512*1024)
+ NewTimeSeededRand().Read(buf)
+ b.SetBytes(int64(512 * 1024))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ Hash(buf)
+ }
+}
+
+func BenchmarkHash1M(b *testing.B) {
+ buf := make([]byte, 1024*1024)
+ NewTimeSeededRand().Read(buf)
+ b.SetBytes(int64(1024 * 1024))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ Hash(buf)
+ }
+}
diff --git a/verifcid/validate.go b/verifcid/validate.go
new file mode 100644
index 0000000000..7b27debc94
--- /dev/null
+++ b/verifcid/validate.go
@@ -0,0 +1,69 @@
+package verifcid
+
+import (
+ "fmt"
+
+ cid "github.com/ipfs/go-cid"
+ mh "github.com/multiformats/go-multihash"
+)
+
+var ErrPossiblyInsecureHashFunction = fmt.Errorf("potentially insecure hash functions not allowed")
+var ErrBelowMinimumHashLength = fmt.Errorf("hashes must be at least %d bytes long", minimumHashLength)
+var ErrAboveMaximumHashLength = fmt.Errorf("hashes must be at most %d bytes long", maximumHashLength)
+
+const minimumHashLength = 20
+const maximumHashLength = 128
+
+var goodset = map[uint64]bool{
+ mh.SHA2_256: true,
+ mh.SHA2_512: true,
+ mh.SHA3_224: true,
+ mh.SHA3_256: true,
+ mh.SHA3_384: true,
+ mh.SHA3_512: true,
+ mh.SHAKE_256: true,
+ mh.DBL_SHA2_256: true,
+ mh.KECCAK_224: true,
+ mh.KECCAK_256: true,
+ mh.KECCAK_384: true,
+ mh.KECCAK_512: true,
+ mh.BLAKE3: true,
+ mh.IDENTITY: true,
+
+ mh.SHA1: true, // not really secure but still useful
+}
+
+func IsGoodHash(code uint64) bool {
+ good, found := goodset[code]
+ if good {
+ return true
+ }
+
+ if !found {
+ if code >= mh.BLAKE2B_MIN+19 && code <= mh.BLAKE2B_MAX {
+ return true
+ }
+ if code >= mh.BLAKE2S_MIN+19 && code <= mh.BLAKE2S_MAX {
+ return true
+ }
+ }
+
+ return false
+}
+
+func ValidateCid(c cid.Cid) error {
+ pref := c.Prefix()
+ if !IsGoodHash(pref.MhType) {
+ return ErrPossiblyInsecureHashFunction
+ }
+
+ if pref.MhType != mh.IDENTITY && pref.MhLength < minimumHashLength {
+ return ErrBelowMinimumHashLength
+ }
+
+ if pref.MhType != mh.IDENTITY && pref.MhLength > maximumHashLength {
+ return ErrAboveMaximumHashLength
+ }
+
+ return nil
+}
diff --git a/verifcid/validate_test.go b/verifcid/validate_test.go
new file mode 100644
index 0000000000..5129b861af
--- /dev/null
+++ b/verifcid/validate_test.go
@@ -0,0 +1,70 @@
+package verifcid
+
+import (
+ "testing"
+
+ mh "github.com/multiformats/go-multihash"
+
+ cid "github.com/ipfs/go-cid"
+)
+
+func TestValidateCids(t *testing.T) {
+ assertTrue := func(v bool) {
+ t.Helper()
+ if !v {
+ t.Fatal("expected success")
+ }
+ }
+ assertFalse := func(v bool) {
+ t.Helper()
+ if v {
+ t.Fatal("expected failure")
+ }
+ }
+
+ assertTrue(IsGoodHash(mh.SHA2_256))
+ assertTrue(IsGoodHash(mh.BLAKE2B_MIN + 32))
+ assertTrue(IsGoodHash(mh.DBL_SHA2_256))
+ assertTrue(IsGoodHash(mh.KECCAK_256))
+ assertTrue(IsGoodHash(mh.SHA3))
+
+ assertTrue(IsGoodHash(mh.SHA1))
+
+ assertFalse(IsGoodHash(mh.BLAKE2B_MIN + 5))
+
+ mhcid := func(code uint64, length int) cid.Cid {
+ mhash, err := mh.Sum([]byte{}, code, length)
+ if err != nil {
+ t.Fatalf("%v: code: %x length: %d", err, code, length)
+ }
+ return cid.NewCidV1(cid.DagCBOR, mhash)
+ }
+
+ cases := []struct {
+ cid cid.Cid
+ err error
+ }{
+ {mhcid(mh.SHA2_256, 32), nil},
+ {mhcid(mh.SHA2_256, 16), ErrBelowMinimumHashLength},
+ {mhcid(mh.MURMUR3X64_64, 4), ErrPossiblyInsecureHashFunction},
+ {mhcid(mh.BLAKE3, 32), nil},
+ {mhcid(mh.BLAKE3, 69), nil},
+ {mhcid(mh.BLAKE3, 128), nil},
+ }
+
+ for i, cas := range cases {
+ if ValidateCid(cas.cid) != cas.err {
+ t.Errorf("wrong result in case of %s (index %d). Expected: %s, got %s",
+ cas.cid, i, cas.err, ValidateCid(cas.cid))
+ }
+ }
+
+ longBlake3Hex := "1e810104e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9791074b7511b59d31c71c62f5a745689fa6c9497f68bdf1061fe07f518d410c0b0c27f41b3cf083f8a7fdc67a877e21790515762a754a45dcb8a356722698a7af5ed2bb608983d5aa75d4d61691ef132efe8631ce0afc15553a08fffc60ee9369b"
+ longBlake3Mh, err := mh.FromHexString(longBlake3Hex)
+ if err != nil {
+ t.Fatalf("failed to produce a multihash from the long blake3 hash: %v", err)
+ }
+ if ValidateCid(cid.NewCidV1(cid.DagCBOR, longBlake3Mh)) != ErrAboveMaximumHashLength {
+ t.Errorf("a CID that was longer than the maximum hash length did not error with ErrAboveMaximumHashLength")
+ }
+}
diff --git a/version.json b/version.json
new file mode 100644
index 0000000000..0ad79e3bfe
--- /dev/null
+++ b/version.json
@@ -0,0 +1,3 @@
+{
+ "version": "v0.8.0"
+}