mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
CI / build (push) Failing after 33s
Go / generate (push) Failing after 46s
The git ceiling named the workspace, which git only applies to a proper ancestor of the directory it starts from, so a request with no dir found, read and wrote a repository above the workspace. The ceiling is now the workspace's parent, which refuses from the root and from any directory under it. patch staged by action id, so two changes naming one file collided and the second failed with the first's bytes already in place, and a delete that could not happen left earlier renames behind. Every change now stages under a random name, the file that was there is kept on a second link until every move has landed, and a move that fails is put back. A diff that names one file twice, gives two names for one file, would delete a directory, or renames is refused. A zero-length packet is a packet on SOCK_SEQPACKET, and reading it as end of file ended the daemon for good. POLLRDHUP tells the two apart. Also: a listing fills a page to what a frame holds instead of promising 4096 entries no frame can carry; write and patch resolve the parent with openat2 on the path as written, so one string cannot mean two places; a refused request is not recorded, so its id is free, while an action that ran is recorded whatever came of it; a staged name is random, so a workspace file is never unlinked to make room; children start from a declared environment instead of the daemon's; write reports the mode on disk and keeps the mode a file had; mode means permission bits in stat as well as read and list; a read past the end is refused; an oversize refusal names the op it refused; the ledger is bounded by bytes as well as count; the diff path is the reading both header lines agree on; the build constraint sits above the package doc; and BUILD embeds a library, which is how the repo builds a main package with tests. Co-authored-by: Hanzo Dev <[email protected]>
472 lines
13 KiB
Go
472 lines
13 KiB
Go
// Copyright 2026 The gVisor Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//go:build linux
|
|
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// null is the path a unified diff gives the missing side of a created or
|
|
// deleted file.
|
|
const null = "/dev/null"
|
|
|
|
// change is one file's worth of a unified diff.
|
|
type change struct {
|
|
path string
|
|
kill bool // the diff deletes the file
|
|
hunks []hunk
|
|
}
|
|
|
|
// hunk is one @@ block. Its lines keep their leading ' ', '-' or '+'.
|
|
type hunk struct {
|
|
old, new int // 1-based first line on each side
|
|
lines []string
|
|
noEndNew bool // the new side ends without a newline
|
|
}
|
|
|
|
// move is one file's worth of a staged diff: what goes in, what was there,
|
|
// and the directory both of them live in.
|
|
type move struct {
|
|
dirfd int
|
|
base string
|
|
path string
|
|
next string // the staged replacement; empty when the diff deletes the file
|
|
prev string // a link to the bytes that were there; empty when there were none
|
|
}
|
|
|
|
// patch applies a unified diff. Every file is verified and staged first, and
|
|
// the bytes that were there are kept on a second link, so a diff that does
|
|
// not fit changes nothing and a move that fails is put back.
|
|
func (d *daemon) patch(r *req) *rep {
|
|
changes, err := parseDiff(string(r.data))
|
|
if err != nil {
|
|
return fail(r, err)
|
|
}
|
|
if len(changes) == 0 {
|
|
return fail(r, errors.New("the diff names no file"))
|
|
}
|
|
|
|
work, err := d.plan(changes)
|
|
if err != nil {
|
|
return fail(r, err)
|
|
}
|
|
defer release(work)
|
|
|
|
if untouched, err := commit(work); err != nil {
|
|
reply := fail(r, err)
|
|
// A diff that was put back did not happen; one the daemon could not
|
|
// put back did, and says so, so a repeat of the id is not run again.
|
|
reply.refused = untouched
|
|
return reply
|
|
}
|
|
|
|
reply := &rep{size: uint64(len(work))}
|
|
for _, m := range work {
|
|
reply.entries = append(reply.entries, entry{name: m.path})
|
|
}
|
|
return reply
|
|
}
|
|
|
|
// plan verifies and stages every change. Nothing in the workspace is moved
|
|
// yet: on the way out with an error, every staged file is gone.
|
|
func (d *daemon) plan(changes []change) ([]move, error) {
|
|
var work []move
|
|
seen := make(map[[2]uint64]string, len(changes))
|
|
for _, c := range changes {
|
|
m, err := d.arrange(c, seen)
|
|
if err != nil {
|
|
release(work)
|
|
return nil, err
|
|
}
|
|
work = append(work, m)
|
|
}
|
|
return work, nil
|
|
}
|
|
|
|
// arrange stages one change: the new bytes in a file of their own, and a link
|
|
// to the old bytes to go back to. seen holds the files the diff has already
|
|
// named, by identity, so two spellings of one file are refused rather than
|
|
// applied one over the other.
|
|
func (d *daemon) arrange(c change, seen map[[2]uint64]string) (move, error) {
|
|
dirfd, base, err := d.root.parent(c.path)
|
|
if err != nil {
|
|
return move{}, err
|
|
}
|
|
m := move{dirfd: dirfd, base: base, path: c.path}
|
|
kept := false
|
|
defer func() {
|
|
if !kept {
|
|
release([]move{m})
|
|
}
|
|
}()
|
|
|
|
var st unix.Stat_t
|
|
here := unix.Fstatat(dirfd, base, &st, 0) == nil
|
|
switch {
|
|
case here && st.Mode&unix.S_IFMT == unix.S_IFDIR:
|
|
return move{}, fmt.Errorf("%s: is a directory", c.path)
|
|
case here:
|
|
if first, ok := seen[[2]uint64{st.Dev, st.Ino}]; ok {
|
|
return move{}, fmt.Errorf("the diff names %s and %s, which are one file", first, c.path)
|
|
}
|
|
seen[[2]uint64{st.Dev, st.Ino}] = c.path
|
|
case c.kill:
|
|
return move{}, fmt.Errorf("delete %s: %w", c.path, unix.ENOENT)
|
|
}
|
|
|
|
if !c.kill {
|
|
old, mode, err := d.current(c.path)
|
|
if err != nil {
|
|
return move{}, err
|
|
}
|
|
next, err := apply(c.path, old, c.hunks)
|
|
if err != nil {
|
|
return move{}, err
|
|
}
|
|
name, fd, err := d.stage(dirfd, mode)
|
|
if err != nil {
|
|
return move{}, err
|
|
}
|
|
m.next = name
|
|
if _, err := settle(os.NewFile(uintptr(fd), name), next, mode); err != nil {
|
|
return move{}, err
|
|
}
|
|
}
|
|
if here {
|
|
prev, err := link(dirfd, base)
|
|
if err != nil {
|
|
return move{}, fmt.Errorf("keep %s to go back to: %w", c.path, err)
|
|
}
|
|
m.prev = prev
|
|
}
|
|
kept = true
|
|
return m, nil
|
|
}
|
|
|
|
// link hard-links the file at base beside it under a name nothing else can
|
|
// name. The link holds the old bytes whatever happens to base.
|
|
func link(dirfd int, base string) (string, error) {
|
|
for try := 0; try < 4; try++ {
|
|
name, err := scratch()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
err = unix.Linkat(dirfd, base, dirfd, name, 0)
|
|
if err == nil {
|
|
return name, nil
|
|
}
|
|
if !errors.Is(err, unix.EEXIST) {
|
|
return "", err
|
|
}
|
|
}
|
|
return "", fmt.Errorf("no free name beside %s", base)
|
|
}
|
|
|
|
// commit moves every staged file into place. A move that fails puts back the
|
|
// moves before it; the bool says whether the tree is as it was, which is what
|
|
// decides whether the action can be asked for again.
|
|
func commit(work []move) (bool, error) {
|
|
var err error
|
|
for i, m := range work {
|
|
if m.next == "" {
|
|
err = unix.Unlinkat(m.dirfd, m.base, 0)
|
|
if err != nil {
|
|
err = fmt.Errorf("delete %s: %w", m.path, err)
|
|
}
|
|
} else if err = unix.Renameat(m.dirfd, m.next, m.dirfd, m.base); err != nil {
|
|
err = fmt.Errorf("rename over %s: %w", m.path, err)
|
|
}
|
|
if err == nil {
|
|
continue
|
|
}
|
|
if back := undo(work[:i]); back != nil {
|
|
return false, fmt.Errorf("%w; and %d of %d files are changed: %w", err, i, len(work), back)
|
|
}
|
|
return true, err
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// undo puts back what commit has moved, newest first: a file that was there is
|
|
// restored from its link, and one the diff created is removed.
|
|
func undo(work []move) error {
|
|
for i := len(work) - 1; i >= 0; i-- {
|
|
m := work[i]
|
|
if m.prev != "" {
|
|
if err := unix.Renameat(m.dirfd, m.prev, m.dirfd, m.base); err != nil {
|
|
return fmt.Errorf("put %s back: %w", m.path, err)
|
|
}
|
|
continue
|
|
}
|
|
if err := unix.Unlinkat(m.dirfd, m.base, 0); err != nil {
|
|
return fmt.Errorf("take %s away again: %w", m.path, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// release lets go of staged work. Before commit it takes the staged files and
|
|
// the links away; after commit the staged file is the file and the link is the
|
|
// last hold on the bytes it replaced, so the same call is what finishes a
|
|
// delete and frees what a write replaced.
|
|
func release(work []move) {
|
|
for _, m := range work {
|
|
if m.next != "" {
|
|
unix.Unlinkat(m.dirfd, m.next, 0)
|
|
}
|
|
if m.prev != "" {
|
|
unix.Unlinkat(m.dirfd, m.prev, 0)
|
|
}
|
|
unix.Close(m.dirfd)
|
|
}
|
|
}
|
|
|
|
// current reads the file a change applies to. A file the diff creates is
|
|
// absent, and that is not an error.
|
|
func (d *daemon) current(name string) ([]byte, uint32, error) {
|
|
f, err := d.root.file(name, unix.O_RDONLY, 0)
|
|
if err != nil {
|
|
if errors.Is(err, unix.ENOENT) {
|
|
return nil, fileMode, nil
|
|
}
|
|
return nil, 0, err
|
|
}
|
|
defer f.Close()
|
|
st, err := f.Stat()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
data, err := io.ReadAll(f)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return data, uint32(st.Mode().Perm()), nil
|
|
}
|
|
|
|
// parseDiff reads a unified diff. Anything it does not understand is an
|
|
// error: a patch is never applied on a guess.
|
|
func parseDiff(text string) ([]change, error) {
|
|
lines := strings.Split(text, "\n")
|
|
var out []change
|
|
var cur *change
|
|
var h *hunk
|
|
named := make(map[string]string)
|
|
|
|
for i := 0; i < len(lines); i++ {
|
|
line := lines[i]
|
|
switch {
|
|
case strings.HasPrefix(line, "--- "):
|
|
if i+1 >= len(lines) || !strings.HasPrefix(lines[i+1], "+++ ") {
|
|
return nil, fmt.Errorf("line %d: --- without +++", i+1)
|
|
}
|
|
name, kill, err := sides(line[4:], lines[i+1][4:])
|
|
i++
|
|
if err != nil {
|
|
return nil, fmt.Errorf("line %d: %w", i+1, err)
|
|
}
|
|
if first, ok := named[path.Clean(name)]; ok {
|
|
return nil, fmt.Errorf("line %d: the diff already changes %s", i+1, first)
|
|
}
|
|
named[path.Clean(name)] = name
|
|
out = append(out, change{path: name, kill: kill})
|
|
cur, h = &out[len(out)-1], nil
|
|
|
|
case strings.HasPrefix(line, "@@"):
|
|
if cur == nil {
|
|
return nil, fmt.Errorf("line %d: a hunk before any file", i+1)
|
|
}
|
|
head, err := parseHunk(line)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("line %d: %w", i+1, err)
|
|
}
|
|
cur.hunks = append(cur.hunks, head)
|
|
h = &cur.hunks[len(cur.hunks)-1]
|
|
|
|
case h != nil && line == `\ No newline at end of file`:
|
|
if n := len(h.lines); n > 0 && h.lines[n-1][0] != '-' {
|
|
h.noEndNew = true
|
|
}
|
|
|
|
case h != nil && line != "" && (line[0] == ' ' || line[0] == '+' || line[0] == '-'):
|
|
h.lines = append(h.lines, line)
|
|
|
|
case line == "":
|
|
// A context line is written as a single space, so an empty line
|
|
// inside a hunk is a diff this daemon will not guess at.
|
|
if h != nil && i != len(lines)-1 {
|
|
return nil, fmt.Errorf("line %d: an empty line inside a hunk", i+1)
|
|
}
|
|
|
|
case strings.HasPrefix(line, "rename ") || strings.HasPrefix(line, "copy "):
|
|
return nil, fmt.Errorf("line %d: %q moves a file, which a diff of hunks cannot say; send the delete and the create", i+1, strings.TrimSpace(line))
|
|
|
|
case strings.HasPrefix(line, "diff ") || strings.HasPrefix(line, "index ") ||
|
|
strings.HasPrefix(line, "old mode") || strings.HasPrefix(line, "new mode") ||
|
|
strings.HasPrefix(line, "similarity") ||
|
|
strings.HasPrefix(line, "new file") || strings.HasPrefix(line, "deleted file") ||
|
|
strings.HasPrefix(line, "Binary files"):
|
|
h = nil
|
|
|
|
default:
|
|
return nil, fmt.Errorf("line %d: %q is neither a header nor a hunk line", i+1, line)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// sides settles the file a diff section changes. git writes the path behind
|
|
// a/ on the --- line and behind b/ on the +++ line; a diff written with no
|
|
// prefixes writes the path itself on both. The reading that makes the two
|
|
// lines agree is the path, and a pair that agrees under neither reading is
|
|
// refused instead of guessed at.
|
|
func sides(from, to string) (string, bool, error) {
|
|
from, to = column(from), column(to)
|
|
stripped := [2]string{strings.TrimPrefix(from, "a/"), strings.TrimPrefix(to, "b/")}
|
|
switch {
|
|
case from == "" || to == "":
|
|
return "", false, errors.New("the file has no name")
|
|
case from == null && to == null:
|
|
return "", false, errors.New("both sides are /dev/null")
|
|
case to == null:
|
|
return stripped[0], true, nil
|
|
case from == null:
|
|
return stripped[1], false, nil
|
|
case stripped[0] == stripped[1]:
|
|
return stripped[1], false, nil
|
|
case from == to:
|
|
return to, false, nil
|
|
}
|
|
return "", false, fmt.Errorf("the diff changes %s on one side and %s on the other", from, to)
|
|
}
|
|
|
|
// column drops the timestamp a diff may write in a second column after the
|
|
// path.
|
|
func column(s string) string {
|
|
if i := strings.IndexByte(s, '\t'); i >= 0 {
|
|
s = s[:i]
|
|
}
|
|
return strings.TrimSpace(s)
|
|
}
|
|
|
|
// parseHunk reads "@@ -old,count +new,count @@".
|
|
func parseHunk(line string) (hunk, error) {
|
|
body := line[2:]
|
|
end := strings.Index(body, "@@")
|
|
if end < 0 {
|
|
return hunk{}, fmt.Errorf("hunk header does not close")
|
|
}
|
|
fields := strings.Fields(body[:end])
|
|
if len(fields) != 2 || !strings.HasPrefix(fields[0], "-") || !strings.HasPrefix(fields[1], "+") {
|
|
return hunk{}, fmt.Errorf("hunk header %q", strings.TrimSpace(body[:end]))
|
|
}
|
|
old, err := firstNum(fields[0][1:])
|
|
if err != nil {
|
|
return hunk{}, err
|
|
}
|
|
next, err := firstNum(fields[1][1:])
|
|
if err != nil {
|
|
return hunk{}, err
|
|
}
|
|
return hunk{old: old, new: next}, nil
|
|
}
|
|
|
|
func firstNum(s string) (int, error) {
|
|
if i := strings.Index(s, ","); i >= 0 {
|
|
s = s[:i]
|
|
}
|
|
n, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("hunk line number %q", s)
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
// apply runs every hunk against the file's current lines. A context or
|
|
// removed line that does not match is refused with its number.
|
|
func apply(name string, old []byte, hunks []hunk) ([]byte, error) {
|
|
lines, endNL := splitLines(old)
|
|
var out []string
|
|
at := 0 // how far into lines we have copied
|
|
|
|
for _, h := range hunks {
|
|
start := h.old - 1
|
|
if h.old == 0 {
|
|
start = 0
|
|
}
|
|
if start < at || start > len(lines) {
|
|
return nil, fmt.Errorf("%s: hunk at line %d does not fit a file of %d lines", name, h.old, len(lines))
|
|
}
|
|
out = append(out, lines[at:start]...)
|
|
at = start
|
|
|
|
for _, l := range h.lines {
|
|
switch l[0] {
|
|
case ' ', '-':
|
|
if at >= len(lines) {
|
|
return nil, fmt.Errorf("%s: hunk at line %d runs past the end of the file", name, h.old)
|
|
}
|
|
if lines[at] != l[1:] {
|
|
return nil, fmt.Errorf("%s:%d: the file has %q where the diff expects %q", name, at+1, lines[at], l[1:])
|
|
}
|
|
at++
|
|
if l[0] == ' ' {
|
|
out = append(out, l[1:])
|
|
}
|
|
case '+':
|
|
out = append(out, l[1:])
|
|
}
|
|
}
|
|
if h.noEndNew {
|
|
endNL = false
|
|
} else if at >= len(lines) {
|
|
endNL = true
|
|
}
|
|
}
|
|
out = append(out, lines[at:]...)
|
|
|
|
if len(out) == 0 {
|
|
return nil, nil
|
|
}
|
|
text := strings.Join(out, "\n")
|
|
if endNL {
|
|
text += "\n"
|
|
}
|
|
return []byte(text), nil
|
|
}
|
|
|
|
// splitLines cuts a file into lines and reports whether it ended with a
|
|
// newline, which the joined result has to reproduce exactly.
|
|
func splitLines(b []byte) ([]string, bool) {
|
|
if len(b) == 0 {
|
|
return nil, false
|
|
}
|
|
s := string(b)
|
|
nl := strings.HasSuffix(s, "\n")
|
|
if nl {
|
|
s = s[:len(s)-1]
|
|
}
|
|
return strings.Split(s, "\n"), nl
|
|
}
|