// Package kernelcat is the on-disk catalog of Firecracker-ready kernel // bundles. Each entry lives at // and contains a // manifest.json alongside the vmlinux, optional initrd.img, and optional // modules/ tree. The package owns the layout, manifest read/write, and // validation; it does not talk to the network (remote pulls are layered on // later). package kernelcat import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "os" "path/filepath" "regexp" "sort" "strings" "time" ) // Filenames used inside an entry directory. const ( manifestFilename = "manifest.json" kernelFilename = "vmlinux" initrdFilename = "initrd.img" modulesDirName = "modules" ) // Entry describes a cataloged kernel bundle. Paths are absolute and // populated from the entry's on-disk layout when read via ReadLocal / // ListLocal; they are never written into the manifest itself. type Entry struct { Name string `json:"name"` Distro string `json:"distro,omitempty"` Arch string `json:"arch,omitempty"` KernelVersion string `json:"kernel_version,omitempty"` SHA256 string `json:"sha256,omitempty"` Source string `json:"source,omitempty"` ImportedAt time.Time `json:"imported_at"` // Populated on read, not persisted: KernelPath string `json:"-"` InitrdPath string `json:"-"` ModulesDir string `json:"-"` } // namePattern matches names that are safe as single filesystem components. // Intentionally strict so entry names stay short and script-friendly. var namePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$`) // ValidateName returns an error unless name is a non-empty identifier made // of alphanumerics, dots, hyphens, and underscores, starting with an // alphanumeric and at most 64 characters long. func ValidateName(name string) error { if strings.TrimSpace(name) == "" { return errors.New("kernel name is required") } if !namePattern.MatchString(name) { return fmt.Errorf("invalid kernel name %q: use alphanumerics, dots, hyphens, underscores (<=64 chars, starts with alphanumeric)", name) } return nil } // EntryDir returns the absolute directory path for name under kernelsDir. func EntryDir(kernelsDir, name string) string { return filepath.Join(kernelsDir, name) } // ReadLocal reads the manifest for name and resolves per-artifact paths. // Returns os.ErrNotExist-compatible error if the entry is missing. func ReadLocal(kernelsDir, name string) (Entry, error) { if err := ValidateName(name); err != nil { return Entry{}, err } dir := EntryDir(kernelsDir, name) data, err := os.ReadFile(filepath.Join(dir, manifestFilename)) if err != nil { return Entry{}, err } var entry Entry if err := json.Unmarshal(data, &entry); err != nil { return Entry{}, fmt.Errorf("parse manifest for %q: %w", name, err) } if entry.Name == "" { entry.Name = name } if entry.Name != name { return Entry{}, fmt.Errorf("manifest name %q does not match directory %q", entry.Name, name) } entry.KernelPath = filepath.Join(dir, kernelFilename) if fi, err := os.Stat(filepath.Join(dir, initrdFilename)); err == nil && !fi.IsDir() { entry.InitrdPath = filepath.Join(dir, initrdFilename) } if fi, err := os.Stat(filepath.Join(dir, modulesDirName)); err == nil && fi.IsDir() { entry.ModulesDir = filepath.Join(dir, modulesDirName) } return entry, nil } // ListLocal returns every entry under kernelsDir with a readable manifest, // sorted by name. Directories without a manifest are skipped silently so // partial imports don't break the list. func ListLocal(kernelsDir string) ([]Entry, error) { dirEntries, err := os.ReadDir(kernelsDir) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } entries := make([]Entry, 0, len(dirEntries)) for _, de := range dirEntries { if !de.IsDir() { continue } name := de.Name() if err := ValidateName(name); err != nil { continue } entry, err := ReadLocal(kernelsDir, name) if err != nil { if os.IsNotExist(err) { continue } return nil, err } entries = append(entries, entry) } sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name }) return entries, nil } // WriteLocal persists entry's manifest.json. The caller is responsible for // placing vmlinux / initrd.img / modules/ under the entry dir first. func WriteLocal(kernelsDir string, entry Entry) error { if err := ValidateName(entry.Name); err != nil { return err } dir := EntryDir(kernelsDir, entry.Name) if err := os.MkdirAll(dir, 0o755); err != nil { return err } if entry.ImportedAt.IsZero() { entry.ImportedAt = time.Now().UTC() } data, err := json.MarshalIndent(entry, "", " ") if err != nil { return err } return os.WriteFile(filepath.Join(dir, manifestFilename), append(data, '\n'), 0o644) } // DeleteLocal removes the entry directory entirely. Missing entries are a // no-op so callers can idempotently clean up. func DeleteLocal(kernelsDir, name string) error { if err := ValidateName(name); err != nil { return err } dir := EntryDir(kernelsDir, name) if _, err := os.Stat(dir); err != nil { if os.IsNotExist(err) { return nil } return err } return os.RemoveAll(dir) } // SumFile returns the hex-encoded SHA256 of the file at path. func SumFile(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err } defer f.Close() hasher := sha256.New() if _, err := io.Copy(hasher, f); err != nil { return "", err } return hex.EncodeToString(hasher.Sum(nil)), nil }