| |
| |
| package compactscan |
|
|
| import ( |
| "errors" |
| "fmt" |
| "io" |
| "os" |
| "path/filepath" |
| "strings" |
|
|
| "huggingface.co/turenlabs/Vigil/source/pkg/bundle" |
| "huggingface.co/turenlabs/Vigil/source/pkg/compactmodel" |
| ) |
|
|
| |
| |
| type Stats struct { |
| DiscoveredFiles int `json:"discovered_files"` |
| ModeledFiles int `json:"modeled_files"` |
| BytesRead int `json:"bytes_read"` |
| } |
|
|
| type member struct { |
| rel string |
| info os.FileInfo |
| } |
|
|
| |
| |
| |
| func Scan(target string) (compactmodel.Package, Stats, error) { |
| if target == "" { |
| return compactmodel.Package{}, Stats{}, errors.New("compact scan target is required") |
| } |
| if hasTraversal(target) { |
| return compactmodel.Package{}, Stats{}, errors.New("compact scan target contains traversal") |
| } |
| abs, err := filepath.Abs(target) |
| if err != nil { |
| return compactmodel.Package{}, Stats{}, fmt.Errorf("resolve compact scan target: %w", err) |
| } |
| targetInfo, err := os.Lstat(abs) |
| if err != nil { |
| return compactmodel.Package{}, Stats{}, fmt.Errorf("inspect compact scan target: %w", err) |
| } |
| if targetInfo.Mode()&os.ModeSymlink != 0 { |
| return compactmodel.Package{}, Stats{}, errors.New("compact scan target must not be a symlink") |
| } |
| rootPath := abs |
| if !targetInfo.IsDir() { |
| if !targetInfo.Mode().IsRegular() || !strings.EqualFold(filepath.Base(abs), "SKILL.md") { |
| return compactmodel.Package{}, Stats{}, errors.New("compact scan requires a package directory or its SKILL.md") |
| } |
| rootPath = filepath.Dir(abs) |
| } |
| rootPath, err = filepath.EvalSymlinks(rootPath) |
| if err != nil { |
| return compactmodel.Package{}, Stats{}, fmt.Errorf("resolve compact package root: %w", err) |
| } |
| if err := preflightPackage(rootPath); err != nil { |
| return compactmodel.Package{}, Stats{}, fmt.Errorf("compact package preflight failed: %w", err) |
| } |
|
|
| scanned, err := bundle.Scan(rootPath) |
| if err != nil { |
| return compactmodel.Package{}, Stats{}, fmt.Errorf("whole-package scanner failed: %w", err) |
| } |
| if scanned.SkillMdPath == "" { |
| return compactmodel.Package{}, Stats{}, errors.New("whole-package scanner found no SKILL.md") |
| } |
| if scanned.ParseErr != nil { |
| return compactmodel.Package{}, Stats{}, errors.New("whole-package scanner could not parse SKILL.md") |
| } |
| if len(scanned.Notes) != 0 { |
| return compactmodel.Package{}, Stats{}, errors.New("whole-package scanner reported an incomplete or ambiguous scan") |
| } |
|
|
| root, err := os.OpenRoot(rootPath) |
| if err != nil { |
| return compactmodel.Package{}, Stats{}, fmt.Errorf("open compact package root: %w", err) |
| } |
| defer root.Close() |
|
|
| members := make([]member, 0, len(scanned.Files)+1) |
| skillRel, err := filepath.Rel(rootPath, scanned.SkillMdPath) |
| if err != nil { |
| return compactmodel.Package{}, Stats{}, errors.New("SKILL.md is outside the package root") |
| } |
| members = append(members, member{rel: filepath.ToSlash(skillRel)}) |
| for _, file := range scanned.Files { |
| if file == nil { |
| return compactmodel.Package{}, Stats{}, errors.New("whole-package scanner returned an empty member") |
| } |
| if file.IsSymlink { |
| return compactmodel.Package{}, Stats{}, errors.New("compact package contains a symlink") |
| } |
| members = append(members, member{rel: filepath.ToSlash(file.RelPath)}) |
| } |
|
|
| lightweight := make([]compactmodel.File, len(members)) |
| seen := make(map[string]struct{}, len(members)) |
| for index := range members { |
| rel := members[index].rel |
| if rel == "" || rel == "." || strings.Contains(rel, "\\") || hasTraversal(rel) || filepath.IsAbs(rel) { |
| return compactmodel.Package{}, Stats{}, errors.New("whole-package scanner returned a non-canonical member") |
| } |
| if _, exists := seen[rel]; exists { |
| return compactmodel.Package{}, Stats{}, errors.New("whole-package scanner returned a duplicate member") |
| } |
| seen[rel] = struct{}{} |
| info, err := root.Lstat(filepath.FromSlash(rel)) |
| if err != nil { |
| return compactmodel.Package{}, Stats{}, errors.New("package member changed after whole-package scan") |
| } |
| if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() { |
| return compactmodel.Package{}, Stats{}, errors.New("compact package member is not an unlinked regular file") |
| } |
| members[index].info = info |
| lightweight[index] = compactmodel.File{Path: rel, Executable: info.Mode().Perm()&0o111 != 0} |
| } |
|
|
| |
| |
| for _, item := range members { |
| file, err := root.Open(filepath.FromSlash(item.rel)) |
| if err != nil { |
| return compactmodel.Package{}, Stats{}, errors.New("open package member after whole-package scan") |
| } |
| opened, statErr := file.Stat() |
| if statErr != nil { |
| file.Close() |
| return compactmodel.Package{}, Stats{}, errors.New("stat package member after whole-package scan") |
| } |
| links, linkErr := openedHardLinkCount(file, opened) |
| closeErr := file.Close() |
| if linkErr != nil || closeErr != nil || links != 1 || !os.SameFile(item.info, opened) { |
| return compactmodel.Package{}, Stats{}, errors.New("compact package contains a changed or hard-linked member") |
| } |
| } |
|
|
| selected, err := compactmodel.SelectFiles(lightweight) |
| if err != nil { |
| return compactmodel.Package{}, Stats{}, err |
| } |
| byPath := make(map[string]member, len(members)) |
| for _, item := range members { |
| byPath[item.rel] = item |
| } |
| result := compactmodel.Package{Files: make([]compactmodel.File, 0, len(selected))} |
| for _, selectedFile := range selected { |
| item := byPath[selectedFile.Path] |
| file, err := root.Open(filepath.FromSlash(item.rel)) |
| if err != nil { |
| return compactmodel.Package{}, Stats{}, errors.New("open selected compact package member") |
| } |
| opened, err := file.Stat() |
| if err != nil || !opened.Mode().IsRegular() || !os.SameFile(item.info, opened) { |
| file.Close() |
| return compactmodel.Package{}, Stats{}, errors.New("selected package member changed while opening") |
| } |
| links, err := openedHardLinkCount(file, opened) |
| if err != nil || links != 1 { |
| file.Close() |
| return compactmodel.Package{}, Stats{}, errors.New("selected package member has ambiguous links") |
| } |
| content, err := sampleFileContent(file, opened.Size()) |
| if err != nil { |
| file.Close() |
| return compactmodel.Package{}, Stats{}, err |
| } |
| final, statErr := file.Stat() |
| if statErr != nil { |
| file.Close() |
| return compactmodel.Package{}, Stats{}, errors.New("stat selected package member after reading") |
| } |
| links, linkErr := openedHardLinkCount(file, final) |
| closeErr := file.Close() |
| if linkErr != nil || closeErr != nil || links != 1 || final.Size() != opened.Size() || !os.SameFile(opened, final) { |
| return compactmodel.Package{}, Stats{}, errors.New("selected package member changed while reading") |
| } |
| selectedFile.Content = content |
| selectedFile.SizeBytes = opened.Size() |
| result.Files = append(result.Files, selectedFile) |
| } |
| document, err := compactmodel.Serialize(result) |
| if err != nil { |
| return compactmodel.Package{}, Stats{}, err |
| } |
| return result, Stats{ |
| DiscoveredFiles: len(members), |
| ModeledFiles: document.FileCount, |
| BytesRead: document.BytesRead, |
| }, nil |
| } |
|
|
| func sampleFileContent(file *os.File, size int64) ([]byte, error) { |
| if size < 0 { |
| return nil, errors.New("negative package member size") |
| } |
| if size <= compactmodel.MaxFileBytes { |
| data := make([]byte, int(size)) |
| if _, err := io.ReadFull(file, data); err != nil { |
| return nil, errors.New("read complete package member") |
| } |
| return data, nil |
| } |
| half := compactmodel.MaxFileBytes / 2 |
| data := make([]byte, compactmodel.MaxFileBytes) |
| if _, err := file.ReadAt(data[:half], 0); err != nil { |
| return nil, errors.New("read package member head") |
| } |
| if _, err := file.ReadAt(data[half:], size-int64(half)); err != nil { |
| return nil, errors.New("read package member tail") |
| } |
| return data, nil |
| } |
|
|
| func hasTraversal(value string) bool { |
| value = strings.ReplaceAll(value, "\\", "/") |
| for _, part := range strings.Split(value, "/") { |
| if part == ".." { |
| return true |
| } |
| } |
| return false |
| } |
|
|