1package vnext23import (4 "bytes"5 "crypto/sha256"6 "errors"7 "fmt"8 "io"9 "os"10 "path/filepath"11 "sort"12)1314type Skipped struct {15 Locator string16 Err error17}1819func (skipped Skipped) Error() string { return skipped.Locator + ": " + skipped.Err.Error() }20func (skipped Skipped) Unwrap() error { return skipped.Err }2122// File owns the operating-system handle behind a Reader.23type File struct {24 *Reader25 source *os.File26 size int6427 path string28}2930func OpenFile(path string, applicationSchema Schema) (*File, error) {31 source, err := os.Open(path)32 if err != nil {33 return nil, fmt.Errorf("open %s: %w", filepath.Base(path), err)34 }35 stat, err := source.Stat()36 if err != nil {37 source.Close()38 return nil, fmt.Errorf("stat %s: %w", filepath.Base(path), err)39 }40 reader, err := Open(source, stat.Size(), applicationSchema)41 if err != nil {42 source.Close()43 return nil, fmt.Errorf("%s: %w", filepath.Base(path), err)44 }45 return &File{Reader: reader, source: source, size: stat.Size(), path: path}, nil46}4748func (file *File) Close() error { return file.source.Close() }4950func (file *File) Descriptor() Descriptor {51 return DescriptorOf(file.path, file.VolumeID, file.Release, file.size)52}5354func Describe(path string, applicationSchema Schema) (Descriptor, error) {55 file, err := OpenFile(path, applicationSchema)56 if err != nil {57 return Descriptor{}, err58 }59 defer file.Close()60 return file.Descriptor(), nil61}6263func Scan(dir string, applicationSchema Schema) ([]Descriptor, []Skipped, error) {64 paths, err := filepath.Glob(filepath.Join(dir, "*"+Extension))65 if err != nil {66 return nil, nil, fmt.Errorf("scan %s: %w", dir, err)67 }68 sort.Strings(paths)69 descriptors := make([]Descriptor, 0, len(paths))70 var skipped []Skipped71 for _, path := range paths {72 descriptor, err := Describe(path, applicationSchema)73 if err != nil {74 skipped = append(skipped, Skipped{Locator: path, Err: err})75 continue76 }77 descriptors = append(descriptors, descriptor)78 }79 return descriptors, skipped, nil80}8182func Install(dir, source string, applicationSchema Schema) (Descriptor, error) {83 descriptor, _, err := InstallWithStatus(dir, source, applicationSchema)84 return descriptor, err85}8687// InstallWithStatus validates and atomically installs the exact opened source.88// present reports that an identical target was already committed.89func InstallWithStatus(dir, source string, applicationSchema Schema) (Descriptor, bool, error) {90 file, err := os.Open(source)91 if err != nil {92 return Descriptor{}, false, fmt.Errorf("open %s: %w", filepath.Base(source), err)93 }94 defer file.Close()95 info, err := file.Stat()96 if err != nil {97 return Descriptor{}, false, fmt.Errorf("stat %s: %w", filepath.Base(source), err)98 }99 return installFrom(dir, file, info.Size(), source, applicationSchema)100}101102type installSource interface {103 io.Reader104 io.ReaderAt105 io.Seeker106}107108func installFrom(dir string, source installSource, size int64, sourcePath string, applicationSchema Schema) (Descriptor, bool, error) {109 opened, err := Open(source, size, applicationSchema)110 if err != nil {111 return Descriptor{}, false, err112 }113 if err := opened.Validate(); err != nil {114 return Descriptor{}, false, fmt.Errorf("validate native source: %w", err)115 }116 descriptor := DescriptorOf(sourcePath, opened.VolumeID, opened.Release, size)117 if err := os.MkdirAll(dir, 0o755); err != nil {118 return Descriptor{}, false, fmt.Errorf("create library: %w", err)119 }120 target := filepath.Join(dir, VersionedFileName(descriptor.Slug, descriptor.release()))121 if sourcePath != "" && sameFile(sourcePath, target) {122 descriptor.Locator = target123 return descriptor, true, nil124 }125 if _, err := os.Stat(target); err == nil {126 return validateExisting(target, source, size, descriptor, applicationSchema)127 } else if !errors.Is(err, os.ErrNotExist) {128 return Descriptor{}, false, fmt.Errorf("stat %s: %w", filepath.Base(target), err)129 }130 if _, err := source.Seek(0, io.SeekStart); err != nil {131 return Descriptor{}, false, fmt.Errorf("rewind validated source: %w", err)132 }133 installed, err := stageInto(dir, source, target)134 if err != nil {135 return Descriptor{}, false, err136 }137 if !installed {138 return validateExisting(target, source, size, descriptor, applicationSchema)139 }140 if err := syncDirectory(dir); err != nil {141 os.Remove(target)142 return Descriptor{}, false, fmt.Errorf("sync Atlas library: %w", err)143 }144 committed, err := validateInstalled(target, applicationSchema)145 if err != nil {146 os.Remove(target)147 _ = syncDirectory(dir)148 return Descriptor{}, false, err149 }150 return committed, false, nil151}152153func InstallBytes(dir string, source []byte, applicationSchema Schema) (Descriptor, error) {154 descriptor, _, err := installFrom(dir, bytes.NewReader(source), int64(len(source)), "", applicationSchema)155 return descriptor, err156}157158func validateInstalled(target string, applicationSchema Schema) (Descriptor, error) {159 installed, err := OpenFile(target, applicationSchema)160 if err != nil {161 return Descriptor{}, fmt.Errorf("installed %s: %w", filepath.Base(target), err)162 }163 defer installed.Close()164 if err := installed.Validate(); err != nil {165 return Descriptor{}, fmt.Errorf("installed %s: %w", filepath.Base(target), err)166 }167 return installed.Descriptor(), nil168}169170func stageInto(dir string, source io.Reader, target string) (bool, error) {171 staged, err := os.CreateTemp(dir, ".installing-*")172 if err != nil {173 return false, fmt.Errorf("stage %s: %w", filepath.Base(target), err)174 }175 stagedName := staged.Name()176 defer os.Remove(stagedName)177 if _, err := io.Copy(staged, source); err != nil {178 staged.Close()179 return false, fmt.Errorf("stage %s: %w", filepath.Base(target), err)180 }181 if err := staged.Sync(); err != nil {182 staged.Close()183 return false, fmt.Errorf("sync %s: %w", filepath.Base(target), err)184 }185 if err := staged.Close(); err != nil {186 return false, fmt.Errorf("close %s: %w", filepath.Base(target), err)187 }188 if err := os.Chmod(stagedName, 0o644); err != nil {189 return false, fmt.Errorf("mode %s: %w", filepath.Base(target), err)190 }191 if err := os.Link(stagedName, target); err != nil {192 if errors.Is(err, os.ErrExist) {193 return false, nil194 }195 return false, fmt.Errorf("install %s: %w", filepath.Base(target), err)196 }197 return true, nil198}199200func validateExisting(target string, source installSource, sourceSize int64, expected Descriptor, applicationSchema Schema) (Descriptor, bool, error) {201 held, err := validateInstalled(target, applicationSchema)202 if err != nil {203 return Descriptor{}, false, fmt.Errorf("existing stamp target is invalid: %w", err)204 }205 if held.Slug != expected.Slug || held.Stamp != expected.Stamp || held.Size != sourceSize {206 return Descriptor{}, false, fmt.Errorf("existing %s disagrees with its stamp identity", filepath.Base(target))207 }208 same, err := sameContent(source, sourceSize, target)209 if err != nil {210 return Descriptor{}, false, err211 }212 if !same {213 return Descriptor{}, false, fmt.Errorf("existing %s differs byte-for-byte for stamp %s", filepath.Base(target), ShortStamp(expected.Stamp))214 }215 return held, true, nil216}217218func sameContent(source io.ReadSeeker, sourceSize int64, target string) (bool, error) {219 if _, err := source.Seek(0, io.SeekStart); err != nil {220 return false, fmt.Errorf("rewind validated source: %w", err)221 }222 left := sha256.New()223 written, err := io.Copy(left, source)224 if err != nil {225 return false, fmt.Errorf("hash validated source: %w", err)226 }227 if written != sourceSize {228 return false, fmt.Errorf("validated source changed length while installing")229 }230 right, err := os.Open(target)231 if err != nil {232 return false, fmt.Errorf("open existing %s: %w", filepath.Base(target), err)233 }234 defer right.Close()235 rightHash := sha256.New()236 otherSize, err := io.Copy(rightHash, right)237 if err != nil {238 return false, fmt.Errorf("hash existing %s: %w", filepath.Base(target), err)239 }240 return otherSize == sourceSize && bytes.Equal(left.Sum(nil), rightHash.Sum(nil)), nil241}242243func sameFile(left, right string) bool {244 leftInfo, leftErr := os.Stat(left)245 rightInfo, rightErr := os.Stat(right)246 return leftErr == nil && rightErr == nil && os.SameFile(leftInfo, rightInfo)247}248249func (descriptor Descriptor) release() Release {250 return Release{251 Title: descriptor.Title, CreatedAt: descriptor.CreatedAt, Revision: descriptor.Revision,252 Stamp: descriptor.Stamp, Worlds: descriptor.Worlds,253 }254}