diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 38ca764c..00000000 --- a/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM golang:1.23 AS builder -WORKDIR /app -COPY . ./ -RUN go mod download -RUN go build -o ./ocfl ./cmd/ocfl - -FROM ubuntu:latest -COPY --from=builder /app/ocfl /usr/local/bin/ocfl \ No newline at end of file diff --git a/examples/commit/main.go b/examples/commit/main.go index 12b630e7..f0d6d0b0 100644 --- a/examples/commit/main.go +++ b/examples/commit/main.go @@ -10,7 +10,6 @@ import ( "github.com/srerickson/ocfl-go" "github.com/srerickson/ocfl-go/backend/local" "github.com/srerickson/ocfl-go/digest" - "github.com/srerickson/ocfl-go/ocflv1" ) var ( @@ -26,7 +25,6 @@ var ( func main() { ctx := context.Background() - ocflv1.Enable() flag.StringVar(&srcDir, "obj", "", "directory of object to commit to") flag.StringVar(&srcDir, "src", "", "directory with new version content") flag.StringVar(&msg, "msg", "", "message field for new version") diff --git a/examples/listobjects/main.go b/examples/listobjects/main.go index eec80849..6d7f2479 100644 --- a/examples/listobjects/main.go +++ b/examples/listobjects/main.go @@ -16,13 +16,11 @@ import ( "github.com/srerickson/ocfl-go" "github.com/srerickson/ocfl-go/backend/s3" "github.com/srerickson/ocfl-go/logging" - "github.com/srerickson/ocfl-go/ocflv1" ) var numgos int func main() { - ocflv1.Enable() ctx := context.Background() // logging.SetDefaultLevel(slog.LevelDebug) logger := logging.DefaultLogger() diff --git a/examples/validate/main.go b/examples/validate/main.go index c61165c2..159a8136 100644 --- a/examples/validate/main.go +++ b/examples/validate/main.go @@ -9,14 +9,12 @@ import ( "github.com/charmbracelet/log" "github.com/srerickson/ocfl-go" - "github.com/srerickson/ocfl-go/ocflv1" ) var objPath string func main() { ctx := context.Background() - ocflv1.Enable() // setup ocflv1 flag.Parse() handl := log.New(os.Stderr) handl.SetLevel(log.WarnLevel) diff --git a/inventory.go b/inventory.go index 9e16b895..0a25624a 100644 --- a/inventory.go +++ b/inventory.go @@ -5,9 +5,10 @@ import ( "encoding/json" "errors" "fmt" - "io" "path" "regexp" + "slices" + "sort" "strings" "time" @@ -21,7 +22,7 @@ var ( invSidecarContentsRexp = regexp.MustCompile(`^([a-fA-F0-9]+)\s+inventory\.json[\n]?$`) ) -type ReadInventory interface { +type Inventory interface { FixitySource ContentDirectory() string Digest() string @@ -30,7 +31,6 @@ type ReadInventory interface { ID() string Manifest() DigestMap Spec() Spec - Validate() *Validation Version(int) ObjectVersion FixityAlgorithms() []string } @@ -48,30 +48,52 @@ type User struct { Address string `json:"address,omitempty"` } -func ReadSidecarDigest(ctx context.Context, fsys FS, name string) (digest string, err error) { - file, err := fsys.OpenFile(ctx, name) +// ReadInventory reads the 'inventory.json' file in dir and validates it. It returns +// an error if the inventory cann't be paresed or if it is invalid. +func ReadInventory(ctx context.Context, fsys FS, dir string) (inv Inventory, err error) { + var byts []byte + var imp ocfl + byts, err = ReadAll(ctx, fsys, path.Join(dir, inventoryBase)) if err != nil { return } - defer file.Close() - cont, err := io.ReadAll(file) + imp, err = getInventoryOCFL(byts) if err != nil { return } - matches := invSidecarContentsRexp.FindSubmatch(cont) + return imp.NewInventory(byts) +} + +// ReadSidecarDigest reads the digest from an inventory.json sidecar file +func ReadSidecarDigest(ctx context.Context, fsys FS, name string) (string, error) { + byts, err := ReadAll(ctx, fsys, name) + if err != nil { + return "", err + } + matches := invSidecarContentsRexp.FindSubmatch(byts) if len(matches) != 2 { - err = fmt.Errorf("reading %s: %w", name, ErrInventorySidecarContents) - return + err := fmt.Errorf("reading %s: %w", name, ErrInventorySidecarContents) + return "", err + } + return string(matches[1]), nil +} + +// ValidateInventoryBytes parses and fully validates the byts as contents of an +// inventory.json file. +func ValidateInventoryBytes(byts []byte) (Inventory, *Validation) { + imp, _ := getInventoryOCFL(byts) + if imp == nil { + // use default OCFL spec + imp = defaultOCFL() } - digest = string(matches[1]) - return + return imp.ValidateInventoryBytes(byts) } // ValidateInventorySidecar reads the inventory sidecar with inv's digest // algorithm (e.g., inventory.json.sha512) in directory dir and return an error // if the sidecar content is not formatted correctly or if the inv's digest // doesn't match the value found in the sidecar. -func ValidateInventorySidecar(ctx context.Context, inv ReadInventory, fsys FS, dir string) error { +func ValidateInventorySidecar(ctx context.Context, inv Inventory, fsys FS, dir string) error { sideCar := path.Join(dir, inventoryBase+"."+inv.DigestAlgorithm().ID()) expSum, err := ReadSidecarDigest(ctx, fsys, sideCar) if err != nil { @@ -88,30 +110,86 @@ func ValidateInventorySidecar(ctx context.Context, inv ReadInventory, fsys FS, d return nil } -// return a ReadInventory for an inventory that may use any version of the ocfl spec. -func readUnknownInventory(ctx context.Context, ocfls *OCLFRegister, fsys FS, dir string) (ReadInventory, error) { - f, err := fsys.OpenFile(ctx, path.Join(dir, inventoryBase)) +func validateInventory(inv Inventory) *Validation { + imp, err := getOCFL(inv.Spec()) if err != nil { - return nil, err - } - defer func() { - if closeErr := f.Close(); closeErr != nil { - err = errors.Join(err, closeErr) - } - }() - raw, err := io.ReadAll(f) - if err != nil { - return nil, err + v := &Validation{} + err := fmt.Errorf("inventory uses unknown or unspecified OCFL version") + v.AddFatal(err) + return v } + return imp.ValidateInventory(inv) +} + +// get the ocfl implementation declared in the inventory bytes +func getInventoryOCFL(byts []byte) (ocfl, error) { invFields := struct { - Type InvType `json:"type"` + Type InventoryType `json:"type"` }{} - if err = json.Unmarshal(raw, &invFields); err != nil { + if err := json.Unmarshal(byts, &invFields); err != nil { return nil, err } - invOCFL, err := ocfls.Get(invFields.Type.Spec) - if err != nil { - return nil, err + return getOCFL(invFields.Type.Spec) +} + +// rawInventory represents the contents of an object's inventory.json file +type rawInventory struct { + ID string `json:"id"` + Type InventoryType `json:"type"` + DigestAlgorithm string `json:"digestAlgorithm"` + Head VNum `json:"head"` + ContentDirectory string `json:"contentDirectory,omitempty"` + Manifest DigestMap `json:"manifest"` + Versions map[VNum]*rawInventoryVersion `json:"versions"` + Fixity map[string]DigestMap `json:"fixity,omitempty"` +} + +func (inv rawInventory) getFixity(dig string) digest.Set { + paths := inv.Manifest[dig] + if len(paths) < 1 { + return nil + } + set := digest.Set{} + for fixAlg, fixMap := range inv.Fixity { + fixMap.EachPath(func(p, fixDigest string) bool { + if slices.Contains(paths, p) { + set[fixAlg] = fixDigest + return false + } + return true + }) } - return invOCFL.NewReadInventory(raw) + return set +} + +func (inv rawInventory) version(v int) *rawInventoryVersion { + if inv.Versions == nil { + return nil + } + if v == 0 { + return inv.Versions[inv.Head] + } + vnum := V(v, inv.Head.Padding()) + return inv.Versions[vnum] +} + +// vnums returns a sorted slice of vnums corresponding to the keys in the +// inventory's 'versions' block. +func (inv rawInventory) vnums() []VNum { + vnums := make([]VNum, len(inv.Versions)) + i := 0 + for v := range inv.Versions { + vnums[i] = v + i++ + } + sort.Sort(VNums(vnums)) + return vnums +} + +// Version represents object version state and metadata +type rawInventoryVersion struct { + Created time.Time `json:"created"` + State DigestMap `json:"state"` + Message string `json:"message,omitempty"` + User *User `json:"user,omitempty"` } diff --git a/ocflv1/inventory_test.go b/inventory_test.go similarity index 67% rename from ocflv1/inventory_test.go rename to inventory_test.go index 97fdfc2a..ae248809 100644 --- a/ocflv1/inventory_test.go +++ b/inventory_test.go @@ -1,4 +1,4 @@ -package ocflv1_test +package ocfl_test import ( "testing" @@ -6,27 +6,17 @@ import ( "github.com/carlmjohnson/be" "github.com/srerickson/ocfl-go" "github.com/srerickson/ocfl-go/internal/testutil" - "github.com/srerickson/ocfl-go/ocflv1" ) -func TestValidateInventory(t *testing.T) { - for desc, test := range testInventories { - t.Run(desc, func(t *testing.T) { - inv, vldr := ocflv1.ValidateInventoryBytes([]byte(test.data), ocfl.Spec1_0) - test.expect(t, inv, vldr) - }) +func TestValidateInventoryBytes(t *testing.T) { + type testCase struct { + inventory string + expect func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) } -} - -type testInventory struct { - data string - expect func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) -} - -var testInventories = map[string]testInventory{ - // Good inventories - `minimal`: { - data: `{ + var testInventories = map[string]testCase{ + // Good inventories + `minimal`: { + inventory: `{ "digestAlgorithm": "sha512", "head": "v1", "id": "http://example.org/minimal_no_content", @@ -41,22 +31,22 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.NilErr(t, v.Err()) - be.Equal(t, "http://example.org/minimal_no_content", inv.ID) - be.Equal(t, "sha512", inv.DigestAlgorithm) - be.Equal(t, "v1", inv.Head.String()) - be.Equal(t, ocfl.Spec1_0.AsInvType(), inv.Type) - version := inv.Versions[inv.Head] - be.Nonzero(t, version.Created) - be.Equal(t, "One version and no content", version.Message) - be.Equal(t, "mailto:Person_A@example.org", version.User.Address) - be.Equal(t, "Person A", version.User.Name) - be.Nonzero(t, inv.Digest()) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.NilErr(t, v.Err()) + be.Equal(t, "http://example.org/minimal_no_content", inv.ID()) + be.Equal(t, "sha512", inv.DigestAlgorithm().ID()) + be.Equal(t, "v1", inv.Head().String()) + be.Equal(t, ocfl.Spec1_0, inv.Spec()) + version := inv.Version(inv.Head().Num()) + be.Nonzero(t, version.Created) + be.Equal(t, "One version and no content", version.Message()) + be.Equal(t, "mailto:Person_A@example.org", version.User().Address) + be.Equal(t, "Person A", version.User().Name) + be.Nonzero(t, inv.Digest()) + }, }, - }, - `complete`: { - data: `{ + `complete`: { + inventory: `{ "contentDirectory": "custom", "digestAlgorithm": "sha512", "fixity": { @@ -96,17 +86,17 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.NilErr(t, v.Err()) - sum := "a8a450d00c6ca7aa90e3e4858864fc195b6b2fe0a75c2d1e078e92eca232ce7be034a129ea9ea9cda2b0efaf11ba8f5ebdbebacb12f7992a4c37cad589e16a4d" - be.Equal(t, "custom", inv.ContentDirectory) - be.Equal(t, "v1/content/file.txt", inv.Fixity[inv.DigestAlgorithm][sum][0]) - be.Equal(t, "v1/content/file.txt", inv.Manifest[sum][0]) - be.Equal(t, "file.txt", inv.Versions[inv.Head].State[sum][0]) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.NilErr(t, v.Err()) + sum := "a8a450d00c6ca7aa90e3e4858864fc195b6b2fe0a75c2d1e078e92eca232ce7be034a129ea9ea9cda2b0efaf11ba8f5ebdbebacb12f7992a4c37cad589e16a4d" + be.Equal(t, "custom", inv.ContentDirectory()) + be.Equal(t, "e8f239a71aabe2231faf696d92c92c20", inv.GetFixity(sum)["md5"]) + be.Equal(t, "v1/content/file.txt", inv.Manifest()[sum][0]) + be.Equal(t, "file.txt", inv.Version(0).State()[sum][0]) + }, }, - }, - `one_version`: { - data: `{ + `one_version`: { + inventory: `{ "digestAlgorithm": "sha512", "head": "v1", "id": "ark:123/abc", @@ -132,13 +122,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.NilErr(t, v.Err()) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.NilErr(t, v.Err()) + }, }, - }, - // Warn inventories - `missing_version_user`: { - data: `{ + // Warn inventories + `missing_version_user`: { + inventory: `{ "digestAlgorithm": "sha512", "head": "v1", "id": "http://example.org/minimal_no_content", @@ -152,14 +142,14 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.NilErr(t, v.Err()) - testutil.ErrorsIncludeOCFLCode(t, "W007", v.WarnErrors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.NilErr(t, v.Err()) + testutil.ErrorsIncludeOCFLCode(t, "W007", v.WarnErrors()...) + }, }, - }, - // Warn inventories - `missing_version_message`: { - data: `{ + // Warn inventories + `missing_version_message`: { + inventory: `{ "digestAlgorithm": "sha512", "head": "v1", "id": "http://example.org/minimal_no_content", @@ -176,14 +166,14 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.NilErr(t, v.Err()) - testutil.ErrorsIncludeOCFLCode(t, "W007", v.WarnErrors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.NilErr(t, v.Err()) + testutil.ErrorsIncludeOCFLCode(t, "W007", v.WarnErrors()...) + }, }, - }, - // Bad inventories - `missing_id`: { - data: `{ + // Bad inventories + `missing_id`: { + inventory: `{ "digestAlgorithm": "sha512", "head": "v1", "manifest": {}, @@ -197,13 +187,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E036", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E036", v.Errors()...) + }, }, - }, - `bad_digestAlgorithm`: { - data: `{ + `bad_digestAlgorithm`: { + inventory: `{ "digestAlgorithm": "sha51", "head": "v1", "id": "http://example.org/minimal_no_content", @@ -218,13 +208,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E025", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E025", v.Errors()...) + }, }, - }, - `missing_digestAlgorithm`: { - data: `{ + `missing_digestAlgorithm`: { + inventory: `{ "head": "v1", "id": "http://example.org/minimal_no_content", "manifest": {}, @@ -238,13 +228,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E036", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E036", v.Errors()...) + }, }, - }, - `null_id`: { - data: `{ + `null_id`: { + inventory: `{ "digestAlgorithm": "sha512", "id": null, "head": "v1", @@ -259,13 +249,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E036", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E036", v.Errors()...) + }, }, - }, - `missing_type`: { - data: `{ + `missing_type`: { + inventory: `{ "digestAlgorithm": "sha512", "id": "ark:123/abc", "head": "v1", @@ -279,13 +269,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E036", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E036", v.Errors()...) + }, }, - }, - `bad_type`: { - data: `{ + `bad_type`: { + inventory: `{ "digestAlgorithm": "sha512", "id": "ark:123/abc", "head": "v1", @@ -300,13 +290,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E038", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E038", v.Errors()...) + }, }, - }, - `bad_contentDirectory`: { - data: `{ + `bad_contentDirectory`: { + inventory: `{ "digestAlgorithm": "sha512", "id": "ark:123/abc", "head": "v1", @@ -322,13 +312,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E017", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E017", v.Errors()...) + }, }, - }, - `missing_head`: { - data: `{ + `missing_head`: { + inventory: `{ "digestAlgorithm": "sha512", "id": "ark:123/abc", "manifest": {}, @@ -342,13 +332,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E040", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E040", v.Errors()...) + }, }, - }, - `bad_head_format`: { - data: `{ + `bad_head_format`: { + inventory: `{ "digestAlgorithm": "sha512", "id": "ark:123/abc", "head": "1", @@ -363,13 +353,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E040", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E040", v.Errors()...) + }, }, - }, - `bad_head_not_last`: { - data: `{ + `bad_head_not_last`: { + inventory: `{ "digestAlgorithm": "sha512", "id": "ark:123/abc", "head": "v1", @@ -390,13 +380,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E040", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E040", v.Errors()...) + }, }, - }, - `missing_manifest`: { - data: `{ + `missing_manifest`: { + inventory: `{ "digestAlgorithm": "sha512", "id": "ark:123/abc", "head": "v1", @@ -410,14 +400,14 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E041", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E041", v.Errors()...) + }, }, - }, - `bad_manifest`: { + `bad_manifest`: { - data: `{ + inventory: `{ "digestAlgorithm": "sha512", "id": "ark:123/abc", "head": "v1", @@ -432,26 +422,26 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E041", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E041", v.Errors()...) + }, }, - }, - `missing_versions`: { - data: `{ + `missing_versions`: { + inventory: `{ "digestAlgorithm": "sha512", "id": "ark:123/abc", "head": "v1", "type": "https://ocfl.io/1.0/spec/#inventory", "manifest": {} }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E043", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E043", v.Errors()...) + }, }, - }, - `bad_versions_empty`: { - data: `{ + `bad_versions_empty`: { + inventory: `{ "digestAlgorithm": "sha512", "id": "ark:123/abc", "head": "v1", @@ -459,13 +449,13 @@ var testInventories = map[string]testInventory{ "manifest": {}, "versions": {} }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E008", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E008", v.Errors()...) + }, }, - }, - `bad_versions_missingv1`: { - data: `{ + `bad_versions_missingv1`: { + inventory: `{ "digestAlgorithm": "sha512", "id": "ark:123/abc", "head": "v2", @@ -480,13 +470,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E010", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E010", v.Errors()...) + }, }, - }, - `bad_versions_padding`: { - data: `{ + `bad_versions_padding`: { + inventory: `{ "digestAlgorithm": "sha512", "id": "ark:123/abc", "head": "v02", @@ -507,13 +497,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E012", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E012", v.Errors()...) + }, }, - }, - `bad_manifest_digestconflict`: { - data: `{ + `bad_manifest_digestconflict`: { + inventory: `{ "digestAlgorithm": "sha512", "head": "v2", "id": "uri:something451", @@ -555,13 +545,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E096", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E096", v.Errors()...) + }, }, - }, - `bad_manifest_basepathconflict`: { - data: `{ + `bad_manifest_basepathconflict`: { + inventory: `{ "digestAlgorithm": "sha512", "head": "v3", "id": "uri:something451", @@ -619,13 +609,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E101", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E101", v.Errors()...) + }, }, - }, - `missing_version_state`: { - data: `{ + `missing_version_state`: { + inventory: `{ "digestAlgorithm": "sha512", "head": "v1", "id": "http://example.org/minimal_no_content", @@ -639,16 +629,16 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E048", v.Errors()...) - }, - }, - `null_version_block`: { - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E048", v.Errors()...) + }, }, - data: `{ + `null_version_block`: { + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + }, + inventory: `{ "digestAlgorithm": "sha512", "head": "v1", "id": "http://example.org/minimal_no_content", @@ -658,9 +648,9 @@ var testInventories = map[string]testInventory{ "v1": null } }`, - }, - `missing_version_created`: { - data: `{ + }, + `missing_version_created`: { + inventory: `{ "digestAlgorithm": "sha512", "head": "v1", "id": "http://example.org/minimal_no_content", @@ -674,13 +664,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E048", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E048", v.Errors()...) + }, }, - }, - `invalid_version_created`: { - data: `{ + `invalid_version_created`: { + inventory: `{ "digestAlgorithm": "sha512", "head": "v1", "id": "http://example.org/minimal_no_content", @@ -695,13 +685,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E049", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E049", v.Errors()...) + }, }, - }, - `missing_version_user_name`: { - data: `{ + `missing_version_user_name`: { + inventory: `{ "digestAlgorithm": "sha512", "head": "v1", "id": "http://example.org/minimal_no_content", @@ -716,13 +706,13 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E054", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E054", v.Errors()...) + }, }, - }, - `empty_version_user_name`: { - data: `{ + `empty_version_user_name`: { + inventory: `{ "digestAlgorithm": "sha512", "head": "v1", "id": "http://example.org/minimal_no_content", @@ -737,9 +727,16 @@ var testInventories = map[string]testInventory{ } } }`, - expect: func(t *testing.T, inv *ocflv1.Inventory, v *ocfl.Validation) { - be.True(t, v.Err() != nil) - testutil.ErrorsIncludeOCFLCode(t, "E054", v.Errors()...) + expect: func(t *testing.T, inv ocfl.Inventory, v *ocfl.Validation) { + be.True(t, v.Err() != nil) + testutil.ErrorsIncludeOCFLCode(t, "E054", v.Errors()...) + }, }, - }, + } + for desc, test := range testInventories { + t.Run(desc, func(t *testing.T) { + inv, v := ocfl.ValidateInventoryBytes([]byte(test.inventory)) + test.expect(t, inv, v) + }) + } } diff --git a/object.go b/object.go index da4d794d..feeff813 100644 --- a/object.go +++ b/object.go @@ -1,12 +1,15 @@ package ocfl import ( + "cmp" "context" "errors" "fmt" "io" "io/fs" "path" + "slices" + "strings" "time" "log/slog" @@ -14,18 +17,19 @@ import ( "github.com/srerickson/ocfl-go/digest" ) -// Object represents and OCFL Object, typically contained in a Root. An Object -// may not exist. +// Object represents and OCFL Object, typically contained in a Root. type Object struct { - reader ReadObject - // global settings - globals Config - // the OCFL implementation used to open the object - ocfl OCFL + // object's storage backend. Must implement WriteFS to commit. + fs FS + // path in FS for object root directory + path string + // object's root inventory + inventory Inventory // object id used to open the object from the root expectID string // the object must exist: don't create a new object. mustExist bool + //TODO pointer to object's storage root. } // NewObject returns an *Object for managing the OCFL object at path in fsys. @@ -34,11 +38,9 @@ func NewObject(ctx context.Context, fsys FS, dir string, opts ...ObjectOption) ( if !fs.ValidPath(dir) { return nil, fmt.Errorf("invalid object path: %q: %w", dir, fs.ErrInvalid) } - obj := &Object{} - for _, optFn := range opts { - optFn(obj) - } - inv, err := readUnknownInventory(ctx, obj.globals.OCFLs(), fsys, dir) + obj := newObject(fsys, dir, opts...) + // read root inventory: we don't know what OCFL spec it uses. + inv, err := ReadInventory(ctx, fsys, dir) if err != nil { var pthError *fs.PathError if !errors.As(err, &pthError) { @@ -53,28 +55,27 @@ func NewObject(ctx context.Context, fsys FS, dir string, opts ...ObjectOption) ( } } if inv != nil { - obj.ocfl = obj.globals.OCFLs().MustGet(inv.Spec()) - obj.reader = obj.ocfl.NewReadObject(fsys, dir, inv) // check that inventory has expected object ID // if the expected object ID is known. if obj.expectID != "" && inv.ID() != obj.expectID { err := fmt.Errorf("object has unexpected ID: %q; expected: %q", inv.ID(), obj.expectID) return nil, err } + obj.setInventory(inv) return obj, nil } - // if inventory.json doesn't exist, try to open as uninitialized object + // inventory.json doesn't exist: open as uninitialized object. The object + // root directory must not exist or be an empty directory. Note, the object's + // ocfl implementation is not set! entries, err := fsys.ReadDir(ctx, dir) if err != nil { if !errors.Is(err, fs.ErrNotExist) { - return nil, fmt.Errorf("reading object root contents: %w", err) + return nil, fmt.Errorf("reading object root directory: %w", err) } } rootState := ParseObjectDir(entries) switch { case rootState.Empty(): - // open as new/uninitialized object w/o an OCFL spec. - obj.reader = &uninitializedObject{fs: fsys, path: dir} return obj, nil case rootState.HasNamaste(): return nil, fmt.Errorf("incomplete OCFL object: %s: %w", inventoryBase, fs.ErrNotExist) @@ -83,49 +84,63 @@ func NewObject(ctx context.Context, fsys FS, dir string, opts ...ObjectOption) ( } } +// create a new *Object with required feilds and apply options +func newObject(fsys FS, dir string, opts ...ObjectOption) *Object { + obj := &Object{fs: fsys, path: dir} + for _, optFn := range opts { + optFn(obj) + } + return obj +} + // Commit creates a new object version based on values in commit. func (obj *Object) Commit(ctx context.Context, commit *Commit) error { - if _, isWriteFS := obj.reader.FS().(WriteFS); !isWriteFS { + if _, isWriteFS := obj.FS().(WriteFS); !isWriteFS { return errors.New("object's backing file system doesn't support write operations") } - // the OCFL implementation to use to create the new object version - var useOCFL OCFL + // the OCFL implementation for the new object version + var useOCFL ocfl switch { case commit.Spec.Empty(): switch { - case obj.Exists(): - useOCFL = obj.ocfl + case !obj.Exists(): + // new object and no ocfl version specified in commit + useOCFL = defaultOCFL() default: - useOCFL = defaultOCFLs.latest + // use existing object's ocfl version + var err error + useOCFL, err = getOCFL(obj.inventory.Spec()) + if err != nil { + err = fmt.Errorf("object's root inventory has errors: %w", err) + return &CommitError{Err: err} + } } commit.Spec = useOCFL.Spec() default: var err error - useOCFL, err = obj.globals.GetSpec(commit.Spec) + useOCFL, err = getOCFL(commit.Spec) if err != nil { - return err + return &CommitError{Err: err} } } // set commit's object id if we have an expected id and commit ID isn't set if obj.expectID != "" && commit.ID != obj.expectID { if commit.ID != "" { - return fmt.Errorf("commit includes unexpected object ID: %s; expected: %q", commit.ID, obj.expectID) + err := fmt.Errorf("commit includes unexpected object ID: %s; expected: %q", commit.ID, obj.expectID) + return &CommitError{Err: err} } commit.ID = obj.expectID } - newSpecObj, err := useOCFL.Commit(ctx, obj.reader, commit) - if err != nil { + if err := useOCFL.Commit(ctx, obj, commit); err != nil { return err } - obj.reader = newSpecObj - if obj.ocfl != useOCFL { - obj.ocfl = useOCFL - } return nil } // Exists returns true if the object has an existing version. -func (obj *Object) Exists() bool { return ObjectExists(obj.reader) } +func (obj *Object) Exists() bool { + return obj.inventory != nil +} // ExtensionNames returns the names of directories in the object's // extensions directory. The ObjectRoot's State is initialized if it is @@ -133,7 +148,7 @@ func (obj *Object) Exists() bool { return ObjectExists(obj.reader) } // is returned. If object root does not include an extensions directory both // return values are nil. func (obj Object) ExtensionNames(ctx context.Context) ([]string, error) { - entries, err := obj.FS().ReadDir(ctx, path.Join(obj.Path(), ExtensionsDir)) + entries, err := obj.FS().ReadDir(ctx, path.Join(obj.path, extensionsDir)) if err != nil { return nil, err } @@ -151,18 +166,18 @@ func (obj Object) ExtensionNames(ctx context.Context) ([]string, error) { // FS returns the FS where object is stored. func (obj *Object) FS() FS { - return obj.reader.FS() + return obj.fs } -// Inventory returns the object's ReadInventory if it exists. If the object +// Inventory returns the object's Inventory if it exists. If the object // doesn't exist, it returns nil. -func (obj *Object) Inventory() ReadInventory { - return obj.reader.Inventory() +func (obj *Object) Inventory() Inventory { + return obj.inventory } // Path returns the Object's path relative to its FS. func (obj *Object) Path() string { - return obj.reader.Path() + return obj.path } // OpenVersion returns an ObjectVersionFS for the version with the given @@ -184,7 +199,7 @@ func (obj *Object) OpenVersion(ctx context.Context, i int) (*ObjectVersionFS, er // FIXME; better error return nil, errors.New("version not found") } - ioFS := obj.reader.VersionFS(ctx, i) + ioFS := obj.VersionFS(ctx, i) if ioFS == nil { // FIXME; better error return nil, errors.New("version not found") @@ -198,9 +213,13 @@ func (obj *Object) OpenVersion(ctx context.Context, i int) (*ObjectVersionFS, er return vfs, nil } +func (obj *Object) setInventory(inv Inventory) { + obj.inventory = inv +} + // ValidateObject fully validates the OCFL Object at dir in fsys func ValidateObject(ctx context.Context, fsys FS, dir string, opts ...ObjectValidationOption) *ObjectValidation { - v := NewObjectValidation(opts...) + v := newObjectValidation(fsys, dir, opts...) if !fs.ValidPath(dir) { err := fmt.Errorf("invalid object path: %q: %w", dir, fs.ErrInvalid) v.AddFatal(err) @@ -212,37 +231,32 @@ func ValidateObject(ctx context.Context, fsys FS, dir string, opts ...ObjectVali return v } state := ParseObjectDir(entries) - impl, err := v.globals.GetSpec(state.Spec) + impl, err := getOCFL(state.Spec) if err != nil { + // unknown OCFL version v.AddFatal(err) return v } - obj, err := impl.ValidateObjectRoot(ctx, fsys, dir, state, v) - if err != nil { + if err := impl.ValidateObjectRoot(ctx, v, state); err != nil { return v } // validate versions using previous specs - versionOCFL, err := v.globals.GetSpec(Spec1_0) - if err != nil { - err = fmt.Errorf("unexpected error during validation: %w", err) - v.AddFatal(err) - return v - } - var prevInv ReadInventory + versionOCFL := lowestOCFL() + var prevInv Inventory for _, vnum := range state.VersionDirs.Head().Lineage() { versionDir := path.Join(dir, vnum.String()) - versionInv, err := readUnknownInventory(ctx, v.globals.OCFLs(), fsys, versionDir) + versionInv, err := ReadInventory(ctx, fsys, versionDir) if err != nil && !errors.Is(err, fs.ErrNotExist) { v.AddFatal(fmt.Errorf("reading %s/inventory.json: %w", vnum, err)) continue } if versionInv != nil { - versionOCFL = v.globals.OCFLs().MustGet(versionInv.Spec()) + versionOCFL = mustGetOCFL(versionInv.Spec()) } - versionOCFL.ValidateObjectVersion(ctx, obj, vnum, versionInv, prevInv, v) + versionOCFL.ValidateObjectVersion(ctx, v, vnum, versionInv, prevInv) prevInv = versionInv } - impl.ValidateObjectContent(ctx, obj, v) + impl.ValidateObjectContent(ctx, v) return v } @@ -283,7 +297,7 @@ func (c CommitError) Unwrap() error { type ObjectVersionFS struct { fsys fs.FS ver ObjectVersion - inv ReadInventory + inv Inventory num int } @@ -322,26 +336,6 @@ func (vfs *ObjectVersionFS) Stage() *Stage { } } -func ObjectExists(obj ReadObject) bool { - if _, isEmpty := obj.(*uninitializedObject); isEmpty { - return false - } - return true -} - -// uninitializedObject is an ObjectReader for an object that doesn't exist yet. -type uninitializedObject struct { - fs FS - path string -} - -var _ (ReadObject) = (*uninitializedObject)(nil) - -func (o *uninitializedObject) FS() FS { return o.fs } -func (o *uninitializedObject) Inventory() ReadInventory { return nil } -func (o *uninitializedObject) Path() string { return o.path } -func (o *uninitializedObject) VersionFS(ctx context.Context, v int) fs.FS { return nil } - // ObjectOptions are used to configure the behavior of NewObject() type ObjectOption func(*Object) @@ -357,3 +351,196 @@ func objectExpectedID(id string) ObjectOption { o.expectID = id } } + +func (o *Object) VersionFS(ctx context.Context, i int) fs.FS { + ver := o.inventory.Version(i) + if ver == nil { + return nil + } + // FIXME: This is a hack to make versionFS replicates the filemode of + // the undering FS. Open a random content file to get the file mode used by + // the underlying FS. + regfileType := fs.FileMode(0) + for _, paths := range o.inventory.Manifest() { + if len(paths) < 1 { + break + } + f, err := o.fs.OpenFile(ctx, path.Join(o.path, paths[0])) + if err != nil { + return nil + } + defer f.Close() + info, err := f.Stat() + if err != nil { + return nil + } + regfileType = info.Mode().Type() + break + } + return &versionFS{ + ctx: ctx, + obj: o, + paths: ver.State().PathMap(), + created: ver.Created(), + regMode: regfileType, + } +} + +type versionFS struct { + ctx context.Context + obj *Object + paths PathMap + created time.Time + regMode fs.FileMode +} + +func (vfs *versionFS) Open(logical string) (fs.File, error) { + if !fs.ValidPath(logical) { + return nil, &fs.PathError{ + Err: fs.ErrInvalid, + Op: "open", + Path: logical, + } + } + if logical == "." { + return vfs.openDir(".") + } + digest := vfs.paths[logical] + if digest == "" { + // name doesn't exist in state. + // try opening as a directory + return vfs.openDir(logical) + } + + realNames := vfs.obj.inventory.Manifest()[digest] + if len(realNames) < 1 { + return nil, &fs.PathError{ + Err: fs.ErrNotExist, + Op: "open", + Path: logical, + } + } + realName := realNames[0] + if !fs.ValidPath(realName) { + return nil, &fs.PathError{ + Err: fs.ErrInvalid, + Op: "open", + Path: logical, + } + } + f, err := vfs.obj.fs.OpenFile(vfs.ctx, path.Join(vfs.obj.path, realName)) + if err != nil { + err = fmt.Errorf("opening file with logical path %q: %w", logical, err) + return nil, err + } + return f, nil +} + +func (vfs *versionFS) openDir(dir string) (fs.File, error) { + prefix := dir + "/" + if prefix == "./" { + prefix = "" + } + children := map[string]*vfsDirEntry{} + for p := range vfs.paths { + if !strings.HasPrefix(p, prefix) { + continue + } + name, _, isdir := strings.Cut(strings.TrimPrefix(p, prefix), "/") + if _, exists := children[name]; exists { + continue + } + entry := &vfsDirEntry{ + name: name, + mode: vfs.regMode, + created: vfs.created, + open: func() (fs.File, error) { return vfs.Open(path.Join(dir, name)) }, + } + if isdir { + entry.mode = entry.mode | fs.ModeDir | fs.ModeIrregular + } + children[name] = entry + } + if len(children) < 1 { + return nil, &fs.PathError{ + Op: "open", + Path: dir, + Err: fs.ErrNotExist, + } + } + + dirFile := &vfsDirFile{ + name: dir, + entries: make([]fs.DirEntry, 0, len(children)), + } + for _, entry := range children { + dirFile.entries = append(dirFile.entries, entry) + } + slices.SortFunc(dirFile.entries, func(a, b fs.DirEntry) int { + return cmp.Compare(a.Name(), b.Name()) + }) + return dirFile, nil +} + +type vfsDirEntry struct { + name string + created time.Time + mode fs.FileMode + open func() (fs.File, error) +} + +var _ fs.DirEntry = (*vfsDirEntry)(nil) + +func (info *vfsDirEntry) Name() string { return info.name } +func (info *vfsDirEntry) IsDir() bool { return info.mode.IsDir() } +func (info *vfsDirEntry) Type() fs.FileMode { return info.mode.Type() } + +func (info *vfsDirEntry) Info() (fs.FileInfo, error) { + f, err := info.open() + if err != nil { + return nil, err + } + stat, err := f.Stat() + return stat, errors.Join(err, f.Close()) +} + +func (info *vfsDirEntry) Size() int64 { return 0 } +func (info *vfsDirEntry) Mode() fs.FileMode { return info.mode | fs.ModeIrregular } +func (info *vfsDirEntry) ModTime() time.Time { return info.created } +func (info *vfsDirEntry) Sys() any { return nil } + +type vfsDirFile struct { + name string + created time.Time + entries []fs.DirEntry + offset int +} + +var _ fs.ReadDirFile = (*vfsDirFile)(nil) + +func (dir *vfsDirFile) ReadDir(n int) ([]fs.DirEntry, error) { + if n <= 0 { + entries := dir.entries[dir.offset:] + dir.offset = len(dir.entries) + return entries, nil + } + if remain := len(dir.entries) - dir.offset; remain < n { + n = remain + } + if n <= 0 { + return nil, io.EOF + } + entries := dir.entries[dir.offset : dir.offset+n] + dir.offset += n + return entries, nil +} + +func (dir *vfsDirFile) Close() error { return nil } +func (dir *vfsDirFile) IsDir() bool { return true } +func (dir *vfsDirFile) Mode() fs.FileMode { return fs.ModeDir | fs.ModeIrregular } +func (dir *vfsDirFile) ModTime() time.Time { return dir.created } +func (dir *vfsDirFile) Name() string { return dir.name } +func (dir *vfsDirFile) Read(_ []byte) (int, error) { return 0, nil } +func (dir *vfsDirFile) Size() int64 { return 0 } +func (dir *vfsDirFile) Stat() (fs.FileInfo, error) { return dir, nil } +func (dir *vfsDirFile) Sys() any { return nil } diff --git a/object_test.go b/object_test.go index a270b81a..edf5b2fa 100644 --- a/object_test.go +++ b/object_test.go @@ -18,12 +18,10 @@ import ( "github.com/srerickson/ocfl-go" "github.com/srerickson/ocfl-go/backend/local" "github.com/srerickson/ocfl-go/digest" - "github.com/srerickson/ocfl-go/ocflv1" "golang.org/x/exp/maps" ) func TestObject(t *testing.T) { - ocflv1.Enable() t.Run("Example", testObjectExample) t.Run("New", testNewObject) t.Run("Commit", testObjectCommit) @@ -277,8 +275,8 @@ func testUpdateFixtures(t *testing.T) { // new stage from the existing version and add a new file currentVersion, err := obj.OpenVersion(ctx, 0) - defer be.NilErr(t, currentVersion.Close()) be.NilErr(t, err) + defer be.NilErr(t, currentVersion.Close()) newContent, err := ocfl.StageBytes(map[string][]byte{ "a-new-file": []byte("new stuff"), }, currentVersion.DigestAlgorithm()) diff --git a/objectstate.go b/objectstate.go index c8ce982c..3469e386 100644 --- a/objectstate.go +++ b/objectstate.go @@ -22,11 +22,9 @@ const ( //HasLogs indicates that an object root includes a directory named "logs" HasLogs - inventoryBase = "inventory.json" - sidecarPrefix = inventoryBase + "." - objectDeclPrefix = "0=" + NamasteTypeObject - maxObjectStateInvalid = 8 + objectDeclPrefix = "0=" + NamasteTypeObject + sidecarPrefix = inventoryBase + "." ) // ObjectState provides details of an OCFL object root based on the names of @@ -57,9 +55,9 @@ func ParseObjectDir(entries []fs.DirEntry) *ObjectState { case e.IsDir(): var v VNum switch { - case name == LogsDir: + case name == logsDir: state.Flags |= HasLogs - case name == ExtensionsDir: + case name == extensionsDir: state.Flags |= HasExtensions case ParseVNum(name, &v) == nil: state.VersionDirs = append(state.VersionDirs, v) diff --git a/ocfl.go b/ocfl.go index 1a3eb045..5297adc9 100644 --- a/ocfl.go +++ b/ocfl.go @@ -8,168 +8,80 @@ import ( "errors" "fmt" "io/fs" - "runtime" - "sync" - "sync/atomic" ) const ( // package version - Version = "0.6.0" - LogsDir = "logs" - ExtensionsDir = "extensions" + Version = "0.6.0" + + Spec1_0 = Spec("1.0") + Spec1_1 = Spec("1.1") + + logsDir = "logs" + contentDir = "content" + extensionsDir = "extensions" + inventoryBase = "inventory.json" ) var ( - ErrOCFLNotImplemented = errors.New("no implementation for the given OCFL specification version") + ErrOCFLNotImplemented = errors.New("unimplemented or missing version of the OCFL specification") ErrObjectNamasteExists = fmt.Errorf("found existing OCFL object declaration: %w", fs.ErrExist) ErrObjectNamasteNotExist = fmt.Errorf("the OCFL object declaration does not exist: %w", ErrNamasteNotExist) - - commitConcurrency atomic.Int32 // FIXME: get rid of this - - // map of OCFL implementations - defaultOCFLs OCLFRegister + ErrObjRootStructure = errors.New("object includes invalid files or directories") ) -func GetOCFL(spec Spec) (OCFL, error) { return defaultOCFLs.Get(spec) } -func MustGetOCFL(spec Spec) OCFL { return defaultOCFLs.MustGet(spec) } -func RegisterOCLF(imp OCFL) bool { return defaultOCFLs.Set(imp) } -func UnsetOCFL(spec Spec) bool { return defaultOCFLs.Unset(spec) } -func LatestOCFL() (OCFL, error) { return defaultOCFLs.Latest() } -func Implementations() []Spec { return defaultOCFLs.Specs() } - -// OCFL is an interface implemented by types that implement a specific -// version of the OCFL specification. -type OCFL interface { +// ocfl is an interface implemented by types that implement a specific +// version of the ocfl specification. +type ocfl interface { + // Spec returns the implemented version of the OCFL specification Spec() Spec - NewReadInventory(raw []byte) (ReadInventory, error) - NewReadObject(fsys FS, path string, inv ReadInventory) ReadObject - Commit(ctx context.Context, obj ReadObject, commit *Commit) (ReadObject, error) - ValidateObjectRoot(ctx context.Context, fs FS, dir string, state *ObjectState, vldr *ObjectValidation) (ReadObject, error) - ValidateObjectVersion(ctx context.Context, obj ReadObject, vnum VNum, versionInv ReadInventory, prevInv ReadInventory, vldr *ObjectValidation) error - ValidateObjectContent(ctx context.Context, obj ReadObject, vldr *ObjectValidation) error -} - -type Config struct { - ocfls *OCLFRegister - //algs digest.Registry -} - -func (c Config) OCFLs() *OCLFRegister { - if c.ocfls == nil { - return &defaultOCFLs - } - return c.ocfls + // NewInventory constructs a new Inventory from bytes. If the inventory is + // invalid, an error is returned. The returned error may not include all + // validation error codes, as ValidateInventoryBytes would. + NewInventory(raw []byte) (Inventory, error) + // Commit creates a new object version. The returned error must be a + // *CommitError. + Commit(ctx context.Context, obj *Object, commit *Commit) error + // ValidateInventory validates an existing Inventory value. + ValidateInventory(Inventory) *Validation + // ValidateInventoryBytes fully validates bytes as a json-encoded inventory. + // It returns the Inventory if the validation result does not included fatal + // errors. + ValidateInventoryBytes([]byte) (Inventory, *Validation) + // Validate all contents of an object root: NAMASTE, inventory, sidecar, etc. + ValidateObjectRoot(ctx context.Context, v *ObjectValidation, state *ObjectState) error + // Validate all contents of an object version directory and add contents to the object validation + ValidateObjectVersion(ctx context.Context, v *ObjectValidation, vnum VNum, versionInv, prevInv Inventory) error + // Validate contents added to the object validation + ValidateObjectContent(ctx context.Context, v *ObjectValidation) error } -func (c Config) GetSpec(spec Spec) (OCFL, error) { - if c.ocfls == nil { - return defaultOCFLs.Get(spec) +// getOCFL is returns the implemenation for a given version of the OCFL spec. +func getOCFL(spec Spec) (ocfl, error) { + switch spec { + case Spec1_0, Spec1_1: + return &ocflV1{v1Spec: spec}, nil + case Spec(""): + return nil, ErrOCFLNotImplemented } - return c.ocfls.Get(spec) + return nil, fmt.Errorf("%w: v%s", ErrOCFLNotImplemented, spec) } -type OCLFRegister struct { - ocfls map[Spec]OCFL - ocflsMx sync.RWMutex - latest OCFL -} +// returns the earliest OCFL implementation (OCFL v1.0) +func lowestOCFL() ocfl { return &ocflV1{Spec1_0} } -func (reg *OCLFRegister) Get(spec Spec) (OCFL, error) { - reg.ocflsMx.RLock() - defer reg.ocflsMx.RUnlock() - if imp := reg.ocfls[spec]; imp != nil { - return imp, nil - } - return nil, ErrOCFLNotImplemented -} +// returns the latest OCFL implementation (OCFL v1.1) +func latestOCFL() ocfl { return &ocflV1{Spec1_1} } -func (reg *OCLFRegister) MustGet(spec Spec) OCFL { - imp, err := reg.Get(spec) +// mustGetOCFL is like getOCFL except it panics if the implemenation is not +// found. +func mustGetOCFL(spec Spec) ocfl { + impl, err := getOCFL(spec) if err != nil { panic(err) } - return imp -} - -func (ocfl *OCLFRegister) Set(imp OCFL) bool { - newSpec := imp.Spec() - if err := newSpec.Valid(); err != nil { - return false - } - ocfl.ocflsMx.Lock() - defer ocfl.ocflsMx.Unlock() - if _, exists := ocfl.ocfls[newSpec]; exists { - return false - } - if ocfl.ocfls == nil { - ocfl.ocfls = map[Spec]OCFL{} - } - ocfl.ocfls[newSpec] = imp - if ocfl.latest == nil || newSpec.Cmp(ocfl.latest.Spec()) > 0 { - ocfl.latest = imp - } - return true -} - -// UnsetOCFL removes the previously set implementation for spec, if -// present. It returns true if the implementation was removed and false if no -// implementation was found for the spec. -func (reg *OCLFRegister) Unset(spec Spec) bool { - reg.ocflsMx.Lock() - defer reg.ocflsMx.Unlock() - if _, exists := reg.ocfls[spec]; !exists { - return false - } - delete(reg.ocfls, spec) - return true -} - -func (reg *OCLFRegister) Latest() (OCFL, error) { - reg.ocflsMx.RLock() - defer reg.ocflsMx.RUnlock() - if reg.latest == nil { - return nil, ErrOCFLNotImplemented - } - return reg.latest, nil + return impl } -func (reg *OCLFRegister) Specs() []Spec { - reg.ocflsMx.RLock() - defer reg.ocflsMx.RUnlock() - specs := make([]Spec, 0, len(reg.ocfls)) - for spec := range reg.ocfls { - specs = append(specs, spec) - } - return specs -} - -type ReadObject interface { - // Inventory returns the object's inventory or nil if - // the object hasn't been created yet. - Inventory() ReadInventory - // FS for accessing object contents - FS() FS - // Path returns the object's path relative to its FS() - Path() string - // VersionFS returns an io/fs.FS for accessing the logical contents of the - // object version state with the index v. - VersionFS(ctx context.Context, v int) fs.FS -} - -// XferConcurrency is a global configuration for the maximum number of files -// transferred concurrently during a commit operation. It defaults to -// runtime.NumCPU(). -func XferConcurrency() int { - i := commitConcurrency.Load() - if i < 1 { - return runtime.NumCPU() - } - return int(i) -} - -// SetXferConcurrency sets the maximum number of files transferred concurrently -// during a commit operation. -func SetXferConcurrency(i int) { - commitConcurrency.Store(int32(i)) -} +// defaultOCFL returns the default OCFL implementation (v1.1). +func defaultOCFL() ocfl { return latestOCFL() } diff --git a/ocflv1.go b/ocflv1.go new file mode 100644 index 00000000..66987a9d --- /dev/null +++ b/ocflv1.go @@ -0,0 +1,1225 @@ +package ocfl + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "net/url" + "path" + "reflect" + "slices" + "sort" + "strings" + "time" + + "github.com/srerickson/ocfl-go/digest" + "github.com/srerickson/ocfl-go/extension" + "github.com/srerickson/ocfl-go/logging" + "github.com/srerickson/ocfl-go/validation/code" + "golang.org/x/sync/errgroup" +) + +// ocflv1 is animplementation of ocfl v1.x +type ocflV1 struct { + v1Spec Spec // "1.0" or "1.1" +} + +var _ ocfl = (*ocflV1)(nil) + +func (imp ocflV1) Spec() Spec { + return Spec(imp.v1Spec) +} + +func (imp ocflV1) NewInventory(byts []byte) (Inventory, error) { + inv := &inventoryV1{} + dec := json.NewDecoder(bytes.NewReader(byts)) + dec.DisallowUnknownFields() + if err := dec.Decode(&inv.raw); err != nil { + return nil, err + } + if err := inv.setJsonDigest(byts); err != nil { + return nil, err + } + if err := validateInventory(inv).Err(); err != nil { + return nil, err + } + return inv, nil +} + +func (imp ocflV1) ValidateInventory(inv Inventory) *Validation { + v := &Validation{} + invV1, ok := inv.(*inventoryV1) + if !ok { + err := errors.New("inventory does not have expected type") + v.AddFatal(err) + } + if invV1.raw.Type.Empty() { + err := errors.New("missing required field: 'type'") + v.AddFatal(err) + } + if invV1.raw.Type.Spec != imp.v1Spec { + err := fmt.Errorf("inventory declares v%s, not v%s", invV1.raw.Type.Spec, imp.v1Spec) + v.AddFatal(err) + } + specStr := string(imp.v1Spec) + if invV1.raw.ID == "" { + err := errors.New("missing required field: 'id'") + v.AddFatal(verr(err, code.E036(specStr))) + } + if invV1.raw.Head.IsZero() { + err := errors.New("missing required field: 'head'") + v.AddFatal(verr(err, code.E036(specStr))) + } + if invV1.raw.Manifest == nil { + err := errors.New("missing required field 'manifest'") + v.AddFatal(verr(err, code.E041(specStr))) + } + if invV1.raw.Versions == nil { + err := errors.New("missing required field: 'versions'") + v.AddFatal(verr(err, code.E041(specStr))) + } + if u, err := url.ParseRequestURI(invV1.raw.ID); err != nil || u.Scheme == "" { + err := fmt.Errorf(`object ID is not a URI: %q`, invV1.raw.ID) + v.AddWarn(verr(err, code.W005(specStr))) + } + switch invV1.raw.DigestAlgorithm { + case digest.SHA512.ID(): + break + case digest.SHA256.ID(): + err := fmt.Errorf(`'digestAlgorithm' is %q`, digest.SHA256.ID()) + v.AddWarn(verr(err, code.W004(specStr))) + default: + err := fmt.Errorf(`'digestAlgorithm' is not %q or %q`, digest.SHA512.ID(), digest.SHA256.ID()) + v.AddFatal(verr(err, code.E025(specStr))) + } + if err := invV1.raw.Head.Valid(); err != nil { + err = fmt.Errorf("head is invalid: %w", err) + v.AddFatal(verr(err, code.E011(specStr))) + } + if strings.Contains(invV1.raw.ContentDirectory, "/") { + err := errors.New("contentDirectory contains '/'") + v.AddFatal(verr(err, code.E017(specStr))) + } + if invV1.raw.ContentDirectory == "." || invV1.raw.ContentDirectory == ".." { + err := errors.New("contentDirectory is '.' or '..'") + v.AddFatal(verr(err, code.E017(specStr))) + } + if invV1.raw.Manifest != nil { + err := invV1.raw.Manifest.Valid() + if err != nil { + var dcErr *MapDigestConflictErr + var pcErr *MapPathConflictErr + var piErr *MapPathInvalidErr + if errors.As(err, &dcErr) { + err = verr(err, code.E096(specStr)) + } else if errors.As(err, &pcErr) { + err = verr(err, code.E101(specStr)) + } else if errors.As(err, &piErr) { + err = verr(err, code.E099(specStr)) + } + v.AddFatal(err) + } + // check that each manifest entry is used in at least one state + for _, digest := range invV1.raw.Manifest.Digests() { + var found bool + for _, version := range invV1.raw.Versions { + if version == nil { + continue + } + if len(version.State[digest]) > 0 { + found = true + break + } + } + if !found { + err := fmt.Errorf("digest in manifest not used in version state: %s", digest) + v.AddFatal(verr(err, code.E107(specStr))) + } + } + } + // version names + var versionNums VNums = invV1.raw.vnums() + if err := versionNums.Valid(); err != nil { + if errors.Is(err, ErrVerEmpty) { + err = verr(err, code.E008(specStr)) + } else if errors.Is(err, ErrVNumMissing) { + err = verr(err, code.E010(specStr)) + } else if errors.Is(err, ErrVNumPadding) { + err = verr(err, code.E012(specStr)) + } + v.AddFatal(err) + } + if versionNums.Head() != invV1.raw.Head { + err := fmt.Errorf(`version head not most recent version: %s`, invV1.raw.Head) + v.AddFatal(verr(err, code.E040(specStr))) + } + // version state + for vname, ver := range invV1.raw.Versions { + if ver == nil { + err := fmt.Errorf(`missing required version block for %q`, vname) + v.AddFatal(verr(err, code.E048(specStr))) + continue + } + if ver.Created.IsZero() { + err := fmt.Errorf(`version %s missing required field: 'created'`, vname) + v.AddFatal(verr(err, code.E048(specStr))) + } + if ver.Message == "" { + err := fmt.Errorf("version %s missing recommended field: 'message'", vname) + v.AddWarn(verr(err, code.W007(specStr))) + } + if ver.User == nil { + err := fmt.Errorf("version %s missing recommended field: 'user'", vname) + v.AddWarn(verr(err, code.W007(specStr))) + } + if ver.User != nil { + if ver.User.Name == "" { + err := fmt.Errorf("version %s user missing required field: 'name'", vname) + v.AddFatal(verr(err, code.E054(specStr))) + } + if ver.User.Address == "" { + err := fmt.Errorf("version %s user missing recommended field: 'address'", vname) + v.AddWarn(verr(err, code.W008(specStr))) + } + if u, err := url.ParseRequestURI(ver.User.Address); err != nil || u.Scheme == "" { + err := fmt.Errorf("version %s user address is not a URI", vname) + v.AddWarn(verr(err, code.W009(specStr))) + } + } + if ver.State == nil { + err := fmt.Errorf(`version %s missing required field: 'state'`, vname) + v.AddFatal(verr(err, code.E048(specStr))) + continue + } + err := ver.State.Valid() + if err != nil { + var dcErr *MapDigestConflictErr + var pcErr *MapPathConflictErr + var piErr *MapPathInvalidErr + if errors.As(err, &dcErr) { + err = verr(err, code.E050(specStr)) + } else if errors.As(err, &pcErr) { + err = verr(err, code.E095(specStr)) + } else if errors.As(err, &piErr) { + err = verr(err, code.E052(specStr)) + } + v.AddFatal(err) + } + // check that each state digest appears in manifest + for _, digest := range ver.State.Digests() { + if len(invV1.raw.Manifest[digest]) == 0 { + err := fmt.Errorf("digest in %s state not in manifest: %s", vname, digest) + v.AddFatal(verr(err, code.E050(specStr))) + } + } + } + //fixity + for _, fixity := range invV1.raw.Fixity { + err := fixity.Valid() + if err != nil { + var dcErr *MapDigestConflictErr + var piErr *MapPathInvalidErr + var pcErr *MapPathConflictErr + if errors.As(err, &dcErr) { + err = verr(err, code.E097(specStr)) + } else if errors.As(err, &piErr) { + err = verr(err, code.E099(specStr)) + } else if errors.As(err, &pcErr) { + err = verr(err, code.E101(specStr)) + } + v.AddFatal(err) + } + } + return v +} + +func (imp ocflV1) ValidateInventoryBytes(raw []byte) (Inventory, *Validation) { + specStr := string(imp.v1Spec) + v := &Validation{} + invMap := map[string]any{} + if err := json.Unmarshal(raw, &invMap); err != nil { + err = fmt.Errorf("decoding inventory json: %w", err) + v.AddFatal(verr(err, code.E033(specStr))) + return nil, v + } + const requiredErrMsg = "required field is missing or has unexpected json value" + const optionalErrMsg = "optional field has unexpected json value" + id, exists, typeOK := jsonMapGet[string](invMap, `id`) + if !exists || !typeOK { + err := errors.New(requiredErrMsg + `: 'id'`) + v.AddFatal(verr(err, code.E036(specStr))) + } + typeStr, exists, typeOK := jsonMapGet[string](invMap, `type`) + if !exists || !typeOK { + err := errors.New(requiredErrMsg + `: 'type'`) + v.AddFatal(verr(err, code.E036(specStr))) + } + if typeStr != "" && typeStr != Spec(imp.v1Spec).InventoryType().String() { + err := fmt.Errorf("invalid inventory type value: %q", typeStr) + v.AddFatal(verr(err, code.E038(specStr))) + } + digestAlg, exists, typeOK := jsonMapGet[string](invMap, `digestAlgorithm`) + if !exists || !typeOK { + err := errors.New(requiredErrMsg + `: 'digestAlgorithm'`) + v.AddFatal(verr(err, code.E036(specStr))) + } + if digestAlg != "" && digestAlg != digest.SHA512.ID() && digestAlg != digest.SHA256.ID() { + err := fmt.Errorf("invalid digest algorithm: %q", digestAlg) + v.AddFatal(verr(err, code.E025(specStr))) + } + head, exists, typeOK := jsonMapGet[string](invMap, `head`) + if !exists || !typeOK { + err := errors.New(requiredErrMsg + `: 'head'`) + v.AddFatal(verr(err, code.E036(specStr))) + } + manifestVals, exists, typeOK := jsonMapGet[map[string]any](invMap, `manifest`) + if !exists || !typeOK { + err := errors.New(requiredErrMsg + `: 'manifest'`) + v.AddFatal(verr(err, code.E041(specStr))) + } + versionsVals, exists, typeOK := jsonMapGet[map[string]any](invMap, `versions`) + if !exists || !typeOK { + err := errors.New(requiredErrMsg + `: 'versions'`) + v.AddFatal(verr(err, code.E043(specStr))) + } + // FIXME: not sure which error code. E108? + contentDirectory, exists, typeOK := jsonMapGet[string](invMap, `contentDirectory`) + if exists && !typeOK { + // contentDirectory is optional + err := errors.New(optionalErrMsg + `: 'contentDirectory'`) + v.AddFatal(err) + } + // fixity is optional + fixityVals, exists, typeOK := jsonMapGet[map[string]any](invMap, `fixity`) + if exists && !typeOK { + err := errors.New(optionalErrMsg + `: 'fixity'`) + v.AddFatal(verr(err, code.E111(specStr))) + } + // any remaining values in invVals are invalid + for extra := range invMap { + err := fmt.Errorf("inventory json has unexpected field: %q", extra) + v.AddFatal(err) + } + inv := &inventoryV1{ + raw: rawInventory{ + ID: id, + ContentDirectory: contentDirectory, + DigestAlgorithm: digestAlg, + Fixity: map[string]DigestMap{}, + Versions: make(map[VNum]*rawInventoryVersion), + }, + } + if err := inv.raw.Type.UnmarshalText([]byte(typeStr)); err != nil { + v.AddFatal(verr(err, code.E038(specStr))) + } + if err := inv.raw.Head.UnmarshalText([]byte(head)); err != nil { + v.AddFatal(verr(err, code.E040(specStr))) + } + var err error + if inv.raw.Manifest, err = convertJSONDigestMap(manifestVals); err != nil { + err = fmt.Errorf("invalid manifest: %w", err) + v.AddFatal(verr(err, code.E092(specStr))) + } + // build versions + for vnumStr, val := range versionsVals { + var ( + vnum VNum + versionVals map[string]any + userVals map[string]any + stateVals map[string]any + createdStr string + created time.Time + message string + state DigestMap + user *User + ) + if err := ParseVNum(vnumStr, &vnum); err != nil { + err = fmt.Errorf("invalid key %q in versions block: %w", vnumStr, err) + v.AddFatal(verr(err, code.E046(specStr))) + continue + } + versionErrPrefix := "version '" + vnumStr + "'" + versionVals, typeOK = val.(map[string]any) + if !typeOK { + err := errors.New(versionErrPrefix + ": value is not a json object") + v.AddFatal(verr(err, code.E045(specStr))) + } + createdStr, exists, typeOK = jsonMapGet[string](versionVals, `created`) + if !exists || !typeOK { + err := fmt.Errorf("%s: %s: %s", versionErrPrefix, requiredErrMsg, `'created'`) + v.AddFatal(verr(err, code.E048(specStr))) + } + if createdStr != "" { + if err := created.UnmarshalText([]byte(createdStr)); err != nil { + err = fmt.Errorf("%s: created: %w", versionErrPrefix, err) + v.AddFatal(verr(err, code.E049(specStr))) + } + } + stateVals, exists, typeOK = jsonMapGet[map[string]any](versionVals, `state`) + if !exists || !typeOK { + err := fmt.Errorf("%s: %s: %q", versionErrPrefix, requiredErrMsg, `state`) + v.AddFatal(verr(err, code.E048(specStr))) + } + // message is optional + message, exists, typeOK = jsonMapGet[string](versionVals, `message`) + if exists && !typeOK { + err := fmt.Errorf("%s: %s: %q", versionErrPrefix, optionalErrMsg, `message`) + v.AddFatal(verr(err, code.E094(specStr))) + } + // user is optional + userVals, exists, typeOK := jsonMapGet[map[string]any](versionVals, `user`) + switch { + case exists && !typeOK: + err := fmt.Errorf("%s: %s: %q", versionErrPrefix, optionalErrMsg, `user`) + v.AddFatal(verr(err, code.E054(specStr))) + case exists: + var userName, userAddress string + userName, exists, typeOK = jsonMapGet[string](userVals, `name`) + if !exists || !typeOK { + err := fmt.Errorf("%s: user: %s: %q", versionErrPrefix, requiredErrMsg, `name`) + v.AddFatal(verr(err, code.E054(specStr))) + } + // address is optional + userAddress, exists, typeOK = jsonMapGet[string](userVals, `address`) + if exists && !typeOK { + err := fmt.Errorf("%s: user: %s: %q", versionErrPrefix, optionalErrMsg, `address`) + v.AddFatal(verr(err, code.E054(specStr))) + } + user = &User{Name: userName, Address: userAddress} + } + // any additional fields in versionVals are invalid. + for extra := range versionVals { + err := fmt.Errorf("%s: invalid key: %q", versionErrPrefix, extra) + v.AddFatal(err) + } + state, err := convertJSONDigestMap(stateVals) + if err != nil { + err = fmt.Errorf("%s: state: %w", versionErrPrefix, err) + v.AddFatal(err) + } + inv.raw.Versions[vnum] = &rawInventoryVersion{ + Created: created, + State: state, + Message: message, + User: user, + } + } + // build fixity + for algStr, val := range fixityVals { + var digestVals map[string]any + digestVals, typeOK = val.(map[string]any) + fixityErrPrefix := "fixity '" + algStr + "'" + if !typeOK { + err := fmt.Errorf("%s: value is not a json object", fixityErrPrefix) + v.AddFatal(verr(err, code.E057(specStr))) + continue + } + digests, err := convertJSONDigestMap(digestVals) + if err != nil { + err = fmt.Errorf("%s: %w", fixityErrPrefix, err) + v.AddFatal(verr(err, code.E057(specStr))) + continue + } + inv.raw.Fixity[algStr] = digests + } + if err := inv.setJsonDigest(raw); err != nil { + v.AddFatal(err) + } + + v.Add(validateInventory(inv)) + if v.Err() != nil { + return nil, v + } + return inv, v +} + +func (imp ocflV1) Commit(ctx context.Context, obj *Object, commit *Commit) error { + writeFS, ok := obj.FS().(WriteFS) + if !ok { + err := errors.New("object's backing file system doesn't support write operations") + return &CommitError{Err: err} + } + newInv, err := imp.newInventoryV1(commit, obj.Inventory()) + if err != nil { + err := fmt.Errorf("building new inventory: %w", err) + return &CommitError{Err: err} + } + logger := commit.Logger + if logger == nil { + logger = logging.DisabledLogger() + } + logger = logger.With("path", obj.Path(), "id", newInv.ID, "head", newInv.Head, "ocfl_spec", newInv.Spec(), "alg", newInv.DigestAlgorithm) + // xfers is a subeset of the manifest with the new content to add + xfers, err := newContentMap(&newInv.raw) + if err != nil { + return &CommitError{Err: err} + } + // check that the stage includes all the new content + for digest := range xfers { + if !commit.Stage.HasContent(digest) { + // FIXME short digest + err := fmt.Errorf("no content for digest: %s", digest) + return &CommitError{Err: err} + } + } + // file changes start here + // 1. create or update NAMASTE object declaration + var oldSpec Spec + if obj.Inventory() != nil { + oldSpec = obj.Inventory().Spec() + } + newSpec := newInv.Spec() + switch { + case obj.Exists() && oldSpec != newSpec: + oldDecl := Namaste{Type: NamasteTypeObject, Version: oldSpec} + logger.DebugContext(ctx, "deleting previous OCFL object declaration", "name", oldDecl) + if err = writeFS.Remove(ctx, path.Join(obj.Path(), oldDecl.Name())); err != nil { + return &CommitError{Err: err, Dirty: true} + } + fallthrough + case !obj.Exists(): + newDecl := Namaste{Type: NamasteTypeObject, Version: newSpec} + logger.DebugContext(ctx, "writing new OCFL object declaration", "name", newDecl) + if err = WriteDeclaration(ctx, writeFS, obj.Path(), newDecl); err != nil { + return &CommitError{Err: err, Dirty: true} + } + } + // 2. tranfser files from stage to object + if len(xfers) > 0 { + copyOpts := ©ContentOpts{ + Source: commit.Stage, + DestFS: writeFS, + DestRoot: obj.Path(), + Manifest: xfers, + } + logger.DebugContext(ctx, "copying new object files", "count", len(xfers)) + if err := copyContent(ctx, copyOpts); err != nil { + err = fmt.Errorf("transferring new object contents: %w", err) + return &CommitError{Err: err, Dirty: true} + } + } + logger.DebugContext(ctx, "writing inventories for new object version") + // 3. write inventory to both object root and version directory + newVersionDir := path.Join(obj.Path(), newInv.Head().String()) + if err := writeInventory(ctx, writeFS, newInv, obj.Path(), newVersionDir); err != nil { + err = fmt.Errorf("writing new inventories or inventory sidecars: %w", err) + return &CommitError{Err: err, Dirty: true} + } + obj.setInventory(newInv) + return nil +} + +func (imp ocflV1) ValidateObjectRoot(ctx context.Context, vldr *ObjectValidation, state *ObjectState) error { + // validate namaste + specStr := string(imp.v1Spec) + decl := Namaste{Type: NamasteTypeObject, Version: imp.v1Spec} + name := path.Join(vldr.path(), decl.Name()) + err := ValidateNamaste(ctx, vldr.fs(), name) + if err != nil { + switch { + case errors.Is(err, fs.ErrNotExist): + err = fmt.Errorf("%s: %w", name, ErrObjectNamasteNotExist) + vldr.AddFatal(verr(err, code.E001(specStr))) + default: + vldr.AddFatal(verr(err, code.E007(specStr))) + } + return err + } + // validate root inventory + invBytes, err := ReadAll(ctx, vldr.fs(), path.Join(vldr.path(), inventoryBase)) + if err != nil { + switch { + case errors.Is(err, fs.ErrNotExist): + vldr.AddFatal(err, verr(err, code.E063(specStr))) + default: + vldr.AddFatal(err) + } + return err + } + inv, invValidation := imp.ValidateInventoryBytes(invBytes) + vldr.PrefixAdd("root inventory.json", invValidation) + if err := invValidation.Err(); err != nil { + return err + } + if err := ValidateInventorySidecar(ctx, inv, vldr.fs(), vldr.path()); err != nil { + switch { + case errors.Is(err, ErrInventorySidecarContents): + vldr.AddFatal(verr(err, code.E061(specStr))) + default: + vldr.AddFatal(verr(err, code.E060(specStr))) + } + } + vldr.PrefixAdd("extensions directory", validateExtensionsDir(ctx, imp.v1Spec, vldr.fs(), vldr.path())) + if err := vldr.addInventory(inv, true); err != nil { + vldr.AddFatal(err) + } + vldr.PrefixAdd("root contents", validateRootState(imp.v1Spec, state)) + if err := vldr.Err(); err != nil { + return err + } + return nil +} + +func (imp ocflV1) ValidateObjectVersion(ctx context.Context, vldr *ObjectValidation, vnum VNum, verInv Inventory, prevInv Inventory) error { + fsys := vldr.fs() + vnumStr := vnum.String() + fullVerDir := path.Join(vldr.path(), vnumStr) // version directory path relative to FS + specStr := string(imp.v1Spec) + rootInv := vldr.obj.Inventory() // rootInv is assumed to be valid + vDirEntries, err := fsys.ReadDir(ctx, fullVerDir) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + // can't read version directory for some reason, but not because it + // doesn't exist. + vldr.AddFatal(err) + return err + } + vdirState := parseVersionDirState(vDirEntries) + for _, f := range vdirState.extraFiles { + err := fmt.Errorf(`unexpected file in %s: %s`, vnum, f) + vldr.AddFatal(verr(err, code.E015(specStr))) + } + if !vdirState.hasInventory { + err := fmt.Errorf("missing %s/inventory.json", vnumStr) + vldr.AddWarn(verr(err, code.W010(specStr))) + } + if verInv != nil { + verInvValidation := imp.ValidateInventory(verInv) + vldr.PrefixAdd(vnumStr+"/inventory.json", verInvValidation) + if err := ValidateInventorySidecar(ctx, verInv, fsys, fullVerDir); err != nil { + err := fmt.Errorf("%s/inventory.json: %w", vnumStr, err) + switch { + case errors.Is(err, ErrInventorySidecarContents): + vldr.AddFatal(verr(err, code.E061(specStr))) + default: + vldr.AddFatal(verr(err, code.E060(specStr))) + } + } + if prevInv != nil && verInv.Spec().Cmp(prevInv.Spec()) < 0 { + err := fmt.Errorf("%s/inventory.json uses an older OCFL specification than than the previous version", vnum) + vldr.AddFatal(verr(err, code.E103(specStr))) + } + if verInv.Head() != vnum { + err := fmt.Errorf("%s/inventory.json: 'head' does not matchs its directory", vnum) + vldr.AddFatal(verr(err, code.E040(specStr))) + } + if verInv.Digest() != rootInv.Digest() { + imp.compareVersionInventory(vldr.obj, vnum, verInv, vldr) + if verInv.Digest() != rootInv.Digest() { + if err := vldr.addInventory(verInv, false); err != nil { + err = fmt.Errorf("%s/inventory.json digests are inconsistent with other inventories: %w", vnum, err) + vldr.AddFatal(verr(err, code.E066(specStr))) + } + } + } + } + cdName := rootInv.ContentDirectory() + for _, d := range vdirState.dirs { + // the only directory in the version directory SHOULD be the content directory + if d != cdName { + err := fmt.Errorf(`extra directory in %s: %s`, vnum, d) + vldr.AddWarn(verr(err, code.W002(specStr))) + continue + } + // add version content files to validation state + var added int + fullVerContDir := path.Join(fullVerDir, cdName) + contentFiles, filesErrFn := WalkFiles(ctx, fsys, fullVerContDir) + for contentFile := range contentFiles { + // convert from path relative to version content directory to path + // relative to the object + vldr.addExistingContent(path.Join(vnumStr, cdName, contentFile.Path)) + added++ + } + if err := filesErrFn(); err != nil { + vldr.AddFatal(err) + return err + } + if added == 0 { + // content directory exists but it's empty + err := fmt.Errorf("content directory (%s) is empty directory", fullVerContDir) + vldr.AddFatal(verr(err, code.E016(specStr))) + } + } + return nil +} + +func (imp ocflV1) ValidateObjectContent(ctx context.Context, v *ObjectValidation) error { + specStr := string(imp.v1Spec) + newVld := &Validation{} + for name := range v.missingContent() { + err := fmt.Errorf("missing content: %s", name) + newVld.AddFatal(verr(err, code.E092(specStr))) + } + for name := range v.unexpectedContent() { + err := fmt.Errorf("unexpected content: %s", name) + newVld.AddFatal(verr(err, code.E023(specStr))) + } + if !v.SkipDigests() { + alg := v.obj.Inventory().DigestAlgorithm() + digests := v.existingContentDigests(v.fs(), v.path(), alg) + numgos := v.DigestConcurrency() + registry := v.ValidationAlgorithms() + for err := range digests.ValidateBatch(ctx, registry, numgos) { + var digestErr *digest.DigestError + switch { + case errors.As(err, &digestErr): + newVld.AddFatal(verr(digestErr, code.E093(specStr))) + default: + newVld.AddFatal(err) + } + } + } + v.Add(newVld) + return newVld.Err() +} + +func (imp ocflV1) compareVersionInventory(obj *Object, dirNum VNum, verInv Inventory, vldr *ObjectValidation) { + rootInv := obj.Inventory() + specStr := string(imp.v1Spec) + if verInv.Head() == rootInv.Head() && verInv.Digest() != rootInv.Digest() { + err := fmt.Errorf("%s/inventor.json is not the same as the root inventory: digests don't match", dirNum) + vldr.AddFatal(verr(err, code.E064(specStr))) + } + if verInv.ID() != rootInv.ID() { + err := fmt.Errorf("%s/inventory.json: 'id' doesn't match value in root inventory", dirNum) + vldr.AddFatal(verr(err, code.E037(specStr))) + } + if verInv.ContentDirectory() != rootInv.ContentDirectory() { + err := fmt.Errorf("%s/inventory.json: 'contentDirectory' doesn't match value in root inventory", dirNum) + vldr.AddFatal(verr(err, code.E019(specStr))) + } + // check that all version blocks in the version inventory + // match version blocks in the root inventory + for _, v := range verInv.Head().Lineage() { + thisVersion := verInv.Version(v.Num()) + rootVersion := rootInv.Version(v.Num()) + if rootVersion == nil { + err := fmt.Errorf("root inventory.json has missing version: %s", v) + vldr.AddFatal(verr(err, code.E046(specStr))) + continue + } + thisVerState := logicalState{ + state: thisVersion.State(), + manifest: verInv.Manifest(), + } + rootVerState := logicalState{ + state: rootVersion.State(), + manifest: rootInv.Manifest(), + } + if !thisVerState.Eq(rootVerState) { + err := fmt.Errorf("%s/inventory.json has different logical state in its %s version block than the root inventory.json", dirNum, v) + vldr.AddFatal(verr(err, code.E066(specStr))) + } + if thisVersion.Message() != rootVersion.Message() { + err := fmt.Errorf("%s/inventory.json has different 'message' in its %s version block than the root inventory.json", dirNum, v) + vldr.AddWarn(verr(err, code.W011(specStr))) + } + if !reflect.DeepEqual(thisVersion.User(), rootVersion.User()) { + err := fmt.Errorf("%s/inventory.json has different 'user' in its %s version block than the root inventory.json", dirNum, v) + vldr.AddWarn(verr(err, code.W011(specStr))) + } + if thisVersion.Created() != rootVersion.Created() { + err := fmt.Errorf("%s/inventory.json has different 'created' in its %s version block than the root inventory.json", dirNum, v) + vldr.AddWarn(verr(err, code.W011(specStr))) + } + } +} + +// build a new inventoryV1 from a commit and an optional previous inventory +func (imp ocflV1) newInventoryV1(commit *Commit, prev Inventory) (*inventoryV1, error) { + if commit.Stage == nil { + return nil, errors.New("commit is missing new version state") + } + if commit.Stage.DigestAlgorithm == nil { + return nil, errors.New("commit has no digest algorithm") + + } + if commit.Stage.State == nil { + commit.Stage.State = DigestMap{} + } + inv := &inventoryV1{ + raw: rawInventory{ + ID: commit.ID, + DigestAlgorithm: commit.Stage.DigestAlgorithm.ID(), + ContentDirectory: contentDir, + }, + } + switch { + case prev != nil: + prevInv, ok := prev.(*inventoryV1) + if !ok { + err := errors.New("inventory is not an OCFLv1 inventory") + return nil, err + } + if inv.raw.DigestAlgorithm != prev.DigestAlgorithm().ID() { + return nil, fmt.Errorf("commit must use same digest algorithm as existing inventory (%s)", prev.DigestAlgorithm()) + } + inv.raw.ID = prev.ID() + inv.raw.ContentDirectory = prevInv.raw.ContentDirectory + inv.raw.Type = prevInv.raw.Type + var err error + inv.raw.Head, err = prev.Head().Next() + if err != nil { + return nil, fmt.Errorf("existing inventory's version scheme doesn't support additional versions: %w", err) + } + if !commit.Spec.Empty() { + // new inventory spec must be >= prev + if commit.Spec.Cmp(prev.Spec()) < 0 { + err = fmt.Errorf("new inventory's OCFL spec can't be lower than the existing inventory's (%s)", prev.Spec()) + return nil, err + } + inv.raw.Type = commit.Spec.InventoryType() + } + if !commit.AllowUnchanged { + lastV := prev.Version(0) + if lastV.State().Eq(commit.Stage.State) { + err := errors.New("version state unchanged") + return nil, err + } + } + + // copy and normalize all digests in the inventory. If we don't do this + // non-normalized digests in previous version states might cause + // problems since the updated manifest/fixity will be normalized. + inv.raw.Manifest, err = prev.Manifest().Normalize() + if err != nil { + return nil, fmt.Errorf("in existing inventory manifest: %w", err) + } + versions := prev.Head().Lineage() + inv.raw.Versions = make(map[VNum]*rawInventoryVersion, len(versions)) + for _, vnum := range versions { + prevVer := prev.Version(vnum.Num()) + newVer := &rawInventoryVersion{ + Created: prevVer.Created(), + Message: prevVer.Message(), + } + newVer.State, err = prevVer.State().Normalize() + if err != nil { + return nil, fmt.Errorf("in existing inventory %s state: %w", vnum, err) + } + if prevVer.User() != nil { + newVer.User = &User{ + Name: prevVer.User().Name, + Address: prevVer.User().Address, + } + } + inv.raw.Versions[vnum] = newVer + } + // transfer fixity + inv.raw.Fixity = make(map[string]DigestMap, len(prevInv.raw.Fixity)) + for alg, m := range prevInv.raw.Fixity { + inv.raw.Fixity[alg], err = m.Normalize() + if err != nil { + return nil, fmt.Errorf("in existing inventory %s fixity: %w", alg, err) + } + } + default: + // FIXME: how whould padding be set for new inventories? + inv.raw.Head = V(1, 0) + inv.raw.Manifest = DigestMap{} + inv.raw.Fixity = map[string]DigestMap{} + inv.raw.Versions = map[VNum]*rawInventoryVersion{} + inv.raw.Type = commit.Spec.InventoryType() + } + + // add new version + newVersion := &rawInventoryVersion{ + State: commit.Stage.State, + Created: commit.Created, + Message: commit.Message, + User: &commit.User, + } + if newVersion.Created.IsZero() { + newVersion.Created = time.Now() + } + newVersion.Created = newVersion.Created.Truncate(time.Second) + inv.raw.Versions[inv.raw.Head] = newVersion + + // build new manifest and fixity entries + newContentFunc := func(paths []string) []string { + // apply user-specified path transform first + if commit.ContentPathFunc != nil { + paths = commit.ContentPathFunc(paths) + } + contDir := inv.raw.ContentDirectory + if contDir == "" { + contDir = contentDir + } + for i, p := range paths { + paths[i] = path.Join(inv.raw.Head.String(), contDir, p) + } + return paths + } + for digest, logicPaths := range newVersion.State { + if len(inv.raw.Manifest[digest]) > 0 { + // version content already exists in the manifest + continue + } + inv.raw.Manifest[digest] = newContentFunc(slices.Clone(logicPaths)) + } + if commit.Stage.FixitySource != nil { + for digest, contentPaths := range inv.raw.Manifest { + fixSet := commit.Stage.FixitySource.GetFixity(digest) + if len(fixSet) < 1 { + continue + } + for fixAlg, fixDigest := range fixSet { + if inv.raw.Fixity[fixAlg] == nil { + inv.raw.Fixity[fixAlg] = DigestMap{} + } + for _, cp := range contentPaths { + fixPaths := inv.raw.Fixity[fixAlg][fixDigest] + if !slices.Contains(fixPaths, cp) { + inv.raw.Fixity[fixAlg][fixDigest] = append(fixPaths, cp) + } + } + } + } + } + // check that resulting inventory is valid + if err := validateInventory(inv).Err(); err != nil { + return nil, fmt.Errorf("generated inventory is not valid: %w", err) + } + return inv, nil +} + +type inventoryV1 struct { + raw rawInventory + jsonDigest string +} + +var _ Inventory = (*inventoryV1)(nil) + +func (inv *inventoryV1) ContentDirectory() string { + if c := inv.raw.ContentDirectory; c != "" { + return c + } + return "content" +} + +func (inv *inventoryV1) Digest() string { return inv.jsonDigest } + +func (inv *inventoryV1) DigestAlgorithm() digest.Algorithm { + // DigestAlgorithm should be sha512 or sha256 + switch inv.raw.DigestAlgorithm { + case digest.SHA256.ID(): + return digest.SHA256 + case digest.SHA512.ID(): + return digest.SHA512 + default: + return nil + } +} + +func (inv *inventoryV1) FixityAlgorithms() []string { + if len(inv.raw.Fixity) < 1 { + return nil + } + algs := make([]string, 0, len(inv.raw.Fixity)) + for alg := range inv.raw.Fixity { + algs = append(algs, alg) + } + return algs +} + +func (inv *inventoryV1) GetFixity(digest string) digest.Set { + return inv.raw.getFixity(digest) +} + +func (inv *inventoryV1) Head() VNum { + return inv.raw.Head +} + +func (inv *inventoryV1) ID() string { + return inv.raw.ID +} + +func (inv *inventoryV1) Manifest() DigestMap { + return inv.raw.Manifest +} + +func (inv *inventoryV1) Spec() Spec { + return inv.raw.Type.Spec +} + +func (inv *inventoryV1) Version(i int) ObjectVersion { + v := inv.raw.version(i) + if v == nil { + return nil + } + return &inventoryVersion{raw: v} +} + +func (inv *inventoryV1) setJsonDigest(raw []byte) error { + digester, err := digest.DefaultRegistry().NewDigester(inv.raw.DigestAlgorithm) + if err != nil { + return err + } + if _, err := io.Copy(digester, bytes.NewReader(raw)); err != nil { + return fmt.Errorf("digesting inventory: %w", err) + } + inv.jsonDigest = digester.String() + return nil +} + +type inventoryVersion struct { + raw *rawInventoryVersion +} + +func (v *inventoryVersion) State() DigestMap { return v.raw.State } +func (v *inventoryVersion) Message() string { return v.raw.Message } +func (v *inventoryVersion) Created() time.Time { return v.raw.Created } +func (v *inventoryVersion) User() *User { return v.raw.User } + +func jsonMapGet[T any](m map[string]any, key string) (val T, exists bool, typeOK bool) { + var anyVal any + anyVal, exists = m[key] + val, typeOK = anyVal.(T) + delete(m, key) + return +} + +func convertJSONDigestMap(jsonMap map[string]any) (DigestMap, error) { + m := DigestMap{} + msg := "invalid json type: expected array of strings" + for key, mapVal := range jsonMap { + slice, isSlice := mapVal.([]any) + if !isSlice { + return nil, errors.New(msg) + } + m[key] = make([]string, len(slice)) + for i := range slice { + strVal, isStr := slice[i].(string) + if !isStr { + return nil, errors.New(msg) + } + m[key][i] = strVal + } + } + return m, nil +} + +type copyContentOpts struct { + Source ContentSource + DestFS WriteFS + DestRoot string + Manifest DigestMap + Concurrency int +} + +// transfer dst/src names in files from srcFS to dstFS +func copyContent(ctx context.Context, c *copyContentOpts) error { + if c.Source == nil { + return errors.New("missing countent source") + } + conc := c.Concurrency + if conc < 1 { + conc = 1 + } + grp, ctx := errgroup.WithContext(ctx) + grp.SetLimit(conc) + for dig, dstNames := range c.Manifest { + srcFS, srcPath := c.Source.GetContent(dig) + if srcFS == nil { + return fmt.Errorf("content source doesn't provide %q", dig) + } + for _, dstName := range dstNames { + srcPath := srcPath + dstPath := path.Join(c.DestRoot, dstName) + grp.Go(func() error { + return Copy(ctx, c.DestFS, dstPath, srcFS, srcPath) + }) + + } + } + return grp.Wait() +} + +// newContentMap returns a DigestMap that is a subset of the inventory +// manifest for the digests and paths of new content +func newContentMap(inv *rawInventory) (DigestMap, error) { + pm := PathMap{} + var err error + inv.Manifest.EachPath(func(pth, dig string) bool { + // ignore manifest entries from previous versions + if !strings.HasPrefix(pth, inv.Head.String()+"/") { + return true + } + if _, exists := pm[pth]; exists { + err = fmt.Errorf("path duplicate in manifest: %q", pth) + return false + } + pm[pth] = dig + return true + }) + if err != nil { + return nil, err + } + return pm.DigestMapValid() +} + +// writeInventory marshals the value pointed to by inv, writing the json to dir/inventory.json in +// fsys. The digest is calculated using alg and the inventory sidecar is also written to +// dir/inventory.alg +func writeInventory(ctx context.Context, fsys WriteFS, inv *inventoryV1, dirs ...string) error { + if err := ctx.Err(); err != nil { + return err + } + byts, err := json.Marshal(inv.raw) + if err != nil { + return fmt.Errorf("encoding inventory: %w", err) + } + if err := inv.setJsonDigest(byts); err != nil { + return fmt.Errorf("generating inventory.json checksum: %w", err) + } + // write inventory.json and sidecar + for _, dir := range dirs { + invFile := path.Join(dir, inventoryBase) + sideFile := invFile + "." + inv.raw.DigestAlgorithm + sideContent := inv.jsonDigest + " " + inventoryBase + "\n" + _, err = fsys.Write(ctx, invFile, bytes.NewReader(byts)) + if err != nil { + return fmt.Errorf("write inventory failed: %w", err) + } + _, err = fsys.Write(ctx, sideFile, strings.NewReader(sideContent)) + if err != nil { + return fmt.Errorf("write inventory sidecar failed: %w", err) + } + } + return nil +} + +type versionDirState struct { + hasInventory bool + sidecarAlg string + extraFiles []string + dirs []string +} + +func parseVersionDirState(entries []fs.DirEntry) versionDirState { + var info versionDirState + for _, e := range entries { + if e.Type().IsDir() { + info.dirs = append(info.dirs, e.Name()) + continue + } + if e.Type().IsRegular() || e.Type() == fs.ModeIrregular { + if e.Name() == inventoryBase { + info.hasInventory = true + continue + } + if strings.HasPrefix(e.Name(), inventoryBase+".") && info.sidecarAlg == "" { + info.sidecarAlg = strings.TrimPrefix(e.Name(), inventoryBase+".") + continue + } + } + // unexpected files + info.extraFiles = append(info.extraFiles, e.Name()) + } + return info +} + +type logicalState struct { + manifest DigestMap + state DigestMap +} + +func (a logicalState) Eq(b logicalState) bool { + if a.state == nil || b.state == nil || a.manifest == nil || b.manifest == nil { + return false + } + if !a.state.EachPath(func(name string, dig string) bool { + otherDig := b.state.GetDigest(name) + if otherDig == "" { + return false + } + contentPaths := a.manifest[dig] + otherPaths := b.manifest[otherDig] + if len(contentPaths) != len(otherPaths) { + return false + } + sort.Strings(contentPaths) + sort.Strings(otherPaths) + for i, p := range contentPaths { + if otherPaths[i] != p { + return false + } + } + return true + }) { + return false + } + // make sure all logical paths in other state are also in state + return b.state.EachPath(func(otherName string, _ string) bool { + return a.state.GetDigest(otherName) != "" + }) +} + +func validateRootState(spec Spec, state *ObjectState) *Validation { + specStr := string(spec) + v := &Validation{} + for _, name := range state.Invalid { + err := fmt.Errorf(`%w: %s`, ErrObjRootStructure, name) + v.AddFatal(verr(err, code.E001(specStr))) + } + if !state.HasInventory() { + err := fmt.Errorf(`root inventory.json: %w`, fs.ErrNotExist) + v.AddFatal(verr(err, code.E063(specStr))) + } + if !state.HasSidecar() { + err := fmt.Errorf(`root inventory.json sidecar: %w`, fs.ErrNotExist) + v.AddFatal(verr(err, code.E058(specStr))) + } + err := state.VersionDirs.Valid() + if err != nil { + if errors.Is(err, ErrVerEmpty) { + err = verr(err, code.E008(specStr)) + } else if errors.Is(err, ErrVNumPadding) { + err = verr(err, code.E011(specStr)) + } else if errors.Is(err, ErrVNumMissing) { + err = verr(err, code.E010(specStr)) + } + v.AddFatal(err) + } + if err == nil && state.VersionDirs.Padding() > 0 { + err := errors.New("version directory names are zero-padded") + v.AddWarn(verr(err, code.W001(specStr))) + } + // if vdirHead := state.VersionDirs.Head().Num(); vdirHead > o.inv.Head.Num() { + // err := errors.New("version directories don't reflect versions in inventory.json") + // v.AddFatal(verr(err, codes.E046(ocflV))) + // } + return v +} + +func validateExtensionsDir(ctx context.Context, spec Spec, fsys FS, objDir string) *Validation { + specStr := string(spec) + v := &Validation{} + extDir := path.Join(objDir, extensionsDir) + items, err := fsys.ReadDir(ctx, extDir) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return nil + } + v.AddFatal(err) + return v + } + for _, i := range items { + if !i.IsDir() { + err := fmt.Errorf(`invalid file: %s`, i.Name()) + v.AddFatal(verr(err, code.E067(specStr))) + continue + } + _, err := extension.Get(i.Name()) + if err != nil { + // unknow extension + err := fmt.Errorf("%w: %s", err, i.Name()) + v.AddWarn(verr(err, code.W013(specStr))) + } + } + return v +} diff --git a/ocflv1/inventory.go b/ocflv1/inventory.go deleted file mode 100644 index 88c053d4..00000000 --- a/ocflv1/inventory.go +++ /dev/null @@ -1,868 +0,0 @@ -package ocflv1 - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/url" - "path" - "slices" - "sort" - "strings" - "time" - - "github.com/srerickson/ocfl-go" - "github.com/srerickson/ocfl-go/digest" - "github.com/srerickson/ocfl-go/ocflv1/codes" - "golang.org/x/exp/maps" -) - -var ( - ErrVersionNotFound = errors.New("version not found in inventory") -) - -// Inventory represents raw contents of an OCFL v1.x inventory.json file -type Inventory struct { - ID string `json:"id"` - Type ocfl.InvType `json:"type"` - DigestAlgorithm string `json:"digestAlgorithm"` - Head ocfl.VNum `json:"head"` - ContentDirectory string `json:"contentDirectory,omitempty"` - Manifest ocfl.DigestMap `json:"manifest"` - Versions map[ocfl.VNum]*Version `json:"versions"` - Fixity map[string]ocfl.DigestMap `json:"fixity,omitempty"` - - // digest of raw inventory using DigestAlgorithm, set during json - // marshal/unmarshal - jsonDigest string -} - -// Version represents object version state and metadata -type Version struct { - Created time.Time `json:"created"` - State ocfl.DigestMap `json:"state"` - Message string `json:"message,omitempty"` - User *ocfl.User `json:"user,omitempty"` -} - -// MarshalJSON implements json.Marhsaller for inventory. The inventory's json -// digest is updated with the digest of the returned bytes. -func (inv *Inventory) MarshalJSON() ([]byte, error) { - type invAlias Inventory - alias := (*invAlias)(inv) - byts, err := json.Marshal(alias) - if err != nil { - return nil, err - } - if err := inv.setJsonDigest(byts); err != nil { - return nil, err - } - return byts, nil -} - -// VNums returns a sorted slice of VNums corresponding to the keys in the -// inventory's 'versions' block. -func (inv Inventory) VNums() []ocfl.VNum { - vnums := make([]ocfl.VNum, len(inv.Versions)) - i := 0 - for v := range inv.Versions { - vnums[i] = v - i++ - } - sort.Sort(ocfl.VNums(vnums)) - return vnums -} - -// Digest of Inventory's source json using the inventory digest. If the -// Inventory wasn't decoded using ValidateInventoryBytes -func (inv Inventory) Digest() string { - return inv.jsonDigest -} - -// ContentPath resolves the logical path from the version state with number v to -// a content path (i.e., a manifest path). The content path is relative to the -// object's root directory. If v is zero, the inventories head version is used. -func (inv Inventory) ContentPath(v int, logical string) (string, error) { - ver := inv.Version(v) - if ver == nil { - return "", ErrVersionNotFound - } - sum := ver.State.GetDigest(logical) - if sum == "" { - return "", fmt.Errorf("no path: %s", logical) - } - paths := inv.Manifest[sum] - if len(paths) == 0 { - return "", fmt.Errorf("missing manifest entry for: %s", sum) - } - return paths[0], nil -} - -// Version returns the version entry from the entry with number v. If v is 0, -// the head version is used. If no version entry exists, nil is returned -func (inv Inventory) Version(v int) *Version { - if inv.Versions == nil { - return nil - } - if v == 0 { - return inv.Versions[inv.Head] - } - return inv.Versions[ocfl.V(v, inv.Head.Padding())] -} - -// GetFixity implements ocfl.FixitySource for Inventory -func (inv Inventory) GetFixity(dig string) digest.Set { - paths := inv.Manifest[dig] - if len(paths) < 1 { - return nil - } - set := digest.Set{} - for fixAlg, fixMap := range inv.Fixity { - fixMap.EachPath(func(p, fixDigest string) bool { - if slices.Contains(paths, p) { - set[fixAlg] = fixDigest - return false - } - return true - }) - } - return set -} - -func (inv Inventory) Inventory() ocfl.ReadInventory { - return &readInventory{raw: inv} -} - -// Validate checks the inventory's structure and internal consistency. Errors -// and warnings are added to any validations. The returned error wraps all fatal -// errors. -func (inv *Inventory) Validate() *ocfl.Validation { - v := &ocfl.Validation{} - if inv.Type.Empty() { - err := errors.New("missing required field: 'type'") - v.AddFatal(err) - } - ocflV := inv.Type.Spec - if inv.ID == "" { - err := errors.New("missing required field: 'id'") - v.AddFatal(ec(err, codes.E036(ocflV))) - } - if inv.Head.IsZero() { - err := errors.New("missing required field: 'head'") - v.AddFatal(ec(err, codes.E036(ocflV))) - } - if inv.Manifest == nil { - err := errors.New("missing required field 'manifest'") - v.AddFatal(ec(err, codes.E041(ocflV))) - } - if inv.Versions == nil { - err := errors.New("missing required field: 'versions'") - v.AddFatal(ec(err, codes.E041(ocflV))) - } - if u, err := url.ParseRequestURI(inv.ID); err != nil || u.Scheme == "" { - err := fmt.Errorf(`object ID is not a URI: %q`, inv.ID) - v.AddWarn(ec(err, codes.W005(ocflV))) - } - switch inv.DigestAlgorithm { - case digest.SHA512.ID(): - break - case digest.SHA256.ID(): - err := fmt.Errorf(`'digestAlgorithm' is %q`, digest.SHA256.ID()) - v.AddWarn(ec(err, codes.W004(ocflV))) - default: - err := fmt.Errorf(`'digestAlgorithm' is not %q or %q`, digest.SHA512.ID(), digest.SHA256.ID()) - v.AddFatal(ec(err, codes.E025(ocflV))) - } - if err := inv.Head.Valid(); err != nil { - err = fmt.Errorf("head is invalid: %w", err) - v.AddFatal(ec(err, codes.E011(ocflV))) - } - if strings.Contains(inv.ContentDirectory, "/") { - err := errors.New("contentDirectory contains '/'") - v.AddFatal(ec(err, codes.E017(ocflV))) - } - if inv.ContentDirectory == "." || inv.ContentDirectory == ".." { - err := errors.New("contentDirectory is '.' or '..'") - v.AddFatal(ec(err, codes.E017(ocflV))) - } - if inv.Manifest != nil { - err := inv.Manifest.Valid() - if err != nil { - var dcErr *ocfl.MapDigestConflictErr - var pcErr *ocfl.MapPathConflictErr - var piErr *ocfl.MapPathInvalidErr - if errors.As(err, &dcErr) { - err = ec(err, codes.E096(ocflV)) - } else if errors.As(err, &pcErr) { - err = ec(err, codes.E101(ocflV)) - } else if errors.As(err, &piErr) { - err = ec(err, codes.E099(ocflV)) - } - v.AddFatal(err) - } - // check that each manifest entry is used in at least one state - for _, digest := range inv.Manifest.Digests() { - var found bool - for _, version := range inv.Versions { - if version == nil { - continue - } - if len(version.State[digest]) > 0 { - found = true - break - } - } - if !found { - err := fmt.Errorf("digest in manifest not used in version state: %s", digest) - v.AddFatal(ec(err, codes.E107(ocflV))) - } - } - } - // version names - var versionNums ocfl.VNums = maps.Keys(inv.Versions) - if err := versionNums.Valid(); err != nil { - if errors.Is(err, ocfl.ErrVerEmpty) { - err = ec(err, codes.E008(ocflV)) - } else if errors.Is(err, ocfl.ErrVNumMissing) { - err = ec(err, codes.E010(ocflV)) - } else if errors.Is(err, ocfl.ErrVNumPadding) { - err = ec(err, codes.E012(ocflV)) - } - v.AddFatal(err) - } - if versionNums.Head() != inv.Head { - err := fmt.Errorf(`version head not most recent version: %s`, inv.Head) - v.AddFatal(ec(err, codes.E040(ocflV))) - } - // version state - for vname, ver := range inv.Versions { - if ver == nil { - err := fmt.Errorf(`missing required version block for %q`, vname) - v.AddFatal(ec(err, codes.E048(ocflV))) - continue - } - if ver.Created.IsZero() { - err := fmt.Errorf(`version %s missing required field: 'created'`, vname) - v.AddFatal(ec(err, codes.E048(ocflV))) - } - if ver.Message == "" { - err := fmt.Errorf("version %s missing recommended field: 'message'", vname) - v.AddWarn(ec(err, codes.W007(ocflV))) - } - if ver.User == nil { - err := fmt.Errorf("version %s missing recommended field: 'user'", vname) - v.AddWarn(ec(err, codes.W007(ocflV))) - } - if ver.User != nil { - if ver.User.Name == "" { - err := fmt.Errorf("version %s user missing required field: 'name'", vname) - v.AddFatal(ec(err, codes.E054(ocflV))) - } - if ver.User.Address == "" { - err := fmt.Errorf("version %s user missing recommended field: 'address'", vname) - v.AddWarn(ec(err, codes.W008(ocflV))) - } - if u, err := url.ParseRequestURI(ver.User.Address); err != nil || u.Scheme == "" { - err := fmt.Errorf("version %s user address is not a URI", vname) - v.AddWarn(ec(err, codes.W009(ocflV))) - } - } - if ver.State == nil { - err := fmt.Errorf(`version %s missing required field: 'state'`, vname) - v.AddFatal(ec(err, codes.E048(ocflV))) - continue - } - err := ver.State.Valid() - if err != nil { - var dcErr *ocfl.MapDigestConflictErr - var pcErr *ocfl.MapPathConflictErr - var piErr *ocfl.MapPathInvalidErr - if errors.As(err, &dcErr) { - err = ec(err, codes.E050(ocflV)) - } else if errors.As(err, &pcErr) { - err = ec(err, codes.E095(ocflV)) - } else if errors.As(err, &piErr) { - err = ec(err, codes.E052(ocflV)) - } - v.AddFatal(err) - } - // check that each state digest appears in manifest - for _, digest := range ver.State.Digests() { - if len(inv.Manifest[digest]) == 0 { - err := fmt.Errorf("digest in %s state not in manifest: %s", vname, digest) - v.AddFatal(ec(err, codes.E050(ocflV))) - } - } - } - //fixity - for _, fixity := range inv.Fixity { - err := fixity.Valid() - if err != nil { - var dcErr *ocfl.MapDigestConflictErr - var piErr *ocfl.MapPathInvalidErr - var pcErr *ocfl.MapPathConflictErr - if errors.As(err, &dcErr) { - err = ec(err, codes.E097(ocflV)) - } else if errors.As(err, &piErr) { - err = ec(err, codes.E099(ocflV)) - } else if errors.As(err, &pcErr) { - err = ec(err, codes.E101(ocflV)) - } - v.AddFatal(err) - } - } - return v -} - -func (inv *Inventory) setJsonDigest(raw []byte) error { - digester, err := digest.DefaultRegistry().NewDigester(inv.DigestAlgorithm) - if err != nil { - return err - } - if _, err := io.Copy(digester, bytes.NewReader(raw)); err != nil { - return fmt.Errorf("digesting inventory: %w", err) - } - inv.jsonDigest = digester.String() - return nil -} - -// NewInventory reads the inventory and sets its digest value using the digest -// algorithm. The returned inventory is not fully validated. -func NewInventory(byts []byte) (*Inventory, error) { - dec := json.NewDecoder(bytes.NewReader(byts)) - dec.DisallowUnknownFields() - var inv Inventory - if err := dec.Decode(&inv); err != nil { - return nil, err - } - if err := inv.setJsonDigest(byts); err != nil { - return nil, err - } - return &inv, nil -} - -// ValidateInventoryBytes unmarshals the raw json bytes and fully validates the -// internal structure of the inventory. The returned ocfl.Validation will use -// use error codes based of the on the ocfl specificatoin spec. -func ValidateInventoryBytes(raw []byte, spec ocfl.Spec) (inv *Inventory, v *ocfl.Validation) { - v = &ocfl.Validation{} - invVals := map[string]any{} - if err := json.Unmarshal(raw, &invVals); err != nil { - err = fmt.Errorf("decoding inventory json: %w", err) - v.AddFatal(ec(err, codes.E033(spec))) - return nil, v - } - const requiredErrMsg = "required field is missing or has unexpected json value" - const optionalErrMsg = "optional field has unexpected json value" - id, exists, typeOK := pullJSONValue[string](invVals, `id`) - if !exists || !typeOK { - err := errors.New(requiredErrMsg + `: 'id'`) - v.AddFatal(ec(err, codes.E036(spec))) - } - typeStr, exists, typeOK := pullJSONValue[string](invVals, `type`) - if !exists || !typeOK { - err := errors.New(requiredErrMsg + `: 'type'`) - v.AddFatal(ec(err, codes.E036(spec))) - } - if typeStr != "" && typeStr != spec.AsInvType().String() { - err := fmt.Errorf("invalid inventory type value: %q", typeStr) - v.AddFatal(ec(err, codes.E038(spec))) - } - digestAlg, exists, typeOK := pullJSONValue[string](invVals, `digestAlgorithm`) - if !exists || !typeOK { - err := errors.New(requiredErrMsg + `: 'digestAlgorithm'`) - v.AddFatal(ec(err, codes.E036(spec))) - } - if digestAlg != "" && digestAlg != digest.SHA512.ID() && digestAlg != digest.SHA256.ID() { - err := fmt.Errorf("invalid digest algorithm: %q", digestAlg) - v.AddFatal(ec(err, codes.E025(spec))) - } - head, exists, typeOK := pullJSONValue[string](invVals, `head`) - if !exists || !typeOK { - err := errors.New(requiredErrMsg + `: 'head'`) - v.AddFatal(ec(err, codes.E036(spec))) - } - manifestVals, exists, typeOK := pullJSONValue[map[string]any](invVals, `manifest`) - if !exists || !typeOK { - err := errors.New(requiredErrMsg + `: 'manifest'`) - v.AddFatal(ec(err, codes.E041(spec))) - } - versionsVals, exists, typeOK := pullJSONValue[map[string]any](invVals, `versions`) - if !exists || !typeOK { - err := errors.New(requiredErrMsg + `: 'versions'`) - v.AddFatal(ec(err, codes.E043(spec))) - } - // FIXME: not sure which error code. E108? - contentDirectory, exists, typeOK := pullJSONValue[string](invVals, `contentDirectory`) - if exists && !typeOK { - // contentDirectory is optional - err := errors.New(optionalErrMsg + `: 'contentDirectory'`) - v.AddFatal(err) - } - // fixity is optional - fixityVals, exists, typeOK := pullJSONValue[map[string]any](invVals, `fixity`) - if exists && !typeOK { - err := errors.New(optionalErrMsg + `: 'fixity'`) - v.AddFatal(ec(err, codes.E111(spec))) - } - // any remaining values in invVals are invalid - for extra := range invVals { - err := fmt.Errorf("inventory json has unexpected field: %q", extra) - v.AddFatal(err) - } - inv = &Inventory{ - ID: id, - ContentDirectory: contentDirectory, - DigestAlgorithm: digestAlg, - Fixity: map[string]ocfl.DigestMap{}, - Versions: make(map[ocfl.VNum]*Version), - } - if err := inv.Type.UnmarshalText([]byte(typeStr)); err != nil { - v.AddFatal(ec(err, codes.E038(spec))) - } - if err := inv.Head.UnmarshalText([]byte(head)); err != nil { - v.AddFatal(ec(err, codes.E040(spec))) - } - var err error - if inv.Manifest, err = convertJSONDigestMap(manifestVals); err != nil { - err = fmt.Errorf("invalid manifest: %w", err) - v.AddFatal(ec(err, codes.E092(spec))) - } - // build versions - for vnumStr, val := range versionsVals { - var ( - vnum ocfl.VNum - versionVals map[string]any - userVals map[string]any - stateVals map[string]any - createdStr string - created time.Time - message string - state ocfl.DigestMap - user *ocfl.User - ) - if err := ocfl.ParseVNum(vnumStr, &vnum); err != nil { - err = fmt.Errorf("invalid key %q in versions block: %w", vnumStr, err) - v.AddFatal(ec(err, codes.E046(spec))) - continue - } - versionErrPrefix := "version '" + vnumStr + "'" - versionVals, typeOK = val.(map[string]any) - if !typeOK { - err := errors.New(versionErrPrefix + ": value is not a json object") - v.AddFatal(ec(err, codes.E045(spec))) - } - createdStr, exists, typeOK = pullJSONValue[string](versionVals, `created`) - if !exists || !typeOK { - err := fmt.Errorf("%s: %s: %s", versionErrPrefix, requiredErrMsg, `'created'`) - v.AddFatal(ec(err, codes.E048(spec))) - } - if createdStr != "" { - if err := created.UnmarshalText([]byte(createdStr)); err != nil { - err = fmt.Errorf("%s: created: %w", versionErrPrefix, err) - v.AddFatal(ec(err, codes.E049(spec))) - } - } - stateVals, exists, typeOK = pullJSONValue[map[string]any](versionVals, `state`) - if !exists || !typeOK { - err := fmt.Errorf("%s: %s: %q", versionErrPrefix, requiredErrMsg, `state`) - v.AddFatal(ec(err, codes.E048(spec))) - } - // message is optional - message, exists, typeOK = pullJSONValue[string](versionVals, `message`) - if exists && !typeOK { - err := fmt.Errorf("%s: %s: %q", versionErrPrefix, optionalErrMsg, `message`) - v.AddFatal(ec(err, codes.E094(spec))) - } - // user is optional - userVals, exists, typeOK := pullJSONValue[map[string]any](versionVals, `user`) - switch { - case exists && !typeOK: - err := fmt.Errorf("%s: %s: %q", versionErrPrefix, optionalErrMsg, `user`) - v.AddFatal(ec(err, codes.E054(spec))) - case exists: - var userName, userAddress string - userName, exists, typeOK = pullJSONValue[string](userVals, `name`) - if !exists || !typeOK { - err := fmt.Errorf("%s: user: %s: %q", versionErrPrefix, requiredErrMsg, `name`) - v.AddFatal(ec(err, codes.E054(spec))) - } - // address is optional - userAddress, exists, typeOK = pullJSONValue[string](userVals, `address`) - if exists && !typeOK { - err := fmt.Errorf("%s: user: %s: %q", versionErrPrefix, optionalErrMsg, `address`) - v.AddFatal(ec(err, codes.E054(spec))) - } - user = &ocfl.User{Name: userName, Address: userAddress} - } - // any additional fields in versionVals are invalid. - for extra := range versionVals { - err := fmt.Errorf("%s: invalid key: %q", versionErrPrefix, extra) - v.AddFatal(err) - } - state, err := convertJSONDigestMap(stateVals) - if err != nil { - err = fmt.Errorf("%s: state: %w", versionErrPrefix, err) - v.AddFatal(err) - } - inv.Versions[vnum] = &Version{ - Created: created, - State: state, - Message: message, - User: user, - } - } - // build fixity - for algStr, val := range fixityVals { - var digestVals map[string]any - digestVals, typeOK = val.(map[string]any) - fixityErrPrefix := "fixity '" + algStr + "'" - if !typeOK { - err := fmt.Errorf("%s: value is not a json object", fixityErrPrefix) - v.AddFatal(ec(err, codes.E057(spec))) - continue - } - digests, err := convertJSONDigestMap(digestVals) - if err != nil { - err = fmt.Errorf("%s: %w", fixityErrPrefix, err) - v.AddFatal(ec(err, codes.E057(spec))) - continue - } - inv.Fixity[algStr] = digests - } - if err := inv.setJsonDigest(raw); err != nil { - v.AddFatal(err) - } - v.Add(inv.Validate()) - if v.Err() != nil { - return nil, v - } - return inv, v -} - -// writeInventory marshals the value pointed to by inv, writing the json to dir/inventory.json in -// fsys. The digest is calculated using alg and the inventory sidecar is also written to -// dir/inventory.alg -func writeInventory(ctx context.Context, fsys ocfl.WriteFS, inv *Inventory, dirs ...string) error { - if err := ctx.Err(); err != nil { - return err - } - byts, err := json.Marshal(inv) - if err != nil { - return fmt.Errorf("encoding inventory: %w", err) - } - // write inventory.json and sidecar - for _, dir := range dirs { - invFile := path.Join(dir, inventoryFile) - sideFile := invFile + "." + inv.DigestAlgorithm - sideContent := inv.jsonDigest + " " + inventoryFile + "\n" - _, err = fsys.Write(ctx, invFile, bytes.NewReader(byts)) - if err != nil { - return fmt.Errorf("write inventory failed: %w", err) - } - _, err = fsys.Write(ctx, sideFile, strings.NewReader(sideContent)) - if err != nil { - return fmt.Errorf("write inventory sidecar failed: %w", err) - } - } - return nil -} - -// NextInventory ... -func buildInventory(prev ocfl.ReadInventory, commit *ocfl.Commit) (*Inventory, error) { - if commit.Stage == nil { - return nil, errors.New("commit is missing new version state") - } - if commit.Stage.DigestAlgorithm == nil { - return nil, errors.New("commit has no digest algorithm") - - } - if commit.Stage.State == nil { - commit.Stage.State = ocfl.DigestMap{} - } - newInv := &Inventory{ - ID: commit.ID, - DigestAlgorithm: commit.Stage.DigestAlgorithm.ID(), - ContentDirectory: contentDir, - } - switch { - case prev != nil: - prevInv, ok := prev.(*readInventory) - if !ok { - err := errors.New("inventory is not an OCFLv1 inventory") - return nil, err - } - if newInv.DigestAlgorithm != prev.DigestAlgorithm().ID() { - return nil, fmt.Errorf("commit must use same digest algorithm as existing inventory (%s)", prev.DigestAlgorithm()) - } - newInv.ID = prev.ID() - newInv.ContentDirectory = prevInv.raw.ContentDirectory - newInv.Type = prevInv.raw.Type - var err error - newInv.Head, err = prev.Head().Next() - if err != nil { - return nil, fmt.Errorf("existing inventory's version scheme doesn't support additional versions: %w", err) - } - if !commit.Spec.Empty() { - // new inventory spec must be >= prev - if commit.Spec.Cmp(prev.Spec()) < 0 { - err = fmt.Errorf("new inventory's OCFL spec can't be lower than the existing inventory's (%s)", prev.Spec()) - return nil, err - } - newInv.Type = commit.Spec.AsInvType() - } - if !commit.AllowUnchanged { - lastV := prev.Version(0) - if lastV.State().Eq(commit.Stage.State) { - err := errors.New("version state unchanged") - return nil, err - } - } - - // copy and normalize all digests in the inventory. If we don't do this - // non-normalized digests in previous version states might cause - // problems since the updated manifest/fixity will be normalized. - newInv.Manifest, err = prev.Manifest().Normalize() - if err != nil { - return nil, fmt.Errorf("in existing inventory manifest: %w", err) - } - versions := prev.Head().Lineage() - newInv.Versions = make(map[ocfl.VNum]*Version, len(versions)) - for _, vnum := range versions { - prevVer := prev.Version(vnum.Num()) - newVer := &Version{ - Created: prevVer.Created(), - Message: prevVer.Message(), - } - newVer.State, err = prevVer.State().Normalize() - if err != nil { - return nil, fmt.Errorf("in existing inventory %s state: %w", vnum, err) - } - if prevVer.User() != nil { - newVer.User = &ocfl.User{ - Name: prevVer.User().Name, - Address: prevVer.User().Address, - } - } - newInv.Versions[vnum] = newVer - } - // transfer fixity - newInv.Fixity = make(map[string]ocfl.DigestMap, len(prevInv.raw.Fixity)) - for alg, m := range prevInv.raw.Fixity { - newInv.Fixity[alg], err = m.Normalize() - if err != nil { - return nil, fmt.Errorf("in existing inventory %s fixity: %w", alg, err) - } - } - default: - // FIXME: how whould padding be set for new inventories? - newInv.Head = ocfl.V(1, 0) - newInv.Manifest = ocfl.DigestMap{} - newInv.Fixity = map[string]ocfl.DigestMap{} - newInv.Versions = map[ocfl.VNum]*Version{} - newInv.Type = commit.Spec.AsInvType() - } - - // add new version - newVersion := &Version{ - State: commit.Stage.State, - Created: commit.Created, - Message: commit.Message, - User: &commit.User, - } - if newVersion.Created.IsZero() { - newVersion.Created = time.Now() - } - newVersion.Created = newVersion.Created.Truncate(time.Second) - newInv.Versions[newInv.Head] = newVersion - - // build new manifest and fixity entries - newContentFunc := func(paths []string) []string { - // apply user-specified path transform first - if commit.ContentPathFunc != nil { - paths = commit.ContentPathFunc(paths) - } - contDir := newInv.ContentDirectory - if contDir == "" { - contDir = contentDir - } - for i, p := range paths { - paths[i] = path.Join(newInv.Head.String(), contDir, p) - } - return paths - } - for digest, logicPaths := range newVersion.State { - if len(newInv.Manifest[digest]) > 0 { - // version content already exists in the manifest - continue - } - newInv.Manifest[digest] = newContentFunc(slices.Clone(logicPaths)) - } - if commit.Stage.FixitySource != nil { - for digest, contentPaths := range newInv.Manifest { - fixSet := commit.Stage.FixitySource.GetFixity(digest) - if len(fixSet) < 1 { - continue - } - for fixAlg, fixDigest := range fixSet { - if newInv.Fixity[fixAlg] == nil { - newInv.Fixity[fixAlg] = ocfl.DigestMap{} - } - for _, cp := range contentPaths { - fixPaths := newInv.Fixity[fixAlg][fixDigest] - if !slices.Contains(fixPaths, cp) { - newInv.Fixity[fixAlg][fixDigest] = append(fixPaths, cp) - } - } - } - } - } - // check that resulting inventory is valid - if err := newInv.Validate().Err(); err != nil { - return nil, fmt.Errorf("generated inventory is not valid: %w", err) - } - return newInv, nil -} - -// readInventory implements ocfl.Inventory -type readInventory struct { - raw Inventory -} - -func (inv *readInventory) MarshalJSON() ([]byte, error) { return json.Marshal(inv.raw) } - -func (inv *readInventory) GetFixity(digest string) digest.Set { return inv.raw.GetFixity(digest) } - -func (inv *readInventory) ContentDirectory() string { - if c := inv.raw.ContentDirectory; c != "" { - return c - } - return contentDir -} - -func (inv *readInventory) Digest() string { return inv.raw.jsonDigest } - -func (inv *readInventory) DigestAlgorithm() digest.Algorithm { - // DigestAlgorithm should be sha512 or sha256 - switch inv.raw.DigestAlgorithm { - case digest.SHA256.ID(): - return digest.SHA256 - case digest.SHA512.ID(): - return digest.SHA512 - default: - return nil - } -} - -func (inv *readInventory) FixityAlgorithms() []string { - if len(inv.raw.Fixity) < 1 { - return nil - } - algs := make([]string, 0, len(inv.raw.Fixity)) - for alg := range inv.raw.Fixity { - algs = append(algs, alg) - } - return algs -} - -func (inv *readInventory) Head() ocfl.VNum { return inv.raw.Head } - -func (inv *readInventory) ID() string { return inv.raw.ID } - -func (inv *readInventory) Manifest() ocfl.DigestMap { return inv.raw.Manifest } - -func (inv *readInventory) Spec() ocfl.Spec { return inv.raw.Type.Spec } - -func (inv *readInventory) Validate() *ocfl.Validation { - if inv.raw.jsonDigest == "" { - err := errors.New("inventory was not initialized correctly: missing file digest value") - v := &ocfl.Validation{} - v.AddFatal(err) - return v - } - return inv.raw.Validate() -} - -func (inv *readInventory) Version(i int) ocfl.ObjectVersion { - v := inv.raw.Version(i) - if v == nil { - return nil - } - return &inventoryVersion{ver: v} -} - -type inventoryVersion struct { - ver *Version -} - -func (v *inventoryVersion) State() ocfl.DigestMap { return v.ver.State } -func (v *inventoryVersion) Message() string { return v.ver.Message } -func (v *inventoryVersion) Created() time.Time { return v.ver.Created } -func (v *inventoryVersion) User() *ocfl.User { return v.ver.User } - -type logicalState struct { - manifest ocfl.DigestMap - state ocfl.DigestMap -} - -func (a logicalState) Eq(b logicalState) bool { - if a.state == nil || b.state == nil || a.manifest == nil || b.manifest == nil { - return false - } - if !a.state.EachPath(func(name string, dig string) bool { - otherDig := b.state.GetDigest(name) - if otherDig == "" { - return false - } - contentPaths := a.manifest[dig] - otherPaths := b.manifest[otherDig] - if len(contentPaths) != len(otherPaths) { - return false - } - sort.Strings(contentPaths) - sort.Strings(otherPaths) - for i, p := range contentPaths { - if otherPaths[i] != p { - return false - } - } - return true - }) { - return false - } - // make sure all logical paths in other state are also in state - return b.state.EachPath(func(otherName string, _ string) bool { - return a.state.GetDigest(otherName) != "" - }) -} - -func pullJSONValue[T any](m map[string]any, key string) (val T, exists bool, typeOK bool) { - var anyVal any - anyVal, exists = m[key] - val, typeOK = anyVal.(T) - delete(m, key) - return -} - -func convertJSONDigestMap(jsonMap map[string]any) (ocfl.DigestMap, error) { - m := ocfl.DigestMap{} - msg := "invalid json type: expected array of strings" - for key, mapVal := range jsonMap { - slice, isSlice := mapVal.([]any) - if !isSlice { - return nil, errors.New(msg) - } - m[key] = make([]string, len(slice)) - for i := range slice { - strVal, isStr := slice[i].(string) - if !isStr { - return nil, errors.New(msg) - } - m[key][i] = strVal - } - } - return m, nil -} diff --git a/ocflv1/object.go b/ocflv1/object.go deleted file mode 100644 index 018f5e47..00000000 --- a/ocflv1/object.go +++ /dev/null @@ -1,232 +0,0 @@ -package ocflv1 - -import ( - "cmp" - "context" - "errors" - "fmt" - "io" - "io/fs" - "path" - "strings" - "time" - - "github.com/srerickson/ocfl-go" - "golang.org/x/exp/slices" -) - -var ( - ErrOCFLVersion = errors.New("unsupported OCFL version") - ErrObjRootStructure = errors.New("object includes invalid files or directories") -) - -// ReadObject implements ocfl.ReadObject for OCFL v1.x objects -type ReadObject struct { - fs ocfl.FS - path string - inv *Inventory -} - -func (o *ReadObject) FS() ocfl.FS { return o.fs } - -func (o *ReadObject) Inventory() ocfl.ReadInventory { - if o.inv == nil { - return nil - } - return &readInventory{raw: *o.inv} -} - -func (o *ReadObject) VersionFS(ctx context.Context, i int) fs.FS { - ver := o.inv.Version(i) - if ver == nil { - return nil - } - // FIXME: This is a hack to make versionFS replicates the filemode of - // the undering FS. Open a random content file to get the file mode used by - // the underlying FS. - regfileType := fs.FileMode(0) - for _, paths := range o.inv.Manifest { - if len(paths) < 1 { - break - } - f, err := o.fs.OpenFile(ctx, path.Join(o.path, paths[0])) - if err != nil { - return nil - } - defer f.Close() - info, err := f.Stat() - if err != nil { - return nil - } - regfileType = info.Mode().Type() - break - } - return &versionFS{ - ctx: ctx, - obj: o, - paths: ver.State.PathMap(), - created: ver.Created, - regMode: regfileType, - } -} - -func (o *ReadObject) Path() string { return o.path } - -type versionFS struct { - ctx context.Context - obj *ReadObject - paths ocfl.PathMap - created time.Time - regMode fs.FileMode -} - -func (vfs *versionFS) Open(logical string) (fs.File, error) { - if !fs.ValidPath(logical) { - return nil, &fs.PathError{ - Err: fs.ErrInvalid, - Op: "open", - Path: logical, - } - } - if logical == "." { - return vfs.openDir(".") - } - digest := vfs.paths[logical] - if digest == "" { - // name doesn't exist in state. - // try opening as a directory - return vfs.openDir(logical) - } - - realNames := vfs.obj.inv.Manifest[digest] - if len(realNames) < 1 { - return nil, &fs.PathError{ - Err: fs.ErrNotExist, - Op: "open", - Path: logical, - } - } - realName := realNames[0] - if !fs.ValidPath(realName) { - return nil, &fs.PathError{ - Err: fs.ErrInvalid, - Op: "open", - Path: logical, - } - } - f, err := vfs.obj.fs.OpenFile(vfs.ctx, path.Join(vfs.obj.path, realName)) - if err != nil { - err = fmt.Errorf("opening file with logical path %q: %w", logical, err) - return nil, err - } - return f, nil -} - -func (vfs *versionFS) openDir(dir string) (fs.File, error) { - prefix := dir + "/" - if prefix == "./" { - prefix = "" - } - children := map[string]*vfsDirEntry{} - for p := range vfs.paths { - if !strings.HasPrefix(p, prefix) { - continue - } - name, _, isdir := strings.Cut(strings.TrimPrefix(p, prefix), "/") - if _, exists := children[name]; exists { - continue - } - entry := &vfsDirEntry{ - name: name, - mode: vfs.regMode, - created: vfs.created, - open: func() (fs.File, error) { return vfs.Open(path.Join(dir, name)) }, - } - if isdir { - entry.mode = entry.mode | fs.ModeDir | fs.ModeIrregular - } - children[name] = entry - } - if len(children) < 1 { - return nil, &fs.PathError{ - Op: "open", - Path: dir, - Err: fs.ErrNotExist, - } - } - - dirFile := &vfsDirFile{ - name: dir, - entries: make([]fs.DirEntry, 0, len(children)), - } - for _, entry := range children { - dirFile.entries = append(dirFile.entries, entry) - } - slices.SortFunc(dirFile.entries, func(a, b fs.DirEntry) int { - return cmp.Compare(a.Name(), b.Name()) - }) - return dirFile, nil -} - -type vfsDirEntry struct { - name string - created time.Time - mode fs.FileMode - open func() (fs.File, error) -} - -var _ fs.DirEntry = (*vfsDirEntry)(nil) - -func (info *vfsDirEntry) Name() string { return info.name } -func (info *vfsDirEntry) IsDir() bool { return info.mode.IsDir() } -func (info *vfsDirEntry) Type() fs.FileMode { return info.mode.Type() } - -func (info *vfsDirEntry) Info() (fs.FileInfo, error) { - f, err := info.open() - if err != nil { - return nil, err - } - stat, err := f.Stat() - return stat, errors.Join(err, f.Close()) -} - -func (info *vfsDirEntry) Size() int64 { return 0 } -func (info *vfsDirEntry) Mode() fs.FileMode { return info.mode | fs.ModeIrregular } -func (info *vfsDirEntry) ModTime() time.Time { return info.created } -func (info *vfsDirEntry) Sys() any { return nil } - -type vfsDirFile struct { - name string - created time.Time - entries []fs.DirEntry - offset int -} - -var _ fs.ReadDirFile = (*vfsDirFile)(nil) - -func (dir *vfsDirFile) ReadDir(n int) ([]fs.DirEntry, error) { - if n <= 0 { - entries := dir.entries[dir.offset:] - dir.offset = len(dir.entries) - return entries, nil - } - if remain := len(dir.entries) - dir.offset; remain < n { - n = remain - } - if n <= 0 { - return nil, io.EOF - } - entries := dir.entries[dir.offset : dir.offset+n] - dir.offset += n - return entries, nil -} - -func (dir *vfsDirFile) Close() error { return nil } -func (dir *vfsDirFile) IsDir() bool { return true } -func (dir *vfsDirFile) Mode() fs.FileMode { return fs.ModeDir | fs.ModeIrregular } -func (dir *vfsDirFile) ModTime() time.Time { return dir.created } -func (dir *vfsDirFile) Name() string { return dir.name } -func (dir *vfsDirFile) Read(_ []byte) (int, error) { return 0, nil } -func (dir *vfsDirFile) Size() int64 { return 0 } -func (dir *vfsDirFile) Stat() (fs.FileInfo, error) { return dir, nil } -func (dir *vfsDirFile) Sys() any { return nil } diff --git a/ocflv1/ocflv1.go b/ocflv1/ocflv1.go deleted file mode 100644 index f31aa010..00000000 --- a/ocflv1/ocflv1.go +++ /dev/null @@ -1,514 +0,0 @@ -// Package [ocflv1] provides an implementation of OCFL v1.0 and v1.1. -package ocflv1 - -import ( - "context" - "errors" - "fmt" - "io/fs" - "path" - "reflect" - "strings" - - "github.com/srerickson/ocfl-go" - "github.com/srerickson/ocfl-go/digest" - "github.com/srerickson/ocfl-go/extension" - "github.com/srerickson/ocfl-go/logging" - "github.com/srerickson/ocfl-go/ocflv1/codes" - "golang.org/x/sync/errgroup" -) - -const ( - // defaults - inventoryFile = `inventory.json` - contentDir = `content` - extensionsDir = "extensions" -) - -func Enable() { - ocfl.RegisterOCLF(&OCFL{spec: ocfl.Spec1_0}) - ocfl.RegisterOCLF(&OCFL{spec: ocfl.Spec1_1}) -} - -// Implementation of OCFL v1.x -type OCFL struct { - spec ocfl.Spec // 1.0 or 1.1 -} - -func (imp OCFL) Spec() ocfl.Spec { return imp.spec } - -func (imp OCFL) NewReadInventory(raw []byte) (ocfl.ReadInventory, error) { - inv, err := NewInventory(raw) - if err != nil { - return nil, err - } - if err := inv.Validate().Err(); err != nil { - return nil, err - } - return inv.Inventory(), nil -} - -func (imp OCFL) NewReadObject(fsys ocfl.FS, path string, inv ocfl.ReadInventory) ocfl.ReadObject { - concreteInv, ok := inv.(*readInventory) - if !ok { - panic("inventory has wrong type") - } - return &ReadObject{fs: fsys, path: path, inv: &concreteInv.raw} -} - -// Commits creates or updates an object by adding a new object version based -// on the implementation. -func (imp OCFL) Commit(ctx context.Context, obj ocfl.ReadObject, commit *ocfl.Commit) (ocfl.ReadObject, error) { - writeFS, ok := obj.FS().(ocfl.WriteFS) - if !ok { - err := errors.New("object's backing file system doesn't support write operations") - return nil, &ocfl.CommitError{Err: err} - } - newInv, err := buildInventory(obj.Inventory(), commit) - if err != nil { - err := fmt.Errorf("building new inventory: %w", err) - return nil, &ocfl.CommitError{Err: err} - } - logger := commit.Logger - if logger == nil { - logger = logging.DisabledLogger() - } - logger = logger.With("path", obj.Path(), "id", newInv.ID, "head", newInv.Head, "ocfl_spec", newInv.Type.Spec, "alg", newInv.DigestAlgorithm) - // xfers is a subeset of the manifest with the new content to add - xfers, err := newContentMap(newInv) - if err != nil { - return nil, &ocfl.CommitError{Err: err} - } - // check that the stage includes all the new content - for digest := range xfers { - if !commit.Stage.HasContent(digest) { - // FIXME short digest - err := fmt.Errorf("no content for digest: %s", digest) - return nil, &ocfl.CommitError{Err: err} - } - } - // file changes start here - // 1. create or update NAMASTE object declaration - var oldSpec ocfl.Spec - if obj.Inventory() != nil { - oldSpec = obj.Inventory().Spec() - } - newSpec := newInv.Type.Spec - switch { - case ocfl.ObjectExists(obj) && oldSpec != newSpec: - oldDecl := ocfl.Namaste{Type: ocfl.NamasteTypeObject, Version: oldSpec} - logger.DebugContext(ctx, "deleting previous OCFL object declaration", "name", oldDecl) - if err = writeFS.Remove(ctx, path.Join(obj.Path(), oldDecl.Name())); err != nil { - return nil, &ocfl.CommitError{Err: err, Dirty: true} - } - fallthrough - case !ocfl.ObjectExists(obj): - newDecl := ocfl.Namaste{Type: ocfl.NamasteTypeObject, Version: newSpec} - logger.DebugContext(ctx, "writing new OCFL object declaration", "name", newDecl) - if err = ocfl.WriteDeclaration(ctx, writeFS, obj.Path(), newDecl); err != nil { - return nil, &ocfl.CommitError{Err: err, Dirty: true} - } - } - // 2. tranfser files from stage to object - if len(xfers) > 0 { - copyOpts := ©ContentOpts{ - Source: commit.Stage, - DestFS: writeFS, - DestRoot: obj.Path(), - Manifest: xfers, - } - logger.DebugContext(ctx, "copying new object files", "count", len(xfers)) - if err := copyContent(ctx, copyOpts); err != nil { - err = fmt.Errorf("transferring new object contents: %w", err) - return nil, &ocfl.CommitError{Err: err, Dirty: true} - } - } - logger.DebugContext(ctx, "writing inventories for new object version") - // 3. write inventory to both object root and version directory - newVersionDir := path.Join(obj.Path(), newInv.Head.String()) - if err := writeInventory(ctx, writeFS, newInv, obj.Path(), newVersionDir); err != nil { - err = fmt.Errorf("writing new inventories or inventory sidecars: %w", err) - return nil, &ocfl.CommitError{Err: err, Dirty: true} - } - return &ReadObject{ - inv: newInv, - fs: obj.FS(), - path: obj.Path(), - }, nil -} - -func (imp OCFL) ValidateObjectRoot(ctx context.Context, fsys ocfl.FS, dir string, state *ocfl.ObjectState, vldr *ocfl.ObjectValidation) (ocfl.ReadObject, error) { - // validate namaste - decl := ocfl.Namaste{Type: ocfl.NamasteTypeObject, Version: imp.spec} - name := path.Join(dir, decl.Name()) - err := ocfl.ValidateNamaste(ctx, fsys, name) - if err != nil { - switch { - case errors.Is(err, fs.ErrNotExist): - err = fmt.Errorf("%s: %w", name, ocfl.ErrObjectNamasteNotExist) - vldr.AddFatal(ec(err, codes.E001(imp.spec))) - default: - vldr.AddFatal(ec(err, codes.E007(imp.spec))) - } - return nil, err - } - // validate root inventory - invBytes, err := ocfl.ReadAll(ctx, fsys, path.Join(dir, inventoryFile)) - if err != nil { - switch { - case errors.Is(err, fs.ErrNotExist): - vldr.AddFatal(err, ec(err, codes.E063(imp.spec))) - default: - vldr.AddFatal(err) - } - return nil, err - } - inv, invValidation := ValidateInventoryBytes(invBytes, imp.spec) - vldr.PrefixAdd("root inventory.json", invValidation) - if err := invValidation.Err(); err != nil { - return nil, err - } - if err := ocfl.ValidateInventorySidecar(ctx, inv.Inventory(), fsys, dir); err != nil { - switch { - case errors.Is(err, ocfl.ErrInventorySidecarContents): - vldr.AddFatal(ec(err, codes.E061(imp.spec))) - default: - vldr.AddFatal(ec(err, codes.E060(imp.spec))) - } - } - vldr.PrefixAdd("extensions directory", validateExtensionsDir(ctx, imp.spec, fsys, dir)) - if err := vldr.AddInventoryDigests(inv.Inventory()); err != nil { - vldr.AddFatal(err) - } - vldr.PrefixAdd("root contents", validateRootState(imp.spec, state)) - if err := vldr.Err(); err != nil { - return nil, err - } - return &ReadObject{fs: fsys, path: dir, inv: inv}, nil -} - -func (imp OCFL) ValidateObjectVersion(ctx context.Context, obj ocfl.ReadObject, vnum ocfl.VNum, verInv ocfl.ReadInventory, prevInv ocfl.ReadInventory, vldr *ocfl.ObjectValidation) error { - fsys := obj.FS() - vnumStr := vnum.String() - fullVerDir := path.Join(obj.Path(), vnumStr) // version directory path relative to FS - vSpec := imp.spec - rootInv := obj.Inventory() // headInv is assumed to be valid - vDirEntries, err := fsys.ReadDir(ctx, fullVerDir) - if err != nil && !errors.Is(err, fs.ErrNotExist) { - // can't read version directory for some reason, but not because it - // doesn't exist. - vldr.AddFatal(err) - return err - } - vdirState := parseVersionDirState(vDirEntries) - for _, f := range vdirState.extraFiles { - err := fmt.Errorf(`unexpected file in %s: %s`, vnum, f) - vldr.AddFatal(ec(err, codes.E015(vSpec))) - } - if !vdirState.hasInventory { - err := fmt.Errorf("missing %s/inventory.json", vnumStr) - vldr.AddWarn(ec(err, codes.W010(vSpec))) - } - if verInv != nil { - verInvValidation := verInv.Validate() - vldr.PrefixAdd(vnumStr+"/inventory.json", verInvValidation) - if err := ocfl.ValidateInventorySidecar(ctx, verInv, fsys, fullVerDir); err != nil { - err := fmt.Errorf("%s/inventory.json: %w", vnumStr, err) - switch { - case errors.Is(err, ocfl.ErrInventorySidecarContents): - vldr.AddFatal(ec(err, codes.E061(imp.spec))) - default: - vldr.AddFatal(ec(err, codes.E060(imp.spec))) - } - } - if prevInv != nil && verInv.Spec().Cmp(prevInv.Spec()) < 0 { - err := fmt.Errorf("%s/inventory.json uses an older OCFL specification than than the previous version", vnum) - vldr.AddFatal(ec(err, codes.E103(vSpec))) - } - if verInv.Head() != vnum { - err := fmt.Errorf("%s/inventory.json: 'head' does not matchs its directory", vnum) - vldr.AddFatal(ec(err, codes.E040(vSpec))) - } - if verInv.Digest() != rootInv.Digest() { - imp.compareVersionInventory(obj, vnum, verInv, vldr) - if verInv.Digest() != rootInv.Digest() { - if err := vldr.AddInventoryDigests(verInv); err != nil { - err = fmt.Errorf("%s/inventory.json digests are inconsistent with other inventories: %w", vnum, err) - vldr.AddFatal(ec(err, codes.E066(vSpec))) - } - } - } - } - cdName := rootInv.ContentDirectory() - for _, d := range vdirState.dirs { - // the only directory in the version directory SHOULD be the content directory - if d != cdName { - err := fmt.Errorf(`extra directory in %s: %s`, vnum, d) - vldr.AddWarn(ec(err, codes.W002(vSpec))) - continue - } - // add version content files to validation state - var added int - fullVerContDir := path.Join(fullVerDir, cdName) - contentFiles, filesErrFn := ocfl.WalkFiles(ctx, fsys, fullVerContDir) - for contentFile := range contentFiles { - // convert from path relative to version content directory to path - // relative to the object - vldr.AddExistingContent(path.Join(vnumStr, cdName, contentFile.Path)) - added++ - } - if err := filesErrFn(); err != nil { - vldr.AddFatal(err) - return err - } - if added == 0 { - // content directory exists but it's empty - err := fmt.Errorf("content directory (%s) is empty directory", fullVerContDir) - vldr.AddFatal(ec(err, codes.E016(vSpec))) - } - } - return nil -} - -func (imp OCFL) ValidateObjectContent(ctx context.Context, obj ocfl.ReadObject, v *ocfl.ObjectValidation) error { - newVld := &ocfl.Validation{} - for name := range v.MissingContent() { - err := fmt.Errorf("missing content: %s", name) - newVld.AddFatal(ec(err, codes.E092(imp.spec))) - } - for name := range v.UnexpectedContent() { - err := fmt.Errorf("unexpected content: %s", name) - newVld.AddFatal(ec(err, codes.E023(imp.spec))) - } - if !v.SkipDigests() { - alg := obj.Inventory().DigestAlgorithm() - digests := v.ExistingContentDigests(obj.FS(), obj.Path(), alg) - numgos := v.DigestConcurrency() - registry := v.ValidationAlgorithms() - for err := range digests.ValidateBatch(ctx, registry, numgos) { - var digestErr *digest.DigestError - switch { - case errors.As(err, &digestErr): - newVld.AddFatal(ec(digestErr, codes.E093(imp.spec))) - default: - newVld.AddFatal(err) - } - } - } - v.Add(newVld) - return newVld.Err() -} - -type versionDirState struct { - hasInventory bool - sidecarAlg string - extraFiles []string - dirs []string -} - -func parseVersionDirState(entries []fs.DirEntry) versionDirState { - var info versionDirState - for _, e := range entries { - if e.Type().IsDir() { - info.dirs = append(info.dirs, e.Name()) - continue - } - if e.Type().IsRegular() || e.Type() == fs.ModeIrregular { - if e.Name() == inventoryFile { - info.hasInventory = true - continue - } - if strings.HasPrefix(e.Name(), inventoryFile+".") && info.sidecarAlg == "" { - info.sidecarAlg = strings.TrimPrefix(e.Name(), inventoryFile+".") - continue - } - } - // unexpected files - info.extraFiles = append(info.extraFiles, e.Name()) - } - return info -} - -func (imp OCFL) compareVersionInventory(obj ocfl.ReadObject, dirNum ocfl.VNum, verInv ocfl.ReadInventory, vldr *ocfl.ObjectValidation) { - rootInv := obj.Inventory() - vSpec := imp.spec - if verInv.Head() == rootInv.Head() && verInv.Digest() != rootInv.Digest() { - err := fmt.Errorf("%s/inventor.json is not the same as the root inventory: digests don't match", dirNum) - vldr.AddFatal(ec(err, codes.E064(vSpec))) - } - if verInv.ID() != rootInv.ID() { - err := fmt.Errorf("%s/inventory.json: 'id' doesn't match value in root inventory", dirNum) - vldr.AddFatal(ec(err, codes.E037(vSpec))) - } - if verInv.ContentDirectory() != rootInv.ContentDirectory() { - err := fmt.Errorf("%s/inventory.json: 'contentDirectory' doesn't match value in root inventory", dirNum) - vldr.AddFatal(ec(err, codes.E019(vSpec))) - } - // check that all version blocks in the version inventory - // match version blocks in the root inventory - for _, v := range verInv.Head().Lineage() { - thisVersion := verInv.Version(v.Num()) - rootVersion := rootInv.Version(v.Num()) - if rootVersion == nil { - err := fmt.Errorf("root inventory.json has missing version: %s", v) - vldr.AddFatal(ec(err, codes.E046(vSpec))) - continue - } - thisVerState := logicalState{ - state: thisVersion.State(), - manifest: verInv.Manifest(), - } - rootVerState := logicalState{ - state: rootVersion.State(), - manifest: rootInv.Manifest(), - } - if !thisVerState.Eq(rootVerState) { - err := fmt.Errorf("%s/inventory.json has different logical state in its %s version block than the root inventory.json", dirNum, v) - vldr.AddFatal(ec(err, codes.E066(vSpec))) - } - if thisVersion.Message() != rootVersion.Message() { - err := fmt.Errorf("%s/inventory.json has different 'message' in its %s version block than the root inventory.json", dirNum, v) - vldr.AddWarn(ec(err, codes.W011(vSpec))) - } - if !reflect.DeepEqual(thisVersion.User(), rootVersion.User()) { - err := fmt.Errorf("%s/inventory.json has different 'user' in its %s version block than the root inventory.json", dirNum, v) - vldr.AddWarn(ec(err, codes.W011(vSpec))) - } - if thisVersion.Created() != rootVersion.Created() { - err := fmt.Errorf("%s/inventory.json has different 'created' in its %s version block than the root inventory.json", dirNum, v) - vldr.AddWarn(ec(err, codes.W011(vSpec))) - } - } -} - -// newContentMap returns a DigestMap that is a subset of the inventory -// manifest for the digests and paths of new content -func newContentMap(inv *Inventory) (ocfl.DigestMap, error) { - pm := ocfl.PathMap{} - var err error - inv.Manifest.EachPath(func(pth, dig string) bool { - // ignore manifest entries from previous versions - if !strings.HasPrefix(pth, inv.Head.String()+"/") { - return true - } - if _, exists := pm[pth]; exists { - err = fmt.Errorf("path duplicate in manifest: %q", pth) - return false - } - pm[pth] = dig - return true - }) - if err != nil { - return nil, err - } - return pm.DigestMapValid() -} - -type copyContentOpts struct { - Source ocfl.ContentSource - DestFS ocfl.WriteFS - DestRoot string - Manifest ocfl.DigestMap - Concurrency int -} - -// transfer dst/src names in files from srcFS to dstFS -func copyContent(ctx context.Context, c *copyContentOpts) error { - if c.Source == nil { - return errors.New("missing countent source") - } - conc := c.Concurrency - if conc < 1 { - conc = 1 - } - grp, ctx := errgroup.WithContext(ctx) - grp.SetLimit(conc) - for dig, dstNames := range c.Manifest { - srcFS, srcPath := c.Source.GetContent(dig) - if srcFS == nil { - return fmt.Errorf("content source doesn't provide %q", dig) - } - for _, dstName := range dstNames { - srcPath := srcPath - dstPath := path.Join(c.DestRoot, dstName) - grp.Go(func() error { - return ocfl.Copy(ctx, c.DestFS, dstPath, srcFS, srcPath) - }) - - } - } - return grp.Wait() -} - -func ec(err error, code *ocfl.ValidationCode) error { - if code == nil { - return err - } - return &ocfl.ValidationError{ - Err: err, - ValidationCode: *code, - } -} - -func validateRootState(ocflV ocfl.Spec, state *ocfl.ObjectState) *ocfl.Validation { - v := &ocfl.Validation{} - for _, name := range state.Invalid { - err := fmt.Errorf(`%w: %s`, ErrObjRootStructure, name) - v.AddFatal(ec(err, codes.E001(ocflV))) - } - if !state.HasInventory() { - err := fmt.Errorf(`root inventory.json: %w`, fs.ErrNotExist) - v.AddFatal(ec(err, codes.E063(ocflV))) - } - if !state.HasSidecar() { - err := fmt.Errorf(`root inventory.json sidecar: %w`, fs.ErrNotExist) - v.AddFatal(ec(err, codes.E058(ocflV))) - } - err := state.VersionDirs.Valid() - if err != nil { - if errors.Is(err, ocfl.ErrVerEmpty) { - err = ec(err, codes.E008(ocflV)) - } else if errors.Is(err, ocfl.ErrVNumPadding) { - err = ec(err, codes.E011(ocflV)) - } else if errors.Is(err, ocfl.ErrVNumMissing) { - err = ec(err, codes.E010(ocflV)) - } - v.AddFatal(err) - } - if err == nil && state.VersionDirs.Padding() > 0 { - err := errors.New("version directory names are zero-padded") - v.AddWarn(ec(err, codes.W001(ocflV))) - } - // if vdirHead := state.VersionDirs.Head().Num(); vdirHead > o.inv.Head.Num() { - // err := errors.New("version directories don't reflect versions in inventory.json") - // v.AddFatal(ec(err, codes.E046(ocflV))) - // } - return v -} - -func validateExtensionsDir(ctx context.Context, ocflV ocfl.Spec, fsys ocfl.FS, objDir string) *ocfl.Validation { - v := &ocfl.Validation{} - extDir := path.Join(objDir, extensionsDir) - items, err := fsys.ReadDir(ctx, extDir) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return nil - } - v.AddFatal(err) - return v - } - for _, i := range items { - if !i.IsDir() { - err := fmt.Errorf(`invalid file: %s`, i.Name()) - v.AddFatal(ec(err, codes.E067(ocflV))) - continue - } - _, err := extension.Get(i.Name()) - if err != nil { - // unknow extension - err := fmt.Errorf("%w: %s", err, i.Name()) - v.AddWarn(ec(err, codes.W013(ocflV))) - } - } - return v -} diff --git a/root.go b/root.go index 711b36b0..1a25b725 100644 --- a/root.go +++ b/root.go @@ -27,7 +27,6 @@ var ErrLayoutUndefined = errors.New("storage root's layout is undefined") type Root struct { fs FS // root's fs dir string // root's director relative to FS - global Config // shared OCFL settings spec Spec // OCFL spec version in storage root declaration layout extension.Layout // layout used to resolve object ids layoutConfig map[string]string // contents of `ocfl_layout.json` @@ -65,7 +64,7 @@ func NewRoot(ctx context.Context, fsys FS, dir string, opts ...RootOption) (*Roo if err != nil { return nil, fmt.Errorf("not an OCFL storage root: %w", err) } - if _, err := r.global.GetSpec(decl.Version); err != nil { + if _, err := getOCFL(decl.Version); err != nil { return nil, fmt.Errorf(" OCFL v%s: %w", decl.Version, err) } // initialize existing Root @@ -194,12 +193,12 @@ func (r *Root) Spec() Spec { // ValidateObject validates the object with the given id. If the id cannot be // resolved, the error is reported as a fatal error in the returned // *ObjectValidation. -func (r *Root) ValidateObject(ctx context.Context, id string, opts ...ObjectValidationOption) (v *ObjectValidation) { +func (r *Root) ValidateObject(ctx context.Context, id string, opts ...ObjectValidationOption) *ObjectValidation { objPath, err := r.ResolveID(id) if err != nil { - v = NewObjectValidation(opts...) + v := newObjectValidation(r.fs, path.Join(r.dir, objPath), opts...) v.AddFatal(err) - return + return v } return r.ValidateObjectDir(ctx, objPath, opts...) } @@ -216,7 +215,7 @@ func (r *Root) init(ctx context.Context) error { if r.initArgs.spec.Empty() { return errors.New("can't initialize storage root: missing OCFL spec version") } - if _, err := r.global.GetSpec(r.initArgs.spec); err != nil { + if _, err := getOCFL(r.initArgs.spec); err != nil { return fmt.Errorf(" OCFL v%s: %w", r.initArgs.spec, err) } writeFS, isWriteFS := r.fs.(WriteFS) @@ -339,7 +338,7 @@ func (r *Root) setLayout(ctx context.Context, layout extension.Layout, desc stri // extensions directory. The value is unmarshalled into the value pointed to by // ext. If the extension config does not exist, nil is returned. func readExtensionConfig(ctx context.Context, fsys FS, root string, name string) (extension.Extension, error) { - confPath := path.Join(root, ExtensionsDir, name, extensionConfigFile) + confPath := path.Join(root, extensionsDir, name, extensionConfigFile) f, err := fsys.OpenFile(ctx, confPath) if err != nil { return nil, fmt.Errorf("can't open config for extension %s: %w", name, err) @@ -355,7 +354,7 @@ func readExtensionConfig(ctx context.Context, fsys FS, root string, name string) // writeExtensionConfig writes the configuration files for the ext to the // extensions directory in the storage root with at root. func writeExtensionConfig(ctx context.Context, fsys WriteFS, root string, config extension.Extension) error { - confPath := path.Join(root, ExtensionsDir, config.Name(), extensionConfigFile) + confPath := path.Join(root, extensionsDir, config.Name(), extensionConfigFile) b, err := json.Marshal(config) if err != nil { return fmt.Errorf("encoding config for extension %s: %w", config.Name(), err) diff --git a/root_test.go b/root_test.go index d7ee4015..0fe2ec7f 100644 --- a/root_test.go +++ b/root_test.go @@ -11,11 +11,9 @@ import ( "github.com/srerickson/ocfl-go/backend/local" "github.com/srerickson/ocfl-go/digest" "github.com/srerickson/ocfl-go/extension" - "github.com/srerickson/ocfl-go/ocflv1" ) func TestRoot(t *testing.T) { - ocflv1.Enable() ctx := context.Background() t.Run("fixture reg-extension-dir-root", func(t *testing.T) { diff --git a/spec.go b/spec.go index 51377281..0cb4acf5 100644 --- a/spec.go +++ b/spec.go @@ -13,9 +13,6 @@ import ( ) const ( - Spec1_0 = Spec("1.0") - Spec1_1 = Spec("1.1") - invTypePrefix = "https://ocfl.io/" invTypeSuffix = "/spec/#inventory" specsDir = "specs" @@ -98,9 +95,9 @@ func (s Spec) parse() (float64, string, error) { return val, suffix, nil } -// AsInvType returns n as an InventoryType -func (s Spec) AsInvType() InvType { - return InvType{Spec: s} +// InventoryType returns n as an InventoryType +func (s Spec) InventoryType() InventoryType { + return InventoryType{Spec: s} } func WriteSpecFile(ctx context.Context, fsys WriteFS, dir string, n Spec) (string, error) { @@ -130,17 +127,17 @@ func WriteSpecFile(ctx context.Context, fsys WriteFS, dir string, n Spec) (strin return dst, nil } -// InvType represents an inventory type string +// InventoryType represents an inventory type string // for example: https://ocfl.io/1.0/spec/#inventory -type InvType struct { +type InventoryType struct { Spec } -func (inv InvType) String() string { +func (inv InventoryType) String() string { return invTypePrefix + string(inv.Spec) + invTypeSuffix } -func (invT *InvType) UnmarshalText(t []byte) error { +func (invT *InventoryType) UnmarshalText(t []byte) error { cut := strings.TrimPrefix(string(t), invTypePrefix) cut = strings.TrimSuffix(cut, invTypeSuffix) if err := Spec(cut).Valid(); err != nil { @@ -150,6 +147,6 @@ func (invT *InvType) UnmarshalText(t []byte) error { return nil } -func (invT InvType) MarshalText() ([]byte, error) { +func (invT InventoryType) MarshalText() ([]byte, error) { return []byte(invT.String()), nil } diff --git a/spec_test.go b/spec_test.go index 78092465..f514b2c8 100644 --- a/spec_test.go +++ b/spec_test.go @@ -88,7 +88,7 @@ func TestParseInventoryType(t *testing.T) { } for i, tcase := range testCases { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { - inv := ocfl.InvType{} + inv := ocfl.InventoryType{} err := inv.UnmarshalText([]byte(tcase.in)) if tcase.valid { be.NilErr(t, err) diff --git a/validation.go b/validation.go index 2bb71c15..05670273 100644 --- a/validation.go +++ b/validation.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/go-multierror" "github.com/srerickson/ocfl-go/digest" + "github.com/srerickson/ocfl-go/validation" ) // Validation represents multiple fatal errors and warning errors. @@ -73,7 +74,10 @@ func (v *Validation) WarnErrors() []error { // ObjectValidation is used to configure and track results from an object validation process. type ObjectValidation struct { Validation - globals Config + obj *Object + + // set with option + objOptions []ObjectOption logger *slog.Logger skipDigests bool concurrency int @@ -81,15 +85,16 @@ type ObjectValidation struct { algRegistry digest.AlgorithmRegistry } -// NewObjectValidation constructs a new *Validation with the given +// newObjectValidation constructs a new *Validation with the given // options -func NewObjectValidation(opts ...ObjectValidationOption) *ObjectValidation { +func newObjectValidation(fsys FS, dir string, opts ...ObjectValidationOption) *ObjectValidation { v := &ObjectValidation{ algRegistry: digest.DefaultRegistry(), } for _, opt := range opts { opt(v) } + v.obj = newObject(fsys, dir, v.objOptions...) return v } @@ -126,7 +131,7 @@ func (v *ObjectValidation) AddFatal(errs ...error) { var validErr *ValidationError switch { case errors.As(err, &validErr): - v.logger.Error(err.Error(), "ocfl_code", validErr.ValidationCode.Code) + v.logger.Error(err.Error(), "ocfl_code", validErr.Code) default: v.logger.Error(err.Error()) } @@ -144,31 +149,60 @@ func (v *ObjectValidation) AddWarn(errs ...error) { var validErr *ValidationError switch { case errors.As(err, &validErr): - v.logger.Warn(err.Error(), "ocfl_code", validErr.ValidationCode.Code) + v.logger.Warn(err.Error(), "ocfl_code", validErr.Code) default: v.logger.Warn(err.Error()) } } } -// AddExistingContent sets the existence status for a content file in the +// Logger returns the validation's logger, which is nil by default. +func (v *ObjectValidation) Logger() *slog.Logger { + return v.logger +} + +// SkipDigests returns true if the validation is configured to skip digest +// checks. It is false by default. +func (v *ObjectValidation) SkipDigests() bool { + return v.skipDigests +} + +// DigestConcurrency returns the configured number of go routines used to read +// and digest contents during validation. The default value is runtime.NumCPU(). +func (v *ObjectValidation) DigestConcurrency() int { + if v.concurrency > 0 { + return v.concurrency + } + return runtime.NumCPU() +} + +// ValidationAlgorithms returns the registry of digest algoriths +// the object validation is configured to use. The default value is +// digest.DefaultRegistry +func (v *ObjectValidation) ValidationAlgorithms() digest.AlgorithmRegistry { + return v.algRegistry +} + +// addExistingContent sets the existence status for a content file in the // validation state. -func (v *ObjectValidation) AddExistingContent(name string) { +func (v *ObjectValidation) addExistingContent(name string) { if v.files == nil { v.files = map[string]*validationFileInfo{} } if v.files[name] == nil { v.files[name] = &validationFileInfo{} } - v.files[name].exists = true + v.files[name].fileExists = true } -// AddInventoryDigests adds digests from the inventory's manifest and fixity -// entries to the object validation for later verification. An error is returned -// if any name/digests entries in the inventory conflic with an existing -// name/digest entry already added to the object validation. The returned error -// wraps a slice of *DigestError values. -func (v *ObjectValidation) AddInventoryDigests(inv ReadInventory) error { +// addInventory adds digests from the inventory's manifest and fixity entries to +// the object validation for later verification. An error is returned if any +// name/digests entries in the inventory conflict with previously added values. +// The returned error wraps a slice of *DigestError values. Errors *are not* +// automatically added to the validation's Fatal errors. +// +// If isRoot is true, v.object's is set to inv +func (v *ObjectValidation) addInventory(inv Inventory, isRoot bool) error { if v.files == nil { v.files = map[string]*validationFileInfo{} } @@ -177,18 +211,18 @@ func (v *ObjectValidation) AddInventoryDigests(inv ReadInventory) error { inv.Manifest().EachPath(func(name string, primaryDigest string) bool { allDigests := inv.GetFixity(primaryDigest) allDigests[primaryAlg.ID()] = primaryDigest - current := v.files[name] - if current == nil { + existing := v.files[name] + if existing == nil { v.files[name] = &validationFileInfo{ - expected: allDigests, + expectedDigests: allDigests, } return true } - if current.expected == nil { - current.expected = allDigests + if existing.expectedDigests == nil { + existing.expectedDigests = allDigests return true } - if err := current.expected.Add(allDigests); err != nil { + if err := existing.expectedDigests.Add(allDigests); err != nil { var digestError *digest.DigestError if errors.As(err, &digestError) { digestError.Path = name @@ -197,21 +231,32 @@ func (v *ObjectValidation) AddInventoryDigests(inv ReadInventory) error { } return true }) - return allErrors.ErrorOrNil() -} - -// Logger returns the validation's logger, which is nil by default. -func (v *ObjectValidation) Logger() *slog.Logger { - return v.logger + if err := allErrors.ErrorOrNil(); err != nil { + return err + } + if isRoot { + v.obj.setInventory(inv) + } + return nil } -// MissingContent returns an iterator the yields the names of files that appear -// in an inventory added to the validation but were not marked as existing. -func (v *ObjectValidation) MissingContent() iter.Seq[string] { - return func(yield func(string) bool) { +// existingContent digests returns an iterator that yields the names and digests +// of files that exist and were referenced in the inventory added to the +// valiation. +func (v *ObjectValidation) existingContentDigests(fsys FS, objPath string, alg digest.Algorithm) FileDigestsSeq { + return func(yield func(*FileDigests) bool) { for name, entry := range v.files { - if !entry.exists && len(entry.expected) > 0 { - if !yield(name) { + if entry.fileExists && len(entry.expectedDigests) > 0 { + fd := &FileDigests{ + FileRef: FileRef{ + FS: fsys, + BaseDir: objPath, + Path: name, + }, + Algorithm: alg, + Digests: entry.expectedDigests, + } + if !yield(fd) { return } } @@ -219,27 +264,16 @@ func (v *ObjectValidation) MissingContent() iter.Seq[string] { } } -// SkipDigests returns true if the validation is configured to skip digest -// checks. It is false by default. -func (v *ObjectValidation) SkipDigests() bool { - return v.skipDigests -} +func (v *ObjectValidation) fs() FS { return v.obj.fs } -// DigestConcurrency returns the configured number of go routines used to read -// and digest contents during validation. The default value is runtime.NumCPU(). -func (v *ObjectValidation) DigestConcurrency() int { - if v.concurrency > 0 { - return v.concurrency - } - return runtime.NumCPU() -} +func (v *ObjectValidation) path() string { return v.obj.path } -// UnexpectedContent returns an iterator that yields the names of existing files -// that were not included in an inventory manifest. -func (v *ObjectValidation) UnexpectedContent() iter.Seq[string] { +// missingContent returns an iterator the yields the names of files that appear +// in an inventory added to the validation but were not marked as existing. +func (v *ObjectValidation) missingContent() iter.Seq[string] { return func(yield func(string) bool) { for name, entry := range v.files { - if entry.exists && len(entry.expected) == 0 { + if !entry.fileExists && len(entry.expectedDigests) > 0 { if !yield(name) { return } @@ -248,23 +282,13 @@ func (v *ObjectValidation) UnexpectedContent() iter.Seq[string] { } } -// ExistingContent digests returns an iterator that yields the names and digests -// of files that exist and were referenced in the inventory added to the -// valiation. -func (v *ObjectValidation) ExistingContentDigests(fsys FS, objPath string, alg digest.Algorithm) FileDigestsSeq { - return func(yield func(*FileDigests) bool) { +// unexpectedContent returns an iterator that yields the names of existing files +// that were not included in an inventory manifest. +func (v *ObjectValidation) unexpectedContent() iter.Seq[string] { + return func(yield func(string) bool) { for name, entry := range v.files { - if entry.exists && len(entry.expected) > 0 { - fd := &FileDigests{ - FileRef: FileRef{ - FS: fsys, - BaseDir: objPath, - Path: name, - }, - Algorithm: alg, - Digests: entry.expected, - } - if !yield(fd) { + if entry.fileExists && len(entry.expectedDigests) == 0 { + if !yield(name) { return } } @@ -272,13 +296,6 @@ func (v *ObjectValidation) ExistingContentDigests(fsys FS, objPath string, alg d } } -// ValidationAlgorithms returns the registry of digest algoriths -// the object validation is configured to use. The default value is -// digest.DefaultRegistry -func (v *ObjectValidation) ValidationAlgorithms() digest.AlgorithmRegistry { - return v.algRegistry -} - type ObjectValidationOption func(*ObjectValidation) func ValidationSkipDigest() ObjectValidationOption { @@ -312,23 +329,14 @@ func ValidationAlgorithms(reg digest.AlgorithmRegistry) ObjectValidationOption { } type validationFileInfo struct { - expected digest.Set - exists bool -} - -// ValidationCode represents a validation error code defined in an -// OCFL specification. See https://ocfl.io/1.1/spec/validation-codes.html -type ValidationCode struct { - Spec Spec // OCFL spec that the code refers to - Code string // Validation error code from OCFL Spec - Description string // error description from spec - URL string // URL to the OCFL specification for the error + expectedDigests digest.Set + fileExists bool } // ValidationError is an error that includes a reference // to a validation error code from the OCFL spec. type ValidationError struct { - ValidationCode + validation.ValidationCode Err error } @@ -339,3 +347,14 @@ func (ver *ValidationError) Error() string { func (ver *ValidationError) Unwrap() error { return ver.Err } + +// helper for constructing new validation code +func verr(err error, code *validation.ValidationCode) error { + if code == nil { + return err + } + return &ValidationError{ + Err: err, + ValidationCode: *code, + } +} diff --git a/validation/code.go b/validation/code.go new file mode 100644 index 00000000..cd008cc4 --- /dev/null +++ b/validation/code.go @@ -0,0 +1,10 @@ +package validation + +// ValidationCode represents a validation error code defined in an +// OCFL specification. See https://ocfl.io/1.1/spec/validation-codes.html +type ValidationCode struct { + Spec string // OCFL spec version that the code refers to (e.g '1.1') + Code string // Validation error code from OCFL Spec + Description string // error description from spec + URL string // URL to the OCFL specification for the error +} diff --git a/ocflv1/codes/generate/codes-ocflv1.0.csv b/validation/code/codes-ocflv1.0.csv similarity index 100% rename from ocflv1/codes/generate/codes-ocflv1.0.csv rename to validation/code/codes-ocflv1.0.csv diff --git a/ocflv1/codes/generate/codes-ocflv1.1.csv b/validation/code/codes-ocflv1.1.csv similarity index 100% rename from ocflv1/codes/generate/codes-ocflv1.1.csv rename to validation/code/codes-ocflv1.1.csv diff --git a/validation/code/codes.go b/validation/code/codes.go new file mode 100644 index 00000000..bf0bd0b5 --- /dev/null +++ b/validation/code/codes.go @@ -0,0 +1,3 @@ +package code + +//go:generate go run generate.go diff --git a/ocflv1/codes/codes.go b/validation/code/codes_gen.go similarity index 80% rename from ocflv1/codes/codes.go rename to validation/code/codes_gen.go index 83afd8b9..a0ca7ab4 100644 --- a/ocflv1/codes/codes.go +++ b/validation/code/codes_gen.go @@ -1,21 +1,21 @@ -package codes +package code -// This is generated code. Do not modify. See gen folder. +// This is generated code. Do not modify. See generate.go -import "github.com/srerickson/ocfl-go" +import "github.com/srerickson/ocfl-go/validation" // E001: The OCFL Object Root must not contain files or directories other than those specified in the following sections. -func E001(spec ocfl.Spec) *ocfl.ValidationCode { +func E001(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E001", Description: "The OCFL Object Root must not contain files or directories other than those specified in the following sections.", URL: "https://ocfl.io/1.0/spec/#E001", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E001", Description: "The OCFL Object Root must not contain files or directories other than those specified in the following sections.", @@ -27,17 +27,17 @@ func E001(spec ocfl.Spec) *ocfl.ValidationCode { } // E002: The version declaration must be formatted according to the NAMASTE specification. -func E002(spec ocfl.Spec) *ocfl.ValidationCode { +func E002(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E002", Description: "The version declaration must be formatted according to the NAMASTE specification.", URL: "https://ocfl.io/1.0/spec/#E002", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E002", Description: "The version declaration must be formatted according to the NAMASTE specification.", @@ -49,17 +49,17 @@ func E002(spec ocfl.Spec) *ocfl.ValidationCode { } // E003: [The version declaration] must be a file in the base directory of the OCFL Object Root giving the OCFL version in the filename. -func E003(spec ocfl.Spec) *ocfl.ValidationCode { +func E003(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E003", Description: "[The version declaration] must be a file in the base directory of the OCFL Object Root giving the OCFL version in the filename.", URL: "https://ocfl.io/1.0/spec/#E003", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E003", Description: "There must be exactly one version declaration file in the base directory of the OCFL Object Root giving the OCFL version in the filename.", @@ -71,17 +71,17 @@ func E003(spec ocfl.Spec) *ocfl.ValidationCode { } // E004: The [version declaration] filename MUST conform to the pattern T=dvalue, where T must be 0, and dvalue must be ocfl_object_, followed by the OCFL specification ocfl.Number. -func E004(spec ocfl.Spec) *ocfl.ValidationCode { +func E004(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E004", Description: "The [version declaration] filename MUST conform to the pattern T=dvalue, where T must be 0, and dvalue must be ocfl_object_, followed by the OCFL specification ocfl.Number.", URL: "https://ocfl.io/1.0/spec/#E004", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E004", Description: "The [version declaration] filename MUST conform to the pattern T=dvalue, where T must be 0, and dvalue must be ocfl_object_, followed by the OCFL specification version number.", @@ -93,17 +93,17 @@ func E004(spec ocfl.Spec) *ocfl.ValidationCode { } // E005: The [version declaration] filename must conform to the pattern T=dvalue, where T MUST be 0, and dvalue must be ocfl_object_, followed by the OCFL specification ocfl.Number. -func E005(spec ocfl.Spec) *ocfl.ValidationCode { +func E005(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E005", Description: "The [version declaration] filename must conform to the pattern T=dvalue, where T MUST be 0, and dvalue must be ocfl_object_, followed by the OCFL specification ocfl.Number.", URL: "https://ocfl.io/1.0/spec/#E005", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E005", Description: "The [version declaration] filename must conform to the pattern T=dvalue, where T MUST be 0, and dvalue must be ocfl_object_, followed by the OCFL specification version number.", @@ -115,17 +115,17 @@ func E005(spec ocfl.Spec) *ocfl.ValidationCode { } // E006: The [version declaration] filename must conform to the pattern T=dvalue, where T must be 0, and dvalue MUST be ocfl_object_, followed by the OCFL specification ocfl.Number. -func E006(spec ocfl.Spec) *ocfl.ValidationCode { +func E006(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E006", Description: "The [version declaration] filename must conform to the pattern T=dvalue, where T must be 0, and dvalue MUST be ocfl_object_, followed by the OCFL specification ocfl.Number.", URL: "https://ocfl.io/1.0/spec/#E006", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E006", Description: "The [version declaration] filename must conform to the pattern T=dvalue, where T must be 0, and dvalue MUST be ocfl_object_, followed by the OCFL specification version number.", @@ -137,17 +137,17 @@ func E006(spec ocfl.Spec) *ocfl.ValidationCode { } // E007: The text contents of the [version declaration] file must be the same as dvalue, followed by a newline (\n). -func E007(spec ocfl.Spec) *ocfl.ValidationCode { +func E007(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E007", Description: "The text contents of the [version declaration] file must be the same as dvalue, followed by a newline (\n).", URL: "https://ocfl.io/1.0/spec/#E007", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E007", Description: "The text contents of the [version declaration] file must be the same as dvalue, followed by a newline (\n).", @@ -159,17 +159,17 @@ func E007(spec ocfl.Spec) *ocfl.ValidationCode { } // E008: OCFL Object content must be stored as a sequence of one or more versions. -func E008(spec ocfl.Spec) *ocfl.ValidationCode { +func E008(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E008", Description: "OCFL Object content must be stored as a sequence of one or more versions.", URL: "https://ocfl.io/1.0/spec/#E008", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E008", Description: "OCFL Object content must be stored as a sequence of one or more versions.", @@ -181,17 +181,17 @@ func E008(spec ocfl.Spec) *ocfl.ValidationCode { } // E009: The ocfl.Number sequence MUST start at 1 and must be continuous without missing integers. -func E009(spec ocfl.Spec) *ocfl.ValidationCode { +func E009(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E009", Description: "The ocfl.Number sequence MUST start at 1 and must be continuous without missing integers.", URL: "https://ocfl.io/1.0/spec/#E009", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E009", Description: "The version number sequence MUST start at 1 and must be continuous without missing integers.", @@ -203,17 +203,17 @@ func E009(spec ocfl.Spec) *ocfl.ValidationCode { } // E010: The ocfl.Number sequence must start at 1 and MUST be continuous without missing integers. -func E010(spec ocfl.Spec) *ocfl.ValidationCode { +func E010(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E010", Description: "The ocfl.Number sequence must start at 1 and MUST be continuous without missing integers.", URL: "https://ocfl.io/1.0/spec/#E010", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E010", Description: "The version number sequence must start at 1 and MUST be continuous without missing integers.", @@ -225,17 +225,17 @@ func E010(spec ocfl.Spec) *ocfl.ValidationCode { } // E011: If zero-padded version directory numbers are used then they must start with the prefix v and then a zero. -func E011(spec ocfl.Spec) *ocfl.ValidationCode { +func E011(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E011", Description: "If zero-padded version directory numbers are used then they must start with the prefix v and then a zero.", URL: "https://ocfl.io/1.0/spec/#E011", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E011", Description: "If zero-padded version directory numbers are used then they must start with the prefix v and then a zero.", @@ -247,17 +247,17 @@ func E011(spec ocfl.Spec) *ocfl.ValidationCode { } // E012: All version directories of an object must use the same naming convention: either a non-padded version directory number, or a zero-padded version directory number of consistent length. -func E012(spec ocfl.Spec) *ocfl.ValidationCode { +func E012(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E012", Description: "All version directories of an object must use the same naming convention: either a non-padded version directory number, or a zero-padded version directory number of consistent length.", URL: "https://ocfl.io/1.0/spec/#E012", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E012", Description: "All version directories of an object must use the same naming convention: either a non-padded version directory number, or a zero-padded version directory number of consistent length.", @@ -269,17 +269,17 @@ func E012(spec ocfl.Spec) *ocfl.ValidationCode { } // E013: Operations that add a new version to an object must follow the version directory naming convention established by earlier versions. -func E013(spec ocfl.Spec) *ocfl.ValidationCode { +func E013(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E013", Description: "Operations that add a new version to an object must follow the version directory naming convention established by earlier versions.", URL: "https://ocfl.io/1.0/spec/#E013", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E013", Description: "Operations that add a new version to an object must follow the version directory naming convention established by earlier versions.", @@ -291,17 +291,17 @@ func E013(spec ocfl.Spec) *ocfl.ValidationCode { } // E014: In all cases, references to files inside version directories from inventory files must use the actual version directory names. -func E014(spec ocfl.Spec) *ocfl.ValidationCode { +func E014(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E014", Description: "In all cases, references to files inside version directories from inventory files must use the actual version directory names.", URL: "https://ocfl.io/1.0/spec/#E014", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E014", Description: "In all cases, references to files inside version directories from inventory files must use the actual version directory names.", @@ -313,17 +313,17 @@ func E014(spec ocfl.Spec) *ocfl.ValidationCode { } // E015: There must be no other files as children of a version directory, other than an inventory file and a inventory digest. -func E015(spec ocfl.Spec) *ocfl.ValidationCode { +func E015(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E015", Description: "There must be no other files as children of a version directory, other than an inventory file and a inventory digest.", URL: "https://ocfl.io/1.0/spec/#E015", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E015", Description: "There must be no other files as children of a version directory, other than an inventory file and a inventory digest.", @@ -335,17 +335,17 @@ func E015(spec ocfl.Spec) *ocfl.ValidationCode { } // E016: Version directories must contain a designated content sub-directory if the version contains files to be preserved, and should not contain this sub-directory otherwise. -func E016(spec ocfl.Spec) *ocfl.ValidationCode { +func E016(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E016", Description: "Version directories must contain a designated content sub-directory if the version contains files to be preserved, and should not contain this sub-directory otherwise.", URL: "https://ocfl.io/1.0/spec/#E016", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E016", Description: "Version directories must contain a designated content sub-directory if the version contains files to be preserved, and should not contain this sub-directory otherwise.", @@ -357,17 +357,17 @@ func E016(spec ocfl.Spec) *ocfl.ValidationCode { } // E017: The contentDirectory value MUST NOT contain the forward slash (/) path separator and must not be either one or two periods (. or ..). -func E017(spec ocfl.Spec) *ocfl.ValidationCode { +func E017(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E017", Description: "The contentDirectory value MUST NOT contain the forward slash (/) path separator and must not be either one or two periods (. or ..).", URL: "https://ocfl.io/1.0/spec/#E017", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E017", Description: "The contentDirectory value MUST NOT contain the forward slash (/) path separator and must not be either one or two periods (. or ..).", @@ -379,17 +379,17 @@ func E017(spec ocfl.Spec) *ocfl.ValidationCode { } // E018: The contentDirectory value must not contain the forward slash (/) path separator and MUST NOT be either one or two periods (. or ..). -func E018(spec ocfl.Spec) *ocfl.ValidationCode { +func E018(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E018", Description: "The contentDirectory value must not contain the forward slash (/) path separator and MUST NOT be either one or two periods (. or ..).", URL: "https://ocfl.io/1.0/spec/#E018", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E018", Description: "The contentDirectory value must not contain the forward slash (/) path separator and MUST NOT be either one or two periods (. or ..).", @@ -401,17 +401,17 @@ func E018(spec ocfl.Spec) *ocfl.ValidationCode { } // E019: If the key contentDirectory is set, it MUST be set in the first version of the object and must not change between versions of the same object. -func E019(spec ocfl.Spec) *ocfl.ValidationCode { +func E019(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E019", Description: "If the key contentDirectory is set, it MUST be set in the first version of the object and must not change between versions of the same object.", URL: "https://ocfl.io/1.0/spec/#E019", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E019", Description: "If the key contentDirectory is set, it MUST be set in the first version of the object and must not change between versions of the same object.", @@ -423,17 +423,17 @@ func E019(spec ocfl.Spec) *ocfl.ValidationCode { } // E020: If the key contentDirectory is set, it must be set in the first version of the object and MUST NOT change between versions of the same object. -func E020(spec ocfl.Spec) *ocfl.ValidationCode { +func E020(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E020", Description: "If the key contentDirectory is set, it must be set in the first version of the object and MUST NOT change between versions of the same object.", URL: "https://ocfl.io/1.0/spec/#E020", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E020", Description: "If the key contentDirectory is set, it must be set in the first version of the object and MUST NOT change between versions of the same object.", @@ -445,17 +445,17 @@ func E020(spec ocfl.Spec) *ocfl.ValidationCode { } // E021: If the key contentDirectory is not present in the inventory file then the name of the designated content sub-directory must be content. -func E021(spec ocfl.Spec) *ocfl.ValidationCode { +func E021(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E021", Description: "If the key contentDirectory is not present in the inventory file then the name of the designated content sub-directory must be content.", URL: "https://ocfl.io/1.0/spec/#E021", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E021", Description: "If the key contentDirectory is not present in the inventory file then the name of the designated content sub-directory must be content.", @@ -467,17 +467,17 @@ func E021(spec ocfl.Spec) *ocfl.ValidationCode { } // E022: OCFL-compliant tools (including any validators) must ignore all directories in the object version directory except for the designated content directory. -func E022(spec ocfl.Spec) *ocfl.ValidationCode { +func E022(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E022", Description: "OCFL-compliant tools (including any validators) must ignore all directories in the object version directory except for the designated content directory.", URL: "https://ocfl.io/1.0/spec/#E022", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E022", Description: "OCFL-compliant tools (including any validators) must ignore all directories in the object version directory except for the designated content directory.", @@ -489,17 +489,17 @@ func E022(spec ocfl.Spec) *ocfl.ValidationCode { } // E023: Every file within a version's content directory must be referenced in the manifest section of the inventory. -func E023(spec ocfl.Spec) *ocfl.ValidationCode { +func E023(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E023", Description: "Every file within a version's content directory must be referenced in the manifest section of the inventory.", URL: "https://ocfl.io/1.0/spec/#E023", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E023", Description: "Every file within a version's content directory must be referenced in the manifest section of the inventory.", @@ -511,17 +511,17 @@ func E023(spec ocfl.Spec) *ocfl.ValidationCode { } // E024: There must not be empty directories within a version's content directory. -func E024(spec ocfl.Spec) *ocfl.ValidationCode { +func E024(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E024", Description: "There must not be empty directories within a version's content directory.", URL: "https://ocfl.io/1.0/spec/#E024", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E024", Description: "There must not be empty directories within a version's content directory.", @@ -533,17 +533,17 @@ func E024(spec ocfl.Spec) *ocfl.ValidationCode { } // E025: For content-addressing, OCFL Objects must use either sha512 or sha256, and should use sha512. -func E025(spec ocfl.Spec) *ocfl.ValidationCode { +func E025(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E025", Description: "For content-addressing, OCFL Objects must use either sha512 or sha256, and should use sha512.", URL: "https://ocfl.io/1.0/spec/#E025", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E025", Description: "For content-addressing, OCFL Objects must use either sha512 or sha256, and should use sha512.", @@ -555,17 +555,17 @@ func E025(spec ocfl.Spec) *ocfl.ValidationCode { } // E026: For storage of additional fixity values, or to support legacy content migration, implementers must choose from the following controlled vocabulary of digest algorithms, or from a list of additional algorithms given in the [Digest-Algorithms-Extension]. -func E026(spec ocfl.Spec) *ocfl.ValidationCode { +func E026(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E026", Description: "For storage of additional fixity values, or to support legacy content migration, implementers must choose from the following controlled vocabulary of digest algorithms, or from a list of additional algorithms given in the [Digest-Algorithms-Extension].", URL: "https://ocfl.io/1.0/spec/#E026", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E026", Description: "For storage of additional fixity values, or to support legacy content migration, implementers must choose from the following controlled vocabulary of digest algorithms, or from a list of additional algorithms given in the [Digest-Algorithms-Extension].", @@ -577,17 +577,17 @@ func E026(spec ocfl.Spec) *ocfl.ValidationCode { } // E027: OCFL clients must support all fixity algorithms given in the table below, and may support additional algorithms from the extensions. -func E027(spec ocfl.Spec) *ocfl.ValidationCode { +func E027(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E027", Description: "OCFL clients must support all fixity algorithms given in the table below, and may support additional algorithms from the extensions.", URL: "https://ocfl.io/1.0/spec/#E027", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E027", Description: "OCFL clients must support all fixity algorithms given in the table below, and may support additional algorithms from the extensions.", @@ -599,17 +599,17 @@ func E027(spec ocfl.Spec) *ocfl.ValidationCode { } // E028: Optional fixity algorithms that are not supported by a client must be ignored by that client. -func E028(spec ocfl.Spec) *ocfl.ValidationCode { +func E028(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E028", Description: "Optional fixity algorithms that are not supported by a client must be ignored by that client.", URL: "https://ocfl.io/1.0/spec/#E028", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E028", Description: "Optional fixity algorithms that are not supported by a client must be ignored by that client.", @@ -621,17 +621,17 @@ func E028(spec ocfl.Spec) *ocfl.ValidationCode { } // E029: SHA-1 algorithm defined by [FIPS-180-4] and must be encoded using hex (base16) encoding [RFC4648]. -func E029(spec ocfl.Spec) *ocfl.ValidationCode { +func E029(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E029", Description: "SHA-1 algorithm defined by [FIPS-180-4] and must be encoded using hex (base16) encoding [RFC4648].", URL: "https://ocfl.io/1.0/spec/#E029", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E029", Description: "SHA-1 algorithm defined by [FIPS-180-4] and must be encoded using hex (base16) encoding [RFC4648].", @@ -643,17 +643,17 @@ func E029(spec ocfl.Spec) *ocfl.ValidationCode { } // E030: SHA-256 algorithm defined by [FIPS-180-4] and must be encoded using hex (base16) encoding [RFC4648]. -func E030(spec ocfl.Spec) *ocfl.ValidationCode { +func E030(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E030", Description: "SHA-256 algorithm defined by [FIPS-180-4] and must be encoded using hex (base16) encoding [RFC4648].", URL: "https://ocfl.io/1.0/spec/#E030", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E030", Description: "SHA-256 algorithm defined by [FIPS-180-4] and must be encoded using hex (base16) encoding [RFC4648].", @@ -665,17 +665,17 @@ func E030(spec ocfl.Spec) *ocfl.ValidationCode { } // E031: SHA-512 algorithm defined by [FIPS-180-4] and must be encoded using hex (base16) encoding [RFC4648]. -func E031(spec ocfl.Spec) *ocfl.ValidationCode { +func E031(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E031", Description: "SHA-512 algorithm defined by [FIPS-180-4] and must be encoded using hex (base16) encoding [RFC4648].", URL: "https://ocfl.io/1.0/spec/#E031", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E031", Description: "SHA-512 algorithm defined by [FIPS-180-4] and must be encoded using hex (base16) encoding [RFC4648].", @@ -687,17 +687,17 @@ func E031(spec ocfl.Spec) *ocfl.ValidationCode { } // E032: [blake2b-512] must be encoded using hex (base16) encoding [RFC4648]. -func E032(spec ocfl.Spec) *ocfl.ValidationCode { +func E032(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E032", Description: "[blake2b-512] must be encoded using hex (base16) encoding [RFC4648].", URL: "https://ocfl.io/1.0/spec/#E032", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E032", Description: "[blake2b-512] must be encoded using hex (base16) encoding [RFC4648].", @@ -709,17 +709,17 @@ func E032(spec ocfl.Spec) *ocfl.ValidationCode { } // E033: An OCFL Object Inventory MUST follow the [JSON] structure described in this section and must be named inventory.json. -func E033(spec ocfl.Spec) *ocfl.ValidationCode { +func E033(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E033", Description: "An OCFL Object Inventory MUST follow the [JSON] structure described in this section and must be named inventory.json.", URL: "https://ocfl.io/1.0/spec/#E033", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E033", Description: "An OCFL Object Inventory MUST follow the [JSON] structure described in this section and must be named inventory.json.", @@ -731,17 +731,17 @@ func E033(spec ocfl.Spec) *ocfl.ValidationCode { } // E034: An OCFL Object Inventory must follow the [JSON] structure described in this section and MUST be named inventory.json. -func E034(spec ocfl.Spec) *ocfl.ValidationCode { +func E034(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E034", Description: "An OCFL Object Inventory must follow the [JSON] structure described in this section and MUST be named inventory.json.", URL: "https://ocfl.io/1.0/spec/#E034", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E034", Description: "An OCFL Object Inventory must follow the [JSON] structure described in this section and MUST be named inventory.json.", @@ -753,17 +753,17 @@ func E034(spec ocfl.Spec) *ocfl.ValidationCode { } // E035: The forward slash (/) path separator must be used in content paths in the manifest and fixity blocks within the inventory. -func E035(spec ocfl.Spec) *ocfl.ValidationCode { +func E035(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E035", Description: "The forward slash (/) path separator must be used in content paths in the manifest and fixity blocks within the inventory.", URL: "https://ocfl.io/1.0/spec/#E035", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E035", Description: "The forward slash (/) path separator must be used in content paths in the manifest and fixity blocks within the inventory.", @@ -775,17 +775,17 @@ func E035(spec ocfl.Spec) *ocfl.ValidationCode { } // E036: An OCFL Object Inventory must include the following keys: [id, type, digestAlgorithm, head] -func E036(spec ocfl.Spec) *ocfl.ValidationCode { +func E036(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E036", Description: "An OCFL Object Inventory must include the following keys: [id, type, digestAlgorithm, head]", URL: "https://ocfl.io/1.0/spec/#E036", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E036", Description: "An OCFL Object Inventory must include the following keys: [id, type, digestAlgorithm, head]", @@ -797,17 +797,17 @@ func E036(spec ocfl.Spec) *ocfl.ValidationCode { } // E037: [id] must be unique in the local context, and should be a URI [RFC3986]. -func E037(spec ocfl.Spec) *ocfl.ValidationCode { +func E037(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E037", Description: "[id] must be unique in the local context, and should be a URI [RFC3986].", URL: "https://ocfl.io/1.0/spec/#E037", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E037", Description: "[id] must be unique in the local context, and should be a URI [RFC3986].", @@ -819,17 +819,17 @@ func E037(spec ocfl.Spec) *ocfl.ValidationCode { } // E038: In the object root inventory [the type value] must be the URI of the inventory section of the specification version matching the object conformance declaration. -func E038(spec ocfl.Spec) *ocfl.ValidationCode { +func E038(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E038", Description: "In the object root inventory [the type value] must be the URI of the inventory section of the specification version matching the object conformance declaration.", URL: "https://ocfl.io/1.0/spec/#E038", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E038", Description: "In the object root inventory [the type value] must be the URI of the inventory section of the specification version matching the object conformance declaration.", @@ -841,17 +841,17 @@ func E038(spec ocfl.Spec) *ocfl.ValidationCode { } // E039: [digestAlgorithm] must be the algorithm used in the manifest and state blocks. -func E039(spec ocfl.Spec) *ocfl.ValidationCode { +func E039(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E039", Description: "[digestAlgorithm] must be the algorithm used in the manifest and state blocks.", URL: "https://ocfl.io/1.0/spec/#E039", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E039", Description: "[digestAlgorithm] must be the algorithm used in the manifest and state blocks.", @@ -863,17 +863,17 @@ func E039(spec ocfl.Spec) *ocfl.ValidationCode { } // E040: [head] must be the version directory name with the highest ocfl.Number. -func E040(spec ocfl.Spec) *ocfl.ValidationCode { +func E040(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E040", Description: "[head] must be the version directory name with the highest ocfl.Number.", URL: "https://ocfl.io/1.0/spec/#E040", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E040", Description: "[head] must be the version directory name with the highest version number.", @@ -885,17 +885,17 @@ func E040(spec ocfl.Spec) *ocfl.ValidationCode { } // E041: In addition to these keys, there must be two other blocks present, manifest and versions, which are discussed in the next two sections. -func E041(spec ocfl.Spec) *ocfl.ValidationCode { +func E041(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E041", Description: "In addition to these keys, there must be two other blocks present, manifest and versions, which are discussed in the next two sections.", URL: "https://ocfl.io/1.0/spec/#E041", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E041", Description: "In addition to these keys, there must be two other blocks present, manifest and versions, which are discussed in the next two sections.", @@ -907,17 +907,17 @@ func E041(spec ocfl.Spec) *ocfl.ValidationCode { } // E042: Content paths within a manifest block must be relative to the OCFL Object Root. -func E042(spec ocfl.Spec) *ocfl.ValidationCode { +func E042(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E042", Description: "Content paths within a manifest block must be relative to the OCFL Object Root.", URL: "https://ocfl.io/1.0/spec/#E042", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E042", Description: "Content paths within a manifest block must be relative to the OCFL Object Root.", @@ -929,17 +929,17 @@ func E042(spec ocfl.Spec) *ocfl.ValidationCode { } // E043: An OCFL Object Inventory must include a block for storing versions. -func E043(spec ocfl.Spec) *ocfl.ValidationCode { +func E043(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E043", Description: "An OCFL Object Inventory must include a block for storing versions.", URL: "https://ocfl.io/1.0/spec/#E043", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E043", Description: "An OCFL Object Inventory must include a block for storing versions.", @@ -951,17 +951,17 @@ func E043(spec ocfl.Spec) *ocfl.ValidationCode { } // E044: This block MUST have the key of versions within the inventory, and it must be a JSON object. -func E044(spec ocfl.Spec) *ocfl.ValidationCode { +func E044(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E044", Description: "This block MUST have the key of versions within the inventory, and it must be a JSON object.", URL: "https://ocfl.io/1.0/spec/#E044", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E044", Description: "This block MUST have the key of versions within the inventory, and it must be a JSON object.", @@ -973,17 +973,17 @@ func E044(spec ocfl.Spec) *ocfl.ValidationCode { } // E045: This block must have the key of versions within the inventory, and it MUST be a JSON object. -func E045(spec ocfl.Spec) *ocfl.ValidationCode { +func E045(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E045", Description: "This block must have the key of versions within the inventory, and it MUST be a JSON object.", URL: "https://ocfl.io/1.0/spec/#E045", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E045", Description: "This block must have the key of versions within the inventory, and it MUST be a JSON object.", @@ -995,17 +995,17 @@ func E045(spec ocfl.Spec) *ocfl.ValidationCode { } // E046: The keys of [the versions object] must correspond to the names of the version directories used. -func E046(spec ocfl.Spec) *ocfl.ValidationCode { +func E046(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E046", Description: "The keys of [the versions object] must correspond to the names of the version directories used.", URL: "https://ocfl.io/1.0/spec/#E046", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E046", Description: "The keys of [the versions object] must correspond to the names of the version directories used.", @@ -1017,17 +1017,17 @@ func E046(spec ocfl.Spec) *ocfl.ValidationCode { } // E047: Each value [of the versions object] must be another JSON object that characterizes the version, as described in the 3.5.3.1 Version section. -func E047(spec ocfl.Spec) *ocfl.ValidationCode { +func E047(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E047", Description: "Each value [of the versions object] must be another JSON object that characterizes the version, as described in the 3.5.3.1 Version section.", URL: "https://ocfl.io/1.0/spec/#E047", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E047", Description: "Each value [of the versions object] must be another JSON object that characterizes the version, as described in the 3.5.3.1 Version section.", @@ -1039,17 +1039,17 @@ func E047(spec ocfl.Spec) *ocfl.ValidationCode { } // E048: A JSON object to describe one OCFL Version, which must include the following keys: [created, state] -func E048(spec ocfl.Spec) *ocfl.ValidationCode { +func E048(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E048", Description: "A JSON object to describe one OCFL Version, which must include the following keys: [created, state]", URL: "https://ocfl.io/1.0/spec/#E048", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E048", Description: "A JSON object to describe one OCFL Version, which must include the following keys: [created, state]", @@ -1061,17 +1061,17 @@ func E048(spec ocfl.Spec) *ocfl.ValidationCode { } // E049: [the value of the \"created\" key] must be expressed in the Internet Date/Time Format defined by [RFC3339]. -func E049(spec ocfl.Spec) *ocfl.ValidationCode { +func E049(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E049", Description: "[the value of the \"created\" key] must be expressed in the Internet Date/Time Format defined by [RFC3339].", URL: "https://ocfl.io/1.0/spec/#E049", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E049", Description: "[the value of the “created” key] must be expressed in the Internet Date/Time Format defined by [RFC3339].", @@ -1083,17 +1083,17 @@ func E049(spec ocfl.Spec) *ocfl.ValidationCode { } // E050: The keys of [the \"state\" JSON object] are digest values, each of which must correspond to an entry in the manifest of the inventory. -func E050(spec ocfl.Spec) *ocfl.ValidationCode { +func E050(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E050", Description: "The keys of [the \"state\" JSON object] are digest values, each of which must correspond to an entry in the manifest of the inventory.", URL: "https://ocfl.io/1.0/spec/#E050", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E050", Description: "The keys of [the “state” JSON object] are digest values, each of which must correspond to an entry in the manifest of the inventory.", @@ -1105,17 +1105,17 @@ func E050(spec ocfl.Spec) *ocfl.ValidationCode { } // E051: The logical path [value of a \"state\" digest key] must be interpreted as a set of one or more path elements joined by a / path separator. -func E051(spec ocfl.Spec) *ocfl.ValidationCode { +func E051(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E051", Description: "The logical path [value of a \"state\" digest key] must be interpreted as a set of one or more path elements joined by a / path separator.", URL: "https://ocfl.io/1.0/spec/#E051", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E051", Description: "The logical path [value of a “state” digest key] must be interpreted as a set of one or more path elements joined by a / path separator.", @@ -1127,17 +1127,17 @@ func E051(spec ocfl.Spec) *ocfl.ValidationCode { } // E052: [logical] Path elements must not be ., .., or empty (//). -func E052(spec ocfl.Spec) *ocfl.ValidationCode { +func E052(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E052", Description: "[logical] Path elements must not be ., .., or empty (//).", URL: "https://ocfl.io/1.0/spec/#E052", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E052", Description: "[logical] Path elements must not be ., .., or empty (//).", @@ -1149,17 +1149,17 @@ func E052(spec ocfl.Spec) *ocfl.ValidationCode { } // E053: Additionally, a logical path must not begin or end with a forward slash (/). -func E053(spec ocfl.Spec) *ocfl.ValidationCode { +func E053(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E053", Description: "Additionally, a logical path must not begin or end with a forward slash (/).", URL: "https://ocfl.io/1.0/spec/#E053", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E053", Description: "Additionally, a logical path must not begin or end with a forward slash (/).", @@ -1171,17 +1171,17 @@ func E053(spec ocfl.Spec) *ocfl.ValidationCode { } // E054: The value of the user key must contain a user name key, \"name\" and should contain an address key, \"address\". -func E054(spec ocfl.Spec) *ocfl.ValidationCode { +func E054(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E054", Description: "The value of the user key must contain a user name key, \"name\" and should contain an address key, \"address\".", URL: "https://ocfl.io/1.0/spec/#E054", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E054", Description: "The value of the user key must contain a user name key, “name” and should contain an address key, “address”.", @@ -1193,17 +1193,17 @@ func E054(spec ocfl.Spec) *ocfl.ValidationCode { } // E055: This block must have the key of fixity within the inventory. -func E055(spec ocfl.Spec) *ocfl.ValidationCode { +func E055(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E055", Description: "This block must have the key of fixity within the inventory.", URL: "https://ocfl.io/1.0/spec/#E055", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E055", Description: "If present, [the fixity] block must have the key of fixity within the inventory.", @@ -1215,17 +1215,17 @@ func E055(spec ocfl.Spec) *ocfl.ValidationCode { } // E056: The fixity block must contain keys corresponding to the controlled vocabulary given in the digest algorithms listed in the Digests section, or in a table given in an Extension. -func E056(spec ocfl.Spec) *ocfl.ValidationCode { +func E056(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E056", Description: "The fixity block must contain keys corresponding to the controlled vocabulary given in the digest algorithms listed in the Digests section, or in a table given in an Extension.", URL: "https://ocfl.io/1.0/spec/#E056", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E056", Description: "The fixity block must contain keys corresponding to the controlled vocabulary given in the digest algorithms listed in the Digests section, or in a table given in an Extension.", @@ -1237,17 +1237,17 @@ func E056(spec ocfl.Spec) *ocfl.ValidationCode { } // E057: The value of the fixity block for a particular digest algorithm must follow the structure of the manifest block; that is, a key corresponding to the digest value, and an array of content paths that match that digest. -func E057(spec ocfl.Spec) *ocfl.ValidationCode { +func E057(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E057", Description: "The value of the fixity block for a particular digest algorithm must follow the structure of the manifest block; that is, a key corresponding to the digest value, and an array of content paths that match that digest.", URL: "https://ocfl.io/1.0/spec/#E057", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E057", Description: "The value of the fixity block for a particular digest algorithm must follow the structure of the manifest block; that is, a key corresponding to the digest value, and an array of content paths that match that digest.", @@ -1259,17 +1259,17 @@ func E057(spec ocfl.Spec) *ocfl.ValidationCode { } // E058: Every occurrence of an inventory file must have an accompanying sidecar file stating its digest. -func E058(spec ocfl.Spec) *ocfl.ValidationCode { +func E058(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E058", Description: "Every occurrence of an inventory file must have an accompanying sidecar file stating its digest.", URL: "https://ocfl.io/1.0/spec/#E058", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E058", Description: "Every occurrence of an inventory file must have an accompanying sidecar file stating its digest.", @@ -1281,17 +1281,17 @@ func E058(spec ocfl.Spec) *ocfl.ValidationCode { } // E059: This value must match the value given for the digestAlgorithm key in the inventory. -func E059(spec ocfl.Spec) *ocfl.ValidationCode { +func E059(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E059", Description: "This value must match the value given for the digestAlgorithm key in the inventory.", URL: "https://ocfl.io/1.0/spec/#E059", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E059", Description: "This value must match the value given for the digestAlgorithm key in the inventory.", @@ -1303,17 +1303,17 @@ func E059(spec ocfl.Spec) *ocfl.ValidationCode { } // E060: The digest sidecar file must contain the digest of the inventory file. -func E060(spec ocfl.Spec) *ocfl.ValidationCode { +func E060(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E060", Description: "The digest sidecar file must contain the digest of the inventory file.", URL: "https://ocfl.io/1.0/spec/#E060", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E060", Description: "The digest sidecar file must contain the digest of the inventory file.", @@ -1325,17 +1325,17 @@ func E060(spec ocfl.Spec) *ocfl.ValidationCode { } // E061: [The digest sidecar file] must follow the format: DIGEST inventory.json -func E061(spec ocfl.Spec) *ocfl.ValidationCode { +func E061(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E061", Description: "[The digest sidecar file] must follow the format: DIGEST inventory.json", URL: "https://ocfl.io/1.0/spec/#E061", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E061", Description: "[The digest sidecar file] must follow the format: DIGEST inventory.json", @@ -1347,17 +1347,17 @@ func E061(spec ocfl.Spec) *ocfl.ValidationCode { } // E062: The digest of the inventory must be computed only after all changes to the inventory have been made, and thus writing the digest sidecar file is the last step in the versioning process. -func E062(spec ocfl.Spec) *ocfl.ValidationCode { +func E062(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E062", Description: "The digest of the inventory must be computed only after all changes to the inventory have been made, and thus writing the digest sidecar file is the last step in the versioning process.", URL: "https://ocfl.io/1.0/spec/#E062", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E062", Description: "The digest of the inventory must be computed only after all changes to the inventory have been made, and thus writing the digest sidecar file is the last step in the versioning process.", @@ -1369,17 +1369,17 @@ func E062(spec ocfl.Spec) *ocfl.ValidationCode { } // E063: Every OCFL Object must have an inventory file within the OCFL Object Root, corresponding to the state of the OCFL Object at the current version. -func E063(spec ocfl.Spec) *ocfl.ValidationCode { +func E063(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E063", Description: "Every OCFL Object must have an inventory file within the OCFL Object Root, corresponding to the state of the OCFL Object at the current version.", URL: "https://ocfl.io/1.0/spec/#E063", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E063", Description: "Every OCFL Object must have an inventory file within the OCFL Object Root, corresponding to the state of the OCFL Object at the current version.", @@ -1391,17 +1391,17 @@ func E063(spec ocfl.Spec) *ocfl.ValidationCode { } // E064: Where an OCFL Object contains inventory.json in version directories, the inventory file in the OCFL Object Root must be the same as the file in the most recent version. -func E064(spec ocfl.Spec) *ocfl.ValidationCode { +func E064(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E064", Description: "Where an OCFL Object contains inventory.json in version directories, the inventory file in the OCFL Object Root must be the same as the file in the most recent version.", URL: "https://ocfl.io/1.0/spec/#E064", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E064", Description: "Where an OCFL Object contains inventory.json in version directories, the inventory file in the OCFL Object Root must be the same as the file in the most recent version.", @@ -1413,17 +1413,17 @@ func E064(spec ocfl.Spec) *ocfl.ValidationCode { } // E066: Each version block in each prior inventory file must represent the same object state as the corresponding version block in the current inventory file. -func E066(spec ocfl.Spec) *ocfl.ValidationCode { +func E066(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E066", Description: "Each version block in each prior inventory file must represent the same object state as the corresponding version block in the current inventory file.", URL: "https://ocfl.io/1.0/spec/#E066", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E066", Description: "Each version block in each prior inventory file must represent the same object state as the corresponding version block in the current inventory file.", @@ -1435,17 +1435,17 @@ func E066(spec ocfl.Spec) *ocfl.ValidationCode { } // E067: The extensions directory must not contain any files, and no sub-directories other than extension sub-directories. -func E067(spec ocfl.Spec) *ocfl.ValidationCode { +func E067(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E067", Description: "The extensions directory must not contain any files, and no sub-directories other than extension sub-directories.", URL: "https://ocfl.io/1.0/spec/#E067", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E067", Description: "The extensions directory must not contain any files or sub-directories other than extension sub-directories.", @@ -1457,10 +1457,10 @@ func E067(spec ocfl.Spec) *ocfl.ValidationCode { } // E068: The specific structure and function of the extension, as well as a declaration of the registered extension name must be defined in one of the following locations: The OCFL Extensions repository OR The Storage Root, as a plain text document directly in the Storage Root. -func E068(spec ocfl.Spec) *ocfl.ValidationCode { +func E068(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E068", Description: "The specific structure and function of the extension, as well as a declaration of the registered extension name must be defined in one of the following locations: The OCFL Extensions repository OR The Storage Root, as a plain text document directly in the Storage Root.", @@ -1472,17 +1472,17 @@ func E068(spec ocfl.Spec) *ocfl.ValidationCode { } // E069: An OCFL Storage Root MUST contain a Root Conformance Declaration identifying it as such. -func E069(spec ocfl.Spec) *ocfl.ValidationCode { +func E069(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E069", Description: "An OCFL Storage Root MUST contain a Root Conformance Declaration identifying it as such.", URL: "https://ocfl.io/1.0/spec/#E069", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E069", Description: "An OCFL Storage Root MUST contain a Root Conformance Declaration identifying it as such.", @@ -1494,17 +1494,17 @@ func E069(spec ocfl.Spec) *ocfl.ValidationCode { } // E070: If present, [the ocfl_layout.json document] MUST include the following two keys in the root JSON object: [key, description] -func E070(spec ocfl.Spec) *ocfl.ValidationCode { +func E070(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E070", Description: "If present, [the ocfl_layout.json document] MUST include the following two keys in the root JSON object: [key, description]", URL: "https://ocfl.io/1.0/spec/#E070", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E070", Description: "If present, [the ocfl_layout.json document] MUST include the following two keys in the root JSON object: [extension, description]", @@ -1516,17 +1516,17 @@ func E070(spec ocfl.Spec) *ocfl.ValidationCode { } // E071: The value of the [ocfl_layout.json] extension key must be the registered extension name for the extension defining the arrangement under the storage root. -func E071(spec ocfl.Spec) *ocfl.ValidationCode { +func E071(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E071", Description: "The value of the [ocfl_layout.json] extension key must be the registered extension name for the extension defining the arrangement under the storage root.", URL: "https://ocfl.io/1.0/spec/#E071", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E071", Description: "The value of the [ocfl_layout.json] extension key must be the registered extension name for the extension defining the arrangement under the storage root.", @@ -1538,17 +1538,17 @@ func E071(spec ocfl.Spec) *ocfl.ValidationCode { } // E072: The directory hierarchy used to store OCFL Objects MUST NOT contain files that are not part of an OCFL Object. -func E072(spec ocfl.Spec) *ocfl.ValidationCode { +func E072(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E072", Description: "The directory hierarchy used to store OCFL Objects MUST NOT contain files that are not part of an OCFL Object.", URL: "https://ocfl.io/1.0/spec/#E072", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E072", Description: "The directory hierarchy used to store OCFL Objects MUST NOT contain files that are not part of an OCFL Object.", @@ -1560,17 +1560,17 @@ func E072(spec ocfl.Spec) *ocfl.ValidationCode { } // E073: Empty directories MUST NOT appear under a storage root. -func E073(spec ocfl.Spec) *ocfl.ValidationCode { +func E073(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E073", Description: "Empty directories MUST NOT appear under a storage root.", URL: "https://ocfl.io/1.0/spec/#E073", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E073", Description: "Empty directories MUST NOT appear under a storage root.", @@ -1582,17 +1582,17 @@ func E073(spec ocfl.Spec) *ocfl.ValidationCode { } // E074: Although implementations may require multiple OCFL Storage Roots - that is, several logical or physical volumes, or multiple \"buckets\" in an object store - each OCFL Storage Root MUST be independent. -func E074(spec ocfl.Spec) *ocfl.ValidationCode { +func E074(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E074", Description: "Although implementations may require multiple OCFL Storage Roots - that is, several logical or physical volumes, or multiple \"buckets\" in an object store - each OCFL Storage Root MUST be independent.", URL: "https://ocfl.io/1.0/spec/#E074", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E074", Description: "Although implementations may require multiple OCFL Storage Roots - that is, several logical or physical volumes, or multiple “buckets” in an object store - each OCFL Storage Root MUST be independent.", @@ -1604,17 +1604,17 @@ func E074(spec ocfl.Spec) *ocfl.ValidationCode { } // E075: The OCFL version declaration MUST be formatted according to the NAMASTE specification. -func E075(spec ocfl.Spec) *ocfl.ValidationCode { +func E075(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E075", Description: "The OCFL version declaration MUST be formatted according to the NAMASTE specification.", URL: "https://ocfl.io/1.0/spec/#E075", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E075", Description: "The OCFL version declaration MUST be formatted according to the NAMASTE specification.", @@ -1626,17 +1626,17 @@ func E075(spec ocfl.Spec) *ocfl.ValidationCode { } // E076: [The OCFL version declaration] MUST be a file in the base directory of the OCFL Storage Root giving the OCFL version in the filename. -func E076(spec ocfl.Spec) *ocfl.ValidationCode { +func E076(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E076", Description: "[The OCFL version declaration] MUST be a file in the base directory of the OCFL Storage Root giving the OCFL version in the filename.", URL: "https://ocfl.io/1.0/spec/#E076", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E076", Description: "There must be exactly one version declaration file in the base directory of the OCFL Storage Root giving the OCFL version in the filename.", @@ -1648,17 +1648,17 @@ func E076(spec ocfl.Spec) *ocfl.ValidationCode { } // E077: [The OCFL version declaration filename] MUST conform to the pattern T=dvalue, where T must be 0, and dvalue must be ocfl_, followed by the OCFL specification ocfl.Number. -func E077(spec ocfl.Spec) *ocfl.ValidationCode { +func E077(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E077", Description: "[The OCFL version declaration filename] MUST conform to the pattern T=dvalue, where T must be 0, and dvalue must be ocfl_, followed by the OCFL specification ocfl.Number.", URL: "https://ocfl.io/1.0/spec/#E077", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E077", Description: "[The OCFL version declaration filename] MUST conform to the pattern T=dvalue, where T must be 0, and dvalue must be ocfl_, followed by the OCFL specification version number.", @@ -1670,17 +1670,17 @@ func E077(spec ocfl.Spec) *ocfl.ValidationCode { } // E078: [The OCFL version declaration filename] must conform to the pattern T=dvalue, where T MUST be 0, and dvalue must be ocfl_, followed by the OCFL specification ocfl.Number. -func E078(spec ocfl.Spec) *ocfl.ValidationCode { +func E078(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E078", Description: "[The OCFL version declaration filename] must conform to the pattern T=dvalue, where T MUST be 0, and dvalue must be ocfl_, followed by the OCFL specification ocfl.Number.", URL: "https://ocfl.io/1.0/spec/#E078", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E078", Description: "[The OCFL version declaration filename] must conform to the pattern T=dvalue, where T MUST be 0, and dvalue must be ocfl_, followed by the OCFL specification version number.", @@ -1692,17 +1692,17 @@ func E078(spec ocfl.Spec) *ocfl.ValidationCode { } // E079: [The OCFL version declaration filename] must conform to the pattern T=dvalue, where T must be 0, and dvalue MUST be ocfl_, followed by the OCFL specification ocfl.Number. -func E079(spec ocfl.Spec) *ocfl.ValidationCode { +func E079(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E079", Description: "[The OCFL version declaration filename] must conform to the pattern T=dvalue, where T must be 0, and dvalue MUST be ocfl_, followed by the OCFL specification ocfl.Number.", URL: "https://ocfl.io/1.0/spec/#E079", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E079", Description: "[The OCFL version declaration filename] must conform to the pattern T=dvalue, where T must be 0, and dvalue MUST be ocfl_, followed by the OCFL specification version number.", @@ -1714,17 +1714,17 @@ func E079(spec ocfl.Spec) *ocfl.ValidationCode { } // E080: The text contents of [the OCFL version declaration file] MUST be the same as dvalue, followed by a newline (\n). -func E080(spec ocfl.Spec) *ocfl.ValidationCode { +func E080(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E080", Description: "The text contents of [the OCFL version declaration file] MUST be the same as dvalue, followed by a newline (\n).", URL: "https://ocfl.io/1.0/spec/#E080", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E080", Description: "The text contents of [the OCFL version declaration file] MUST be the same as dvalue, followed by a newline (\n).", @@ -1736,17 +1736,17 @@ func E080(spec ocfl.Spec) *ocfl.ValidationCode { } // E081: OCFL Objects within the OCFL Storage Root also include a conformance declaration which MUST indicate OCFL Object conformance to the same or earlier version of the specification. -func E081(spec ocfl.Spec) *ocfl.ValidationCode { +func E081(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E081", Description: "OCFL Objects within the OCFL Storage Root also include a conformance declaration which MUST indicate OCFL Object conformance to the same or earlier version of the specification.", URL: "https://ocfl.io/1.0/spec/#E081", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E081", Description: "OCFL Objects within the OCFL Storage Root also include a conformance declaration which MUST indicate OCFL Object conformance to the same or earlier version of the specification.", @@ -1758,17 +1758,17 @@ func E081(spec ocfl.Spec) *ocfl.ValidationCode { } // E082: OCFL Object Roots MUST be stored either as the terminal resource at the end of a directory storage hierarchy or as direct children of a containing OCFL Storage Root. -func E082(spec ocfl.Spec) *ocfl.ValidationCode { +func E082(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E082", Description: "OCFL Object Roots MUST be stored either as the terminal resource at the end of a directory storage hierarchy or as direct children of a containing OCFL Storage Root.", URL: "https://ocfl.io/1.0/spec/#E082", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E082", Description: "OCFL Object Roots MUST be stored either as the terminal resource at the end of a directory storage hierarchy or as direct children of a containing OCFL Storage Root.", @@ -1780,17 +1780,17 @@ func E082(spec ocfl.Spec) *ocfl.ValidationCode { } // E083: There MUST be a deterministic mapping from an object identifier to a unique storage path. -func E083(spec ocfl.Spec) *ocfl.ValidationCode { +func E083(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E083", Description: "There MUST be a deterministic mapping from an object identifier to a unique storage path.", URL: "https://ocfl.io/1.0/spec/#E083", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E083", Description: "There MUST be a deterministic mapping from an object identifier to a unique storage path.", @@ -1802,17 +1802,17 @@ func E083(spec ocfl.Spec) *ocfl.ValidationCode { } // E084: Storage hierarchies MUST NOT include files within intermediate directories. -func E084(spec ocfl.Spec) *ocfl.ValidationCode { +func E084(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E084", Description: "Storage hierarchies MUST NOT include files within intermediate directories.", URL: "https://ocfl.io/1.0/spec/#E084", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E084", Description: "Storage hierarchies MUST NOT include files within intermediate directories.", @@ -1824,17 +1824,17 @@ func E084(spec ocfl.Spec) *ocfl.ValidationCode { } // E085: Storage hierarchies MUST be terminated by OCFL Object Roots. -func E085(spec ocfl.Spec) *ocfl.ValidationCode { +func E085(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E085", Description: "Storage hierarchies MUST be terminated by OCFL Object Roots.", URL: "https://ocfl.io/1.0/spec/#E085", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E085", Description: "Storage hierarchies MUST be terminated by OCFL Object Roots.", @@ -1846,10 +1846,10 @@ func E085(spec ocfl.Spec) *ocfl.ValidationCode { } // E086: The storage root extensions directory MUST conform to the same guidelines and limitations as those defined for object extensions. -func E086(spec ocfl.Spec) *ocfl.ValidationCode { +func E086(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E086", Description: "The storage root extensions directory MUST conform to the same guidelines and limitations as those defined for object extensions.", @@ -1861,17 +1861,17 @@ func E086(spec ocfl.Spec) *ocfl.ValidationCode { } // E087: An OCFL validator MUST ignore any files in the storage root it does not understand. -func E087(spec ocfl.Spec) *ocfl.ValidationCode { +func E087(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E087", Description: "An OCFL validator MUST ignore any files in the storage root it does not understand.", URL: "https://ocfl.io/1.0/spec/#E087", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E087", Description: "An OCFL validator MUST ignore any files in the storage root it does not understand.", @@ -1883,17 +1883,17 @@ func E087(spec ocfl.Spec) *ocfl.ValidationCode { } // E088: An OCFL Storage Root MUST NOT contain directories or sub-directories other than as a directory hierarchy used to store OCFL Objects or for storage root extensions. -func E088(spec ocfl.Spec) *ocfl.ValidationCode { +func E088(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E088", Description: "An OCFL Storage Root MUST NOT contain directories or sub-directories other than as a directory hierarchy used to store OCFL Objects or for storage root extensions.", URL: "https://ocfl.io/1.0/spec/#E088", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E088", Description: "An OCFL Storage Root MUST NOT contain directories or sub-directories other than as a directory hierarchy used to store OCFL Objects or for storage root extensions.", @@ -1905,17 +1905,17 @@ func E088(spec ocfl.Spec) *ocfl.ValidationCode { } // E089: If the preservation of non-OCFL-compliant features is required then the content MUST be wrapped in a suitable disk or filesystem image format which OCFL can treat as a regular file. -func E089(spec ocfl.Spec) *ocfl.ValidationCode { +func E089(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E089", Description: "If the preservation of non-OCFL-compliant features is required then the content MUST be wrapped in a suitable disk or filesystem image format which OCFL can treat as a regular file.", URL: "https://ocfl.io/1.0/spec/#E089", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E089", Description: "If the preservation of non-OCFL-compliant features is required then the content MUST be wrapped in a suitable disk or filesystem image format which OCFL can treat as a regular file.", @@ -1927,17 +1927,17 @@ func E089(spec ocfl.Spec) *ocfl.ValidationCode { } // E090: Hard and soft (symbolic) links are not portable and MUST NOT be used within OCFL Storage hierarchies. -func E090(spec ocfl.Spec) *ocfl.ValidationCode { +func E090(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E090", Description: "Hard and soft (symbolic) links are not portable and MUST NOT be used within OCFL Storage hierarchies.", URL: "https://ocfl.io/1.0/spec/#E090", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E090", Description: "Hard and soft (symbolic) links are not portable and MUST NOT be used within OCFL Storage hierarchies.", @@ -1949,17 +1949,17 @@ func E090(spec ocfl.Spec) *ocfl.ValidationCode { } // E091: Filesystems MUST preserve the case of OCFL filepaths and filenames. -func E091(spec ocfl.Spec) *ocfl.ValidationCode { +func E091(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E091", Description: "Filesystems MUST preserve the case of OCFL filepaths and filenames.", URL: "https://ocfl.io/1.0/spec/#E091", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E091", Description: "Filesystems MUST preserve the case of OCFL filepaths and filenames.", @@ -1971,17 +1971,17 @@ func E091(spec ocfl.Spec) *ocfl.ValidationCode { } // E092: The value for each key in the manifest must be an array containing the content paths of files in the OCFL Object that have content with the given digest. -func E092(spec ocfl.Spec) *ocfl.ValidationCode { +func E092(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E092", Description: "The value for each key in the manifest must be an array containing the content paths of files in the OCFL Object that have content with the given digest.", URL: "https://ocfl.io/1.0/spec/#E092", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E092", Description: "The value for each key in the manifest must be an array containing the content paths of files in the OCFL Object that have content with the given digest.", @@ -1993,17 +1993,17 @@ func E092(spec ocfl.Spec) *ocfl.ValidationCode { } // E093: Where included in the fixity block, the digest values given must match the digests of the files at the corresponding content paths. -func E093(spec ocfl.Spec) *ocfl.ValidationCode { +func E093(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E093", Description: "Where included in the fixity block, the digest values given must match the digests of the files at the corresponding content paths.", URL: "https://ocfl.io/1.0/spec/#E093", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E093", Description: "Where included in the fixity block, the digest values given must match the digests of the files at the corresponding content paths.", @@ -2015,17 +2015,17 @@ func E093(spec ocfl.Spec) *ocfl.ValidationCode { } // E094: The value of [the message] key is freeform text, used to record the rationale for creating this version. It must be a JSON string. -func E094(spec ocfl.Spec) *ocfl.ValidationCode { +func E094(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E094", Description: "The value of [the message] key is freeform text, used to record the rationale for creating this version. It must be a JSON string.", URL: "https://ocfl.io/1.0/spec/#E094", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E094", Description: "The value of [the message] key is freeform text, used to record the rationale for creating this version. It must be a JSON string.", @@ -2037,17 +2037,17 @@ func E094(spec ocfl.Spec) *ocfl.ValidationCode { } // E095: Within a version, logical paths must be unique and non-conflicting, so the logical path for a file cannot appear as the initial part of another logical path. -func E095(spec ocfl.Spec) *ocfl.ValidationCode { +func E095(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E095", Description: "Within a version, logical paths must be unique and non-conflicting, so the logical path for a file cannot appear as the initial part of another logical path.", URL: "https://ocfl.io/1.0/spec/#E095", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E095", Description: "Within a version, logical paths must be unique and non-conflicting, so the logical path for a file cannot appear as the initial part of another logical path.", @@ -2059,17 +2059,17 @@ func E095(spec ocfl.Spec) *ocfl.ValidationCode { } // E096: As JSON keys are case sensitive, while digests may not be, there is an additional requirement that each digest value must occur only once in the manifest regardless of case. -func E096(spec ocfl.Spec) *ocfl.ValidationCode { +func E096(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E096", Description: "As JSON keys are case sensitive, while digests may not be, there is an additional requirement that each digest value must occur only once in the manifest regardless of case.", URL: "https://ocfl.io/1.0/spec/#E096", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E096", Description: "As JSON keys are case sensitive, while digests may not be, there is an additional requirement that each digest value must occur only once in the manifest regardless of case.", @@ -2081,17 +2081,17 @@ func E096(spec ocfl.Spec) *ocfl.ValidationCode { } // E097: As JSON keys are case sensitive, while digests may not be, there is an additional requirement that each digest value must occur only once in the fixity block for any digest algorithm, regardless of case. -func E097(spec ocfl.Spec) *ocfl.ValidationCode { +func E097(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E097", Description: "As JSON keys are case sensitive, while digests may not be, there is an additional requirement that each digest value must occur only once in the fixity block for any digest algorithm, regardless of case.", URL: "https://ocfl.io/1.0/spec/#E097", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E097", Description: "As JSON keys are case sensitive, while digests may not be, there is an additional requirement that each digest value must occur only once in the fixity block for any digest algorithm, regardless of case.", @@ -2103,17 +2103,17 @@ func E097(spec ocfl.Spec) *ocfl.ValidationCode { } // E098: The content path must be interpreted as a set of one or more path elements joined by a / path separator. -func E098(spec ocfl.Spec) *ocfl.ValidationCode { +func E098(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E098", Description: "The content path must be interpreted as a set of one or more path elements joined by a / path separator.", URL: "https://ocfl.io/1.0/spec/#E098", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E098", Description: "The content path must be interpreted as a set of one or more path elements joined by a / path separator.", @@ -2125,17 +2125,17 @@ func E098(spec ocfl.Spec) *ocfl.ValidationCode { } // E099: [content] path elements must not be ., .., or empty (//). -func E099(spec ocfl.Spec) *ocfl.ValidationCode { +func E099(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E099", Description: "[content] path elements must not be ., .., or empty (//).", URL: "https://ocfl.io/1.0/spec/#E099", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E099", Description: "[content] path elements must not be ., .., or empty (//).", @@ -2147,17 +2147,17 @@ func E099(spec ocfl.Spec) *ocfl.ValidationCode { } // E100: A content path must not begin or end with a forward slash (/). -func E100(spec ocfl.Spec) *ocfl.ValidationCode { +func E100(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E100", Description: "A content path must not begin or end with a forward slash (/).", URL: "https://ocfl.io/1.0/spec/#E100", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E100", Description: "A content path must not begin or end with a forward slash (/).", @@ -2169,17 +2169,17 @@ func E100(spec ocfl.Spec) *ocfl.ValidationCode { } // E101: Within an inventory, content paths must be unique and non-conflicting, so the content path for a file cannot appear as the initial part of another content path. -func E101(spec ocfl.Spec) *ocfl.ValidationCode { +func E101(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E101", Description: "Within an inventory, content paths must be unique and non-conflicting, so the content path for a file cannot appear as the initial part of another content path.", URL: "https://ocfl.io/1.0/spec/#E101", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E101", Description: "Within an inventory, content paths must be unique and non-conflicting, so the content path for a file cannot appear as the initial part of another content path.", @@ -2191,17 +2191,17 @@ func E101(spec ocfl.Spec) *ocfl.ValidationCode { } // E102: An inventory file must not contain keys that are not specified. -func E102(spec ocfl.Spec) *ocfl.ValidationCode { +func E102(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "E102", Description: "An inventory file must not contain keys that are not specified.", URL: "https://ocfl.io/1.0/spec/#E102", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E102", Description: "An inventory file must not contain keys that are not specified.", @@ -2213,10 +2213,10 @@ func E102(spec ocfl.Spec) *ocfl.ValidationCode { } // E103: Each version directory within an OCFL Object MUST conform to either the same or a later OCFL specification version as the preceding version directory. -func E103(spec ocfl.Spec) *ocfl.ValidationCode { +func E103(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E103", Description: "Each version directory within an OCFL Object MUST conform to either the same or a later OCFL specification version as the preceding version directory.", @@ -2228,10 +2228,10 @@ func E103(spec ocfl.Spec) *ocfl.ValidationCode { } // E104: Version directory names MUST be constructed by prepending v to the version number. -func E104(spec ocfl.Spec) *ocfl.ValidationCode { +func E104(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E104", Description: "Version directory names MUST be constructed by prepending v to the version number.", @@ -2243,10 +2243,10 @@ func E104(spec ocfl.Spec) *ocfl.ValidationCode { } // E105: The version number MUST be taken from the sequence of positive, base-ten integers: 1, 2, 3, etc. -func E105(spec ocfl.Spec) *ocfl.ValidationCode { +func E105(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E105", Description: "The version number MUST be taken from the sequence of positive, base-ten integers: 1, 2, 3, etc.", @@ -2258,10 +2258,10 @@ func E105(spec ocfl.Spec) *ocfl.ValidationCode { } // E106: The value of the manifest key MUST be a JSON object. -func E106(spec ocfl.Spec) *ocfl.ValidationCode { +func E106(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E106", Description: "The value of the manifest key MUST be a JSON object.", @@ -2273,10 +2273,10 @@ func E106(spec ocfl.Spec) *ocfl.ValidationCode { } // E107: The value of the manifest key must be a JSON object, and each key MUST correspond to a digest value key found in one or more state blocks of the current and/or previous version blocks of the OCFL Object. -func E107(spec ocfl.Spec) *ocfl.ValidationCode { +func E107(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E107", Description: "The value of the manifest key must be a JSON object, and each key MUST correspond to a digest value key found in one or more state blocks of the current and/or previous version blocks of the OCFL Object.", @@ -2288,10 +2288,10 @@ func E107(spec ocfl.Spec) *ocfl.ValidationCode { } // E108: The contentDirectory value MUST represent a direct child directory of the version directory in which it is found. -func E108(spec ocfl.Spec) *ocfl.ValidationCode { +func E108(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E108", Description: "The contentDirectory value MUST represent a direct child directory of the version directory in which it is found.", @@ -2303,10 +2303,10 @@ func E108(spec ocfl.Spec) *ocfl.ValidationCode { } // E110: A unique identifier for the OCFL Object MUST NOT change between versions of the same object. -func E110(spec ocfl.Spec) *ocfl.ValidationCode { +func E110(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E110", Description: "A unique identifier for the OCFL Object MUST NOT change between versions of the same object.", @@ -2318,10 +2318,10 @@ func E110(spec ocfl.Spec) *ocfl.ValidationCode { } // E111: If present, [the value of the fixity key] MUST be a JSON object, which may be empty. -func E111(spec ocfl.Spec) *ocfl.ValidationCode { +func E111(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E111", Description: "If present, [the value of the fixity key] MUST be a JSON object, which may be empty.", @@ -2333,10 +2333,10 @@ func E111(spec ocfl.Spec) *ocfl.ValidationCode { } // E112: The extensions directory must not contain any files or sub-directories other than extension sub-directories. -func E112(spec ocfl.Spec) *ocfl.ValidationCode { +func E112(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "E112", Description: "The extensions directory must not contain any files or sub-directories other than extension sub-directories.", @@ -2348,17 +2348,17 @@ func E112(spec ocfl.Spec) *ocfl.ValidationCode { } // W001: Implementations SHOULD use version directory names constructed without zero-padding the ocfl.Number, ie. v1, v2, v3, etc. -func W001(spec ocfl.Spec) *ocfl.ValidationCode { +func W001(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "W001", Description: "Implementations SHOULD use version directory names constructed without zero-padding the ocfl.Number, ie. v1, v2, v3, etc.", URL: "https://ocfl.io/1.0/spec/#W001", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "W001", Description: "Implementations SHOULD use version directory names constructed without zero-padding the version number, ie. v1, v2, v3, etc.", @@ -2370,17 +2370,17 @@ func W001(spec ocfl.Spec) *ocfl.ValidationCode { } // W002: The version directory SHOULD NOT contain any directories other than the designated content sub-directory. Once created, the contents of a version directory are expected to be immutable. -func W002(spec ocfl.Spec) *ocfl.ValidationCode { +func W002(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "W002", Description: "The version directory SHOULD NOT contain any directories other than the designated content sub-directory. Once created, the contents of a version directory are expected to be immutable.", URL: "https://ocfl.io/1.0/spec/#W002", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "W002", Description: "The version directory SHOULD NOT contain any directories other than the designated content sub-directory. Once created, the contents of a version directory are expected to be immutable.", @@ -2392,17 +2392,17 @@ func W002(spec ocfl.Spec) *ocfl.ValidationCode { } // W003: Version directories must contain a designated content sub-directory if the version contains files to be preserved, and SHOULD NOT contain this sub-directory otherwise. -func W003(spec ocfl.Spec) *ocfl.ValidationCode { +func W003(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "W003", Description: "Version directories must contain a designated content sub-directory if the version contains files to be preserved, and SHOULD NOT contain this sub-directory otherwise.", URL: "https://ocfl.io/1.0/spec/#W003", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "W003", Description: "Version directories must contain a designated content sub-directory if the version contains files to be preserved, and SHOULD NOT contain this sub-directory otherwise.", @@ -2414,17 +2414,17 @@ func W003(spec ocfl.Spec) *ocfl.ValidationCode { } // W004: For content-addressing, OCFL Objects SHOULD use sha512. -func W004(spec ocfl.Spec) *ocfl.ValidationCode { +func W004(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "W004", Description: "For content-addressing, OCFL Objects SHOULD use sha512.", URL: "https://ocfl.io/1.0/spec/#W004", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "W004", Description: "For content-addressing, OCFL Objects SHOULD use sha512.", @@ -2436,17 +2436,17 @@ func W004(spec ocfl.Spec) *ocfl.ValidationCode { } // W005: The OCFL Object Inventory id SHOULD be a URI. -func W005(spec ocfl.Spec) *ocfl.ValidationCode { +func W005(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "W005", Description: "The OCFL Object Inventory id SHOULD be a URI.", URL: "https://ocfl.io/1.0/spec/#W005", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "W005", Description: "The OCFL Object Inventory id SHOULD be a URI.", @@ -2458,17 +2458,17 @@ func W005(spec ocfl.Spec) *ocfl.ValidationCode { } // W007: In the OCFL Object Inventory, the JSON object describing an OCFL Version, SHOULD include the message and user keys. -func W007(spec ocfl.Spec) *ocfl.ValidationCode { +func W007(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "W007", Description: "In the OCFL Object Inventory, the JSON object describing an OCFL Version, SHOULD include the message and user keys.", URL: "https://ocfl.io/1.0/spec/#W007", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "W007", Description: "In the OCFL Object Inventory, the JSON object describing an OCFL Version, SHOULD include the message and user keys.", @@ -2480,17 +2480,17 @@ func W007(spec ocfl.Spec) *ocfl.ValidationCode { } // W008: In the OCFL Object Inventory, in the version block, the value of the user key SHOULD contain an address key, address. -func W008(spec ocfl.Spec) *ocfl.ValidationCode { +func W008(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "W008", Description: "In the OCFL Object Inventory, in the version block, the value of the user key SHOULD contain an address key, address.", URL: "https://ocfl.io/1.0/spec/#W008", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "W008", Description: "In the OCFL Object Inventory, in the version block, the value of the user key SHOULD contain an address key, address.", @@ -2502,17 +2502,17 @@ func W008(spec ocfl.Spec) *ocfl.ValidationCode { } // W009: In the OCFL Object Inventory, in the version block, the address value SHOULD be a URI: either a mailto URI [RFC6068] with the e-mail address of the user or a URL to a personal identifier, e.g., an ORCID iD. -func W009(spec ocfl.Spec) *ocfl.ValidationCode { +func W009(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "W009", Description: "In the OCFL Object Inventory, in the version block, the address value SHOULD be a URI: either a mailto URI [RFC6068] with the e-mail address of the user or a URL to a personal identifier, e.g., an ORCID iD.", URL: "https://ocfl.io/1.0/spec/#W009", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "W009", Description: "In the OCFL Object Inventory, in the version block, the address value SHOULD be a URI: either a mailto URI [RFC6068] with the e-mail address of the user or a URL to a personal identifier, e.g., an ORCID iD.", @@ -2524,17 +2524,17 @@ func W009(spec ocfl.Spec) *ocfl.ValidationCode { } // W010: In addition to the inventory in the OCFL Object Root, every version directory SHOULD include an inventory file that is an Inventory of all content for versions up to and including that particular version. -func W010(spec ocfl.Spec) *ocfl.ValidationCode { +func W010(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "W010", Description: "In addition to the inventory in the OCFL Object Root, every version directory SHOULD include an inventory file that is an Inventory of all content for versions up to and including that particular version.", URL: "https://ocfl.io/1.0/spec/#W010", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "W010", Description: "In addition to the inventory in the OCFL Object Root, every version directory SHOULD include an inventory file that is an Inventory of all content for versions up to and including that particular version.", @@ -2546,17 +2546,17 @@ func W010(spec ocfl.Spec) *ocfl.ValidationCode { } // W011: In the case that prior version directories include an inventory file, the values of the created, message and user keys in each version block in each prior inventory file SHOULD have the same values as the corresponding keys in the corresponding version block in the current inventory file. -func W011(spec ocfl.Spec) *ocfl.ValidationCode { +func W011(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "W011", Description: "In the case that prior version directories include an inventory file, the values of the created, message and user keys in each version block in each prior inventory file SHOULD have the same values as the corresponding keys in the corresponding version block in the current inventory file.", URL: "https://ocfl.io/1.0/spec/#W011", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "W011", Description: "In the case that prior version directories include an inventory file, the values of the created, message and user keys in each version block in each prior inventory file SHOULD have the same values as the corresponding keys in the corresponding version block in the current inventory file.", @@ -2568,17 +2568,17 @@ func W011(spec ocfl.Spec) *ocfl.ValidationCode { } // W012: Implementers SHOULD use the logs directory, if present, for storing files that contain a record of actions taken on the object. -func W012(spec ocfl.Spec) *ocfl.ValidationCode { +func W012(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "W012", Description: "Implementers SHOULD use the logs directory, if present, for storing files that contain a record of actions taken on the object.", URL: "https://ocfl.io/1.0/spec/#W012", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "W012", Description: "Implementers SHOULD use the logs directory, if present, for storing files that contain a record of actions taken on the object.", @@ -2590,17 +2590,17 @@ func W012(spec ocfl.Spec) *ocfl.ValidationCode { } // W013: In an OCFL Object, extension sub-directories SHOULD be named according to a registered extension name. -func W013(spec ocfl.Spec) *ocfl.ValidationCode { +func W013(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "W013", Description: "In an OCFL Object, extension sub-directories SHOULD be named according to a registered extension name.", URL: "https://ocfl.io/1.0/spec/#W013", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "W013", Description: "In an OCFL Object, extension sub-directories SHOULD be named according to a registered extension name.", @@ -2612,17 +2612,17 @@ func W013(spec ocfl.Spec) *ocfl.ValidationCode { } // W014: Storage hierarchies within the same OCFL Storage Root SHOULD use just one layout pattern. -func W014(spec ocfl.Spec) *ocfl.ValidationCode { +func W014(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "W014", Description: "Storage hierarchies within the same OCFL Storage Root SHOULD use just one layout pattern.", URL: "https://ocfl.io/1.0/spec/#W014", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "W014", Description: "Storage hierarchies within the same OCFL Storage Root SHOULD use just one layout pattern.", @@ -2634,17 +2634,17 @@ func W014(spec ocfl.Spec) *ocfl.ValidationCode { } // W015: Storage hierarchies within the same OCFL Storage Root SHOULD consistently use either a directory hierarchy of OCFL Objects or top-level OCFL Objects. -func W015(spec ocfl.Spec) *ocfl.ValidationCode { +func W015(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.0"): - return &ocfl.ValidationCode{ + case "1.0": + return &validation.ValidationCode{ Spec: spec, Code: "W015", Description: "Storage hierarchies within the same OCFL Storage Root SHOULD consistently use either a directory hierarchy of OCFL Objects or top-level OCFL Objects.", URL: "https://ocfl.io/1.0/spec/#W015", } - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "W015", Description: "Storage hierarchies within the same OCFL Storage Root SHOULD consistently use either a directory hierarchy of OCFL Objects or top-level OCFL Objects.", @@ -2656,10 +2656,10 @@ func W015(spec ocfl.Spec) *ocfl.ValidationCode { } // W016: In the Storage Root, extension sub-directories SHOULD be named according to a registered extension name. -func W016(spec ocfl.Spec) *ocfl.ValidationCode { +func W016(spec string) *validation.ValidationCode { switch spec { - case ocfl.Spec("1.1"): - return &ocfl.ValidationCode{ + case "1.1": + return &validation.ValidationCode{ Spec: spec, Code: "W016", Description: "In the Storage Root, extension sub-directories SHOULD be named according to a registered extension name.", diff --git a/ocflv1/codes/generate/codes_gen.go.tmpl b/validation/code/codes_gen.go.tmpl similarity index 55% rename from ocflv1/codes/generate/codes_gen.go.tmpl rename to validation/code/codes_gen.go.tmpl index 9b23fdf0..dd9b86bf 100644 --- a/ocflv1/codes/generate/codes_gen.go.tmpl +++ b/validation/code/codes_gen.go.tmpl @@ -1,17 +1,17 @@ -package codes +package code -// This is generated code. Do not modify. See gen folder. +// This is generated code. Do not modify. See generate.go -import "github.com/srerickson/ocfl-go" +import "github.com/srerickson/ocfl-go/validation" {{- range $code, $element := . }} // {{ $element.Comment }} -func {{ $code -}}(spec ocfl.Spec) *ocfl.ValidationCode { +func {{ $code -}}(spec string) *validation.ValidationCode { switch spec { {{- range $spec, $ref := $element.Specs }} - case ocfl.Spec("{{ $spec -}}"): - return &ocfl.ValidationCode{ + case "{{ $spec -}}": + return &validation.ValidationCode{ Spec: spec, Code: "{{ $code }}", Description: "{{ $ref.Description }}", diff --git a/ocflv1/codes/generate/gen.go b/validation/code/generate.go similarity index 67% rename from ocflv1/codes/generate/gen.go rename to validation/code/generate.go index 496dbaff..dea1c665 100644 --- a/ocflv1/codes/generate/gen.go +++ b/validation/code/generate.go @@ -1,9 +1,8 @@ +//go:build ignore + package main -// This program generates error codes objects based on ocfl ocfl. -// -// from this directory: -// go run gen.go > ../codes.go +// This program generates error code for ocfl validatoin errors import ( _ "embed" @@ -15,6 +14,8 @@ import ( "text/template" ) +const filename = "codes_gen.go" + //go:embed codes-ocflv1.0.csv var codes1_0 string @@ -26,20 +27,26 @@ var specs = map[string]string{ "1.1": codes1_1, } -type SpecRef struct { +type specRef struct { Description string // code description URL string // URL to spect } -type CodeEntry struct { +type codeEntry struct { Num string Comment string - Specs map[string]SpecRef + Specs map[string]specRef } func main() { tpl := template.Must(template.ParseFiles(`codes_gen.go.tmpl`)) - codes := map[string]*CodeEntry{} + codes := map[string]*codeEntry{} + + f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + log.Fatal(err) + } + defer f.Close() for specnum, raw := range specs { reader := csv.NewReader(strings.NewReader(raw)) @@ -55,13 +62,13 @@ func main() { url := row[2] comment := fmt.Sprintf("%s: %s", num, desc) if codes[num] == nil { - codes[num] = &CodeEntry{ + codes[num] = &codeEntry{ Num: num, Comment: comment, - Specs: map[string]SpecRef{}, + Specs: map[string]specRef{}, } } - codes[num].Specs[specnum] = SpecRef{ + codes[num].Specs[specnum] = specRef{ Description: desc, URL: url, } @@ -70,5 +77,5 @@ func main() { log.Fatal(err) } } - tpl.Execute(os.Stdout, codes) + tpl.Execute(f, codes) }