package hostfiles import ( "crypto/sha256" "encoding/hex" "errors" "io" "os" "path/filepath" "github.com/carlosframework/platform/internal/instancefs" ) // Present reports, read-only, whether dir and envPath hold exactly what m // names (region HA ruling 52, VerifyLocal): every live, non-skipped, // non-torn entry that Restore would write present as a regular file at // its size and sha256, and the env file a regular file at its platform // entry's size and sha256. A live entry Restore supersedes (a file and an // entry beneath it, resolved by sequence: superseded) is not looked for, // so a restore that Restore reports complete is present. Extra files are // not checked: the app may write after P's opening, and Present is called // only before it. // // A manifest without a live env entry is not evidence that the host has // no env file (platform items have no tombstone: StagePlatformDelete drops // the item, so a deleted env reads as one never published). envAbsent is // the caller's own positive evidence that it has none; only then is an // absent env file present, and any file at envPath is not. Without it, a // manifest with no env entry, or a tombstone (which no publisher writes), // is never present. func Present(dir, envPath string, m Manifest, envAbsent bool) (bool, error) { loser := superseded(m) for rel, e := range m.Entries { if e.Deleted || e.Torn || e.Skipped != "" || loser[rel] { continue } ok, err := presentFile(dir, filepath.FromSlash(rel), e.Size, e.SHA256) if err != nil || !ok { return false, err } } // The env file is the platform entry's exact bytes (Astra round 2 // finding 9): a regular file at its size and digest. envDir, envName := filepath.Dir(envPath), filepath.Base(envPath) e, ok := m.Platform[EnvItem] switch { case ok && (e.Deleted || e.Torn || e.Skipped != "" || e.SHA256 == ""): return false, nil case ok: return presentFile(envDir, envName, e.Size, e.SHA256) case !envAbsent: return false, nil } // No env file is present only on positive evidence, decided in one // place (instancefs.OpenBeneathOrAbsent): the directory opened once, // with no symlink at any component, and the name was not found beneath // that same descriptor. A directory that is missing or vanishes in // between is not that evidence. f, absent, err := instancefs.OpenBeneathOrAbsent(envDir, envName) switch { case absent: return true, nil case err == nil: f.Close() return false, nil case errors.Is(err, instancefs.ErrDirUnopened): if errors.Is(err, os.ErrNotExist) { return false, nil } return false, err // a linked directory: refused, never read through case notThere(err): return false, nil // something that is not a regular file: not "none" } return false, err } // notThere is what instancefs.OpenBeneath answers for a name that holds // something other than a regular file reached without a link: a symlink // at the name or beneath dir, a directory, a FIFO or a device. None of // those is the file a restore wrote, so the file is not present; none of // them is read. func notThere(err error) bool { return errors.Is(err, instancefs.ErrSymlink) || errors.Is(err, instancefs.ErrNotRegular) } // presentFile reports whether rel beneath dir is a regular file of size // bytes whose sha256 is sum. Both dir and everything beneath it are the // tenant's to write, so it opens through main's one safe open for that // ground (instancefs.OpenBeneathOrAbsent: openat2 with no symlink at any // component, O_NONBLOCK, a regular file only) and reads no more than size // bytes plus one, so a file grown or swapped after the look cannot make it // read without bound. func presentFile(dir, rel string, size int64, sum string) (bool, error) { f, absent, err := instancefs.OpenBeneathOrAbsent(dir, rel) switch { case absent: return false, nil case errors.Is(err, instancefs.ErrDirUnopened): if errors.Is(err, os.ErrNotExist) { return false, nil // nothing restored yet } return false, err // a linked root: refused, never read through case notThere(err): return false, nil case err != nil: return false, err } defer f.Close() info, err := f.Stat() if err != nil { return false, err } if info.Size() != size { return false, nil } h := sha256.New() n, err := io.Copy(h, io.LimitReader(f, size+1)) if err != nil { return false, err } return n == size && hex.EncodeToString(h.Sum(nil)) == sum, nil }