Skip to content

Commit 801f0c0

Browse files
committed
ipfs: all - add support for symbolic links
1 parent 158db7a commit 801f0c0

File tree

10 files changed

+642
-87
lines changed

10 files changed

+642
-87
lines changed

internal/filesystem/ipfs/ipfs.go

Lines changed: 113 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
coreiface "github.com/ipfs/boxo/coreiface"
1515
coreoptions "github.com/ipfs/boxo/coreiface/options"
1616
corepath "github.com/ipfs/boxo/coreiface/path"
17+
files "github.com/ipfs/boxo/files"
1718
ipath "github.com/ipfs/boxo/path"
1819
"github.com/ipfs/boxo/path/resolver"
1920
"github.com/ipfs/go-cid"
@@ -37,6 +38,7 @@ type (
3738
dirCache *ipfsDirCache
3839
info nodeInfo
3940
nodeTimeout time.Duration
41+
linkLimit uint
4042
}
4143
ipfsSettings struct {
4244
*IPFS
@@ -65,6 +67,7 @@ func NewIPFS(core coreiface.CoreAPI, options ...IPFSOption) (*IPFS, error) {
6567
},
6668
core: core,
6769
nodeTimeout: 1 * time.Minute,
70+
linkLimit: 40, // Arbitrary.
6871
}
6972
settings = ipfsSettings{
7073
IPFS: fsys,
@@ -145,23 +148,20 @@ func WithDirectoryCacheCount(cacheCount int) IPFSOption {
145148
}
146149
}
147150

148-
// WithNodeTimeout sets a timeout duration to use
149-
// when communicating with the IPFS API/node.
150-
// If <= 0, operations will not time out,
151-
// and will remain pending until the file system is closed.
152-
func WithNodeTimeout(duration time.Duration) IPFSOption {
153-
return func(ifs *ipfsSettings) error {
154-
ifs.nodeTimeout = duration
155-
return nil
156-
}
157-
}
158-
159151
func (*IPFS) ID() filesystem.ID { return IPFSID }
160152

161153
func (fsys *IPFS) setContext(ctx context.Context) {
162154
fsys.ctx, fsys.cancel = context.WithCancel(ctx)
163155
}
164156

157+
func (fsys *IPFS) setNodeTimeout(timeout time.Duration) {
158+
fsys.nodeTimeout = timeout
159+
}
160+
161+
func (fsys *IPFS) setLinkLimit(limit uint) {
162+
fsys.linkLimit = limit
163+
}
164+
165165
func (fsys *IPFS) setPermissions(permissions fs.FileMode) {
166166
fsys.info.mode = fsys.info.mode.Type() | permissions.Perm()
167167
}
@@ -171,20 +171,91 @@ func (fsys *IPFS) Close() error {
171171
return nil
172172
}
173173

174-
func (fsys *IPFS) Stat(name string) (fs.FileInfo, error) {
175-
const op = "stat"
174+
func (fsys *IPFS) Lstat(name string) (fs.FileInfo, error) {
175+
const op = "lstat"
176+
info, _, err := fsys.lstat(op, name)
177+
return info, err
178+
}
179+
180+
func (fsys *IPFS) lstat(op, name string) (fs.FileInfo, cid.Cid, error) {
176181
if name == filesystem.Root {
177-
return &fsys.info, nil
182+
return &fsys.info, cid.Cid{}, nil
178183
}
179184
cid, err := fsys.toCID(op, name)
180185
if err != nil {
181-
return nil, err
186+
return nil, cid, err
182187
}
183188
info, err := fsys.getInfo(name, cid)
184189
if err != nil {
185-
return nil, fserrors.New(op, name, err, fserrors.IO)
190+
const kind = fserrors.IO
191+
return nil, cid, fserrors.New(op, name, err, kind)
192+
}
193+
return info, cid, nil
194+
}
195+
196+
func (fsys *IPFS) Stat(name string) (fs.FileInfo, error) {
197+
const depth = 0
198+
return fsys.stat(name, depth)
199+
}
200+
201+
func (fsys *IPFS) stat(name string, depth uint) (fs.FileInfo, error) {
202+
const op = "stat"
203+
info, cid, err := fsys.lstat(op, name)
204+
if err != nil {
205+
return nil, err
206+
}
207+
if isLink := info.Mode()&fs.ModeSymlink != 0; !isLink {
208+
return info, nil
209+
}
210+
if depth++; depth >= fsys.linkLimit {
211+
return nil, linkLimitError(op, name, fsys.linkLimit)
212+
}
213+
target, err := fsys.resolveCIDSymlink(op, name, cid)
214+
if err != nil {
215+
return nil, err
216+
}
217+
return fsys.stat(target, depth)
218+
}
219+
220+
func (fsys *IPFS) Readlink(name string) (string, error) {
221+
const op = "readlink"
222+
if name == filesystem.Root {
223+
const kind = fserrors.InvalidItem
224+
return "", fserrors.New(op, name, errRootLink, kind)
225+
}
226+
cid, err := fsys.toCID(op, name)
227+
if err != nil {
228+
return "", err
186229
}
187-
return info, nil
230+
return fsys.resolveCIDSymlink(op, name, cid)
231+
}
232+
233+
func readNodeLink(op, name string, node files.Node) (string, error) {
234+
link, ok := node.(*files.Symlink)
235+
if !ok {
236+
const kind = fserrors.InvalidItem
237+
err := fmt.Errorf(
238+
"expected node type: %T but got: %T",
239+
link, node,
240+
)
241+
return "", fserrors.New(op, name, err, kind)
242+
}
243+
target := link.Target
244+
if len(target) == 0 {
245+
const kind = fserrors.InvalidItem
246+
return "", fserrors.New(op, name, errEmptyLink, kind)
247+
}
248+
return target, nil
249+
}
250+
251+
func (fsys *IPFS) resolveCIDSymlink(op, name string, cid cid.Cid) (string, error) {
252+
var (
253+
ufs = fsys.core.Unixfs()
254+
ctx, cancel = fsys.nodeContext()
255+
)
256+
defer cancel()
257+
const allowedPrefix = "/ipfs/"
258+
return getUnixFSLink(ctx, op, name, ufs, cid, allowedPrefix)
188259
}
189260

190261
func (fsys *IPFS) toCID(op, goPath string) (cid.Cid, error) {
@@ -314,6 +385,11 @@ func (fsys *IPFS) resolvePath(goPath string) (cid.Cid, error) {
314385
}
315386

316387
func (fsys *IPFS) Open(name string) (fs.File, error) {
388+
const depth = 0
389+
return fsys.open(name, depth)
390+
}
391+
392+
func (fsys *IPFS) open(name string, depth uint) (fs.File, error) {
317393
if name == filesystem.Root {
318394
return emptyRoot{info: &fsys.info}, nil
319395
}
@@ -325,23 +401,25 @@ func (fsys *IPFS) Open(name string) (fs.File, error) {
325401
if err != nil {
326402
return nil, err
327403
}
328-
file, err := fsys.openCid(name, cid)
329-
if err != nil {
330-
return nil, fserrors.New(op, name, err, fserrors.IO)
331-
}
332-
return file, nil
333-
}
334-
335-
func (fsys *IPFS) openCid(name string, cid cid.Cid) (fs.File, error) {
336404
info, err := fsys.getInfo(name, cid)
337405
if err != nil {
338-
return nil, err
406+
const kind = fserrors.IO
407+
return nil, fserrors.New(op, name, err, kind)
339408
}
340409
switch typ := info.mode.Type(); typ {
341410
case fs.FileMode(0):
342411
return fsys.openFile(cid, info)
343412
case fs.ModeDir:
344413
return fsys.openDir(cid, info)
414+
case fs.ModeSymlink:
415+
if depth++; depth >= fsys.linkLimit {
416+
return nil, linkLimitError(op, name, fsys.linkLimit)
417+
}
418+
target, err := fsys.resolveCIDSymlink(op, name, cid)
419+
if err != nil {
420+
return nil, err
421+
}
422+
return fsys.open(target, depth)
345423
default:
346424
return nil, fmt.Errorf(
347425
"%w got: \"%s\" want: regular file or directory",
@@ -521,3 +599,12 @@ func (id *ipfsDirectory) Close() error {
521599
}
522600
return fserrors.New(op, id.info.name, fs.ErrClosed, fserrors.InvalidItem)
523601
}
602+
603+
func linkLimitError(op, name string, limit uint) error {
604+
const kind = fserrors.Recursion
605+
err := fmt.Errorf(
606+
"reached symbolic link resolution limit (%d) during operation",
607+
limit,
608+
)
609+
return fserrors.New(op, name, err, kind)
610+
}

internal/filesystem/ipfs/ipfs_internal_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ var (
1212
_ fs.FS = (*IPFS)(nil)
1313
_ fs.StatFS = (*IPFS)(nil)
1414
_ filesystem.IDFS = (*IPFS)(nil)
15+
_ symlinkRFS = (*IPFS)(nil)
1516
_ fs.File = (*ipfsDirectory)(nil)
1617
_ fs.ReadDirFile = (*ipfsDirectory)(nil)
1718
_ filesystem.StreamDirFile = (*ipfsDirectory)(nil)
@@ -30,5 +31,7 @@ func testIPFSOptions(t *testing.T) {
3031
nil,
3132
WithContext[IPFSOption](context.Background()),
3233
WithPermissions[IPFSOption](0),
34+
WithNodeTimeout[IPFSOption](0),
35+
WithLinkLimit[IPFSOption](0),
3336
)
3437
}

internal/filesystem/ipfs/ipns.go

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type (
3737
info nodeInfo
3838
nodeTimeout time.Duration
3939
expiry time.Duration
40+
linkLimit uint
4041
}
4142
ipnsSettings struct {
4243
*IPNS
@@ -63,6 +64,7 @@ func NewIPNS(core coreiface.CoreAPI, ipfs fs.FS, options ...IPNSOption) (*IPNS,
6364
readAll | executeAll,
6465
},
6566
nodeTimeout: 1 * time.Minute,
67+
linkLimit: 40, // Arbitrary.
6668
}
6769
settings = ipnsSettings{
6870
IPNS: fsys,
@@ -137,6 +139,10 @@ func (fsys *IPNS) setContext(ctx context.Context) {
137139
fsys.ctx, fsys.cancel = context.WithCancel(ctx)
138140
}
139141

142+
func (fsys *IPNS) setLinkLimit(limit uint) {
143+
fsys.linkLimit = limit
144+
}
145+
140146
func (fsys *IPNS) setPermissions(permissions fs.FileMode) {
141147
fsys.info.mode = fsys.info.mode.Type() | permissions.Perm()
142148
}
@@ -149,16 +155,25 @@ func (fsys *IPNS) Close() error {
149155
return nil
150156
}
151157

158+
func (fsys *IPNS) Lstat(name string) (fs.FileInfo, error) {
159+
const op = "lstat"
160+
return fsys.stat(op, name, filesystem.Lstat)
161+
}
162+
152163
func (fsys *IPNS) Stat(name string) (fs.FileInfo, error) {
153164
const op = "stat"
165+
return fsys.stat(op, name, fs.Stat)
166+
}
167+
168+
func (fsys *IPNS) stat(op, name string, statFn statFunc) (fs.FileInfo, error) {
154169
if name == filesystem.Root {
155170
return &fsys.info, nil
156171
}
157172
cid, err := fsys.toCID(op, name)
158173
if err != nil {
159174
return nil, err
160175
}
161-
return fs.Stat(fsys.ipfs, cid.String())
176+
return statFn(fsys.ipfs, cid.String())
162177
}
163178

164179
func (fsys *IPNS) toCID(op, goPath string) (cid.Cid, error) {
@@ -229,14 +244,11 @@ func (fsys *IPNS) resolvePath(goPath string) (cid.Cid, error) {
229244
}
230245

231246
func (fsys *IPNS) nodeContext() (context.Context, context.CancelFunc) {
232-
var (
233-
ctx = fsys.ctx
234-
timeout = fsys.nodeTimeout
235-
)
236-
if timeout <= 0 {
237-
return context.WithCancel(ctx)
247+
ctx := fsys.ctx
248+
if timeout := fsys.nodeTimeout; timeout > 0 {
249+
return context.WithTimeout(ctx, timeout)
238250
}
239-
return context.WithTimeout(ctx, timeout)
251+
return context.WithCancel(ctx)
240252
}
241253

242254
func (fsys *IPNS) fetchCID(ctx context.Context, goPath string) (cid.Cid, error) {
@@ -251,7 +263,35 @@ func (fsys *IPNS) fetchCID(ctx context.Context, goPath string) (cid.Cid, error)
251263
return resolved.Cid(), nil
252264
}
253265

266+
func (fsys *IPNS) Readlink(name string) (string, error) {
267+
const op = "readlink"
268+
if name == filesystem.Root {
269+
const kind = fserrors.InvalidItem
270+
return "", fserrors.New(op, name, errRootLink, kind)
271+
}
272+
cid, err := fsys.toCID(op, name)
273+
if err != nil {
274+
return "", err
275+
}
276+
return filesystem.Readlink(fsys.ipfs, cid.String())
277+
}
278+
279+
func (fsys *IPNS) resolveCIDSymlink(op, name string, cid cid.Cid) (string, error) {
280+
var (
281+
ufs = fsys.core.Unixfs()
282+
ctx, cancel = fsys.nodeContext()
283+
)
284+
defer cancel()
285+
const allowedPrefix = "/ipns/"
286+
return getUnixFSLink(ctx, op, name, ufs, cid, allowedPrefix)
287+
}
288+
254289
func (fsys *IPNS) Open(name string) (fs.File, error) {
290+
const depth = 0
291+
return fsys.open(name, depth)
292+
}
293+
294+
func (fsys *IPNS) open(name string, depth uint) (fs.File, error) {
255295
if name == filesystem.Root {
256296
return emptyRoot{info: &fsys.info}, nil
257297
}
@@ -264,9 +304,23 @@ func (fsys *IPNS) Open(name string) (fs.File, error) {
264304
return nil, err
265305
}
266306
ipfs := fsys.ipfs
307+
info, err := filesystem.Lstat(ipfs, cid.String())
308+
if err != nil {
309+
return nil, err
310+
}
311+
if info.Mode().Type() == fs.ModeSymlink {
312+
if depth++; depth >= fsys.linkLimit {
313+
return nil, linkLimitError(op, name, fsys.linkLimit)
314+
}
315+
target, err := fsys.resolveCIDSymlink(op, name, cid)
316+
if err != nil {
317+
return nil, err
318+
}
319+
return fsys.open(target, depth)
320+
}
267321
file, err := ipfs.Open(cid.String())
268322
if err != nil {
269-
return nil, fserrors.New(op, name, err, fserrors.IO)
323+
return nil, err
270324
}
271325
nFile := ipnsFile{
272326
file: file,
@@ -412,19 +466,18 @@ func (nf *ipnsFile) ReadDir(count int) ([]fs.DirEntry, error) {
412466
if err := nf.refreshFn(); err != nil {
413467
return nil, err
414468
}
415-
// TODO: these kinds of things should
416-
// use the new [errors.ErrUnsupported] value too.
417469
file := nf.file
418470
if directory, ok := file.(fs.ReadDirFile); ok {
419471
return directory.ReadDir(count)
420472
}
421473
var (
422474
name string
423-
err error = filesystem.ErrIsDir
424-
kind = fserrors.NotDir
475+
err error = errors.ErrUnsupported
476+
kind fserrors.Kind
425477
)
426478
if info, sErr := file.Stat(); sErr == nil {
427479
name = info.Name()
480+
kind = fserrors.InvalidOperation
428481
} else {
429482
err = errors.Join(err, sErr)
430483
kind = fserrors.IO

0 commit comments

Comments
 (0)