Implement final fixes and close out the implemetation roadmap
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
@@ -50,7 +51,7 @@ func (s *filesystemStore) Load(sourceDigest string) (pipeline.ChunkPlanRecord, p
|
||||
}
|
||||
defer root.Close()
|
||||
|
||||
state, err := inspectDirectory(root, digestDir)
|
||||
digestRoot, state, err := openDigestRoot(root, digestDir, false)
|
||||
if err != nil {
|
||||
return pipeline.ChunkPlanRecord{}, pipeline.ChunkPlanDecision{}, fmt.Errorf("inspect chunk plan directory: %w", err)
|
||||
}
|
||||
@@ -60,11 +61,11 @@ func (s *filesystemStore) Load(sourceDigest string) (pipeline.ChunkPlanRecord, p
|
||||
if state == entryRejected {
|
||||
return invalidDecision()
|
||||
}
|
||||
defer digestRoot.Close()
|
||||
|
||||
target := planPath(digestDir)
|
||||
state, err = inspectPlan(root, target)
|
||||
data, state, err := readPlan(digestRoot)
|
||||
if err != nil {
|
||||
return pipeline.ChunkPlanRecord{}, pipeline.ChunkPlanDecision{}, fmt.Errorf("inspect chunk plan file: %w", err)
|
||||
return pipeline.ChunkPlanRecord{}, pipeline.ChunkPlanDecision{}, fmt.Errorf("read chunk plan: %w", err)
|
||||
}
|
||||
if state == entryMissing {
|
||||
return pipeline.ChunkPlanRecord{}, pipeline.ChunkPlanDecision{Status: pipeline.ChunkPlanMissing, Reason: lookupReason(pipeline.ChunkPlanMissing)}, nil
|
||||
@@ -73,14 +74,6 @@ func (s *filesystemStore) Load(sourceDigest string) (pipeline.ChunkPlanRecord, p
|
||||
return invalidDecision()
|
||||
}
|
||||
|
||||
data, err := root.ReadFile(target)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return pipeline.ChunkPlanRecord{}, pipeline.ChunkPlanDecision{Status: pipeline.ChunkPlanMissing, Reason: lookupReason(pipeline.ChunkPlanMissing)}, nil
|
||||
}
|
||||
return pipeline.ChunkPlanRecord{}, pipeline.ChunkPlanDecision{}, fmt.Errorf("read chunk plan: %w", err)
|
||||
}
|
||||
|
||||
var record pipeline.ChunkPlanRecord
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
@@ -116,11 +109,15 @@ func (s *filesystemStore) Save(record pipeline.ChunkPlanRecord) error {
|
||||
return fmt.Errorf("open chunk plan root: %w", err)
|
||||
}
|
||||
defer root.Close()
|
||||
if err := ensureDirectory(root, digestDir); err != nil {
|
||||
digestRoot, state, err := openDigestRoot(root, digestDir, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("prepare chunk plan directory: %w", err)
|
||||
}
|
||||
target := planPath(digestDir)
|
||||
state, err := inspectPlan(root, target)
|
||||
if state == entryRejected {
|
||||
return fmt.Errorf("chunk plan directory has an unsupported type")
|
||||
}
|
||||
defer digestRoot.Close()
|
||||
state, err = inspectPlan(digestRoot, planFileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect chunk plan file: %w", err)
|
||||
}
|
||||
@@ -131,7 +128,7 @@ func (s *filesystemStore) Save(record pipeline.ChunkPlanRecord) error {
|
||||
if writer == nil {
|
||||
writer = writeAtomic
|
||||
}
|
||||
if err := writer(root, target, data); err != nil {
|
||||
if err := writer(digestRoot, planFileName, data); err != nil {
|
||||
return fmt.Errorf("write chunk plan: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -174,52 +171,89 @@ const (
|
||||
entryRejected
|
||||
)
|
||||
|
||||
func inspectDirectory(root *os.Root, digestDir string) (entryState, error) {
|
||||
func inspectDirectory(root *os.Root, digestDir string) (entryState, os.FileInfo, error) {
|
||||
info, err := root.Lstat(digestDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return entryMissing, nil
|
||||
return entryMissing, nil, nil
|
||||
}
|
||||
return entryPresent, err
|
||||
return entryPresent, nil, err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
||||
return entryRejected, nil
|
||||
return entryRejected, info, nil
|
||||
}
|
||||
return entryPresent, nil
|
||||
return entryPresent, info, nil
|
||||
}
|
||||
|
||||
func ensureDirectory(root *os.Root, digestDir string) error {
|
||||
type digestOpenHooks struct {
|
||||
BeforeOpen func() error
|
||||
}
|
||||
|
||||
func openDigestRoot(root *os.Root, digestDir string, create bool) (*os.Root, entryState, error) {
|
||||
return openDigestRootWithHooks(root, digestDir, create, digestOpenHooks{})
|
||||
}
|
||||
|
||||
func openDigestRootWithHooks(root *os.Root, digestDir string, create bool, hooks digestOpenHooks) (*os.Root, entryState, error) {
|
||||
for {
|
||||
state, err := inspectDirectory(root, digestDir)
|
||||
state, before, err := inspectDirectory(root, digestDir)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, entryPresent, err
|
||||
}
|
||||
switch state {
|
||||
case entryRejected:
|
||||
return fmt.Errorf("chunk plan directory has an unsupported type")
|
||||
return nil, entryRejected, nil
|
||||
case entryMissing:
|
||||
if !create {
|
||||
return nil, entryMissing, nil
|
||||
}
|
||||
if err := root.Mkdir(digestDir, 0o700); err != nil && !errors.Is(err, os.ErrExist) {
|
||||
return err
|
||||
return nil, entryPresent, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
directory, err := root.Open(digestDir)
|
||||
if hooks.BeforeOpen != nil {
|
||||
if err := hooks.BeforeOpen(); err != nil {
|
||||
return nil, entryPresent, err
|
||||
}
|
||||
}
|
||||
digestRoot, err := root.OpenRoot(digestDir)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, entryPresent, err
|
||||
}
|
||||
info, statErr := directory.Stat()
|
||||
if statErr == nil && !info.IsDir() {
|
||||
statErr = fmt.Errorf("chunk plan directory has an unsupported type")
|
||||
opened, statErr := digestRoot.Stat(".")
|
||||
afterState, after, afterErr := inspectDirectory(root, digestDir)
|
||||
if statErr != nil || afterErr != nil {
|
||||
_ = digestRoot.Close()
|
||||
if statErr != nil {
|
||||
return nil, entryPresent, statErr
|
||||
}
|
||||
return nil, entryPresent, afterErr
|
||||
}
|
||||
if statErr == nil {
|
||||
statErr = directory.Chmod(0o700)
|
||||
if afterState != entryPresent || !os.SameFile(before, after) || !os.SameFile(opened, after) {
|
||||
_ = digestRoot.Close()
|
||||
if afterState == entryRejected {
|
||||
return nil, entryRejected, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
closeErr := directory.Close()
|
||||
if statErr != nil {
|
||||
return statErr
|
||||
if create {
|
||||
directory, openErr := digestRoot.Open(".")
|
||||
if openErr != nil {
|
||||
_ = digestRoot.Close()
|
||||
return nil, entryPresent, openErr
|
||||
}
|
||||
chmodErr := directory.Chmod(0o700)
|
||||
closeErr := directory.Close()
|
||||
if chmodErr != nil || closeErr != nil {
|
||||
_ = digestRoot.Close()
|
||||
if chmodErr != nil {
|
||||
return nil, entryPresent, chmodErr
|
||||
}
|
||||
return nil, entryPresent, closeErr
|
||||
}
|
||||
}
|
||||
return closeErr
|
||||
return digestRoot, entryPresent, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,8 +271,54 @@ func inspectPlan(root *os.Root, target string) (entryState, error) {
|
||||
return entryPresent, nil
|
||||
}
|
||||
|
||||
func planPath(digestDir string) string {
|
||||
return digestDir + "/" + planFileName
|
||||
type planReadHooks struct {
|
||||
BeforeOpen func() error
|
||||
}
|
||||
|
||||
func readPlan(root *os.Root) ([]byte, entryState, error) {
|
||||
return readPlanWithHooks(root, planReadHooks{})
|
||||
}
|
||||
|
||||
func readPlanWithHooks(root *os.Root, hooks planReadHooks) ([]byte, entryState, error) {
|
||||
state, err := inspectPlan(root, planFileName)
|
||||
if err != nil || state != entryPresent {
|
||||
return nil, state, err
|
||||
}
|
||||
if hooks.BeforeOpen != nil {
|
||||
if err := hooks.BeforeOpen(); err != nil {
|
||||
return nil, entryPresent, err
|
||||
}
|
||||
}
|
||||
file, err := root.OpenFile(planFileName, os.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, entryMissing, nil
|
||||
}
|
||||
if errors.Is(err, syscall.ELOOP) {
|
||||
return nil, entryRejected, nil
|
||||
}
|
||||
return nil, entryPresent, err
|
||||
}
|
||||
defer file.Close()
|
||||
opened, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, entryPresent, err
|
||||
}
|
||||
if !opened.Mode().IsRegular() {
|
||||
return nil, entryRejected, nil
|
||||
}
|
||||
currentState, currentErr := inspectPlan(root, planFileName)
|
||||
if currentErr != nil {
|
||||
return nil, entryPresent, currentErr
|
||||
}
|
||||
if currentState == entryRejected {
|
||||
return nil, entryRejected, nil
|
||||
}
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, entryPresent, err
|
||||
}
|
||||
return data, entryPresent, nil
|
||||
}
|
||||
|
||||
func digestPathSegment(digest string) (string, error) {
|
||||
|
||||
@@ -163,6 +163,61 @@ func TestFilesystemStoreRejectsSymlinkedEntries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenDigestRootRejectsEntryReplacedDuringOpen(t *testing.T) {
|
||||
rootPath := t.TempDir()
|
||||
digestDir := strings.TrimPrefix(testSourceDigest, "sha256:")
|
||||
if err := os.Mkdir(filepath.Join(rootPath, digestDir), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(rootPath, "redirect"), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root, err := os.OpenRoot(rootPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer root.Close()
|
||||
|
||||
traced := false
|
||||
opened, state, err := openDigestRootWithHooks(root, digestDir, false, digestOpenHooks{BeforeOpen: func() error {
|
||||
if traced {
|
||||
return nil
|
||||
}
|
||||
traced = true
|
||||
if err := os.Rename(filepath.Join(rootPath, digestDir), filepath.Join(rootPath, "original")); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Symlink("redirect", filepath.Join(rootPath, digestDir))
|
||||
}})
|
||||
if opened != nil {
|
||||
_ = opened.Close()
|
||||
}
|
||||
if err != nil || state != entryRejected {
|
||||
t.Fatalf("openDigestRootWithHooks() root=%v state=%v error=%v, want nil/rejected/nil", opened, state, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPlanRejectsEntryReplacedDuringOpen(t *testing.T) {
|
||||
rootPath := t.TempDir()
|
||||
writeFile(t, filepath.Join(rootPath, planFileName), []byte("original"), 0o600)
|
||||
writeFile(t, filepath.Join(rootPath, "redirect.json"), []byte("redirect"), 0o600)
|
||||
root, err := os.OpenRoot(rootPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer root.Close()
|
||||
|
||||
data, state, err := readPlanWithHooks(root, planReadHooks{BeforeOpen: func() error {
|
||||
if err := os.Remove(filepath.Join(rootPath, planFileName)); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Symlink("redirect.json", filepath.Join(rootPath, planFileName))
|
||||
}})
|
||||
if err != nil || state != entryRejected || data != nil {
|
||||
t.Fatalf("readPlanWithHooks() data=%q state=%v error=%v, want nil/rejected/nil", data, state, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemStoreRejectsUnexpectedEntryTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user