Skip to content
Merged
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ Run these steps in order before considering work complete:
- after editing CLI help text in `core/commands/`, verify width: `go test ./test/cli/... -run TestCommandDocsWidth`
- config options are documented in `docs/config.md`
- changelogs in `docs/changelogs/`: only edit the Table of Contents and the Highlights section; the Changelog and Contributors sections are auto-generated and must not be modified
- avoid unnecessary line wrapping in `docs/changelogs/*`; let lines be long
- follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
- keep commit titles short and messages terse

Expand Down
47 changes: 47 additions & 0 deletions config/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import (
"strings"

chunk "github.com/ipfs/boxo/chunker"
merkledag "github.com/ipfs/boxo/ipld/merkledag"
"github.com/ipfs/boxo/ipld/unixfs/importer/helpers"
uio "github.com/ipfs/boxo/ipld/unixfs/io"
"github.com/ipfs/boxo/mfs"
"github.com/ipfs/boxo/verifcid"
cid "github.com/ipfs/go-cid"
mh "github.com/multiformats/go-multihash"
)

Expand Down Expand Up @@ -259,3 +262,47 @@ func (i *Import) UnixFSSplitterFunc() chunk.SplitterGen {
return s
}
}

// MFSRootOptions returns all MFS root options derived from Import config.
func (i *Import) MFSRootOptions() ([]mfs.Option, error) {
cidBuilder, err := i.UnixFSCidBuilder()
if err != nil {
return nil, err
}
sizeEstimationMode := i.HAMTSizeEstimationMode()
return []mfs.Option{
mfs.WithCidBuilder(cidBuilder),
mfs.WithChunker(i.UnixFSSplitterFunc()),
mfs.WithMaxLinks(int(i.UnixFSDirectoryMaxLinks.WithDefault(DefaultUnixFSDirectoryMaxLinks))),
mfs.WithMaxHAMTFanout(int(i.UnixFSHAMTDirectoryMaxFanout.WithDefault(DefaultUnixFSHAMTDirectoryMaxFanout))),
mfs.WithHAMTShardingSize(int(i.UnixFSHAMTDirectorySizeThreshold.WithDefault(DefaultUnixFSHAMTDirectorySizeThreshold))),
mfs.WithSizeEstimationMode(sizeEstimationMode),
}, nil
}

// UnixFSCidBuilder returns a cid.Builder based on Import.CidVersion and
// Import.HashFunction. Always builds an explicit prefix so that MFS
// respects kubo defaults even when they differ from boxo's internal
// CIDv0/sha2-256 default (see https://github.com/ipfs/kubo/issues/4143).
func (i *Import) UnixFSCidBuilder() (cid.Builder, error) {
cidVer := int(i.CidVersion.WithDefault(DefaultCidVersion))
hashFunc := i.HashFunction.WithDefault(DefaultHashFunction)

if hashFunc != DefaultHashFunction && cidVer == 0 {
cidVer = 1
}

prefix, err := merkledag.PrefixForCidVersion(cidVer)
if err != nil {
return nil, err
}

hashCode, ok := mh.Names[strings.ToLower(hashFunc)]
if !ok {
return nil, fmt.Errorf("Import.HashFunction unrecognized: %q", hashFunc)
}
prefix.MhType = hashCode
prefix.MhLength = -1

return &prefix, nil
}
89 changes: 89 additions & 0 deletions config/import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,95 @@ func TestValidateImportConfig_DAGLayout(t *testing.T) {
}
}

func TestImport_UnixFSCidBuilder(t *testing.T) {
defaultMhType := mh.Names[strings.ToLower(DefaultHashFunction)]

tests := []struct {
name string
cfg Import
wantCidVer uint64
wantMhType uint64
}{
{
name: "CIDv1 explicit",
cfg: Import{CidVersion: *NewOptionalInteger(1)},
wantCidVer: 1,
wantMhType: defaultMhType,
},
{
name: "CIDv0 explicit",
cfg: Import{CidVersion: *NewOptionalInteger(0)},
wantCidVer: 0,
wantMhType: defaultMhType,
},
{
name: "non-default hash upgrades CIDv0 to CIDv1",
cfg: Import{HashFunction: *NewOptionalString("sha2-512")},
wantCidVer: 1,
wantMhType: mh.SHA2_512,
},
{
name: "CIDv1 with sha2-512",
cfg: Import{
CidVersion: *NewOptionalInteger(1),
HashFunction: *NewOptionalString("sha2-512"),
},
wantCidVer: 1,
wantMhType: mh.SHA2_512,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
builder, err := tt.cfg.UnixFSCidBuilder()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if builder == nil {
t.Fatal("expected non-nil builder")
}
c, err := builder.Sum([]byte("test"))
if err != nil {
t.Fatalf("builder.Sum failed: %v", err)
}
pref := c.Prefix()
if pref.Version != tt.wantCidVer {
t.Errorf("CID version = %d, want %d", pref.Version, tt.wantCidVer)
}
if pref.MhType != tt.wantMhType {
t.Errorf("multihash type = 0x%x, want 0x%x", pref.MhType, tt.wantMhType)
}
})
}
}

// TestImport_UnixFSCidBuilderDefaults verifies that UnixFSCidBuilder always
// returns an explicit builder even when no config is set, so that MFS
// respects kubo's DefaultCidVersion rather than relying on boxo's internal
// CIDv0 default (relevant for https://github.com/ipfs/kubo/issues/4143).
func TestImport_UnixFSCidBuilderDefaults(t *testing.T) {
cfg := &Import{}
builder, err := cfg.UnixFSCidBuilder()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if builder == nil {
t.Fatal("expected non-nil builder at defaults")
}
c, err := builder.Sum([]byte("test"))
if err != nil {
t.Fatalf("builder.Sum failed: %v", err)
}
pref := c.Prefix()
if pref.Version != uint64(DefaultCidVersion) {
t.Errorf("CID version = %d, want DefaultCidVersion (%d)", pref.Version, DefaultCidVersion)
}
wantMhType := mh.Names[strings.ToLower(DefaultHashFunction)]
if pref.MhType != wantMhType {
t.Errorf("multihash type = 0x%x, want 0x%x (DefaultHashFunction=%s)", pref.MhType, wantMhType, DefaultHashFunction)
}
}

func TestImport_HAMTSizeEstimationMode(t *testing.T) {
tests := []struct {
cfg string
Expand Down
144 changes: 53 additions & 91 deletions core/commands/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (
offline "github.com/ipfs/boxo/exchange/offline"
dag "github.com/ipfs/boxo/ipld/merkledag"
ft "github.com/ipfs/boxo/ipld/unixfs"
uio "github.com/ipfs/boxo/ipld/unixfs/io"
mfs "github.com/ipfs/boxo/mfs"
"github.com/ipfs/boxo/path"
cid "github.com/ipfs/go-cid"
Expand Down Expand Up @@ -505,7 +504,7 @@ being GC'ed.
return err
}

prefix, err := getPrefixNew(req, &cfg.Import)
prefix, err := getPrefix(req, &cfg.Import)
if err != nil {
return err
}
Expand Down Expand Up @@ -558,7 +557,11 @@ being GC'ed.
if mkParents {
maxDirLinks := int(cfg.Import.UnixFSDirectoryMaxLinks.WithDefault(config.DefaultUnixFSDirectoryMaxLinks))
sizeEstimationMode := cfg.Import.HAMTSizeEstimationMode()
err := ensureContainingDirectoryExists(nd.FilesRoot, dst, prefix, maxDirLinks, &sizeEstimationMode)
err := ensureContainingDirectoryExists(nd.FilesRoot, dst,
mfs.WithCidBuilder(prefix),
mfs.WithMaxLinks(maxDirLinks),
mfs.WithSizeEstimationMode(sizeEstimationMode),
)
if err != nil {
return err
}
Expand Down Expand Up @@ -1060,7 +1063,7 @@ See '--to-files' in 'ipfs add --help' for more information.
rawLeaves = cfg.Import.UnixFSRawLeaves.WithDefault(config.DefaultUnixFSRawLeaves)
}

prefix, err := getPrefixNew(req, &cfg.Import)
prefix, err := getPrefix(req, &cfg.Import)
if err != nil {
return err
}
Expand All @@ -1073,7 +1076,11 @@ See '--to-files' in 'ipfs add --help' for more information.
if mkParents {
maxDirLinks := int(cfg.Import.UnixFSDirectoryMaxLinks.WithDefault(config.DefaultUnixFSDirectoryMaxLinks))
sizeEstimationMode := cfg.Import.HAMTSizeEstimationMode()
err := ensureContainingDirectoryExists(nd.FilesRoot, path, prefix, maxDirLinks, &sizeEstimationMode)
err := ensureContainingDirectoryExists(nd.FilesRoot, path,
mfs.WithCidBuilder(prefix),
mfs.WithMaxLinks(maxDirLinks),
mfs.WithSizeEstimationMode(sizeEstimationMode),
)
if err != nil {
return err
}
Expand Down Expand Up @@ -1203,13 +1210,11 @@ Examples:
maxDirLinks := int(cfg.Import.UnixFSDirectoryMaxLinks.WithDefault(config.DefaultUnixFSDirectoryMaxLinks))
sizeEstimationMode := cfg.Import.HAMTSizeEstimationMode()

err = mfs.Mkdir(root, dirtomake, mfs.MkdirOpts{
Mkparents: dashp,
Flush: flush,
CidBuilder: prefix,
MaxLinks: maxDirLinks,
SizeEstimationMode: &sizeEstimationMode,
})
err = mfs.Mkdir(root, dirtomake, mfs.MkdirOpts{Mkparents: dashp, Flush: flush},
mfs.WithCidBuilder(prefix),
mfs.WithMaxLinks(maxDirLinks),
mfs.WithSizeEstimationMode(sizeEstimationMode),
)

return err
},
Expand Down Expand Up @@ -1264,10 +1269,15 @@ var filesChcidCmd = &cmds.Command{
Tagline: "Change the CID version or hash function of the root node of a given path.",
ShortDescription: `
Change the CID version or hash function of the root node of a given path.

Note: the MFS root ('/') CID format is controlled by Import.CidVersion and
Import.HashFunction in the config and cannot be changed with this command.
Use 'ipfs config' to modify these values instead. This command only works
on subdirectories of the MFS root.
`,
},
Arguments: []cmds.Argument{
cmds.StringArg("path", false, false, "Path to change. Default: '/'."),
cmds.StringArg("path", true, false, "Path to change (must not be '/')."),
},
Options: []cmds.Option{
cidVersionOption,
Expand All @@ -1279,9 +1289,10 @@ Change the CID version or hash function of the root node of a given path.
return err
}

path := "/"
if len(req.Arguments) > 0 {
path = req.Arguments[0]
path := req.Arguments[0]
if path == "/" {
return fmt.Errorf("cannot change CID format of the MFS root; " +
"use 'ipfs config Import.CidVersion' and 'ipfs config Import.HashFunction' instead")
}

flush, _ := req.Options[filesFlushOptionName].(bool)
Expand Down Expand Up @@ -1446,97 +1457,48 @@ func removePath(filesRoot *mfs.Root, path string, force bool, dashr bool) error
return pdir.Flush()
}

func getPrefixNew(req *cmds.Request, importCfg *config.Import) (cid.Builder, error) {
cidVer, cidVerSet := req.Options[filesCidVersionOptionName].(int)
hashFunStr, hashFunSet := req.Options[filesHashOptionName].(string)

// Fall back to Import config if CLI options not set
if !cidVerSet && importCfg != nil && !importCfg.CidVersion.IsDefault() {
cidVer = int(importCfg.CidVersion.WithDefault(config.DefaultCidVersion))
cidVerSet = true
}
if !hashFunSet && importCfg != nil && !importCfg.HashFunction.IsDefault() {
hashFunStr = importCfg.HashFunction.WithDefault(config.DefaultHashFunction)
hashFunSet = true
}

if !cidVerSet && !hashFunSet {
return nil, nil
}

if hashFunSet && cidVer == 0 {
cidVer = 1
}

prefix, err := dag.PrefixForCidVersion(cidVer)
if err != nil {
return nil, err
}

if hashFunSet {
hashFunCode, ok := mh.Names[strings.ToLower(hashFunStr)]
if !ok {
return nil, fmt.Errorf("unrecognized hash function: %s", strings.ToLower(hashFunStr))
}
prefix.MhType = hashFunCode
prefix.MhLength = -1
}

return &prefix, nil
}

// getPrefix builds a cid.Builder from CLI flags, falling back to importCfg
// when provided. Returns (nil, nil) when neither CLI nor config set a value.
func getPrefix(req *cmds.Request, importCfg *config.Import) (cid.Builder, error) {
cidVer, cidVerSet := req.Options[filesCidVersionOptionName].(int)
hashFunStr, hashFunSet := req.Options[filesHashOptionName].(string)

// Fall back to Import config if CLI options not set
if !cidVerSet && importCfg != nil && !importCfg.CidVersion.IsDefault() {
cidVer = int(importCfg.CidVersion.WithDefault(config.DefaultCidVersion))
cidVerSet = true
}
if !hashFunSet && importCfg != nil && !importCfg.HashFunction.IsDefault() {
hashFunStr = importCfg.HashFunction.WithDefault(config.DefaultHashFunction)
hashFunSet = true
}

if !cidVerSet && !hashFunSet {
return nil, nil
}

if hashFunSet && cidVer == 0 {
cidVer = 1
}

prefix, err := dag.PrefixForCidVersion(cidVer)
if err != nil {
return nil, err
if cidVerSet || hashFunSet {
// CLI flags take precedence: build prefix from them directly.
if hashFunSet && cidVer == 0 {
cidVer = 1
}
prefix, err := dag.PrefixForCidVersion(cidVer)
if err != nil {
return nil, err
}
if hashFunSet {
hashFunCode, ok := mh.Names[strings.ToLower(hashFunStr)]
if !ok {
return nil, fmt.Errorf("unrecognized hash function: %q", hashFunStr)
}
prefix.MhType = hashFunCode
prefix.MhLength = -1
}
return &prefix, nil
}

if hashFunSet {
hashFunCode, ok := mh.Names[strings.ToLower(hashFunStr)]
if !ok {
return nil, fmt.Errorf("unrecognized hash function: %s", strings.ToLower(hashFunStr))
}
prefix.MhType = hashFunCode
prefix.MhLength = -1
// No CLI flags: fall back to Import config.
if importCfg != nil {
return importCfg.UnixFSCidBuilder()
}

return &prefix, nil
return nil, nil
}

func ensureContainingDirectoryExists(r *mfs.Root, path string, builder cid.Builder, maxLinks int, sizeEstimationMode *uio.SizeEstimationMode) error {
func ensureContainingDirectoryExists(r *mfs.Root, path string, opts ...mfs.Option) error {
dirtomake := gopath.Dir(path)

if dirtomake == "/" {
return nil
}

return mfs.Mkdir(r, dirtomake, mfs.MkdirOpts{
Mkparents: true,
CidBuilder: builder,
MaxLinks: maxLinks,
SizeEstimationMode: sizeEstimationMode,
})
return mfs.Mkdir(r, dirtomake, mfs.MkdirOpts{Mkparents: true}, opts...)
}

func getFileHandle(r *mfs.Root, path string, create bool, builder cid.Builder) (*mfs.File, error) {
Expand Down
Loading
Loading