Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
539a1c16cd | ||
|
|
01fc9e1d73 | ||
|
|
10f76cafc4 |
@@ -16,8 +16,7 @@ RUN CGO_ENABLED=0 go build -trimpath -ldflags '-s -w' -o /mesh-builder ./cmd/mes
|
||||
FROM ${ALPINE_BASE}
|
||||
# git to clone what it is asked to build, and the docker client to build and push it. The daemon
|
||||
# is the machine's, reached through its socket — a build machine shares the runtime it was given
|
||||
# rather than running one inside itself. xz to read an upstream release an archive is pinned to
|
||||
# (novox/hq ADR 0316): Go's standard library reads gzip and bzip2 and not xz.
|
||||
RUN apk add --no-cache git docker-cli xz
|
||||
# rather than running one inside itself.
|
||||
RUN apk add --no-cache git docker-cli
|
||||
COPY --from=build /mesh-builder /usr/local/bin/mesh-builder
|
||||
ENTRYPOINT ["/usr/local/bin/mesh-builder"]
|
||||
|
||||
+15
-67
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"io"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -55,9 +54,6 @@ func assignWith(ctx context.Context, open *stores, node string, opts assignOptio
|
||||
if err := refusedMovingTheController(modules); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Every line of the answer comes from the one resolution after the write (novox/hq issue 237):
|
||||
// what the reads before it would say about a machine left out is not the answer's.
|
||||
ctx = leftOutSaidBy(ctx, io.Discard)
|
||||
// Held while it is recorded, so it cannot land between a converge's preview and its flip and
|
||||
// be taken without ever having been previewed (novox/hq ADR 0100).
|
||||
ctx, release, err := holdNodes(ctx, open, []string{node})
|
||||
@@ -129,28 +125,22 @@ func assignWith(ctx context.Context, open *stores, node string, opts assignOptio
|
||||
for _, line := range settled {
|
||||
answer += "\n " + line
|
||||
}
|
||||
// What this act changed about this node's unmet seat dependencies, and nothing else (novox/hq
|
||||
// ADR 0207): a dependency of a module just assigned, or one this assignment met. The rest of the
|
||||
// node's list, and every other node's, is `status`'s.
|
||||
for _, line := range unheldChange(shelf, node, before, append(append([]string(nil), before...), added...)) {
|
||||
answer += "\n " + line
|
||||
}
|
||||
// Its bus credential, in the same act (novox/hq issue 203): an assignment pushed before its
|
||||
// credential exists delivers a process that cannot authenticate and crash-loops until somebody
|
||||
// runs a second verb and a second push. Issued here when the module speaks on the bus and has
|
||||
// no credential yet; kept when it has one, so re-assigning rotates nothing.
|
||||
var issued []string
|
||||
for _, module := range added {
|
||||
if line := issueOnAssign(ctx, open, node, module); line != "" {
|
||||
issued = append(issued, line)
|
||||
answer += "\n " + line
|
||||
}
|
||||
}
|
||||
// **The one resolution the rest of the answer is said from** (novox/hq issue 237), taken after
|
||||
// the write: what this act changed about this node's unmet seat dependencies, and nothing else
|
||||
// (novox/hq ADR 0207) — a dependency of a module just assigned, or one this assignment met; the
|
||||
// rest of the node's list, and every other node's, is `status`'s — and whether the node resolves.
|
||||
after := resolvedAfter(ctx, open, shelf, node)
|
||||
for _, line := range unheldChangeTo(shelf, node, before, after.modules, after.unheld) {
|
||||
answer += "\n " + line
|
||||
}
|
||||
for _, line := range issued {
|
||||
answer += "\n " + line
|
||||
}
|
||||
plan, err := after.plan, after.err
|
||||
plan, _, err := planFor(ctx, open, node)
|
||||
if err != nil {
|
||||
// Kept, and still refused. Both halves are the answer, and the rest of the mesh is still
|
||||
// worth reporting: this machine's refusal is rarely the only consequence.
|
||||
@@ -228,8 +218,6 @@ func unassign(ctx context.Context, open *stores, node string, modules ...string)
|
||||
if len(modules) == 0 {
|
||||
return "", fmt.Errorf("unassign %s names no module", node)
|
||||
}
|
||||
// One resolution after the write, as assign's answer (novox/hq issue 237).
|
||||
ctx = leftOutSaidBy(ctx, io.Discard)
|
||||
ctx, release, err := holdNodes(ctx, open, []string{node})
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -274,21 +262,14 @@ func unassign(ctx context.Context, open *stores, node string, modules ...string)
|
||||
}
|
||||
answer := fmt.Sprintf("%s no longer runs %s — run `push %s` to make it so",
|
||||
node, strings.Join(modules, ", "), node)
|
||||
after := resolvedAfter(ctx, open, shelf, node)
|
||||
for _, line := range unheldChangeTo(shelf, node, assigned, after.modules, after.unheld) {
|
||||
answer += "\n " + line
|
||||
var left []string
|
||||
for _, a := range assigned {
|
||||
if !slices.Contains(modules, a) {
|
||||
left = append(left, a)
|
||||
}
|
||||
}
|
||||
// Whether the node itself still resolves, from the same resolution: before, only the rest of the
|
||||
// mesh's reads said it, on standard error and once per other machine.
|
||||
if after.err != nil {
|
||||
if unresolvable(after.err) {
|
||||
answer += fmt.Sprintf("\n but %s does not resolve now, so nothing will be sent to it:", node)
|
||||
} else {
|
||||
answer += fmt.Sprintf("\n but whether %s still resolves could not be read:", node)
|
||||
}
|
||||
for _, line := range strings.Split(strings.TrimRight(after.err.Error(), "\n"), "\n") {
|
||||
answer += "\n " + strings.TrimSpace(line)
|
||||
}
|
||||
for _, line := range unheldChange(shelf, node, assigned, left) {
|
||||
answer += "\n " + line
|
||||
}
|
||||
// What it leaves behind that is irreplaceable is kept and retired, never removed (novox/hq ADR 0233).
|
||||
for _, line := range keptOnUnassign(ctx, open.inventory, node, modules) {
|
||||
@@ -297,39 +278,6 @@ func unassign(ctx context.Context, open *stores, node string, modules ...string)
|
||||
return answer + blockedElsewhere(ctx, open, node), nil
|
||||
}
|
||||
|
||||
// afterAct is the one resolution of a node an act's answer is said from, taken after its write
|
||||
// (novox/hq issue 237): the plan or why there is none, the modules it has on the node, and its unmet
|
||||
// seat dependencies.
|
||||
type afterAct struct {
|
||||
plan catalogue.Resolution
|
||||
err error
|
||||
modules []string
|
||||
unheld []catalogue.Unheld
|
||||
}
|
||||
|
||||
// resolvedAfter resolves the node as `plan` does, once, after the act's write. When the node resolves,
|
||||
// its modules and unmet seat dependencies are the resolution's — the closure, so a holder pulled in by
|
||||
// a requirement counts as `plan` counts it. When it does not, they are judged over the assignments read
|
||||
// after the write, which can only find more unmet than the closure would: a seat said held there is
|
||||
// held in the closure too.
|
||||
func resolvedAfter(ctx context.Context, open *stores, shelf map[string]catalogue.Manifest, node string) afterAct {
|
||||
plan, _, err := planFor(ctx, open, node)
|
||||
if err == nil {
|
||||
out := afterAct{plan: plan, unheld: plan.Unheld}
|
||||
for _, m := range plan.Modules {
|
||||
out.modules = append(out.modules, m.Module)
|
||||
}
|
||||
return out
|
||||
}
|
||||
out := afterAct{err: err}
|
||||
assigned, readErr := open.inventory.Assigned(ctx, node)
|
||||
if readErr != nil {
|
||||
return out
|
||||
}
|
||||
out.modules, out.unheld = assigned, unheldOver(shelf, node, assigned)
|
||||
return out
|
||||
}
|
||||
|
||||
// splitModules is a surface's one `module` field as the modules it names: several, comma-separated,
|
||||
// are one act (novox/hq ADR 0207), so the holders that depend on each other go on together from the
|
||||
// command API and the controller seat's verbs as they do from the command line.
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
// novox/hq issue 237: every line of an act's answer comes from one resolution of the node taken after
|
||||
// its write, so the answer agrees with `plan` for the node right after.
|
||||
|
||||
func aProviderAndItsConsumer(t *testing.T, open *stores) {
|
||||
t.Helper()
|
||||
register(t, open, catalogue.Manifest{Module: "db", Version: "1",
|
||||
Provides: []catalogue.Offer{{Name: "rows", Scope: catalogue.ScopeNode}}})
|
||||
register(t, open, catalogue.Manifest{Module: "app", Version: "1", Requires: []string{"rows"}})
|
||||
}
|
||||
|
||||
// twoRivalsAndANote is a node that does not resolve — two modules claim one node seat there — and a
|
||||
// module beside them that changes nothing about it.
|
||||
func twoRivalsAndANote(t *testing.T, open *stores) {
|
||||
t.Helper()
|
||||
one, two := rivals()
|
||||
register(t, open, one)
|
||||
register(t, open, two)
|
||||
register(t, open, catalogue.Manifest{Module: "notes", Version: "1"})
|
||||
}
|
||||
|
||||
func TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
twoRivalsAndANote(t, open)
|
||||
// Straight into the store, as a node that came to this before the act.
|
||||
for _, m := range []string{"rival-one", "rival-two", "notes"} {
|
||||
if _, err := open.inventory.Assign(ctx, "laptop", m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
said := answered(t, func() error { return assignCommand(ctx, "unassign", []string{"laptop", "notes"}) })
|
||||
|
||||
if _, _, err := planFor(ctx, open, "laptop"); err == nil || !unresolvable(err) {
|
||||
t.Fatalf("laptop resolves with two claimants of one seat: %v", err)
|
||||
}
|
||||
if n := strings.Count(said, "laptop does not resolve now"); n != 1 {
|
||||
t.Errorf("the answer says laptop does not resolve %d times, want once:\n%s", n, said)
|
||||
}
|
||||
if strings.Contains(said, "is left out of the rest of the mesh") {
|
||||
t.Errorf("the answer carries a line from a read that is not its resolution:\n%s", said)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
aProviderAndItsConsumer(t, open)
|
||||
if _, err := assign(ctx, open, "laptop", "db", "app"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
said, err := unassign(ctx, open, "laptop", "app")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := planFor(ctx, open, "laptop"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(said, "does not resolve") || strings.Contains(said, "could not be read") {
|
||||
t.Errorf("a node that resolves is said not to:\n%s", said)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
twoRivalsAndANote(t, open)
|
||||
if _, err := assign(ctx, open, "laptop", "rival-one"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Kept and not resolvable: a second claimant of the seat, which assignment keeps.
|
||||
said, err := assign(ctx, open, "laptop", "rival-two")
|
||||
if err == nil {
|
||||
t.Fatalf("two claimants of one node seat resolved:\n%s", said)
|
||||
}
|
||||
_, _, planErr := planFor(ctx, open, "laptop")
|
||||
if planErr == nil || planErr.Error() != err.Error() {
|
||||
t.Errorf("the act's refusal is not the plan's right after:\nact: %v\nplan: %v", err, planErr)
|
||||
}
|
||||
if strings.Contains(said, "now has") {
|
||||
t.Errorf("the answer says a seat is now held on a node that does not resolve:\n%s", said)
|
||||
}
|
||||
}
|
||||
|
||||
// The other readers of the whole mesh still say a machine they leave out, on standard error (novox/hq
|
||||
// issue 188); an act sends it elsewhere.
|
||||
func TestAMachineLeftOutIsSaidWhereTheContextSays(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
sayLeftOut(leftOutSaidBy(context.Background(), &buf), "laptop", errors.New("why"))
|
||||
if buf.String() != "laptop is left out of the rest of the mesh: it does not resolve: why\n" {
|
||||
t.Errorf("said %q", buf.String())
|
||||
}
|
||||
got := answered(t, func() error {
|
||||
sayLeftOut(context.Background(), "laptop", errors.New("why"))
|
||||
return nil
|
||||
})
|
||||
if !strings.Contains(got, "laptop is left out") {
|
||||
t.Errorf("without an act's writer the line did not reach standard error: %q", got)
|
||||
}
|
||||
}
|
||||
+17
-61
@@ -292,7 +292,7 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory,
|
||||
if err != nil {
|
||||
// Said, not skipped: a machine dropped here offers nothing and holds nothing as far
|
||||
// as every other machine's plan can tell (novox/hq issue 188).
|
||||
sayLeftOut(ctx, o.node.Name, err)
|
||||
fmt.Fprintf(os.Stderr, "%s is left out of the rest of the mesh: it does not resolve: %v\n", o.node.Name, err)
|
||||
continue
|
||||
}
|
||||
firstHeld = append(firstHeld, got.Claims...)
|
||||
@@ -337,7 +337,7 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory,
|
||||
// resolved without its offers, and one that consumes them cannot resolve here by
|
||||
// design — that is not the machine being dropped, it is the view being partial.
|
||||
if exclude == "" {
|
||||
sayLeftOut(ctx, o.node.Name, err)
|
||||
fmt.Fprintf(os.Stderr, "%s is left out of the rest of the mesh: it does not resolve: %v\n", o.node.Name, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -347,35 +347,6 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory,
|
||||
return world, nil
|
||||
}
|
||||
|
||||
// leftOutKey carries where theRestOfTheMesh says a machine it leaves out, for an act that answers with
|
||||
// its own resolution instead (novox/hq issue 237).
|
||||
type leftOutKey struct{}
|
||||
|
||||
// leftOutSaidBy is ctx with theRestOfTheMesh's "left out" lines written to w rather than to standard
|
||||
// error.
|
||||
//
|
||||
// **An act's answer is one resolution taken after its write** (novox/hq issue 237, decided 2026-10-11).
|
||||
// `assign` read the whole mesh once before its write — to record the seats held by derivation — and that
|
||||
// read said, on standard error, that the node did not resolve for lack of the seat the act was about to
|
||||
// give it. The controller seat's verb answers with both streams, so the stale line came last, after the
|
||||
// act's own "now has it held", and named as its remedy the assignment just made. An act gives io.Discard
|
||||
// here: what the node resolves to after the write is said by its own planFor, and every other machine
|
||||
// that does not resolve by blockedElsewhere, both read after the write.
|
||||
func leftOutSaidBy(ctx context.Context, w io.Writer) context.Context {
|
||||
return context.WithValue(ctx, leftOutKey{}, w)
|
||||
}
|
||||
|
||||
// sayLeftOut says that a machine is left out of the rest of the mesh, where ctx says to: standard
|
||||
// error unless an act said otherwise. Said, not skipped: a machine dropped there offers nothing and
|
||||
// holds nothing as far as every other machine's plan can tell (novox/hq issue 188).
|
||||
func sayLeftOut(ctx context.Context, node string, err error) {
|
||||
w, ok := ctx.Value(leftOutKey{}).(io.Writer)
|
||||
if !ok {
|
||||
w = os.Stderr
|
||||
}
|
||||
fmt.Fprintf(w, "%s is left out of the rest of the mesh: it does not resolve: %v\n", node, err)
|
||||
}
|
||||
|
||||
// declarationFor is everything a node would be sent.
|
||||
//
|
||||
// One place, because there were three and one of them was written before credentials existed and
|
||||
@@ -1780,43 +1751,28 @@ func (l *unheldLogger) log(node string, unheld []catalogue.Unheld) {
|
||||
// which includes every one of a module just assigned — and each now met that was not. Nothing about
|
||||
// any other node, and nothing that was already true before the act.
|
||||
func unheldChange(shelf map[string]catalogue.Manifest, node string, before, after []string) []string {
|
||||
return unheldChangeTo(shelf, node, before, after, unheldOver(shelf, node, after))
|
||||
}
|
||||
|
||||
// unheldOver is the node's unmet seat dependencies judged over a set of module names alone.
|
||||
func unheldOver(shelf map[string]catalogue.Manifest, node string, names []string) []catalogue.Unheld {
|
||||
var set []catalogue.Manifest
|
||||
for _, n := range names {
|
||||
if m, known := shelf[n]; known {
|
||||
set = append(set, m)
|
||||
judge := func(names []string) map[string]catalogue.Unheld {
|
||||
var set []catalogue.Manifest
|
||||
for _, n := range names {
|
||||
if m, known := shelf[n]; known {
|
||||
set = append(set, m)
|
||||
}
|
||||
}
|
||||
out := map[string]catalogue.Unheld{}
|
||||
for _, u := range catalogue.UnheldDependencies(shelf, node, set, nil) {
|
||||
out[u.Module+" "+u.Seat] = u
|
||||
}
|
||||
return out
|
||||
}
|
||||
return catalogue.UnheldDependencies(shelf, node, set, nil)
|
||||
}
|
||||
|
||||
// unheldChangeTo is unheldChange with the node's dependencies as they now stand given: `now` is what
|
||||
// the act's one resolution after its write says is unmet, and `after` the modules that resolution has
|
||||
// on the node (novox/hq issue 237). A line about how things stand after the act is never judged from
|
||||
// anything else, so it cannot disagree with `plan` for the node.
|
||||
func unheldChangeTo(shelf map[string]catalogue.Manifest, node string, before, after []string,
|
||||
now []catalogue.Unheld) []string {
|
||||
key := func(u catalogue.Unheld) string { return u.Module + " " + u.Seat }
|
||||
was := map[string]catalogue.Unheld{}
|
||||
for _, u := range unheldOver(shelf, node, before) {
|
||||
was[key(u)] = u
|
||||
}
|
||||
is := map[string]catalogue.Unheld{}
|
||||
for _, u := range now {
|
||||
is[key(u)] = u
|
||||
}
|
||||
was, now := judge(before), judge(after)
|
||||
var lines []string
|
||||
for _, k := range sortedNames(is) {
|
||||
for _, k := range sortedNames(now) {
|
||||
if _, already := was[k]; !already {
|
||||
lines = append(lines, "but "+is[k].String())
|
||||
lines = append(lines, "but "+now[k].String())
|
||||
}
|
||||
}
|
||||
for _, k := range sortedNames(was) {
|
||||
if _, still := is[k]; still {
|
||||
if _, still := now[k]; still {
|
||||
continue
|
||||
}
|
||||
u := was[k]
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
// TestReplay237 replays novox/hq issue 237 (2026-10-04): a workstation already ran a module that
|
||||
// depends on a node seat nothing there held, and the module holding that seat was assigned to it. The
|
||||
// answer said the seat was now held, and then that the workstation was left out of the rest of the mesh
|
||||
// for lack of it, naming as the remedy the assignment just made. `plan` for the node, right after,
|
||||
// resolved. The outcome asserted: every line of the answer, as the controller seat's verb gives it
|
||||
// (standard output, then standard error), agrees with `plan` for the node right after the act. Written
|
||||
// with only what the controller had before its fix — register, assign's command, planFor — so it is laid
|
||||
// over the commit before.
|
||||
func TestReplay237(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
register(t, open, aContainer("web"))
|
||||
register(t, open, catalogue.Manifest{Module: "docker", Version: "1",
|
||||
Claims: []catalogue.Claim{{Name: catalogue.ContainerRuntimeSeat, Scope: catalogue.ScopeNode}}})
|
||||
// Already on the node, from before the rule refused it: the node does not resolve until the
|
||||
// holder comes.
|
||||
if _, err := open.inventory.Assign(ctx, "laptop", "web"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
said := answered(t, func() error { return assignCommand(ctx, "assign", []string{"laptop", "docker"}) })
|
||||
|
||||
if _, _, err := planFor(ctx, open, "laptop"); err != nil {
|
||||
t.Fatalf("laptop does not resolve right after the act: %v\nthe answer was:\n%s", err, said)
|
||||
}
|
||||
if !strings.Contains(said, "web on laptop now has "+catalogue.ContainerRuntimeSeat+" held") {
|
||||
t.Errorf("the answer does not say the seat is now held:\n%s", said)
|
||||
}
|
||||
for _, not := range []string{"laptop is left out", "does not resolve", "which nothing on laptop holds"} {
|
||||
if strings.Contains(said, not) {
|
||||
t.Errorf("the answer says %q, which `plan` right after contradicts:\n%s", not, said)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// answered is what a command says as the controller seat's verb answers it: standard output, then
|
||||
// standard error (answerOf).
|
||||
func answered(t *testing.T, f func() error) string {
|
||||
t.Helper()
|
||||
read := func(w **os.File) (func() string, error) {
|
||||
old := *w
|
||||
r, pw, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
*w = pw
|
||||
got := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, r)
|
||||
got <- buf.String()
|
||||
}()
|
||||
return func() string {
|
||||
_ = pw.Close()
|
||||
*w = old
|
||||
return <-got
|
||||
}, nil
|
||||
}
|
||||
stdout, err := read(&os.Stdout)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stderr, err := read(&os.Stderr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runErr := f()
|
||||
said := stdout() + stderr()
|
||||
if runErr != nil {
|
||||
t.Fatalf("%v\n%s", runErr, said)
|
||||
}
|
||||
return said
|
||||
}
|
||||
@@ -34,10 +34,8 @@ type seatRow struct {
|
||||
Delivers string `json:"delivers,omitempty"`
|
||||
Decision string `json:"decision"`
|
||||
// Replicated says the seat may be held on several machines at once (novox/hq ADR 0223).
|
||||
Replicated bool `json:"replicated,omitempty"`
|
||||
// Paths are the paths on its machine the seat's holder owns (novox/hq ADR 0287).
|
||||
Paths []catalogue.SeatPath `json:"paths,omitempty"`
|
||||
Holders []seatHolder `json:"holders"`
|
||||
Replicated bool `json:"replicated,omitempty"`
|
||||
Holders []seatHolder `json:"holders"`
|
||||
}
|
||||
|
||||
// seatsHeld is every seat the mesh defines with its holders, and every claim held that names no
|
||||
@@ -51,7 +49,7 @@ func seatsHeld(seats []catalogue.Seat, held []catalogue.Held) ([]seatRow, []cata
|
||||
rows := make([]seatRow, 0, len(seats))
|
||||
for _, s := range seats {
|
||||
row := seatRow{Seat: s.Name, Scope: s.Scope, Delivers: s.Delivers, Decision: s.Decision,
|
||||
Replicated: s.Replicated, Paths: s.Paths, Holders: []seatHolder{}}
|
||||
Replicated: s.Replicated, Holders: []seatHolder{}}
|
||||
seen := map[seatHolder]bool{}
|
||||
for _, h := range held {
|
||||
// Resolve the held claim to a seat rather than comparing names, so a record naming a
|
||||
@@ -257,16 +255,12 @@ func seatsCommand(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
rows, outside := seatsHeld(catalogue.Seats(), world.Held)
|
||||
// Which module owns each seat's path on each machine (novox/hq ADR 0287): the seat naming it, and
|
||||
// that seat's holder there, with the stepping back applied.
|
||||
owners := catalogue.PathOwners(catalogue.Seats(), world.Held)
|
||||
|
||||
if *asJSON {
|
||||
out := struct {
|
||||
Seats []seatRow `json:"seats"`
|
||||
Owners []catalogue.PathOwner `json:"path-owners,omitempty"`
|
||||
Outside []catalogue.Held `json:"outside,omitempty"`
|
||||
}{rows, owners, outside}
|
||||
Seats []seatRow `json:"seats"`
|
||||
Outside []catalogue.Held `json:"outside,omitempty"`
|
||||
}{rows, outside}
|
||||
body, err := json.MarshalIndent(out, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -276,7 +270,7 @@ func seatsCommand(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "SEAT\tSCOPE\tDELIVERS\tPATHS\tHELD BY")
|
||||
fmt.Fprintln(w, "SEAT\tSCOPE\tDELIVERS\tHELD BY")
|
||||
for _, r := range rows {
|
||||
delivers := r.Delivers
|
||||
if delivers == "" {
|
||||
@@ -290,22 +284,11 @@ func seatsCommand(ctx context.Context, args []string) error {
|
||||
}
|
||||
holders = strings.Join(parts, ", ")
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", r.Seat, r.Scope, delivers, seatPathsText(r.Paths), holders)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.Seat, r.Scope, delivers, holders)
|
||||
}
|
||||
if err := w.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(owners) > 0 {
|
||||
fmt.Println("\nwho owns each seat's path, by machine (novox/hq ADR 0287):")
|
||||
w = tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "PATH\tMACHINE\tSEAT\tMODULE")
|
||||
for _, o := range owners {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", o.Path, o.Node, o.Seat, o.Module)
|
||||
}
|
||||
if err := w.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(outside) > 0 {
|
||||
fmt.Println("\nheld, and not a seat this mesh defines (registered before the set closed — novox/hq ADR 0110):")
|
||||
for _, h := range outside {
|
||||
@@ -314,19 +297,3 @@ func seatsCommand(ctx context.Context, args []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// seatPathsText is a seat's paths in one column: each path, and the seat it steps back for, if any.
|
||||
func seatPathsText(paths []catalogue.SeatPath) string {
|
||||
if len(paths) == 0 {
|
||||
return "—"
|
||||
}
|
||||
parts := make([]string, 0, len(paths))
|
||||
for _, p := range paths {
|
||||
if p.StepsBackFor != "" {
|
||||
parts = append(parts, p.Path+" (steps back for "+p.StepsBackFor+")")
|
||||
continue
|
||||
}
|
||||
parts = append(parts, p.Path)
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
@@ -74,22 +74,3 @@ func TestAKindedBenchIsNotHandedOver(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The listing shows each seat's paths, and the seat a path steps back for (novox/hq ADR 0287).
|
||||
func TestTheSeatsListingShowsEachSeatsPaths(t *testing.T) {
|
||||
rows, _ := seatsHeld(catalogue.Seats(), nil)
|
||||
got := map[string]string{}
|
||||
for _, r := range rows {
|
||||
got[r.Seat] = seatPathsText(r.Paths)
|
||||
}
|
||||
for seat, want := range map[string]string{
|
||||
"node-hostname": "/etc/hosts, /etc/hostname",
|
||||
"node-uplink": "/etc/resolv.conf (steps back for node-resolver)",
|
||||
"node-resolver": "/etc/resolv.conf",
|
||||
"node-packet-filter": "—",
|
||||
} {
|
||||
if got[seat] != want {
|
||||
t.Errorf("%s lists its paths as %q, not %q", seat, got[seat], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,58 +344,3 @@ func TestAnyKeyOfAModuleWithATrustedMergeableFileIsRefusedThroughAVerb(t *testin
|
||||
t.Fatalf("a file marked untrusted through the verb: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Who owns a module's directory is said at the terminal alone (novox/hq issue 279): a directory's owner may name
|
||||
// the settings the module runs as, root chowns the folder to them at the next push, and through the settings verb
|
||||
// any caller could have set puid to 0 or to its own uid. The key is refused through every verb route even where a
|
||||
// file nothing trusts also asks for it, and taken at the terminal; the layer a refused call meets is unchanged.
|
||||
func TestADirectoryOwnersKeyIsRefusedThroughAVerb(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
register(t, open, catalogue.Manifest{Module: "sonarr", Version: "1",
|
||||
Resources: []map[string]any{
|
||||
{"id": "recycle-bin", "type": "directory", "mode": "0755", "owner": "${setting:puid}:${setting:pgid}"},
|
||||
{"id": "identity", "type": "file", "path": "/etc/sonarr.env", "mode": "0644", "trusted": false,
|
||||
"content": "PUID=${setting:puid}\nPGID=${setting:pgid}\nX=${setting:x}\n"}}})
|
||||
if err := atTheTerminal(t, "settings", "set", "sonarr", `{"puid":1001,"pgid":2000,"x":0}`,
|
||||
"--node", "anchor"); err != nil {
|
||||
t.Fatalf("the owner at the terminal: %v", err)
|
||||
}
|
||||
if _, err := assign(ctx, open, "anchor", "sonarr"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
layer := func() string {
|
||||
t.Helper()
|
||||
values, _, err := open.inventory.Layer(ctx, "anchor", "sonarr")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, _ := json.Marshal(values)
|
||||
return string(raw)
|
||||
}
|
||||
kept := layer()
|
||||
refused := func(what string, err error) {
|
||||
t.Helper()
|
||||
if err == nil || !strings.Contains(err.Error(), "controller's terminal") {
|
||||
t.Fatalf("%s: %v", what, err)
|
||||
}
|
||||
}
|
||||
refused("puid 0 through the verb", throughVerb(t, "settings", map[string]any{"module": "sonarr",
|
||||
"node": "anchor", "values": `{"puid":0,"pgid":2000,"x":0}`}))
|
||||
refused("pgid through the verb, mesh-wide", throughVerb(t, "settings", map[string]any{"module": "sonarr",
|
||||
"values": `{"pgid":0}`}))
|
||||
refused("cleared through the verb", throughVerb(t, "settings", map[string]any{"module": "sonarr",
|
||||
"node": "anchor", "clear": "true"}))
|
||||
if err := throughVerb(t, "command", map[string]any{
|
||||
"command": `settings set sonarr '{"puid":0,"pgid":2000,"x":0}' --node anchor`}); err == nil {
|
||||
t.Fatal("the command verb set the owner")
|
||||
}
|
||||
if got := layer(); got != kept {
|
||||
t.Fatalf("a refused call changed the layer: %s, was %s", got, kept)
|
||||
}
|
||||
// Another key, the owner kept, is still the verb's.
|
||||
if err := throughVerb(t, "settings", map[string]any{"module": "sonarr", "node": "anchor",
|
||||
"values": `{"puid":1001,"pgid":2000,"x":1}`}); err != nil {
|
||||
t.Fatalf("another key through the verb, the owner kept: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ require (
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
github.com/novox/mesh-host v0.0.0
|
||||
github.com/therootcompany/xz v1.0.1
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/sys v0.48.0
|
||||
|
||||
@@ -38,6 +38,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
|
||||
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
|
||||
BIN
Binary file not shown.
@@ -14,13 +14,13 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/therootcompany/xz"
|
||||
)
|
||||
|
||||
// An archive whose source is somebody else's published file, pinned by URL and sha256 (novox/hq ADR 0316
|
||||
@@ -183,7 +183,6 @@ func unpackUpstream(ctx context.Context, file, into, within string) error {
|
||||
buffered := bufio.NewReader(f)
|
||||
magic, _ := buffered.Peek(6)
|
||||
var stream io.Reader
|
||||
var wait func() error
|
||||
switch {
|
||||
case bytes.HasPrefix(magic, []byte{0x1f, 0x8b}):
|
||||
zipped, err := gzip.NewReader(buffered)
|
||||
@@ -195,53 +194,33 @@ func unpackUpstream(ctx context.Context, file, into, within string) error {
|
||||
case bytes.HasPrefix(magic, []byte("BZh")):
|
||||
stream = bzip2.NewReader(buffered)
|
||||
case bytes.HasPrefix(magic, []byte{0xfd, '7', 'z', 'X', 'Z', 0x00}):
|
||||
// Go's standard library reads no xz; the build seat's image carries xz (cmd/mesh-builder/Dockerfile).
|
||||
if _, err := exec.LookPath("xz"); err != nil {
|
||||
return errors.New("it is compressed with xz, and this build seat has no xz to read it")
|
||||
// Read in Go (github.com/therootcompany/xz, a port of xz-embedded), never by a tool the build seat's image
|
||||
// may not carry: the seat's image is the catalogue's build-agent recipe, which installs git and docker-cli
|
||||
// alone, and a release pinned as .tar.xz failed there on 2026-10-12 (hq issue 345). xzDictMax is a real
|
||||
// maximum: a block whose LZMA2 dictionary is larger is refused before its dictionary is allocated
|
||||
// (ErrMemlimit). The reader keeps a filter decoder for every block of a stream, so the file's framing is
|
||||
// walked first and a file of more blocks than xzBlocksMax is refused before anything is decoded
|
||||
// (xz_framing.go). What the stream unpacks to is bounded by untar (maxUpstream, maxEntries).
|
||||
if err := xzWithinLimits(file); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "xz", "-dc")
|
||||
cmd.Stdin = buffered
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
out, err := cmd.StdoutPipe()
|
||||
zipped, err := xz.NewReader(buffered, xzDictMax)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("it is not an xz stream this build seat reads (a dictionary of %d MiB at most): %v",
|
||||
xzDictMax>>20, err)
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
stream = out
|
||||
wait = func() error {
|
||||
if err := cmd.Wait(); err != nil {
|
||||
return fmt.Errorf("xz could not read it: %v %s", err, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
if wait != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
_ = cmd.Wait()
|
||||
}
|
||||
}()
|
||||
stream = zipped
|
||||
default:
|
||||
return errors.New("it is not a tar archive compressed with gzip, bzip2 or xz; a single file is kept " +
|
||||
"whole by naming it in `file`")
|
||||
}
|
||||
if err := untar(stream, into, within); err != nil {
|
||||
return err
|
||||
}
|
||||
if wait != nil {
|
||||
// Whatever follows the tar's end is read before xz is waited for: xz blocks on a full pipe otherwise.
|
||||
if _, err := io.Copy(io.Discard, stream); err != nil {
|
||||
return err
|
||||
}
|
||||
w := wait
|
||||
wait = nil
|
||||
return w()
|
||||
}
|
||||
return nil
|
||||
return untar(stream, into, within)
|
||||
}
|
||||
|
||||
// xzDictMax is the largest xz dictionary read: xz's own presets use at most 64 MiB (-9); a stream asking for
|
||||
// more is refused rather than given the memory.
|
||||
const xzDictMax = 64 << 20
|
||||
|
||||
// untar writes a tar stream's files and directories within `within` into a directory.
|
||||
func untar(stream io.Reader, into, within string) error {
|
||||
reader := tar.NewReader(stream)
|
||||
|
||||
@@ -6,8 +6,10 @@ import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -15,6 +17,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -383,3 +386,84 @@ func TestARedirectOffHttpsIsNotFollowed(t *testing.T) {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnXzReleaseIsReadWithoutAnyXzToolOnTheBuildSeat(t *testing.T) {
|
||||
// The build seat's image (the catalogue's build-agent recipe) carries git and docker-cli alone: Betterbird's
|
||||
// .tar.xz failed there on 2026-10-12 with "this build seat has no xz to read it" (hq issue 345). It is
|
||||
// read in Go, with nothing on PATH.
|
||||
t.Setenv("PATH", "")
|
||||
body, err := os.ReadFile(filepath.Join("testdata", "release.tar.xz"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v := &vendor{files: map[string][]byte{"/r.tar.xz": body}}
|
||||
base := serveVendor(t, v)
|
||||
packed, err := upstreamArchive(context.Background(), t.TempDir(), catalogue.Artifact{Name: "archive", Kind: "archive",
|
||||
URL: base + "/r.tar.xz", SHA256: sumHex(body), Version: "1", Within: "betterbird"}, func(string, string, ...any) {})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e := entriesOf(t, packed)
|
||||
if len(e) != 2 || e["betterbird"] != "#!/bin/sh\n -rwxr-xr-x" || e["defaults/p.js"] != "pref(\"a\", 1);\n -rw-r--r--" {
|
||||
t.Fatalf("%v", e)
|
||||
}
|
||||
}
|
||||
|
||||
// withDictionary is an xz stream whose first block's LZMA2 dictionary property is props, its header's CRC32
|
||||
// recomputed: a stream asking for a dictionary of its own choosing.
|
||||
func withDictionary(t *testing.T, stream []byte, props byte) []byte {
|
||||
t.Helper()
|
||||
out := append([]byte{}, stream...)
|
||||
const at = 12 // the stream header
|
||||
size := (int(out[at]) + 1) * 4
|
||||
header := out[at : at+size]
|
||||
flags := header[1]
|
||||
i := 2
|
||||
readVarint := func() {
|
||||
for header[i]&0x80 != 0 {
|
||||
i++
|
||||
}
|
||||
i++
|
||||
}
|
||||
if flags&0x40 != 0 {
|
||||
readVarint()
|
||||
}
|
||||
if flags&0x80 != 0 {
|
||||
readVarint()
|
||||
}
|
||||
if header[i] != 0x21 || header[i+1] != 0x01 {
|
||||
t.Fatalf("the fixture's first filter is not LZMA2: % x", header)
|
||||
}
|
||||
header[i+2] = props
|
||||
binary.LittleEndian.PutUint32(header[size-4:], crc32.ChecksumIEEE(header[:size-4]))
|
||||
return out
|
||||
}
|
||||
|
||||
func TestAnXzStreamAskingForTooLargeADictionaryIsRefused(t *testing.T) {
|
||||
body, err := os.ReadFile(filepath.Join("testdata", "release.tar.xz"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 1 GiB (props 36) and 4 GiB less one (props 40): over the 64 MiB maximum, refused before allocation.
|
||||
for _, props := range []byte{36, 40} {
|
||||
crafted := withDictionary(t, body, props)
|
||||
v := &vendor{files: map[string][]byte{"/r.tar.xz": crafted}}
|
||||
base := serveVendor(t, v)
|
||||
var before, after runtime.MemStats
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&before)
|
||||
_, err := upstreamArchive(context.Background(), t.TempDir(), catalogue.Artifact{Name: "archive", Kind: "archive",
|
||||
URL: base + "/r.tar.xz", SHA256: sumHex(crafted), Version: "1", Within: "betterbird"}, func(string, string, ...any) {})
|
||||
runtime.ReadMemStats(&after)
|
||||
if err == nil || !strings.Contains(err.Error(), "dictionary") {
|
||||
t.Errorf("props %d: a dictionary over the maximum was not refused: %v", props, err)
|
||||
}
|
||||
if grew := after.TotalAlloc - before.TotalAlloc; grew > 256<<20 {
|
||||
t.Errorf("props %d: %d bytes were allocated for a refused stream", props, grew)
|
||||
}
|
||||
}
|
||||
// The fixture as it is (a small dictionary) still reads.
|
||||
if crafted := withDictionary(t, body, 0); len(crafted) == 0 {
|
||||
t.Fatal("no fixture")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// xzBlocksMax and xzStreamsMax are the most blocks and the most concatenated streams an xz file may hold for
|
||||
// the build seat to read it.
|
||||
//
|
||||
// **Why a block count is limited at all.** The xz reader (github.com/therootcompany/xz v1.0.1) keeps one
|
||||
// filter decoder for every block that names a delta or BCJ filter, and frees them only when a stream ends:
|
||||
// its block counters are reset per stream, not per block. A stream of many tiny blocks therefore grows the
|
||||
// heap with its length: an `xz --block-size=1 --delta` file of 6 MB kept about 67 MiB live, which comes to
|
||||
// about 5.6 GiB at the 512 MiB download cap (review 68 on mesh-controller pull request 264, hq issue 345).
|
||||
//
|
||||
// **Why 1,024.** At about 340 bytes of decoder a block, 1,024 blocks keep at most about 0.35 MiB live. A
|
||||
// real release is far below it: Betterbird's archive is 1 stream with 1 block, and multi-threaded xz cuts
|
||||
// blocks of three times the dictionary (24 MiB at the default preset), so even 512 MiB unpacked in blocks
|
||||
// of 512 KiB stays within it. Streams are limited to 16 because a release is one stream; the decoders are
|
||||
// reused from one stream to the next, so the stream limit only bounds the work of this walk.
|
||||
const (
|
||||
xzBlocksMax = 1024
|
||||
xzStreamsMax = 16
|
||||
)
|
||||
|
||||
// xzWithinLimits walks an xz file's framing, without decompressing anything, and refuses one holding more
|
||||
// blocks or streams than the build seat reads.
|
||||
//
|
||||
// **The framing, not the index.** Each stream's index at its end lists its blocks, but the reader compares
|
||||
// the index with the blocks only after it has decoded them all, so a file whose index lies would still be
|
||||
// decoded block by block. This walk follows the same framing the reader follows: stream header, each block
|
||||
// header, the block's LZMA2 chunks (each chunk header carries its own compressed size, so a chunk is skipped
|
||||
// without being decoded), its padding and check, then the index and the footer. It counts the blocks the
|
||||
// reader would decode, and it also refuses an index whose count of blocks is not the count walked.
|
||||
func xzWithinLimits(file string) error {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, _, err = xzFraming(bufio.NewReader(f))
|
||||
if errors.Is(err, errXzTooManyBlocks) || errors.Is(err, errXzTooManyStreams) {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("it is not an xz file this build seat reads: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
errXzTooManyBlocks = fmt.Errorf("it holds more than %d xz blocks, the most this build seat reads: "+
|
||||
"its xz reader keeps a filter decoder for every block (hq issue 345)", xzBlocksMax)
|
||||
errXzTooManyStreams = fmt.Errorf("it holds more than %d concatenated xz streams, the most this build "+
|
||||
"seat reads", xzStreamsMax)
|
||||
)
|
||||
|
||||
// xzFraming counts the streams and blocks of an xz file, checking its framing as it goes. It stops with
|
||||
// errXzTooManyBlocks or errXzTooManyStreams at the first block or stream over the limit.
|
||||
func xzFraming(r *bufio.Reader) (streams, blocks int, err error) {
|
||||
full := func(n int) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
if _, err := io.ReadFull(r, b); err != nil {
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
skip := func(n int) error {
|
||||
if got, err := r.Discard(n); err != nil || got != n {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for {
|
||||
header, err := full(12)
|
||||
if err != nil {
|
||||
return streams, blocks, fmt.Errorf("stream %d: header: %v", streams+1, err)
|
||||
}
|
||||
if !bytes.Equal(header[:6], []byte{0xfd, '7', 'z', 'X', 'Z', 0x00}) {
|
||||
return streams, blocks, fmt.Errorf("stream %d: no xz stream header", streams+1)
|
||||
}
|
||||
if header[6] != 0 || header[7]&0xf0 != 0 ||
|
||||
crc32.ChecksumIEEE(header[6:8]) != binary.LittleEndian.Uint32(header[8:12]) {
|
||||
return streams, blocks, fmt.Errorf("stream %d: a corrupt stream header", streams+1)
|
||||
}
|
||||
if streams++; streams > xzStreamsMax {
|
||||
return streams, blocks, errXzTooManyStreams
|
||||
}
|
||||
checkSize := 0
|
||||
if check := int(header[7]); check != 0 {
|
||||
checkSize = 4 << ((check - 1) / 3)
|
||||
}
|
||||
inStream := 0
|
||||
for {
|
||||
first, err := r.ReadByte()
|
||||
if err != nil {
|
||||
return streams, blocks, fmt.Errorf("stream %d: no index: %v", streams, io.ErrUnexpectedEOF)
|
||||
}
|
||||
if first == 0 {
|
||||
break // the index indicator
|
||||
}
|
||||
rest, err := full(int(first)*4 + 3)
|
||||
if err != nil {
|
||||
return streams, blocks, fmt.Errorf("stream %d, block %d: header: %v", streams, inStream+1, err)
|
||||
}
|
||||
if err := xzBlockHeader(append([]byte{first}, rest...)); err != nil {
|
||||
return streams, blocks, fmt.Errorf("stream %d, block %d: %v", streams, inStream+1, err)
|
||||
}
|
||||
inStream++
|
||||
if blocks++; blocks > xzBlocksMax {
|
||||
return streams, blocks, errXzTooManyBlocks
|
||||
}
|
||||
// The block's LZMA2 chunks, up to the end marker.
|
||||
size := 0
|
||||
for {
|
||||
control, err := r.ReadByte()
|
||||
if err != nil {
|
||||
return streams, blocks, fmt.Errorf("stream %d, block %d: %v", streams, inStream, io.ErrUnexpectedEOF)
|
||||
}
|
||||
size++
|
||||
if control == 0 {
|
||||
break
|
||||
}
|
||||
switch {
|
||||
case control == 1 || control == 2: // stored: a 16-bit size less one, then the bytes
|
||||
b, err := full(2)
|
||||
if err != nil {
|
||||
return streams, blocks, err
|
||||
}
|
||||
n := int(binary.BigEndian.Uint16(b)) + 1
|
||||
if err := skip(n); err != nil {
|
||||
return streams, blocks, err
|
||||
}
|
||||
size += 2 + n
|
||||
case control >= 0x80: // LZMA: unpacked and packed sizes, the properties byte on a reset
|
||||
b, err := full(4)
|
||||
if err != nil {
|
||||
return streams, blocks, err
|
||||
}
|
||||
n := int(binary.BigEndian.Uint16(b[2:])) + 1
|
||||
size += 4
|
||||
if control >= 0xc0 {
|
||||
if err := skip(1); err != nil {
|
||||
return streams, blocks, err
|
||||
}
|
||||
size++
|
||||
}
|
||||
if err := skip(n); err != nil {
|
||||
return streams, blocks, err
|
||||
}
|
||||
size += n
|
||||
default:
|
||||
return streams, blocks, fmt.Errorf("stream %d, block %d: an LZMA2 chunk of kind %#x",
|
||||
streams, inStream, control)
|
||||
}
|
||||
}
|
||||
if pad := (4 - size%4) % 4; pad > 0 {
|
||||
b, err := full(pad)
|
||||
if err != nil || !bytes.Equal(b, make([]byte, pad)) {
|
||||
return streams, blocks, fmt.Errorf("stream %d, block %d: corrupt block padding", streams, inStream)
|
||||
}
|
||||
}
|
||||
if err := skip(checkSize); err != nil {
|
||||
return streams, blocks, err
|
||||
}
|
||||
}
|
||||
// The index: its count of records, two numbers each, padding, CRC32.
|
||||
used := 1
|
||||
count, n, err := xzVLI(r)
|
||||
if err != nil {
|
||||
return streams, blocks, fmt.Errorf("stream %d: index: %v", streams, err)
|
||||
}
|
||||
used += n
|
||||
if count != uint64(inStream) {
|
||||
return streams, blocks, fmt.Errorf("stream %d: its index lists %d blocks, but it holds %d",
|
||||
streams, count, inStream)
|
||||
}
|
||||
for i := 0; i < 2*inStream; i++ {
|
||||
_, n, err := xzVLI(r)
|
||||
if err != nil {
|
||||
return streams, blocks, fmt.Errorf("stream %d: index: %v", streams, err)
|
||||
}
|
||||
used += n
|
||||
}
|
||||
if err := skip((4-used%4)%4 + 4); err != nil {
|
||||
return streams, blocks, fmt.Errorf("stream %d: index: %v", streams, err)
|
||||
}
|
||||
footer, err := full(12)
|
||||
if err != nil || footer[10] != 'Y' || footer[11] != 'Z' {
|
||||
return streams, blocks, fmt.Errorf("stream %d: no stream footer", streams)
|
||||
}
|
||||
// Stream padding: zero bytes, a multiple of four, then the end or another stream.
|
||||
zeros := 0
|
||||
for {
|
||||
b, err := r.ReadByte()
|
||||
if err == io.EOF {
|
||||
if zeros%4 != 0 {
|
||||
return streams, blocks, errors.New("stream padding not a multiple of four bytes")
|
||||
}
|
||||
return streams, blocks, nil
|
||||
}
|
||||
if err != nil {
|
||||
return streams, blocks, err
|
||||
}
|
||||
if b != 0 {
|
||||
_ = r.UnreadByte()
|
||||
break
|
||||
}
|
||||
zeros++
|
||||
}
|
||||
if zeros%4 != 0 {
|
||||
return streams, blocks, errors.New("stream padding not a multiple of four bytes")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// xzBlockHeader checks one block header: its CRC32, its flags, and that its last filter is LZMA2, the only
|
||||
// one whose chunks this walk can skip (and the only last filter the reader decodes).
|
||||
func xzBlockHeader(h []byte) error {
|
||||
size := len(h)
|
||||
if crc32.ChecksumIEEE(h[:size-4]) != binary.LittleEndian.Uint32(h[size-4:]) {
|
||||
return errors.New("a corrupt block header")
|
||||
}
|
||||
flags := h[1]
|
||||
if flags&0x3c != 0 {
|
||||
return errors.New("a block header with reserved flags")
|
||||
}
|
||||
body := bytes.NewReader(h[2 : size-4])
|
||||
if flags&0x40 != 0 {
|
||||
if _, _, err := xzVLI(body); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if flags&0x80 != 0 {
|
||||
if _, _, err := xzVLI(body); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var last uint64
|
||||
for i := 0; i <= int(flags&3); i++ {
|
||||
id, _, err := xzVLI(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
props, _, err := xzVLI(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if props > uint64(body.Len()) {
|
||||
return errors.New("a corrupt block header")
|
||||
}
|
||||
if _, err := body.Seek(int64(props), io.SeekCurrent); err != nil {
|
||||
return err
|
||||
}
|
||||
last = id
|
||||
}
|
||||
if last != 0x21 {
|
||||
return fmt.Errorf("a block whose last filter is %#x, not LZMA2", last)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// xzVLI reads one of xz's variable-length integers (at most 9 bytes, 7 bits each), and how many bytes it took.
|
||||
func xzVLI(r io.ByteReader) (uint64, int, error) {
|
||||
var v uint64
|
||||
for i := 0; i < 9; i++ {
|
||||
b, err := r.ReadByte()
|
||||
if err != nil {
|
||||
return 0, i, io.ErrUnexpectedEOF
|
||||
}
|
||||
v |= uint64(b&0x7f) << (7 * i)
|
||||
if b&0x80 == 0 {
|
||||
if i > 0 && b == 0 {
|
||||
return 0, i + 1, errors.New("a number not in its shortest form")
|
||||
}
|
||||
return v, i + 1, nil
|
||||
}
|
||||
}
|
||||
return 0, 9, errors.New("a number longer than nine bytes")
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"hash/crc32"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// xzStream is one xz stream (no integrity check) of data cut into blocks of blockSize bytes, each block's
|
||||
// LZMA2 data stored uncompressed, each block a delta filter before LZMA2 when delta is set: the shape of
|
||||
// `xz --block-size=N --filters=delta,lzma2`, built without any xz tool. listed is how many blocks its index
|
||||
// lists (-1: as many as it holds), so a stream whose index lies can be built too.
|
||||
func xzStream(data []byte, blockSize int, delta bool, listed int) []byte {
|
||||
var out bytes.Buffer
|
||||
flags := []byte{0, 0}
|
||||
out.Write([]byte{0xfd, '7', 'z', 'X', 'Z', 0x00})
|
||||
out.Write(flags)
|
||||
out.Write(binary.LittleEndian.AppendUint32(nil, crc32.ChecksumIEEE(flags)))
|
||||
type record struct{ unpadded, uncompressed uint64 }
|
||||
var records []record
|
||||
for at := 0; at < len(data); at += blockSize {
|
||||
block := data[at:min(at+blockSize, len(data))]
|
||||
if delta {
|
||||
// Delta-encoded with a distance of one; the decoder's history starts empty at each block.
|
||||
encoded := make([]byte, len(block))
|
||||
for i := range block {
|
||||
encoded[i] = block[i]
|
||||
if i > 0 {
|
||||
encoded[i] -= block[i-1]
|
||||
}
|
||||
}
|
||||
block = encoded
|
||||
}
|
||||
header := []byte{2, 0} // 12 bytes
|
||||
if delta {
|
||||
header[1] = 1 // two filters
|
||||
header = append(header, 0x03, 0x01, 0x00)
|
||||
}
|
||||
header = append(header, 0x21, 0x01, 0x00) // LZMA2, a 4 KiB dictionary
|
||||
for len(header) < 8 {
|
||||
header = append(header, 0)
|
||||
}
|
||||
header = binary.LittleEndian.AppendUint32(header, crc32.ChecksumIEEE(header))
|
||||
out.Write(header)
|
||||
size := 0
|
||||
for i, chunk := 0, block; len(chunk) > 0; i++ {
|
||||
n := min(len(chunk), 1<<16)
|
||||
control := byte(2)
|
||||
if i == 0 {
|
||||
control = 1 // the first chunk of a block resets the dictionary
|
||||
}
|
||||
out.WriteByte(control)
|
||||
out.Write(binary.BigEndian.AppendUint16(nil, uint16(n-1)))
|
||||
out.Write(chunk[:n])
|
||||
size += 3 + n
|
||||
chunk = chunk[n:]
|
||||
}
|
||||
out.WriteByte(0)
|
||||
size++
|
||||
out.Write(make([]byte, (4-size%4)%4))
|
||||
records = append(records, record{uint64(len(header) + size), uint64(len(block))})
|
||||
}
|
||||
if listed >= 0 {
|
||||
for len(records) < listed {
|
||||
records = append(records, records[0])
|
||||
}
|
||||
records = records[:listed]
|
||||
}
|
||||
vli := func(b []byte, v uint64) []byte {
|
||||
for v >= 0x80 {
|
||||
b = append(b, byte(v)|0x80)
|
||||
v >>= 7
|
||||
}
|
||||
return append(b, byte(v))
|
||||
}
|
||||
index := vli([]byte{0}, uint64(len(records)))
|
||||
for _, r := range records {
|
||||
index = vli(vli(index, r.unpadded), r.uncompressed)
|
||||
}
|
||||
for len(index)%4 != 0 {
|
||||
index = append(index, 0)
|
||||
}
|
||||
index = binary.LittleEndian.AppendUint32(index, crc32.ChecksumIEEE(index))
|
||||
out.Write(index)
|
||||
footer := binary.LittleEndian.AppendUint32(nil, uint32(len(index)/4-1))
|
||||
footer = append(footer, flags...)
|
||||
out.Write(binary.LittleEndian.AppendUint32(nil, crc32.ChecksumIEEE(footer)))
|
||||
out.Write(footer)
|
||||
out.WriteString("YZ")
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
// tarHolding is a tar holding betterbird/data with body.
|
||||
func tarHolding(t *testing.T, body []byte) []byte {
|
||||
t.Helper()
|
||||
var out bytes.Buffer
|
||||
w := tar.NewWriter(&out)
|
||||
if err := w.WriteHeader(&tar.Header{Name: "betterbird/data", Mode: 0o644, Size: int64(len(body)),
|
||||
Typeflag: tar.TypeReg}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Write(body)
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
func writeTemp(t *testing.T, body []byte) string {
|
||||
t.Helper()
|
||||
file := filepath.Join(t.TempDir(), "r.tar.xz")
|
||||
if err := os.WriteFile(file, body, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
func TestAnXzFileOfAFewDeltaBlocksAndTwoStreamsIsRead(t *testing.T) {
|
||||
// The streams this test builds are ones the reader decodes, so what the walk refuses below is the
|
||||
// same framing the reader follows.
|
||||
body := bytes.Repeat([]byte("betterbird "), 300)
|
||||
whole := tarHolding(t, body)
|
||||
half := len(whole) / 2
|
||||
for name, file := range map[string][]byte{
|
||||
"one stream of delta blocks": xzStream(whole, 512, true, -1),
|
||||
"two streams with padding": append(append(xzStream(whole[:half], 700, true, -1), 0, 0, 0, 0),
|
||||
xzStream(whole[half:], 100, false, -1)...),
|
||||
} {
|
||||
path := writeTemp(t, file)
|
||||
if xzPath, err := exec.LookPath("xz"); err == nil {
|
||||
if out, err := exec.Command(xzPath, "-t", path).CombinedOutput(); err != nil {
|
||||
t.Fatalf("%s: xz itself refuses the built stream: %v %s", name, err, out)
|
||||
}
|
||||
}
|
||||
into := t.TempDir()
|
||||
if err := unpackUpstream(context.Background(), path, into, "betterbird"); err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
got, err := os.ReadFile(filepath.Join(into, "data"))
|
||||
if err != nil || !bytes.Equal(got, body) {
|
||||
t.Fatalf("%s: unpacked %d bytes, want %d: %v", name, len(got), len(body), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnXzFileOfManyDeltaBlocksIsRefusedBeforeItIsDecoded(t *testing.T) {
|
||||
// Review 68 on pull request 264: `xz --block-size=1 --delta` of a 200 kB tar kept about 67 MiB live in
|
||||
// the reader, one delta decoder per block. Its index here lists one block, as a file crafted to pass a
|
||||
// check of the index alone would; the walk counts the blocks the reader would decode.
|
||||
body := make([]byte, 200_000)
|
||||
for i := range body {
|
||||
body[i] = byte(i * 7)
|
||||
}
|
||||
crafted := xzStream(tarHolding(t, body), 1, true, 1)
|
||||
path := writeTemp(t, crafted)
|
||||
var before, after runtime.MemStats
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&before)
|
||||
err := unpackUpstream(context.Background(), path, t.TempDir(), "betterbird")
|
||||
runtime.ReadMemStats(&after)
|
||||
if err == nil || !strings.Contains(err.Error(), "more than 1024 xz blocks") {
|
||||
t.Fatalf("a file of %d bytes in blocks of one byte was not refused for its blocks: %v", len(crafted), err)
|
||||
}
|
||||
if grew := after.TotalAlloc - before.TotalAlloc; grew > 4<<20 {
|
||||
t.Fatalf("%d bytes were allocated for a refused file", grew)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheXzBlockLimitIsExact(t *testing.T) {
|
||||
data := make([]byte, xzBlocksMax+1)
|
||||
streams, blocks, err := xzFraming(bufio.NewReader(bytes.NewReader(xzStream(data[:xzBlocksMax], 1, true, -1))))
|
||||
if err != nil || streams != 1 || blocks != xzBlocksMax {
|
||||
t.Fatalf("%d blocks: %d streams, %d blocks, %v", xzBlocksMax, streams, blocks, err)
|
||||
}
|
||||
if _, _, err := xzFraming(bufio.NewReader(bytes.NewReader(xzStream(data, 1, true, -1)))); !errors.Is(err, errXzTooManyBlocks) {
|
||||
t.Fatalf("%d blocks: %v", xzBlocksMax+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnXzFileOfTooManyStreamsIsRefused(t *testing.T) {
|
||||
var file []byte
|
||||
for i := 0; i <= xzStreamsMax; i++ {
|
||||
file = append(file, xzStream([]byte("x"), 1, false, -1)...)
|
||||
}
|
||||
err := unpackUpstream(context.Background(), writeTemp(t, file), t.TempDir(), "")
|
||||
if err == nil || !strings.Contains(err.Error(), "more than 16 concatenated xz streams") {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnXzIndexThatDoesNotListItsBlocksIsRefused(t *testing.T) {
|
||||
file := xzStream(tarHolding(t, []byte("x")), 512, false, 1)
|
||||
err := unpackUpstream(context.Background(), writeTemp(t, file), t.TempDir(), "betterbird")
|
||||
if err == nil || !strings.Contains(err.Error(), "its index lists 1 blocks, but it holds 4") {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
@@ -1718,7 +1718,6 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
// facts about the catalogue and are checked at registration (CatalogueProblems).
|
||||
problems = append(problems, declaredSeatProblems(m)...)
|
||||
problems = append(problems, RunsAsProblems(m)...)
|
||||
problems = append(problems, ReadsHolderProblems(m)...)
|
||||
if m.Computed != "" && len(m.Resources) > 0 {
|
||||
// One or the other. A module that both ships files and has them computed would leave
|
||||
// nobody able to say where a given file came from.
|
||||
@@ -1859,7 +1858,6 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
problems = append(problems, invokeProblems(m)...)
|
||||
problems = append(problems, replacesProblems(m)...)
|
||||
problems = append(problems, UnitSettingProblems(m)...)
|
||||
problems = append(problems, OwnerSettingProblems(m)...)
|
||||
problems = append(problems, endpointNameProblems(m)...)
|
||||
problems = append(problems, RouteProblems(m)...)
|
||||
for _, port := range m.Guards {
|
||||
|
||||
@@ -66,54 +66,56 @@ func holdsSeat(m Manifest, seat string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// stepsBack is the module as it composes among the modules of one machine (novox/hq ADR 0287, ADR 0247):
|
||||
// **where it holds a seat that names a path stepping back for another seat, and that other seat's holder
|
||||
// is on the machine and declares the path, its own declaration of the path is left out**, and nothing else
|
||||
// of it changes. The stepping back is the seat's definition — node-uplink stepping back from the resolver
|
||||
// file for node-resolver — so any other module declaring the path beside the owner's is still two owners
|
||||
// of one path, and is refused as before.
|
||||
func stepsBack(m Manifest, modules []Manifest) Manifest {
|
||||
drop := map[string]bool{}
|
||||
for _, c := range m.Claims {
|
||||
seat, known := SeatNamed(c.Name)
|
||||
if !known {
|
||||
continue
|
||||
}
|
||||
for _, p := range seat.Paths {
|
||||
if p.StepsBackFor == "" {
|
||||
continue
|
||||
}
|
||||
for _, o := range modules {
|
||||
if o.Module != m.Module && holdsSeat(o, p.StepsBackFor) && declaresPath(o, p.Path) {
|
||||
drop[p.Path] = true
|
||||
}
|
||||
}
|
||||
// rendersResolverFile says whether a module asks the mesh to render the machine's resolver file.
|
||||
func rendersResolverFile(m Manifest) bool {
|
||||
for _, f := range m.Facts {
|
||||
if !f.Home && f.Path == ResolverFile {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if len(drop) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// ResolverFileOwner is the module that writes the machine's resolver file among the modules on one
|
||||
// machine (novox/hq ADR 0247): the holder of node-resolver when one renders it, else nobody is named here
|
||||
// and the file is whoever's it always was — the uplink holder's (ADR 0223).
|
||||
func ResolverFileOwner(modules []Manifest) string {
|
||||
for _, m := range modules {
|
||||
if holdsSeat(m, ResolverSeat) && rendersResolverFile(m) {
|
||||
return m.Module
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// stepsBack is the module as it composes on a machine whose resolver file is the node-resolver holder's:
|
||||
// **the uplink holder's rendering of that file is left out**, and nothing else of it changes. Only the
|
||||
// uplink's holder steps back — any other module rendering the file beside the resolver's is still two
|
||||
// owners of one path, and is refused as before.
|
||||
func stepsBack(m Manifest, modules []Manifest) Manifest {
|
||||
if !holdsSeat(m, "node-uplink") || !rendersResolverFile(m) {
|
||||
return m
|
||||
}
|
||||
owner := ResolverFileOwner(modules)
|
||||
if owner == "" || owner == m.Module {
|
||||
return m
|
||||
}
|
||||
facts := make(map[string]RosterFile, len(m.Facts))
|
||||
for name, f := range m.Facts {
|
||||
if !f.Home && drop[f.Path] {
|
||||
if !f.Home && f.Path == ResolverFile {
|
||||
continue
|
||||
}
|
||||
facts[name] = f
|
||||
}
|
||||
m.Facts = facts
|
||||
var resources []map[string]any
|
||||
for _, r := range m.Resources {
|
||||
if p, ok := r["path"].(string); ok && drop[p] {
|
||||
continue
|
||||
}
|
||||
resources = append(resources, r)
|
||||
}
|
||||
m.Resources = resources
|
||||
return m
|
||||
}
|
||||
|
||||
// steppedBack is every module of one machine as it composes there (stepsBack, each).
|
||||
func steppedBack(modules []Manifest) []Manifest {
|
||||
if ResolverFileOwner(modules) == "" {
|
||||
return modules
|
||||
}
|
||||
out := make([]Manifest, len(modules))
|
||||
for i, m := range modules {
|
||||
out[i] = stepsBack(m, modules)
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A node's files and accounts, read (novox/hq ADR 0310, extending ADR 0268 §5; hq to-be 41 WP8).
|
||||
//
|
||||
// **A seat of its own on every node.** Reading a file, searching one, listing a directory, the owner and mode
|
||||
// of each component of a path, and looking up an account or a group are the reads ADR 0268 §5 has narrow tools
|
||||
// take over from the login shell's `execute`, so an approval is asked for an act and never for a read. Every
|
||||
// node needs them, the control-node included, and another module could offer them instead of the tool
|
||||
// runner's (ADR 0126 §9): a seat, so the mesh finds them on every node by one name.
|
||||
//
|
||||
// **Read as an account that holds no secret.** The holder runs as an account of its own (runs-as, ADR 0259
|
||||
// §8), so the node's file modes bound what it sees — never the operator's account, which sees the
|
||||
// operator's keys and the agent's credentials. On top, it refuses by name the places a secret is kept,
|
||||
// which the controller names for it (secretPlaces), and every answer passes the content rule's secret class
|
||||
// (mesh-sdk's outward, ADR 0310 §4).
|
||||
|
||||
// ReadsSeat is the role that reads a node's files and accounts (novox/hq ADR 0310).
|
||||
const ReadsSeat = "node-reads"
|
||||
|
||||
// ReadsSecretPlaces is the word the holder of node-reads is given: the places on its node where a secret is
|
||||
// kept, as a JSON list of absolute paths, each refused by name before any read (novox/hq ADR 0310 §5).
|
||||
const ReadsSecretPlaces = "MESH_SECRET_PLACES"
|
||||
|
||||
// readsVerbs is the contract every holder of node-reads serves (novox/hq ADR 0310 §2). The bounds are the
|
||||
// build's to set and each verb's description to state (ADR 0310, Consequences); the holder states them in
|
||||
// every answer it cuts.
|
||||
func readsVerbs() []Verb {
|
||||
return []Verb{
|
||||
{Name: "stat", Description: "Each component of one path or several, from / down: its type, owner and group " +
|
||||
"(name and number), mode, whether it is a link and to what, and whether it is a mount point and of which " +
|
||||
"file system. A link is not followed unless follow is \"true\". Read as the holder's own account, which " +
|
||||
"holds no secret: a component it may not reach says so. At most 200 paths in one call.",
|
||||
Input: schema(map[string]string{
|
||||
"paths": "one absolute path, or several separated by line breaks",
|
||||
"follow": "\"true\": also say where the path leads once every link is followed",
|
||||
}, []string{"paths"}, "follow"),
|
||||
Replaces: []string{"stat", "ls -ld", "namei -l", "findmnt -T"}},
|
||||
{Name: "readlink", Description: "What one link points to, as written, and where the path leads once every " +
|
||||
"link is followed, and whether that exists.",
|
||||
Input: schema(map[string]string{"path": "an absolute path"}, []string{"path"}),
|
||||
Replaces: []string{"readlink", "readlink -f", "realpath"}},
|
||||
{Name: "list", Description: "Every entry of one directory with its type, owner, group, mode, size and, for a " +
|
||||
"link, its target. At most 2000 entries; a longer directory is cut and says so. A place a secret is kept " +
|
||||
"is refused by name.",
|
||||
Input: schema(map[string]string{"path": "an absolute path to a directory"}, []string{"path"}),
|
||||
Replaces: []string{"ls", "ls -la"}},
|
||||
{Name: "read", Description: "One text file, or a part of it by line: at most 256 KiB in one answer " +
|
||||
"(64 KiB unless more is asked), cut at a line and said so. A binary file is refused; a place a secret is " +
|
||||
"kept, or a known credential file, is refused by name; a line shaped like a secret is withheld and its " +
|
||||
"number said. /proc and /sys are read as far as the holder's account may.",
|
||||
Input: schema(map[string]string{
|
||||
"path": "an absolute path to a file",
|
||||
"from": "the first line to answer, counted from 1 (optional)",
|
||||
"lines": "how many lines to answer (optional)",
|
||||
"bytes": "the most bytes to answer, up to 262144 (optional, 65536 unless given)",
|
||||
}, []string{"path"}),
|
||||
Replaces: []string{"cat", "head", "tail", "less"}},
|
||||
{Name: "search", Description: "Find a fixed string (never a pattern) in one file or under one directory: " +
|
||||
"every matching line with its file and number. Bounded: at most 500 matches (100 unless more is asked), " +
|
||||
"8 directories deep (4 unless more is asked), 5000 files and 4 MiB per file; links are not followed and " +
|
||||
"binary files are skipped. A place a secret is kept is skipped and named; a matching line shaped like a " +
|
||||
"secret is withheld. The answer says which bound it met.",
|
||||
Input: schema(map[string]string{
|
||||
"text": "the fixed string to find",
|
||||
"path": "an absolute path to a file or a directory",
|
||||
"depth": "how many directories deep, up to 8 (optional)",
|
||||
"matches": "the most matches to answer, up to 500 (optional)",
|
||||
"case": "\"false\": match whatever the case (optional)",
|
||||
}, []string{"text", "path"}, "case"),
|
||||
Replaces: []string{"grep", "grep -r", "grep -F"}},
|
||||
{Name: "account", Description: "One account or group by name or number, as this node's account files say: " +
|
||||
"an account's number, group, home, shell and every group it is in; a group's number and members. With " +
|
||||
"neither, every account and group. Never a password field.",
|
||||
Input: schema(map[string]string{
|
||||
"name": "an account's name or number (optional)",
|
||||
"group": "a group's name or number (optional)",
|
||||
}, nil),
|
||||
Replaces: []string{"getent passwd", "getent group", "id", "groups"}},
|
||||
}
|
||||
}
|
||||
|
||||
// ReadsHolderProblems is what a holder of node-reads is held to (novox/hq ADR 0310 §3): it reads as an
|
||||
// account of its own, so the node's file modes bound what it sees. Carried by the machine's runtime it would
|
||||
// read as the operator's account, which sees the operator's keys and the agent's credentials.
|
||||
func ReadsHolderProblems(m Manifest) []string {
|
||||
if !m.ClaimsSeat(ReadsSeat) {
|
||||
return nil
|
||||
}
|
||||
if m.RunsAs == "" {
|
||||
return []string{fmt.Sprintf("%s claims %s and must run as an account of its own (runs-as): its reads are "+
|
||||
"bounded by that account's file modes, and the machine's runtime runs as the operator's account "+
|
||||
"(novox/hq ADR 0310 §3)", m.Module, ReadsSeat)}
|
||||
}
|
||||
// In no group beside its own: a group is a way to a secret (docker's is root's), and the account's file modes
|
||||
// are what bound its reads.
|
||||
for _, r := range m.Resources {
|
||||
if fmt.Sprint(r["type"]) != "user" || fmt.Sprint(r["name"]) != m.RunsAs {
|
||||
continue
|
||||
}
|
||||
if groups, said := r["groups"]; said {
|
||||
if list, ok := groups.([]any); !ok || len(list) > 0 {
|
||||
return []string{fmt.Sprintf("%s claims %s, and its account %s is put in groups %v: the account that "+
|
||||
"reads is in no group beside its own (novox/hq ADR 0310 §3)", m.Module, ReadsSeat, m.RunsAs, groups)}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// secretPlaces is every place on this node a secret is kept, as the placed manifests say (novox/hq ADR 0310
|
||||
// §5): the directory of every module's own secret, every credential file a module is given and every grant
|
||||
// directory a provider is handed its consumers' credentials in, the place a module keeps the operator-sealed
|
||||
// secrets, and every directory of the module that provides `secret` — the vault — wherever the node places it,
|
||||
// a directory declared without a path included. Sorted, each once, and a place under another dropped, so the
|
||||
// list says each refusal once.
|
||||
//
|
||||
// **Failure is loud.** A place still holding a placeholder, or not absolute, fails the composition with the
|
||||
// module named: a list that silently shrank would be a refusal the decision promises and nothing does.
|
||||
func (r Resolution) secretPlaces(with Rendering) ([]string, error) {
|
||||
seen := map[string]bool{}
|
||||
var problems []string
|
||||
add := func(module, what, path string) {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") || strings.Contains(path, "${") {
|
||||
problems = append(problems, fmt.Sprintf("%s's %s is %q, which is not a place on %s", module, what, path, r.Node))
|
||||
return
|
||||
}
|
||||
path = filepath.Clean(path)
|
||||
if path == "/" {
|
||||
problems = append(problems, fmt.Sprintf("%s's %s is the root directory", module, what))
|
||||
return
|
||||
}
|
||||
seen[path] = true
|
||||
}
|
||||
for _, m := range r.Modules {
|
||||
for name, own := range m.OwnSecrets {
|
||||
add(m.Module, "own secret "+name, own.Path)
|
||||
if strings.HasPrefix(own.Path, "/") {
|
||||
add(m.Module, "own secret "+name+"'s directory", filepath.Dir(own.Path))
|
||||
}
|
||||
}
|
||||
for _, to := range m.SecretRequirements() {
|
||||
for _, f := range m.SecretFiles(to) {
|
||||
add(m.Module, "credential file for "+to, f.Path)
|
||||
}
|
||||
}
|
||||
for to, where := range m.Grants {
|
||||
add(m.Module, "grant directory for "+to, where)
|
||||
}
|
||||
add(m.Module, "place for the operator-sealed secrets (keeps)", m.Keeps)
|
||||
if providesSecret(m) {
|
||||
dirs := dirsFor(m, with)
|
||||
for _, id := range sortedKeys(dirs) {
|
||||
add(m.Module, "directory "+id, dirs[id])
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(problems) > 0 {
|
||||
sort.Strings(problems)
|
||||
return nil, fmt.Errorf("the holder of %s on %s cannot be told where a secret is kept, so it would read "+
|
||||
"one (novox/hq ADR 0310 §5): %s", ReadsSeat, r.Node, strings.Join(problems, "; "))
|
||||
}
|
||||
all := make([]string, 0, len(seen))
|
||||
for p := range seen {
|
||||
all = append(all, p)
|
||||
}
|
||||
sort.Strings(all)
|
||||
var out []string
|
||||
for _, p := range all {
|
||||
if n := len(out); n > 0 && strings.HasPrefix(p, out[n-1]+"/") {
|
||||
continue
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func providesSecret(m Manifest) bool {
|
||||
for _, p := range m.Provides {
|
||||
if p.Name == "secret" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// readsWords is what the holder of node-reads is given beside its own words: the node's secret places.
|
||||
func (r Resolution) readsWords(m Manifest, words map[string]string, with Rendering) (map[string]string, error) {
|
||||
if !m.ClaimsSeat(ReadsSeat) {
|
||||
return words, nil
|
||||
}
|
||||
places, err := r.secretPlaces(with)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := json.Marshal(places)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if words == nil {
|
||||
words = map[string]string{}
|
||||
}
|
||||
if _, has := words[ReadsSecretPlaces]; has {
|
||||
return nil, fmt.Errorf("%s gives its own %s; the node's secret places are the mesh's to say (novox/hq ADR 0310 §5)",
|
||||
m.Module, ReadsSecretPlaces)
|
||||
}
|
||||
words[ReadsSecretPlaces] = string(body)
|
||||
return words, nil
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// novox/hq ADR 0310 §1, §2: node-reads is a node seat, and its verbs say the commands they replace, so
|
||||
// mesh_search finds them by `cat`, `grep` and `getent passwd` (ADR 0245 §3).
|
||||
func TestNodeReadsIsANodeSeatWhoseVerbsSayWhatTheyReplace(t *testing.T) {
|
||||
s, ok := SeatNamed(ReadsSeat)
|
||||
if !ok {
|
||||
t.Fatalf("the mesh defines no seat %s", ReadsSeat)
|
||||
}
|
||||
if s.Scope != ScopeNode || s.Delivers != "" || !strings.Contains(s.Decision, "0310") {
|
||||
t.Errorf("%s: scope %q, delivers %q, decision %q", ReadsSeat, s.Scope, s.Delivers, s.Decision)
|
||||
}
|
||||
replaced := map[string]string{}
|
||||
for _, v := range s.Serves {
|
||||
if v.Optional {
|
||||
t.Errorf("%s.%s is optional: a new seat's holder serves every verb", ReadsSeat, v.Name)
|
||||
}
|
||||
if len(v.Replaces) == 0 {
|
||||
t.Errorf("%s.%s says nothing it replaces", ReadsSeat, v.Name)
|
||||
}
|
||||
for _, c := range v.Replaces {
|
||||
replaced[c] = v.Name
|
||||
}
|
||||
}
|
||||
for command, verb := range map[string]string{"cat": "read", "grep": "search", "getent passwd": "account",
|
||||
"getent group": "account", "id": "account", "stat": "stat", "readlink": "readlink", "ls": "list"} {
|
||||
if replaced[command] != verb {
|
||||
t.Errorf("%q is replaced by %q, want %s.%s", command, replaced[command], ReadsSeat, verb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0310 §3: a holder of node-reads reads as an account of its own; one the machine's runtime would
|
||||
// carry — as the operator's account — is refused at parse.
|
||||
func TestAHolderOfNodeReadsWithoutAnAccountOfItsOwnIsRefused(t *testing.T) {
|
||||
_, err := ParseManifest([]byte(`{"module": "reads", "version": "1",
|
||||
"claims": [{"name": "node-reads", "scope": "node"}]}`))
|
||||
if err == nil || !strings.Contains(err.Error(), "runs-as") || !strings.Contains(err.Error(), "0310") {
|
||||
t.Errorf("a holder of node-reads without runs-as was not refused for it: %v", err)
|
||||
}
|
||||
if p := ReadsHolderProblems(Manifest{Module: "reads", RunsAs: "mesh-reads",
|
||||
Claims: []Claim{{Name: ReadsSeat}}}); len(p) != 0 {
|
||||
t.Errorf("a holder running as its own account was refused: %v", p)
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0310 §5: the holder is told, by the composition, where its node keeps a secret — the directory
|
||||
// of every module's own secret, every credential file and grant directory, the vault's directories and the
|
||||
// place the operator-sealed secrets are kept — and no other module is told.
|
||||
func TestTheHolderOfNodeReadsIsToldWhereItsNodeKeepsASecret(t *testing.T) {
|
||||
with := Rendering{ArtifactStore: "anchor.internal:5101",
|
||||
Needed: map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"},
|
||||
"reads": {"broker": "own"}, "desk": {"broker": "own"}}}
|
||||
goRuntime := Manifest{Module: RuntimeModule, Version: "1",
|
||||
OwnSecrets: OwnSecrets{"broker": {Path: "/var/lib/mesh/" + RuntimeModule + "/broker"}},
|
||||
Build: &Build{Artifacts: []Artifact{{Name: "runtime", Kind: ArtifactBundle, Language: "go",
|
||||
System: "arch", From: "cmd/node-tools"}}}}
|
||||
goRuntime, err := goRuntime.Resolve([]Built{{Name: "runtime", Kind: ArtifactBundle,
|
||||
Reference: ArtifactStoreScheme + RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reads := aToolsModule(t, "reads", "tools/index.js")
|
||||
reads.RunsAs, reads.SecretsOwner = "mesh-reads", "mesh-reads"
|
||||
reads.Claims = []Claim{{Name: ReadsSeat}}
|
||||
reads.OwnSecrets = OwnSecrets{"broker": {Path: "/var/lib/mesh-reads/broker"}}
|
||||
desk := aToolsModule(t, "desk", "tools/index.js")
|
||||
desk.RunsAs, desk.SecretsOwner = "desk", "desk"
|
||||
desk.OwnSecrets = OwnSecrets{"broker": {Path: "/var/lib/desk/broker"}}
|
||||
// The vault as the catalogue declares it: its directories have no path of their own, and the node places them.
|
||||
vault, err := placedManifest(catalogueManifest(t, "mesh-vault"), with)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app := Manifest{Module: "app", Version: "1", Secrets: map[string]string{"postgres-database": "/etc/app/db.secret"},
|
||||
OwnSecrets: OwnSecrets{"token": {Path: "/home/ops/.config/app/token"}}}
|
||||
r := Resolution{Node: "anchor", Account: "ops", Modules: []Manifest{reads, desk, vault, app, goRuntime}}
|
||||
owns, err := r.ownRuntimes(with)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var given map[string]map[string]string
|
||||
for _, p := range owns {
|
||||
if p["id"] == "reads."+OwnRuntimeID() {
|
||||
if err := json.Unmarshal([]byte(p["env"].(map[string]string)[RuntimeToolEnv]), &given); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if p["id"] == "desk."+OwnRuntimeID() && strings.Contains(p["env"].(map[string]string)[RuntimeToolEnv], ReadsSecretPlaces) {
|
||||
t.Error("a module not holding node-reads is told the node's secret places")
|
||||
}
|
||||
}
|
||||
var places []string
|
||||
if err := json.Unmarshal([]byte(given["reads"][ReadsSecretPlaces]), &places); err != nil {
|
||||
t.Fatalf("the holder is not told its node's secret places: %v (%v)", err, given)
|
||||
}
|
||||
want := []string{"/etc/app/db.secret", "/home/ops/.config/app", "/var/lib/desk", "/var/lib/mesh-reads",
|
||||
"/var/lib/mesh-vault", "/var/lib/mesh/" + RuntimeModule}
|
||||
if strings.Join(places, " ") != strings.Join(want, " ") {
|
||||
t.Errorf("secret places:\n got %v\nwant %v", places, want)
|
||||
}
|
||||
}
|
||||
|
||||
// The review of #257: a secret place the controller cannot resolve fails the composition, naming the module,
|
||||
// rather than leaving the list one place shorter.
|
||||
func TestASecretPlaceThatCannotBeResolvedFailsTheComposition(t *testing.T) {
|
||||
r := Resolution{Node: "anchor", Modules: []Manifest{
|
||||
{Module: "app", Version: "1", Keeps: "${dir:keep}/sealed"},
|
||||
}}
|
||||
if _, err := r.secretPlaces(Rendering{}); err == nil || !strings.Contains(err.Error(), "app") {
|
||||
t.Errorf("an unresolved place was dropped: %v", err)
|
||||
}
|
||||
r.Modules[0].Keeps = "relative/place"
|
||||
if _, err := r.secretPlaces(Rendering{}); err == nil {
|
||||
t.Error("a relative place was dropped")
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0310 §3: the account that reads is in no group beside its own; one put in a group is refused.
|
||||
func TestTheReadsAccountIsInNoGroup(t *testing.T) {
|
||||
m := Manifest{Module: "reads", RunsAs: "mesh-reads", Claims: []Claim{{Name: ReadsSeat}},
|
||||
Resources: []map[string]any{{"id": "account", "type": "user", "name": "mesh-reads", "groups": []any{"docker"}}}}
|
||||
if p := ReadsHolderProblems(m); len(p) != 1 || !strings.Contains(p[0], "docker") {
|
||||
t.Errorf("an account in docker was not refused: %v", p)
|
||||
}
|
||||
m.Resources[0]["groups"] = []any{}
|
||||
if p := ReadsHolderProblems(m); len(p) != 0 {
|
||||
t.Errorf("an account in no group was refused: %v", p)
|
||||
}
|
||||
}
|
||||
@@ -957,9 +957,7 @@ func checkResources(modules []Manifest) []string {
|
||||
// Judged as the machine composes them: where the node-resolver holder writes the resolver file, the
|
||||
// uplink holder's rendering of it is not composed, so it is not a second owner (novox/hq ADR 0247).
|
||||
modules = steppedBack(modules)
|
||||
// And only a claimant of a seat naming a path declares it — the controller's generated resources
|
||||
// included, which are judged here at composition (novox/hq ADR 0287).
|
||||
problems := seatPathsOnMachine(modules)
|
||||
var problems []string
|
||||
owner := map[string]string{}
|
||||
ownedPath := map[string]string{} // path → owning module, for the access check below
|
||||
packagePresent, packageAbsent := map[string][]string{}, map[string][]string{}
|
||||
|
||||
@@ -303,10 +303,6 @@ func (r Resolution) ownRuntimes(with Rendering) ([]map[string]any, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The holder of node-reads is told where this node keeps its secrets (novox/hq ADR 0310 §5).
|
||||
if words, err = r.readsWords(m, words, with); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(words) > 0 {
|
||||
body, err := json.Marshal(map[string]map[string]string{m.Module: words})
|
||||
if err != nil {
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The paths a seat's holder owns (novox/hq ADR 0287).
|
||||
//
|
||||
// **Who owns a machine-wide file is the role's question, answered once.** Until this, which module owned
|
||||
// /etc/hosts or /etc/resolv.conf was decided record by record (ADRs 0199, 0222, 0223, 0241) and enforced
|
||||
// only after the fact, by checkResources finding two modules on one machine declaring the same path: a
|
||||
// module that did not hold node-hostname could declare /etc/hosts on a machine where nobody held it, and
|
||||
// nothing refused it. Now a seat's definition names the paths the role owns on any machine, and three
|
||||
// refusals follow from it, each where the mesh can still say no:
|
||||
//
|
||||
// - a claim whose module does not declare every path its seat names, by CanHold, at registration and at
|
||||
// handover;
|
||||
// - a module declaring a path a seat names while claiming none of the seats that name it, at
|
||||
// registration (CatalogueProblems), and what the controller generates for a machine, at composition;
|
||||
// - a module's own seat naming a path another seat already names, at registration — the second loses.
|
||||
//
|
||||
// Reading a seat's path is access, never a resource, so it stays allowed.
|
||||
|
||||
// SeatPath is one path a seat's holder owns: absolute, literal, on the machine where the holder runs.
|
||||
type SeatPath struct {
|
||||
Path string `json:"path"`
|
||||
// StepsBackFor is a node seat whose holder owns the path instead, on a machine where that seat is held
|
||||
// (novox/hq ADR 0287, ADR 0247): this seat's holder still declares the path, and its declaration is not
|
||||
// composed there. Only the mesh's own seats step back, each by a record.
|
||||
StepsBackFor string `json:"steps-back-for,omitempty"`
|
||||
}
|
||||
|
||||
// seatPathProblem is what is wrong with one path a seat names, judged alone: absolute, written literally
|
||||
// with no placeholder, and clean, so it compares with what a module declares byte for byte.
|
||||
func seatPathProblem(seat, scope, p string) string {
|
||||
switch {
|
||||
case scope != ScopeNode:
|
||||
return fmt.Sprintf("the seat %s names the path %q and is a %s seat; only a node seat names paths, "+
|
||||
"because a path is a machine's (novox/hq ADR 0287)", seat, p, scope)
|
||||
case strings.Contains(p, "${"):
|
||||
return fmt.Sprintf("the seat %s names the path %q, which has a placeholder; a seat's path is written "+
|
||||
"literally (novox/hq ADR 0287)", seat, p)
|
||||
case !strings.HasPrefix(p, "/") || path.Clean(p) != p:
|
||||
return fmt.Sprintf("the seat %s names the path %q, which is not a clean absolute path (novox/hq ADR 0287)",
|
||||
seat, p)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SeatPathProblems is what is wrong with the paths a set of the mesh's own seats names (novox/hq ADR
|
||||
// 0287): only a node seat names one, each absolute and literal; every path has exactly one seat naming it
|
||||
// without stepping back; and a seat steps back only for another node seat naming the same path without
|
||||
// stepping back, so the stepping back cannot cycle.
|
||||
func SeatPathProblems(set []Seat) []string {
|
||||
var problems []string
|
||||
owners := map[string][]string{} // path → the seats naming it without stepping back
|
||||
stepping := map[string][]SeatPath{} // seat → the paths it names stepping back
|
||||
byName := map[string]Seat{}
|
||||
for _, s := range set {
|
||||
byName[s.Name] = s
|
||||
for _, p := range s.Paths {
|
||||
if why := seatPathProblem(s.Name, s.Scope, p.Path); why != "" {
|
||||
problems = append(problems, why)
|
||||
}
|
||||
if p.StepsBackFor == "" {
|
||||
owners[p.Path] = append(owners[p.Path], s.Name)
|
||||
} else {
|
||||
stepping[s.Name] = append(stepping[s.Name], p)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, s := range set {
|
||||
for _, p := range stepping[s.Name] {
|
||||
other, known := byName[p.StepsBackFor]
|
||||
switch {
|
||||
case !known || other.Scope != ScopeNode:
|
||||
problems = append(problems, fmt.Sprintf("the seat %s steps back from %q for %q, which is not a "+
|
||||
"node seat of the mesh's (novox/hq ADR 0287)", s.Name, p.Path, p.StepsBackFor))
|
||||
case !namesWithoutSteppingBack(other, p.Path):
|
||||
problems = append(problems, fmt.Sprintf("the seat %s steps back from %q for %s, which does not "+
|
||||
"own that path itself; a seat steps back only for the one that owns it (novox/hq ADR 0287)",
|
||||
s.Name, p.Path, other.Name))
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range sortedKeys(owners) {
|
||||
if len(owners[p]) > 1 {
|
||||
problems = append(problems, fmt.Sprintf("the path %q is named by %s, each without stepping back; "+
|
||||
"no two seats name one path (novox/hq ADR 0287)", p, strings.Join(owners[p], " and ")))
|
||||
}
|
||||
}
|
||||
for _, s := range set {
|
||||
for _, p := range stepping[s.Name] {
|
||||
if len(owners[p.Path]) == 0 {
|
||||
problems = append(problems, fmt.Sprintf("the path %q has no seat naming it without stepping back "+
|
||||
"(novox/hq ADR 0287)", p.Path))
|
||||
}
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
func namesWithoutSteppingBack(s Seat, p string) bool {
|
||||
for _, sp := range s.Paths {
|
||||
if sp.Path == p && sp.StepsBackFor == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// declaredPaths is every machine path a module declares it owns: a resource's literal path, and a file
|
||||
// the mesh renders for it outside the operator's home. An access is not here — reading is not owning.
|
||||
func declaredPaths(m Manifest) map[string]bool {
|
||||
out := map[string]bool{}
|
||||
for _, r := range m.Resources {
|
||||
if p, ok := r["path"].(string); ok && strings.HasPrefix(p, "/") {
|
||||
out[p] = true
|
||||
}
|
||||
}
|
||||
for _, f := range m.Facts {
|
||||
if !f.Home && strings.HasPrefix(f.Path, "/") {
|
||||
out[f.Path] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// declaresPath says whether a module declares one path, as a resource or as a file the mesh renders.
|
||||
func declaresPath(m Manifest, p string) bool { return declaredPaths(m)[p] }
|
||||
|
||||
// missingPaths is each path a seat names that a module does not declare, in the seat's order.
|
||||
func missingPaths(m Manifest, paths []SeatPath) []string {
|
||||
if len(paths) == 0 {
|
||||
return nil
|
||||
}
|
||||
has := declaredPaths(m)
|
||||
var missing []string
|
||||
for _, p := range paths {
|
||||
if !has[p.Path] {
|
||||
missing = append(missing, p.Path)
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
// pathSeats is which seats name each path: the mesh's own, and then those modules declare, in the order
|
||||
// given. A module's seat naming a path a seat already names loses it, and the refusal says so (novox/hq
|
||||
// ADR 0287 — as for a seat's name, the second declaration loses).
|
||||
func pathSeats(order []string, shelf Shelf, declaredBy map[string]string) (map[string][]string, []string) {
|
||||
named := map[string][]string{}
|
||||
for _, s := range Seats() {
|
||||
for _, p := range s.Paths {
|
||||
named[p.Path] = append(named[p.Path], s.Name)
|
||||
}
|
||||
}
|
||||
var problems []string
|
||||
for _, module := range order {
|
||||
for _, s := range shelf[module].DefinesSeats {
|
||||
if declaredBy != nil && declaredBy[s.Name] != module {
|
||||
continue // a second declaration of the seat's name, refused for that already
|
||||
}
|
||||
for _, p := range s.Paths {
|
||||
if others := named[p]; len(others) > 0 {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s declares the seat %s naming the path %q, which %s already names; no two seats "+
|
||||
"name one path (novox/hq ADR 0287)", module, s.Name, p, strings.Join(others, " and ")))
|
||||
continue
|
||||
}
|
||||
named[p] = append(named[p], s.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return named, problems
|
||||
}
|
||||
|
||||
// unclaimedSeatPaths is each path a module declares that a seat names while it claims none of the seats
|
||||
// naming it (novox/hq ADR 0287): only a claimant declares a seat's path, and a module claiming any one of
|
||||
// them may.
|
||||
func unclaimedSeatPaths(m Manifest, named map[string][]string) []string {
|
||||
claims := map[string]bool{}
|
||||
for _, c := range m.Claims {
|
||||
claims[canonicalSeat(c.Name)] = true
|
||||
}
|
||||
var problems []string
|
||||
declared := declaredPaths(m)
|
||||
for _, p := range sortedKeys(declared) {
|
||||
seats := named[p]
|
||||
if len(seats) == 0 {
|
||||
continue
|
||||
}
|
||||
claimsOne := false
|
||||
for _, s := range seats {
|
||||
claimsOne = claimsOne || claims[s]
|
||||
}
|
||||
if !claimsOne {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s declares the path %q, which the seat %s names, and claims none of them; only a holder of "+
|
||||
"a seat naming a path declares it (novox/hq ADR 0287)", m.Module, p, strings.Join(seats, " and ")))
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// declaredSeatPathProblems is what one module's own seat declaration can be judged on alone: a path
|
||||
// only on a node seat, absolute, literal and clean.
|
||||
func declaredSeatPathProblems(m Manifest, s SeatDeclaration) []string {
|
||||
var problems []string
|
||||
for _, p := range s.Paths {
|
||||
if why := seatPathProblem(s.Name, s.At(), p); why != "" {
|
||||
problems = append(problems, m.Module+": "+why)
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// asSeatPaths is a module's own seat's paths in the shape the mesh's seats name theirs. A module's seat
|
||||
// never steps back: only the mesh's own seats do, each by a record.
|
||||
func asSeatPaths(paths []string) []SeatPath {
|
||||
out := make([]SeatPath, 0, len(paths))
|
||||
for _, p := range paths {
|
||||
out = append(out, SeatPath{Path: p})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// PathOwner is the module that owns one seat's path on one machine (novox/hq ADR 0287): the seat naming
|
||||
// the path, and that seat's holder there, with the stepping back applied.
|
||||
type PathOwner struct {
|
||||
Path string `json:"path"`
|
||||
Node string `json:"node"`
|
||||
Seat string `json:"seat"`
|
||||
Module string `json:"module"`
|
||||
}
|
||||
|
||||
// PathOwners is, for every path a seat in the set names and every machine where a seat naming it is held,
|
||||
// who owns the path there. A seat that steps back for another held on the same machine is passed over.
|
||||
func PathOwners(set []Seat, held []Held) []PathOwner {
|
||||
// node → seat → holding module
|
||||
holders := map[string]map[string]string{}
|
||||
for _, h := range held {
|
||||
s, ok := SeatNamed(h.Claim)
|
||||
if !ok || h.Scope != ScopeNode || s.Scope != ScopeNode {
|
||||
continue
|
||||
}
|
||||
if holders[h.Node] == nil {
|
||||
holders[h.Node] = map[string]string{}
|
||||
}
|
||||
if _, taken := holders[h.Node][s.Name]; !taken {
|
||||
holders[h.Node][s.Name] = h.Module
|
||||
}
|
||||
}
|
||||
naming := map[string][]Seat{} // path → the seats naming it
|
||||
steps := map[string]map[string]string{} // path → seat → the seat it steps back for
|
||||
for _, s := range set {
|
||||
for _, p := range s.Paths {
|
||||
naming[p.Path] = append(naming[p.Path], s)
|
||||
if p.StepsBackFor != "" {
|
||||
if steps[p.Path] == nil {
|
||||
steps[p.Path] = map[string]string{}
|
||||
}
|
||||
steps[p.Path][s.Name] = p.StepsBackFor
|
||||
}
|
||||
}
|
||||
}
|
||||
var out []PathOwner
|
||||
for _, p := range sortedKeys(naming) {
|
||||
for _, node := range sortedKeys(holders) {
|
||||
for _, s := range naming[p] {
|
||||
module, holds := holders[node][s.Name]
|
||||
if !holds {
|
||||
continue
|
||||
}
|
||||
if forSeat := steps[p][s.Name]; forSeat != "" {
|
||||
if _, there := holders[node][forSeat]; there {
|
||||
continue
|
||||
}
|
||||
}
|
||||
out = append(out, PathOwner{Path: p, Node: node, Seat: s.Name, Module: module})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// seatPathsOnMachine is each path the modules of one machine declare that a seat names, for a module
|
||||
// claiming none of the seats naming it (novox/hq ADR 0287). Registration refuses the same of a manifest;
|
||||
// this holds what the controller generates for a machine to it too, because a computed module's resources
|
||||
// exist only once composed (checkResources is asked them there). The seats are the mesh's own and those the
|
||||
// machine's modules declare.
|
||||
func seatPathsOnMachine(modules []Manifest) []string {
|
||||
shelf := Shelf{}
|
||||
var order []string
|
||||
for _, m := range modules {
|
||||
shelf[m.Module] = m
|
||||
order = append(order, m.Module)
|
||||
}
|
||||
named, _ := pathSeats(order, shelf, nil)
|
||||
var problems []string
|
||||
for _, m := range modules {
|
||||
problems = append(problems, unclaimedSeatPaths(m, named)...)
|
||||
}
|
||||
return problems
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Defends novox/hq ADR 0287 (issue 412): a seat names the paths its holder owns, a claim without them is
|
||||
// refused, and only a claimant declares a seat's path.
|
||||
|
||||
// The starting set, exactly: node-hostname names /etc/hosts and /etc/hostname, node-uplink the resolver
|
||||
// file stepping back for node-resolver, node-resolver the resolver file, and every other seat names none.
|
||||
func TestTheSeatsThatNamePathsAreTheStartingSet(t *testing.T) {
|
||||
want := map[string]string{
|
||||
"node-hostname": "/etc/hosts /etc/hostname",
|
||||
"node-uplink": "/etc/resolv.conf>node-resolver",
|
||||
ResolverSeat: "/etc/resolv.conf",
|
||||
}
|
||||
for _, s := range DefaultSeats() {
|
||||
var got []string
|
||||
for _, p := range s.Paths {
|
||||
if p.StepsBackFor != "" {
|
||||
got = append(got, p.Path+">"+p.StepsBackFor)
|
||||
} else {
|
||||
got = append(got, p.Path)
|
||||
}
|
||||
}
|
||||
if strings.Join(got, " ") != want[s.Name] {
|
||||
t.Errorf("%s names %v; ADR 0287 gives it %q", s.Name, got, want[s.Name])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The compiled set keeps the rules: node seats only, absolute and literal, one seat per path without
|
||||
// stepping back, and a stepping back that cannot cycle.
|
||||
func TestTheCompiledSeatPathsKeepTheRules(t *testing.T) {
|
||||
if p := SeatPathProblems(DefaultSeats()); len(p) != 0 {
|
||||
t.Fatalf("the compiled set breaks ADR 0287: %v", p)
|
||||
}
|
||||
for _, c := range []struct {
|
||||
why string
|
||||
set []Seat
|
||||
want string
|
||||
}{
|
||||
{"a mesh seat naming a path", []Seat{{Name: "mesh-x", Scope: ScopeMesh, Paths: []SeatPath{{Path: "/etc/x"}}}},
|
||||
"only a node seat names paths"},
|
||||
{"a placeholder", []Seat{{Name: "node-x", Scope: ScopeNode, Paths: []SeatPath{{Path: "/etc/${setting:x}"}}}},
|
||||
"placeholder"},
|
||||
{"a relative path", []Seat{{Name: "node-x", Scope: ScopeNode, Paths: []SeatPath{{Path: "etc/x"}}}},
|
||||
"not a clean absolute path"},
|
||||
{"two owners", []Seat{
|
||||
{Name: "node-a", Scope: ScopeNode, Paths: []SeatPath{{Path: "/etc/x"}}},
|
||||
{Name: "node-b", Scope: ScopeNode, Paths: []SeatPath{{Path: "/etc/x"}}}}, "no two seats name one path"},
|
||||
{"a cycle", []Seat{
|
||||
{Name: "node-a", Scope: ScopeNode, Paths: []SeatPath{{Path: "/etc/x", StepsBackFor: "node-b"}}},
|
||||
{Name: "node-b", Scope: ScopeNode, Paths: []SeatPath{{Path: "/etc/x", StepsBackFor: "node-a"}}}},
|
||||
"does not own that path itself"},
|
||||
{"stepping back for nobody", []Seat{
|
||||
{Name: "node-a", Scope: ScopeNode, Paths: []SeatPath{{Path: "/etc/x", StepsBackFor: "node-z"}}}},
|
||||
"not a node seat"},
|
||||
} {
|
||||
if got := strings.Join(SeatPathProblems(c.set), "; "); !strings.Contains(got, c.want) {
|
||||
t.Errorf("%s: %q does not say %q", c.why, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// What the store's rows carry is never the paths: a row read back keeps the compiled ones.
|
||||
func TestTheSeatPathsSurviveTheStoresRows(t *testing.T) {
|
||||
before := seats
|
||||
t.Cleanup(func() { seats = before })
|
||||
UseSeats([]Seat{{Name: "node-hostname", Scope: ScopeNode}})
|
||||
s, _ := SeatNamed("node-hostname")
|
||||
if len(s.Paths) != 2 {
|
||||
t.Fatalf("node-hostname read back from its row names %v", s.Paths)
|
||||
}
|
||||
}
|
||||
|
||||
func hostnameSeat(t *testing.T) Seat {
|
||||
t.Helper()
|
||||
s, ok := SeatNamed("node-hostname")
|
||||
if !ok {
|
||||
t.Fatal("no node-hostname seat")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func hostnameHolder(resources ...map[string]any) Manifest {
|
||||
return Manifest{Module: "names", Version: "1", Claims: []Claim{{Name: "node-hostname", Scope: ScopeNode,
|
||||
Serves: []string{"entries", "add", "remove"}}}, Resources: resources}
|
||||
}
|
||||
|
||||
// A holder declares every path its seat names, as a resource or as a file the mesh renders for it, or
|
||||
// CanHold refuses it naming the seat and each missing path — the judgement registration and handover share.
|
||||
func TestAClaimWithoutTheSeatsPathsIsRefused(t *testing.T) {
|
||||
seat := hostnameSeat(t)
|
||||
hosts := map[string]any{"id": "hosts", "type": "file", "path": "/etc/hosts", "content": "x"}
|
||||
name := map[string]any{"id": "name", "type": "file", "path": "/etc/hostname", "content": "x"}
|
||||
|
||||
if err := CanHold(hostnameHolder(hosts, name), seat); err != nil {
|
||||
t.Errorf("a holder declaring both paths as resources was refused: %v", err)
|
||||
}
|
||||
rendered := hostnameHolder(hosts)
|
||||
rendered.Facts = map[string]RosterFile{"name": {Path: "/etc/hostname", Template: "{{.Node}}\n"}}
|
||||
if err := CanHold(rendered, seat); err != nil {
|
||||
t.Errorf("a holder declaring a path as a file the mesh renders was refused: %v", err)
|
||||
}
|
||||
err := CanHold(hostnameHolder(), seat)
|
||||
if err == nil {
|
||||
t.Fatal("a holder declaring neither path was accepted")
|
||||
}
|
||||
for _, want := range []string{"node-hostname", "/etc/hosts", "/etc/hostname", "ADR 0287"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("the refusal does not name %s: %v", want, err)
|
||||
}
|
||||
}
|
||||
// A file under the operator's home is placed per account: it is not the machine's path.
|
||||
home := hostnameHolder(hosts)
|
||||
home.Facts = map[string]RosterFile{"name": {Path: "/etc/hostname", Home: true, Template: "x"}}
|
||||
if err := CanHold(home, seat); err == nil {
|
||||
t.Error("a file in the operator's home counted as the machine's /etc/hostname")
|
||||
}
|
||||
// At registration, the same refusal.
|
||||
if got := strings.Join(CatalogueProblems(Shelf{"names": hostnameHolder(hosts)}), "; "); !strings.Contains(got,
|
||||
"does not declare /etc/hostname") {
|
||||
t.Errorf("registration let a holder without /etc/hostname through: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Only a claimant declares a seat's path: a module claiming no seat that names /etc/hosts and declaring
|
||||
// it is refused at registration naming the seat; the uplink's holder and the resolver's both declaring
|
||||
// the resolver file are each a claimant of a seat naming it. Reading a seat's path is access, and allowed.
|
||||
func TestOnlyAClaimantDeclaresASeatsPath(t *testing.T) {
|
||||
stranger := Manifest{Module: "stranger", Version: "1", Resources: []map[string]any{
|
||||
{"id": "hosts", "type": "file", "path": "/etc/hosts", "into": "block", "content": "10.0.0.1 x\n"}}}
|
||||
got := strings.Join(CatalogueProblems(Shelf{"stranger": stranger}), "; ")
|
||||
if !strings.Contains(got, `stranger declares the path "/etc/hosts", which the seat node-hostname names`) {
|
||||
t.Errorf("a module declaring /etc/hosts without claiming node-hostname was not refused: %q", got)
|
||||
}
|
||||
reader := Manifest{Module: "reader", Version: "1", Accesses: []Access{{ID: "hosts", Path: "/etc/hosts"}}}
|
||||
if got := CatalogueProblems(Shelf{"reader": reader}); len(got) != 0 {
|
||||
t.Errorf("reading /etc/hosts was refused: %v", got)
|
||||
}
|
||||
uplink := Manifest{Module: "uplink", Version: "1", Claims: []Claim{{Name: "node-uplink", Scope: ScopeNode}},
|
||||
Facts: map[string]RosterFile{"resolvers": {Path: ResolverFile, Template: "nameserver 10.42.0.1\n"}}}
|
||||
resolver := localResolver()
|
||||
resolver.Claims[0].Serves = []string{"routes", "route", "unroute"}
|
||||
if got := CatalogueProblems(Shelf{"uplink": uplink, resolver.Module: resolver}); len(got) != 0 {
|
||||
t.Errorf("the uplink's holder and the resolver's declaring the resolver file were refused: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A module's own seat names paths under the same rules: a node seat only, literal, and none another seat
|
||||
// names — the second declaration loses. Its holder declares them, as a mesh seat's does.
|
||||
func TestAModulesOwnSeatNamesPathsUnderTheSameRules(t *testing.T) {
|
||||
meshScoped := Manifest{Module: "a", Version: "1", DefinesSeats: []SeatDeclaration{
|
||||
{Name: "a-seat", Paths: []string{"/etc/a"}}}}
|
||||
if got := strings.Join(declaredSeatProblems(meshScoped), "; "); !strings.Contains(got, "only a node seat names paths") {
|
||||
t.Errorf("a module's mesh seat naming a path was accepted: %q", got)
|
||||
}
|
||||
placeholder := Manifest{Module: "a", Version: "1", DefinesSeats: []SeatDeclaration{
|
||||
{Name: "a-seat", Scope: ScopeNode, Paths: []string{"${dir:state}/a"}}}}
|
||||
if got := strings.Join(declaredSeatProblems(placeholder), "; "); !strings.Contains(got, "placeholder") {
|
||||
t.Errorf("a module's seat naming a path with a placeholder was accepted: %q", got)
|
||||
}
|
||||
|
||||
taken := Manifest{Module: "b", Version: "1", DefinesSeats: []SeatDeclaration{
|
||||
{Name: "b-seat", Scope: ScopeNode, Paths: []string{"/etc/hosts"}}}}
|
||||
if got := strings.Join(CatalogueProblems(Shelf{"b": taken}), "; "); !strings.Contains(got,
|
||||
`b declares the seat b-seat naming the path "/etc/hosts", which node-hostname already names`) {
|
||||
t.Errorf("a module's seat naming /etc/hosts was accepted: %q", got)
|
||||
}
|
||||
|
||||
own := Manifest{Module: "c", Version: "1",
|
||||
DefinesSeats: []SeatDeclaration{{Name: "c-seat", Scope: ScopeNode, Paths: []string{"/etc/c.conf"}}},
|
||||
Claims: []Claim{{Name: "c-seat", Scope: ScopeNode}}}
|
||||
if got := strings.Join(CatalogueProblems(Shelf{"c": own}), "; "); !strings.Contains(got,
|
||||
"c claims c-seat but does not declare /etc/c.conf") {
|
||||
t.Errorf("a holder of a module's seat without its path was accepted: %q", got)
|
||||
}
|
||||
own.Resources = []map[string]any{{"id": "conf", "type": "file", "path": "/etc/c.conf", "content": "x"}}
|
||||
if got := CatalogueProblems(Shelf{"c": own}); len(got) != 0 {
|
||||
t.Errorf("a holder declaring its own seat's path was refused: %v", got)
|
||||
}
|
||||
other := Manifest{Module: "d", Version: "1", Resources: []map[string]any{
|
||||
{"id": "conf", "type": "file", "path": "/etc/c.conf", "content": "x"}}}
|
||||
if got := strings.Join(CatalogueProblems(Shelf{"c": own, "d": other}), "; "); !strings.Contains(got,
|
||||
`d declares the path "/etc/c.conf", which the seat c-seat names`) {
|
||||
t.Errorf("a module not claiming c-seat declared its path: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
type writesHosts struct{}
|
||||
|
||||
func (writesHosts) Resources(string) ([]map[string]any, bool, error) {
|
||||
return []map[string]any{{"id": "hosts", "type": "file", "path": "/etc/hosts", "into": "block",
|
||||
"content": "10.42.0.1 anchor\n"}}, true, nil
|
||||
}
|
||||
|
||||
// The controller is bound too: what it generates for a machine on a seat's path is refused at composition.
|
||||
func TestAGeneratedResourceOnASeatsPathIsRefused(t *testing.T) {
|
||||
r := Resolution{Node: "workstation", Modules: []Manifest{
|
||||
{Module: "mesh-names", Computed: "mesh-names"},
|
||||
}}
|
||||
_, err := r.Declaration(Rendering{Generators: map[string]Generator{"mesh-names": writesHosts{}}})
|
||||
if err == nil {
|
||||
t.Fatal("a generated resource on /etc/hosts was composed")
|
||||
}
|
||||
for _, want := range []string{"mesh-names", "/etc/hosts", "node-hostname"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("the refusal does not name %s: %v", want, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Who owns a seat's path on a machine: the seat naming it and its holder there, with the stepping back
|
||||
// applied — the resolver's holder where node-resolver is held, else the uplink's.
|
||||
func TestPathOwnersApplyTheSteppingBack(t *testing.T) {
|
||||
held := []Held{
|
||||
{Claim: "node-uplink", Scope: ScopeNode, Node: "laptop", Module: "networkmanager"},
|
||||
{Claim: ResolverSeat, Scope: ScopeNode, Node: "laptop", Module: "systemd-resolved"},
|
||||
{Claim: "node-uplink", Scope: ScopeNode, Node: "server", Module: "systemd-networkd"},
|
||||
{Claim: "node-hostname", Scope: ScopeNode, Node: "server", Module: "hostname"},
|
||||
}
|
||||
var got []string
|
||||
for _, o := range PathOwners(Seats(), held) {
|
||||
got = append(got, o.Path+" "+o.Node+" "+o.Seat+" "+o.Module)
|
||||
}
|
||||
want := []string{
|
||||
"/etc/hostname server node-hostname hostname",
|
||||
"/etc/hosts server node-hostname hostname",
|
||||
"/etc/resolv.conf laptop node-resolver systemd-resolved",
|
||||
"/etc/resolv.conf server node-uplink systemd-networkd",
|
||||
}
|
||||
if strings.Join(got, "\n") != strings.Join(want, "\n") {
|
||||
t.Errorf("the owners are\n%s\nnot\n%s", strings.Join(got, "\n"), strings.Join(want, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
// The stepping back comes from the seat: an uplink holder declaring the resolver file as a resource rather
|
||||
// than a rendered file steps back too, and keeps everything else.
|
||||
func TestTheUplinkStepsBackByTheSeatsDefinition(t *testing.T) {
|
||||
uplink := Manifest{Module: "uplink", Version: "1", Claims: []Claim{{Name: "node-uplink"}},
|
||||
Resources: []map[string]any{
|
||||
{"id": "resolvers", "type": "file", "path": ResolverFile, "content": "nameserver 10.42.0.1\n"},
|
||||
{"id": "conf", "type": "file", "path": "/etc/uplink.conf", "content": "x"}}}
|
||||
got := stepsBack(uplink, []Manifest{uplink, localResolver()})
|
||||
if len(got.Resources) != 1 || got.Resources[0]["id"] != "conf" {
|
||||
t.Errorf("the uplink's holder composes %v beside the resolver", got.Resources)
|
||||
}
|
||||
if p := checkResources([]Manifest{uplink, localResolver()}); len(p) != 0 {
|
||||
t.Errorf("the uplink's holder and the resolver's were refused together: %v", p)
|
||||
}
|
||||
}
|
||||
|
||||
// The catalogue as it is passes: no holder of a seat naming a path is missing one, and nobody else
|
||||
// declares one.
|
||||
func TestTheCataloguePassesTheSeatPathRules(t *testing.T) {
|
||||
shelf := Shelf{}
|
||||
for _, m := range theCatalogue(t) {
|
||||
shelf[m.Module] = m
|
||||
}
|
||||
for _, p := range CatalogueProblems(shelf) {
|
||||
if strings.Contains(p, "ADR 0287") {
|
||||
t.Errorf("the catalogue breaks ADR 0287: %s", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,11 +53,6 @@ type Seat struct {
|
||||
// ${contribution:<seat>:<kind>}. Compiled, never stored: like the protocol, it is the mesh's
|
||||
// definition of the role, and the store's rows carry no column for it.
|
||||
Receives []Receivable
|
||||
// Paths are the paths on its machine the seat's holder owns (novox/hq ADR 0287): a holder declares
|
||||
// every one, and a module claiming no seat that names a path may not declare it. Only a node seat
|
||||
// names paths. Compiled, never stored, like Receives: adding or taking away a path is a decision,
|
||||
// recorded, and the store's rows carry no column for it.
|
||||
Paths []SeatPath
|
||||
// Decision is the record that made it a seat.
|
||||
Decision string
|
||||
}
|
||||
@@ -183,8 +178,6 @@ var defaultSeats = append([]Seat{
|
||||
// holds none of it. Named node-hosts-file until ADR 0223; the former name resolves to it as an
|
||||
// alias on a mesh that knew it.
|
||||
{Name: "node-hostname", Scope: ScopeNode, Decision: "novox/hq ADR 0199, ADR 0223",
|
||||
// The machine's names are the role's files, whatever program fills it (novox/hq ADR 0287).
|
||||
Paths: []SeatPath{{Path: "/etc/hosts"}, {Path: "/etc/hostname"}},
|
||||
Serves: []Verb{
|
||||
{Name: "entries", Description: "Every line of this machine's /etc/hosts, each marked whose it is: " +
|
||||
"the operator's, or the block of the module or tool that writes it.",
|
||||
@@ -306,25 +299,17 @@ var defaultSeats = append([]Seat{
|
||||
// resolver file as it is and who wrote it, and every link with its addresses, routes and the resolvers
|
||||
// the manager knows for it — the same for every holder, so a question about a machine's names is asked
|
||||
// the same way whatever manages its network. Optional until both holders serve them.
|
||||
//
|
||||
// It owns the resolver file, stepping back for node-resolver where that is held (novox/hq ADR 0287,
|
||||
// ADR 0247).
|
||||
{Name: "node-uplink", Scope: ScopeNode, Decision: "novox/hq ADR 0117", Serves: uplinkVerbs(),
|
||||
Paths: []SeatPath{{Path: ResolverFile, StepsBackFor: ResolverSeat}}},
|
||||
{Name: "node-uplink", Scope: ScopeNode, Decision: "novox/hq ADR 0117", Serves: uplinkVerbs()},
|
||||
// The machine's own resolver (novox/hq ADR 0247): held only where something requires `split-dns` — a
|
||||
// VPN client whose domains must go to its own servers while every other name goes to the mesh's.
|
||||
// Where it is held, its holder writes the resolver file and the uplink's holder steps back from it
|
||||
// (node_resolver.go). It knows nothing of any VPN: its verbs route domains to servers over a link.
|
||||
{Name: ResolverSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0247", Serves: resolverVerbs(),
|
||||
Paths: []SeatPath{{Path: ResolverFile}}},
|
||||
{Name: ResolverSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0247", Serves: resolverVerbs()},
|
||||
// A machine's shares and the shares it mounts (novox/hq ADR 0263): the holder of node-nfs-server
|
||||
// exports a machine's folders to the private network and provides each as `nfs-share`; the holder of
|
||||
// node-mounts writes a mount and an automount unit per share on a machine that asks (shares.go).
|
||||
{Name: NFSServerSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0263", Serves: nfsServerVerbs()},
|
||||
{Name: MountsSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0263", Serves: mountsVerbs()},
|
||||
// A node's files and accounts, read (novox/hq ADR 0310, reads.go): by an account of the holder's own that holds
|
||||
// no secret, on every node, so the reads ADR 0268 §5 takes over from the login shell's execute have a tool.
|
||||
{Name: ReadsSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0310", Serves: readsVerbs()},
|
||||
},
|
||||
// The graphical session's roles (novox/hq ADR 0208), last because they are a workstation's.
|
||||
graphicalSessionSeats()...)
|
||||
@@ -376,11 +361,10 @@ func UseSeats(s []Seat) {
|
||||
row.Accepts, row.Emits, row.Serves = d.Accepts, d.Emits, d.Serves
|
||||
}
|
||||
}
|
||||
// What a seat receives, whether it is replicated and the paths it names are never stored
|
||||
// (novox/hq ADR 0212, ADR 0223, ADR 0287), so they are always the compiled ones.
|
||||
// What a seat receives and whether it is replicated are never stored (novox/hq ADR 0212, ADR
|
||||
// 0223), so they are always the compiled ones.
|
||||
if d, known := byName[row.Name]; known {
|
||||
row.Receives = d.Receives
|
||||
row.Paths = d.Paths
|
||||
row.Replicated = d.Replicated
|
||||
row.Serves = optionalAsCompiled(row.Serves, d.Serves)
|
||||
}
|
||||
@@ -554,14 +538,6 @@ func CanHold(m Manifest, seat Seat) error {
|
||||
return fmt.Errorf("%s claims %s, whose holder answers for %q, and %s does not provide %q at %s scope",
|
||||
m.Module, seat.Name, seat.Delivers, m.Module, seat.Delivers, seat.Scope)
|
||||
}
|
||||
// **A holder owns what the role owns** (novox/hq ADR 0287): every path the seat names, declared as
|
||||
// a resource or as a file the mesh renders for it, so which module owns /etc/hosts on a machine is
|
||||
// answered by the seat's definition and not by reading every manifest.
|
||||
if missing := missingPaths(m, seat.Paths); len(missing) > 0 {
|
||||
return fmt.Errorf("%s claims %s but does not declare %s, which that seat's holder owns (novox/hq "+
|
||||
"ADR 0287) — a holder declares every path its seat names, as a resource or as a file the mesh "+
|
||||
"renders for it", m.Module, seat.Name, strings.Join(missing, ", "))
|
||||
}
|
||||
// **Serving the seat's tools is a condition of holding it** (novox/hq ADR 0132). A holder that
|
||||
// does not answer what the role promises is every caller's timeout, found at registration and
|
||||
// at handover instead, naming the verbs rather than the fact that something is missing.
|
||||
|
||||
@@ -67,10 +67,6 @@ type SeatDeclaration struct {
|
||||
// Records are state buckets of the declaring module that each user reads under its own name — the
|
||||
// keys `<user>.…` and no other (ADR 0259 §3).
|
||||
Records []string `json:"records,omitempty"`
|
||||
// Paths are the paths on its machine the seat's holder owns (novox/hq ADR 0287), under the rules the
|
||||
// mesh's own seats name theirs by: a node seat only, each absolute and literal, and none another seat
|
||||
// names. A module's seat never steps back.
|
||||
Paths []string `json:"paths,omitempty"`
|
||||
}
|
||||
|
||||
// KindedBenches are the seats that may be kinded (novox/hq ADR 0234 §2): `channel` sends to the operator,
|
||||
@@ -167,7 +163,6 @@ func declaredSeatProblems(m Manifest) []string {
|
||||
}
|
||||
}
|
||||
problems = append(problems, trafficProblems(m, s)...)
|
||||
problems = append(problems, declaredSeatPathProblems(m, s)...)
|
||||
}
|
||||
|
||||
for _, u := range m.Uses {
|
||||
@@ -276,10 +271,6 @@ func CatalogueProblems(shelf Shelf) []string {
|
||||
return ok
|
||||
}
|
||||
|
||||
// Which seats name each path (novox/hq ADR 0287): a module's seat naming one another seat names loses.
|
||||
named, pathProblems := pathSeats(shelfOrder(shelf), shelf, declaredBy)
|
||||
problems = append(problems, pathProblems...)
|
||||
|
||||
// Who claims each kind of a kinded bench, so a second claim of one kind is refused (ADR 0234 §2).
|
||||
kindsTaken := map[string]string{}
|
||||
|
||||
@@ -336,15 +327,7 @@ func CatalogueProblems(shelf Shelf) []string {
|
||||
"%s claims %s but does not serve %s, which that seat's protocol promises",
|
||||
module, c.Name, strings.Join(missing, ", ")))
|
||||
}
|
||||
// And owns what the seat's holder owns, as a holder of the mesh's own seats does (ADR 0287).
|
||||
if missing := missingPaths(m, asSeatPaths(s.Paths)); len(missing) > 0 {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s claims %s but does not declare %s, which that seat's holder owns (novox/hq ADR 0287)",
|
||||
module, c.Name, strings.Join(missing, ", ")))
|
||||
}
|
||||
}
|
||||
// Only a claimant declares a seat's path (novox/hq ADR 0287).
|
||||
problems = append(problems, unclaimedSeatPaths(m, named)...)
|
||||
}
|
||||
// **A trusted holder runs as its own account** (novox/hq ADR 0259 §8): a module saying warrants, or
|
||||
// speaking for a kind that proves its sender, is never carried by a machine's runtime.
|
||||
|
||||
@@ -48,7 +48,7 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
||||
delivered[s.Delivers] = s.Name
|
||||
}
|
||||
}
|
||||
// Forty-one with node-reads (novox/hq ADR 0310); forty with node-nfs-server and node-mounts (novox/hq ADR 0263); thirty-eight with node-resolver (novox/hq ADR 0247); thirty-seven with mesh-delivery (novox/hq ADR 0239); thirty-six since node-resolver-config retired
|
||||
// Forty with node-nfs-server and node-mounts (novox/hq ADR 0263); thirty-eight with node-resolver (novox/hq ADR 0247); thirty-seven with mesh-delivery (novox/hq ADR 0239); thirty-six since node-resolver-config retired
|
||||
// into node-uplink (novox/hq ADR 0223); thirty-seven
|
||||
// since the retired node-dns-resolver went (novox/hq ADR 0220); thirty-eight with
|
||||
// node-backup (novox/hq ADR 0214); thirty-seven with node-message-bus (novox/hq ADR 0215);
|
||||
@@ -59,8 +59,8 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
||||
// node-container-runtime (ADR 0207); nineteen with node-environment and node-login-shell (ADR 0203,
|
||||
// ADR 0204); seventeen with node-build-agent (ADR 0190). One fewer once the retired
|
||||
// mesh-build-machine row goes, when no registered manifest claims it.
|
||||
if len(Seats()) != 41 {
|
||||
t.Errorf("the mesh defines %d seats rather than 41; the set is closed, so a change here is "+
|
||||
if len(Seats()) != 40 {
|
||||
t.Errorf("the mesh defines %d seats rather than 40; the set is closed, so a change here is "+
|
||||
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A declared directory may be owned by the account its module runs as (novox/hq issue 279): a media
|
||||
// manager runs as the puid and pgid its assignment sets per node, and a folder it must write was owned
|
||||
// by the manifest's literal, so the app refused it on a machine whose identity was not the image's.
|
||||
|
||||
func recycler() Manifest {
|
||||
return Manifest{Module: "sonarr",
|
||||
Resources: []map[string]any{
|
||||
{"id": "recycle-bin", "type": "directory", "path": "/srv/sonarr/recycle", "mode": "0755",
|
||||
"owner": "${setting:puid}:${setting:pgid}"},
|
||||
{"id": "identity", "type": "file", "path": "/srv/sonarr/identity.env", "mode": "0644",
|
||||
"content": "PUID=${setting:puid}\nPGID=${setting:pgid}\n"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func copied(r map[string]any) map[string]any {
|
||||
out := map[string]any{}
|
||||
for k, v := range r {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// The node's identity reaches the directory's owner, the node's layer over the mesh's, a number given
|
||||
// as a string or as a JSON number alike.
|
||||
func TestASettingOwnsADirectory(t *testing.T) {
|
||||
m := recycler()
|
||||
layers := []Layer{
|
||||
{From: "the mesh", Values: map[string]any{"puid": "1000", "pgid": "1000"}},
|
||||
{From: "this node", Values: map[string]any{"puid": float64(1001), "pgid": "2000"}},
|
||||
}
|
||||
r := copied(m.Resources[0])
|
||||
if err := settingInto(r, layers, m.Module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r["owner"] != "1001:2000" {
|
||||
t.Fatalf("owned by %q, not 1001:2000", r["owner"])
|
||||
}
|
||||
user := copied(m.Resources[0])
|
||||
user["owner"] = "${setting:puid}"
|
||||
if err := settingInto(user, layers, m.Module); err != nil || user["owner"] != "1001" {
|
||||
t.Fatalf("user alone: %v, %v", user["owner"], err)
|
||||
}
|
||||
}
|
||||
|
||||
// Composed for a machine, the way a push writes it: the directory reaches the node-engine owned by the
|
||||
// number the node's settings name, never by the placeholder.
|
||||
func TestAComposedMachineGetsTheOwnerTheSettingNames(t *testing.T) {
|
||||
r := anAdoptedAnchor()
|
||||
r.Modules = append(r.Modules, recycler())
|
||||
with := anchorRendering(false)
|
||||
with.Settings["sonarr"] = []Layer{{From: "anchor", Values: map[string]any{"puid": "1001", "pgid": "2000"}}}
|
||||
composed, err := r.Compose(with)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if why, left := composed.LeftOut["sonarr"]; left {
|
||||
t.Fatalf("left out: %s", why)
|
||||
}
|
||||
got := byID(composed.Resources)["sonarr.recycle-bin"]
|
||||
if got["owner"] != "1001:2000" {
|
||||
t.Fatalf("composed owned by %v, not 1001:2000", got["owner"])
|
||||
}
|
||||
}
|
||||
|
||||
// Only a whole number owns a directory: an account's name, text, a sign, a fraction or a value past the
|
||||
// kernel's ids is refused by name, and the directory is left as it was. So is a key nothing sets.
|
||||
func TestAnOwnerSettingThatIsNoNumberIsRefused(t *testing.T) {
|
||||
m := recycler()
|
||||
for _, bad := range []any{"abc", "root", "", "-1", "+5", "1000:1000", "10 00", "1000\nx", float64(1.5),
|
||||
float64(-3), "4294967295", "99999999999", true} {
|
||||
r := copied(m.Resources[0])
|
||||
err := settingInto(r, []Layer{{From: "ace", Values: map[string]any{"puid": bad, "pgid": "1000"}}}, m.Module)
|
||||
if err == nil || !strings.Contains(err.Error(), `"puid"`) || !strings.Contains(err.Error(), "whole number") {
|
||||
t.Errorf("%v: %v", bad, err)
|
||||
}
|
||||
if r["owner"] != m.Resources[0]["owner"] {
|
||||
t.Errorf("%v: the owner was changed on refusal: %v", bad, r["owner"])
|
||||
}
|
||||
}
|
||||
r := copied(m.Resources[0])
|
||||
if err := settingInto(r, nil, m.Module); err == nil || !strings.Contains(err.Error(), "${setting:puid}") {
|
||||
t.Errorf("nothing set: %v", err)
|
||||
}
|
||||
if err := JudgeSettings(m, []Layer{{From: "ace", Values: map[string]any{"puid": "media", "pgid": "1000"}}}, false); err == nil ||
|
||||
!strings.Contains(err.Error(), "whole number") {
|
||||
t.Errorf("a stored name is not refused when judged: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Where the manifest is read: an owner holding a setting is a whole placeholder or a whole number on
|
||||
// each side of the colon, and every key is one the module asks for elsewhere or declares a default for.
|
||||
func TestAnOwnerSettingIsJudgedWhereTheManifestIsRead(t *testing.T) {
|
||||
ok := []string{"${setting:puid}:${setting:pgid}", "${setting:puid}", "${setting:puid}:1000", "1000:${setting:pgid}",
|
||||
"1000:1000", "media:media"}
|
||||
bad := []string{
|
||||
"${setting:puid}x",
|
||||
"x${setting:puid}",
|
||||
"${setting:puid}:${setting:pgid}:1",
|
||||
"${setting:puid}${setting:pgid}",
|
||||
"media:${setting:pgid}",
|
||||
"${setting:puid}:",
|
||||
"${setting:other}:${setting:pgid}",
|
||||
}
|
||||
for _, owner := range append(ok, bad...) {
|
||||
m := recycler()
|
||||
m.Resources[0] = copied(m.Resources[0])
|
||||
m.Resources[0]["owner"] = owner
|
||||
err := parsed(t, m)
|
||||
refused := err != nil && strings.Contains(err.Error(), "owner")
|
||||
want := false
|
||||
for _, b := range bad {
|
||||
want = want || b == owner
|
||||
}
|
||||
if refused != want {
|
||||
t.Errorf("%s: refused %v, want %v (%v)", owner, refused, want, err)
|
||||
}
|
||||
}
|
||||
// A key with a default is declared, though nothing else asks for it.
|
||||
m := recycler()
|
||||
m.Resources = m.Resources[:1]
|
||||
m.Resources[0] = copied(m.Resources[0])
|
||||
m.Resources[0]["owner"] = "${setting:workers}"
|
||||
m.Settings = map[string]SettingDeclaration{"workers": {Kind: KindPreference, Default: float64(4), Why: "a test"}}
|
||||
if err := parsed(t, m); err != nil && strings.Contains(err.Error(), "owner") {
|
||||
t.Errorf("a declared key was refused: %v", err)
|
||||
}
|
||||
// A file's owner takes no setting: the sweep says so.
|
||||
f := recycler()
|
||||
f.Resources[1] = copied(f.Resources[1])
|
||||
f.Resources[1]["owner"] = "${setting:puid}"
|
||||
if err := parsed(t, f); err == nil || !strings.Contains(err.Error(), "not filled in this field") {
|
||||
t.Errorf("a file's owner setting passed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A key a directory's owner asks for is a destination, so setting it is not called stray.
|
||||
func TestASettingAnOwnerAsksForIsNotStray(t *testing.T) {
|
||||
m := recycler()
|
||||
m.Resources = m.Resources[:1]
|
||||
stray := strings.Join(UnusedSettings(m, []Layer{{From: "ace", Values: map[string]any{"puid": "1001", "pgid": "2000"}}}), "; ")
|
||||
if strings.Contains(stray, "puid") || strings.Contains(stray, "pgid") {
|
||||
t.Errorf("called stray: %s", stray)
|
||||
}
|
||||
}
|
||||
|
||||
// An owner is written as the number parsed: "0001" and "00" reach the node-engine as 1 and 0, never as text.
|
||||
// Uid 0 is allowed, because a key an owner asks for is the terminal's alone.
|
||||
func TestAnOwnerSettingIsWrittenAsTheNumber(t *testing.T) {
|
||||
m := recycler()
|
||||
r := copied(m.Resources[0])
|
||||
if err := settingInto(r, []Layer{{From: "ace", Values: map[string]any{"puid": "0001", "pgid": "00"}}}, m.Module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r["owner"] != "1:0" {
|
||||
t.Fatalf("owned by %q, not 1:0", r["owner"])
|
||||
}
|
||||
}
|
||||
|
||||
// Every key a directory's owner asks for is the controller's terminal's alone (novox/hq issue 279, issue 339):
|
||||
// root chowns the folder to it, so it is terminal-only whatever else asks for it — a file nothing trusts, a
|
||||
// default, or nothing at all.
|
||||
func TestAnOwnersKeyIsTheTerminals(t *testing.T) {
|
||||
m := recycler()
|
||||
m.Resources[1] = copied(m.Resources[1])
|
||||
m.Resources[1]["trusted"] = false
|
||||
m.Settings = map[string]SettingDeclaration{"pgid": {Kind: KindPreference, Default: "1000", Why: "a test"}}
|
||||
keys := strings.Join(TerminalKeys(m), ",")
|
||||
for _, k := range []string{"puid", "pgid"} {
|
||||
if !strings.Contains(","+keys+",", ","+k+",") {
|
||||
t.Errorf("%s is not the terminal's: %s", k, keys)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -16,8 +15,7 @@ import (
|
||||
// (novox/hq issues 122, 134). ADR 0112 names the operator as one of the four providers; this is the
|
||||
// operator answering.
|
||||
//
|
||||
// `${setting:<key>}` in a file's content, in a service's unit name and in a directory's owner (a uid or
|
||||
// gid only, issue 279) is filled from the module's settings layers — the mesh's,
|
||||
// `${setting:<key>}` in a file's content, and in a service's unit name, is filled from the module's settings layers — the mesh's,
|
||||
// then this node's — the same layers a mergeable JSON file and a contribution already take, so
|
||||
// `settings set <module>` is the one place a person's values go. **Refused when no layer sets it**,
|
||||
// naming the key and the remedy: a definition that carried a default for a mail domain would be
|
||||
@@ -61,9 +59,6 @@ func settingInto(resource map[string]any, layers []Layer, module string) error {
|
||||
if fmt.Sprint(resource["type"]) == "service" {
|
||||
return settingIntoUnit(resource, layers, module)
|
||||
}
|
||||
if fmt.Sprint(resource["type"]) == "directory" {
|
||||
return settingIntoOwner(resource, layers, module)
|
||||
}
|
||||
if fmt.Sprint(resource["type"]) != "file" {
|
||||
return nil
|
||||
}
|
||||
@@ -148,107 +143,6 @@ func settingIntoUnit(resource map[string]any, layers []Layer, module string) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// A declared directory may be owned by the account its module runs as (novox/hq issue 279).
|
||||
//
|
||||
// A containerised module runs as the identity its assignment sets per node — its puid and pgid
|
||||
// settings, written into the environment file its image reads (ADR 0112, issue 153) — and a directory
|
||||
// its app must write, beyond the one its image re-owns at start, was owned by whatever the manifest
|
||||
// spelt: right on a machine whose identity matched the image's default, refused by the app on one whose
|
||||
// did not. So a directory's `owner` may name the settings that identity is: `${setting:<key>}` for the
|
||||
// user alone, `${setting:<user>}:${setting:<group>}` for both, filled from the same layers at
|
||||
// composition.
|
||||
//
|
||||
// **Whole numbers only.** An owner a setting fills is a uid or a gid — never an account's name, never
|
||||
// text — so no value can put anything into the ownership the node-engine applies but a number; one that
|
||||
// is not is refused at composition, naming the key, and the directory is left as it was. The shape is
|
||||
// judged where the manifest is read (OwnerSettingProblems): each side of the colon a whole placeholder
|
||||
// or a whole number, and every key one the module itself asks for elsewhere — in a file, a unit, a
|
||||
// contribution or a served fact — or declares a default for, so the owner follows the identity its
|
||||
// process is told it runs as, never a key that reaches nothing else.
|
||||
|
||||
// ownerSide is one side of an owner a setting may fill: a whole placeholder or a whole number.
|
||||
var ownerSide = `(?:\$\{setting:[a-z0-9][a-z0-9_.-]*\}|[0-9]+)`
|
||||
|
||||
// ownerWithSetting is the only shape an owner holding a setting may have: user, or user:group.
|
||||
var ownerWithSetting = regexp.MustCompile(`^` + ownerSide + `(?::` + ownerSide + `)?$`)
|
||||
|
||||
// wholeID is a value as a uid or a gid, and whether it is one: a whole number below the kernel's "no id"
|
||||
// (2^32-1). Uid 0 is one: a key an owner asks for is the controller's terminal's alone (TerminalKeys).
|
||||
func wholeID(v string) (uint64, bool) {
|
||||
if v == "" || strings.TrimLeft(v, "0123456789") != "" {
|
||||
return 0, false
|
||||
}
|
||||
n, err := strconv.ParseUint(v, 10, 32)
|
||||
return n, err == nil && n < 1<<32-1
|
||||
}
|
||||
|
||||
// OwnerSettingProblems refuses, where a manifest is read, a directory whose owner carries a setting
|
||||
// in any shape but a whole placeholder or number on each side of the colon, or names a key the module
|
||||
// asks for nowhere else and declares no default for.
|
||||
func OwnerSettingProblems(m Manifest) []string {
|
||||
var problems []string
|
||||
asked := settingKeysAskedOutsideOwners(m)
|
||||
for k := range m.Settings {
|
||||
asked[k] = true
|
||||
}
|
||||
for _, r := range m.Resources {
|
||||
owner, _ := r["owner"].(string)
|
||||
if !strings.Contains(owner, "${setting:") {
|
||||
continue
|
||||
}
|
||||
if fmt.Sprint(r["type"]) != "directory" {
|
||||
// Said by the placeholder sweep: only a directory's owner takes a setting.
|
||||
continue
|
||||
}
|
||||
if !ownerWithSetting.MatchString(owner) {
|
||||
problems = append(problems, fmt.Sprintf("%s: the directory %v's owner %q carries a setting in a shape "+
|
||||
"the mesh does not fill; an owner names the account by number only, each side of the colon a whole "+
|
||||
"${setting:key} or a whole number (${setting:puid}:${setting:pgid}) (novox/hq issue 279)",
|
||||
m.Module, r["id"], owner))
|
||||
continue
|
||||
}
|
||||
for _, key := range settingsUsed(owner) {
|
||||
if !asked[key] {
|
||||
problems = append(problems, fmt.Sprintf("%s: the directory %v's owner says ${setting:%s}, and the "+
|
||||
"module asks for %q nowhere else and declares no default for it: an owner follows the identity "+
|
||||
"the module's process is told it runs as (its identity file's ${setting:%s}), never a key of its "+
|
||||
"own (novox/hq issue 279)", m.Module, r["id"], key, key, key))
|
||||
}
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// settingIntoOwner fills ${setting:…} in a directory's owner, refusing — with the key and why, the
|
||||
// resource left as it was — a key nothing sets and a value that is not a whole number.
|
||||
func settingIntoOwner(resource map[string]any, layers []Layer, module string) error {
|
||||
owner, ok := resource["owner"].(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for _, key := range settingsUsed(owner) {
|
||||
value, set := settingValue(layers, key)
|
||||
if !set {
|
||||
return &UnsetSettingError{Module: module, Setting: key, said: fmt.Sprintf(
|
||||
"%s has a directory %v whose owner says ${setting:%s}, and nothing sets %q for it — an operator's "+
|
||||
"value is the assignment's, never the definition's (novox/hq ADR 0112): "+
|
||||
"`settings set %s <file>` with {%q: …}%s",
|
||||
module, resource["id"], key, key, module, key, orNoSettings(layers))}
|
||||
}
|
||||
v := plainly(value)
|
||||
id, whole := wholeID(v)
|
||||
if !whole {
|
||||
return fmt.Errorf("%s: the setting %q is %q, which cannot own the directory %v: an owner a setting "+
|
||||
"names is a uid or a gid, a whole number, never an account's name or other text (novox/hq issue 279)",
|
||||
module, key, v, resource["id"])
|
||||
}
|
||||
// Written as the number parsed, so "0001" reaches the node-engine as 1, never as text.
|
||||
owner = strings.ReplaceAll(owner, "${setting:"+key+"}", strconv.FormatUint(id, 10))
|
||||
}
|
||||
resource["owner"] = owner
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingValue(layers []Layer, key string) (any, bool) {
|
||||
var value any
|
||||
set := false
|
||||
@@ -277,25 +171,9 @@ func orNoSettings(layers []Layer) string {
|
||||
return "; set today: " + strings.Join(keys, ", ")
|
||||
}
|
||||
|
||||
// settingKeysUsedBy is every key a module's files, units, directory owners, contributions and served
|
||||
// facts ask for, so a setting that lands in one is not called stray.
|
||||
// settingKeysUsedBy is every key a module's files, contributions and served facts ask for, so a
|
||||
// setting that lands in one is not called stray.
|
||||
func settingKeysUsedBy(m Manifest) map[string]bool {
|
||||
used := settingKeysAskedOutsideOwners(m)
|
||||
for _, r := range m.Resources {
|
||||
if fmt.Sprint(r["type"]) == "directory" {
|
||||
if owner, ok := r["owner"].(string); ok {
|
||||
for _, k := range settingsUsed(owner) {
|
||||
used[k] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return used
|
||||
}
|
||||
|
||||
// settingKeysAskedOutsideOwners is every key a module's files, units, contributions and served facts
|
||||
// ask for: what a directory's owner may follow (OwnerSettingProblems).
|
||||
func settingKeysAskedOutsideOwners(m Manifest) map[string]bool {
|
||||
used := map[string]bool{}
|
||||
note := func(s string) {
|
||||
for _, k := range settingsUsed(s) {
|
||||
|
||||
@@ -33,9 +33,6 @@ import (
|
||||
// 4. **A module's whole layer, when it has a mergeable file not marked `"trusted": false`** (novox/hq issue 340,
|
||||
// TrustedMergeable). A mergeable file takes any key a layer sets, not only those its content names, so no list
|
||||
// of keys covers it: an empty runtime configuration a provider reads would take a `url` of the caller's.
|
||||
// 5. **Every setting a directory's owner asks for** (novox/hq issue 279): root chowns the directory to the number
|
||||
// at the next push, so through a verb any caller could hand itself — or uid 0 — a module's folder. Whatever
|
||||
// else asks for the key, a file marked `"trusted": false` included, the owner makes it the terminal's.
|
||||
//
|
||||
// Derived from the manifest, never listed by hand, so a provider or a trusted file added tomorrow is covered.
|
||||
|
||||
@@ -45,8 +42,8 @@ import (
|
||||
const TrustedField = "trusted"
|
||||
|
||||
// TerminalKeys are the settings keys of a module that are set at the controller's terminal alone: places and
|
||||
// accesses, every key its provisions serve and every setting a served value asks for, every setting a file
|
||||
// asks for unless it says `"trusted": false`, and every setting a directory's owner asks for. Places and accesses first, then the rest sorted.
|
||||
// accesses, every key its provisions serve and every setting a served value asks for, and every setting a file
|
||||
// asks for unless it says `"trusted": false`. Places and accesses first, then the rest sorted.
|
||||
func TerminalKeys(m Manifest) []string {
|
||||
keys := map[string]bool{}
|
||||
for _, served := range m.Serves {
|
||||
@@ -60,13 +57,6 @@ func TerminalKeys(m Manifest) []string {
|
||||
}
|
||||
}
|
||||
for _, r := range m.Resources {
|
||||
if fmt.Sprint(r["type"]) == "directory" {
|
||||
owner, _ := r["owner"].(string)
|
||||
for _, asked := range settingsUsed(owner) {
|
||||
keys[asked] = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if fmt.Sprint(r["type"]) != "file" {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ type filler struct {
|
||||
// seatInto, machineInto and contributionsInto. A pass that comes to read another field adds it here,
|
||||
// or the sweep refuses the placeholder it would have filled.
|
||||
var fillers = []filler{
|
||||
{namespace: "setting", patterns: []*regexp.Regexp{settingRef}, fields: map[string][]string{"file": {"content"}, "service": {"unit"}, "directory": {"owner"}}},
|
||||
{namespace: "setting", patterns: []*regexp.Regexp{settingRef}, fields: map[string][]string{"file": {"content"}, "service": {"unit"}}},
|
||||
{namespace: "dir", patterns: []*regexp.Regexp{dirRef}, fields: map[string][]string{"*": {"path", "content", "volumes", "env", "env-file"}}},
|
||||
{namespace: "access", patterns: []*regexp.Regexp{accessRef}, fields: map[string][]string{"*": {"path", "content", "volumes", "env", "env-file"}}},
|
||||
{namespace: "secret", patterns: []*regexp.Regexp{placeholder}, fields: map[string][]string{"file": {"content"}},
|
||||
|
||||
@@ -33,9 +33,7 @@ func TestTheUplinkSeatPromisesItsVerbsAndRequiresNoneYet(t *testing.T) {
|
||||
if !slices.Equal(names, []string{"resolvers", "links"}) {
|
||||
t.Fatalf("node-uplink promises %v", names)
|
||||
}
|
||||
// The resolver file is the seat's path (novox/hq ADR 0287), which every holder of today renders.
|
||||
today := Manifest{Module: "networkmanager", Version: "1", Claims: []Claim{{Name: "node-uplink", Scope: ScopeNode}},
|
||||
Facts: map[string]RosterFile{"resolvers": {Path: ResolverFile, Template: "x"}}}
|
||||
today := Manifest{Module: "networkmanager", Version: "1", Claims: []Claim{{Name: "node-uplink", Scope: ScopeNode}}}
|
||||
if err := CanHold(today, seat); err != nil {
|
||||
t.Fatalf("a holder that serves no verb yet is refused: %v", err)
|
||||
}
|
||||
@@ -57,8 +55,7 @@ func TestTheUplinkVerbsReadBackFromTheRowStayOptional(t *testing.T) {
|
||||
// The row as seeding stores it: both verbs, the mark not a column.
|
||||
UseSeats([]Seat{{Name: "node-uplink", Scope: ScopeNode, Serves: []Verb{{Name: "resolvers"}, {Name: "links"}}}})
|
||||
seat := uplinkSeat(t)
|
||||
holder := Manifest{Module: "systemd-networkd", Version: "1", Claims: []Claim{{Name: "node-uplink", Scope: ScopeNode}},
|
||||
Facts: map[string]RosterFile{"resolvers": {Path: ResolverFile, Template: "x"}}}
|
||||
holder := Manifest{Module: "systemd-networkd", Version: "1", Claims: []Claim{{Name: "node-uplink", Scope: ScopeNode}}}
|
||||
if err := CanHold(holder, seat); err != nil {
|
||||
t.Fatalf("read back from the row, the verbs are required: %v", err)
|
||||
}
|
||||
|
||||
Vendored
-5
@@ -22,8 +22,3 @@
|
||||
./cmd/mesh-controller TestAWalkEndedByCancelOrClearIsSaidEnded
|
||||
./cmd/mesh-controller TestAWalkRestartedByARebuildIsSaidStarted
|
||||
./cmd/mesh-controller TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain
|
||||
./cmd/mesh-controller TestReplay237
|
||||
./cmd/mesh-controller TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer
|
||||
./cmd/mesh-controller TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt
|
||||
./cmd/mesh-controller TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal
|
||||
./cmd/mesh-controller TestADirectoryOwnersKeyIsRefusedThroughAVerb
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
# Package xz authors
|
||||
|
||||
Michael Cross <https://github.com/xi2>
|
||||
|
||||
# XZ Embedded authors
|
||||
|
||||
Lasse Collin <lasse.collin@tukaani.org>
|
||||
Igor Pavlov <http://7-zip.org/>
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
Creative Commons CC0 1.0 Universal
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL
|
||||
SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT
|
||||
RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS.
|
||||
CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE
|
||||
INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES
|
||||
RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
HEREUNDER.
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator and
|
||||
subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for the
|
||||
purpose of contributing to a commons of creative, cultural and scientific
|
||||
works ("Commons") that the public can reliably and without fear of later
|
||||
claims of infringement build upon, modify, incorporate in other works, reuse
|
||||
and redistribute as freely as possible in any form whatsoever and for any
|
||||
purposes, including without limitation commercial purposes. These owners may
|
||||
contribute to the Commons to promote the ideal of a free culture and the
|
||||
further production of creative, cultural and scientific works, or to gain
|
||||
reputation or greater distribution for their Work in part through the use and
|
||||
efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any expectation
|
||||
of additional consideration or compensation, the person associating CC0 with a
|
||||
Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
|
||||
and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
|
||||
and publicly distribute the Work under its terms, with knowledge of his or her
|
||||
Copyright and Related Rights in the Work and the meaning and intended legal
|
||||
effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display, communicate,
|
||||
and translate a Work;
|
||||
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
|
||||
iv. rights protecting against unfair competition in regards to a Work, subject
|
||||
to the limitations in paragraph 4(a), below;
|
||||
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data in a
|
||||
Work;
|
||||
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation thereof,
|
||||
including any amended or successor version of such directive); and
|
||||
|
||||
vii. other similar, equivalent or corresponding rights throughout the world
|
||||
based on applicable law or treaty, and any national implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
|
||||
b. Affirmer offers the Work as-is and makes no representations or warranties
|
||||
of any kind concerning the Work, express, implied, statutory or otherwise,
|
||||
including without limitation warranties of title, merchantability, fitness for
|
||||
a particular purpose, non infringement, or the absence of latent or other
|
||||
defects, accuracy, or the present or absence of errors, whether or not
|
||||
discoverable, all to the greatest extent permissible under applicable law.
|
||||
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons that
|
||||
may apply to the Work or any use thereof, including without limitation any
|
||||
person's Copyright and Related Rights in the Work. Further, Affirmer
|
||||
disclaims responsibility for obtaining any necessary consents, permissions or
|
||||
other rights required for any use of the Work.
|
||||
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a party
|
||||
to this document and has no duty or obligation with respect to this CC0 or use
|
||||
of the Work.
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Xz
|
||||
|
||||
Package xz implements XZ decompression natively in Go.
|
||||
|
||||
Documentation at <https://pkg.go.dev/github.com/therootcompany/xz>.
|
||||
|
||||
Download and install with `go get github.com/xi2/xz`.
|
||||
|
||||
If you need compression as well as decompression, you might want to
|
||||
look at <https://github.com/ulikunitz/xz>.
|
||||
|
||||
# LICENSE
|
||||
|
||||
This was originally released into the public domain by the AUTHORS.
|
||||
|
||||
Here it is licensed more explicitly as Creative Commons CC0 1.0 Universal
|
||||
(CC0-1.0) so that it can be detected by automated tooling, and satisfy the legal
|
||||
requirements for vendor integration for cases in which a "public domain"
|
||||
statement is not sufficient.
|
||||
+461
@@ -0,0 +1,461 @@
|
||||
/*
|
||||
* Branch/Call/Jump (BCJ) filter decoders
|
||||
*
|
||||
* Authors: Lasse Collin <lasse.collin@tukaani.org>
|
||||
* Igor Pavlov <http://7-zip.org/>
|
||||
*
|
||||
* Translation to Go: Michael Cross <https://github.com/xi2>
|
||||
*
|
||||
* This file has been put into the public domain.
|
||||
* You can do whatever you want with this file.
|
||||
*/
|
||||
|
||||
package xz
|
||||
|
||||
/* from linux/lib/xz/xz_dec_bcj.c *************************************/
|
||||
|
||||
type xzDecBCJ struct {
|
||||
/* Type of the BCJ filter being used */
|
||||
typ xzFilterID
|
||||
/*
|
||||
* Return value of the next filter in the chain. We need to preserve
|
||||
* this information across calls, because we must not call the next
|
||||
* filter anymore once it has returned xzStreamEnd
|
||||
*/
|
||||
ret xzRet
|
||||
/*
|
||||
* Absolute position relative to the beginning of the uncompressed
|
||||
* data (in a single .xz Block).
|
||||
*/
|
||||
pos int
|
||||
/* x86 filter state */
|
||||
x86PrevMask uint32
|
||||
/* Temporary space to hold the variables from xzBuf */
|
||||
out []byte
|
||||
outPos int
|
||||
temp struct {
|
||||
/* Amount of already filtered data in the beginning of buf */
|
||||
filtered int
|
||||
/*
|
||||
* Buffer to hold a mix of filtered and unfiltered data. This
|
||||
* needs to be big enough to hold Alignment + 2 * Look-ahead:
|
||||
*
|
||||
* Type Alignment Look-ahead
|
||||
* x86 1 4
|
||||
* PowerPC 4 0
|
||||
* IA-64 16 0
|
||||
* ARM 4 0
|
||||
* ARM-Thumb 2 2
|
||||
* SPARC 4 0
|
||||
*/
|
||||
buf []byte // slice buf will be backed by bufArray
|
||||
bufArray [16]byte
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This is used to test the most significant byte of a memory address
|
||||
* in an x86 instruction.
|
||||
*/
|
||||
func bcjX86TestMSByte(b byte) bool {
|
||||
return b == 0x00 || b == 0xff
|
||||
}
|
||||
|
||||
func bcjX86Filter(s *xzDecBCJ, buf []byte) int {
|
||||
var maskToAllowedStatus = []bool{
|
||||
true, true, true, false, true, false, false, false,
|
||||
}
|
||||
var maskToBitNum = []byte{0, 1, 2, 2, 3, 3, 3, 3}
|
||||
var i int
|
||||
var prevPos int = -1
|
||||
var prevMask uint32 = s.x86PrevMask
|
||||
var src uint32
|
||||
var dest uint32
|
||||
var j uint32
|
||||
var b byte
|
||||
if len(buf) <= 4 {
|
||||
return 0
|
||||
}
|
||||
for i = 0; i < len(buf)-4; i++ {
|
||||
if buf[i]&0xfe != 0xe8 {
|
||||
continue
|
||||
}
|
||||
prevPos = i - prevPos
|
||||
if prevPos > 3 {
|
||||
prevMask = 0
|
||||
} else {
|
||||
prevMask = (prevMask << (uint(prevPos) - 1)) & 7
|
||||
if prevMask != 0 {
|
||||
b = buf[i+4-int(maskToBitNum[prevMask])]
|
||||
if !maskToAllowedStatus[prevMask] || bcjX86TestMSByte(b) {
|
||||
prevPos = i
|
||||
prevMask = prevMask<<1 | 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
prevPos = i
|
||||
if bcjX86TestMSByte(buf[i+4]) {
|
||||
src = getLE32(buf[i+1:])
|
||||
for {
|
||||
dest = src - uint32(s.pos+i+5)
|
||||
if prevMask == 0 {
|
||||
break
|
||||
}
|
||||
j = uint32(maskToBitNum[prevMask]) * 8
|
||||
b = byte(dest >> (24 - j))
|
||||
if !bcjX86TestMSByte(b) {
|
||||
break
|
||||
}
|
||||
src = dest ^ (1<<(32-j) - 1)
|
||||
}
|
||||
dest &= 0x01FFFFFF
|
||||
dest |= 0 - dest&0x01000000
|
||||
putLE32(dest, buf[i+1:])
|
||||
i += 4
|
||||
} else {
|
||||
prevMask = prevMask<<1 | 1
|
||||
}
|
||||
}
|
||||
prevPos = i - prevPos
|
||||
if prevPos > 3 {
|
||||
s.x86PrevMask = 0
|
||||
} else {
|
||||
s.x86PrevMask = prevMask << (uint(prevPos) - 1)
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func bcjPowerPCFilter(s *xzDecBCJ, buf []byte) int {
|
||||
var i int
|
||||
var instr uint32
|
||||
for i = 0; i+4 <= len(buf); i += 4 {
|
||||
instr = getBE32(buf[i:])
|
||||
if instr&0xFC000003 == 0x48000001 {
|
||||
instr &= 0x03FFFFFC
|
||||
instr -= uint32(s.pos + i)
|
||||
instr &= 0x03FFFFFC
|
||||
instr |= 0x48000001
|
||||
putBE32(instr, buf[i:])
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
var bcjIA64BranchTable = [...]byte{
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
4, 4, 6, 6, 0, 0, 7, 7,
|
||||
4, 4, 0, 0, 4, 4, 0, 0,
|
||||
}
|
||||
|
||||
func bcjIA64Filter(s *xzDecBCJ, buf []byte) int {
|
||||
var branchTable = bcjIA64BranchTable[:]
|
||||
/*
|
||||
* The local variables take a little bit stack space, but it's less
|
||||
* than what LZMA2 decoder takes, so it doesn't make sense to reduce
|
||||
* stack usage here without doing that for the LZMA2 decoder too.
|
||||
*/
|
||||
/* Loop counters */
|
||||
var i int
|
||||
var j int
|
||||
/* Instruction slot (0, 1, or 2) in the 128-bit instruction word */
|
||||
var slot uint32
|
||||
/* Bitwise offset of the instruction indicated by slot */
|
||||
var bitPos uint32
|
||||
/* bit_pos split into byte and bit parts */
|
||||
var bytePos uint32
|
||||
var bitRes uint32
|
||||
/* Address part of an instruction */
|
||||
var addr uint32
|
||||
/* Mask used to detect which instructions to convert */
|
||||
var mask uint32
|
||||
/* 41-bit instruction stored somewhere in the lowest 48 bits */
|
||||
var instr uint64
|
||||
/* Instruction normalized with bit_res for easier manipulation */
|
||||
var norm uint64
|
||||
for i = 0; i+16 <= len(buf); i += 16 {
|
||||
mask = uint32(branchTable[buf[i]&0x1f])
|
||||
for slot, bitPos = 0, 5; slot < 3; slot, bitPos = slot+1, bitPos+41 {
|
||||
if (mask>>slot)&1 == 0 {
|
||||
continue
|
||||
}
|
||||
bytePos = bitPos >> 3
|
||||
bitRes = bitPos & 7
|
||||
instr = 0
|
||||
for j = 0; j < 6; j++ {
|
||||
instr |= uint64(buf[i+j+int(bytePos)]) << (8 * uint(j))
|
||||
}
|
||||
norm = instr >> bitRes
|
||||
if (norm>>37)&0x0f == 0x05 && (norm>>9)&0x07 == 0 {
|
||||
addr = uint32((norm >> 13) & 0x0fffff)
|
||||
addr |= (uint32(norm>>36) & 1) << 20
|
||||
addr <<= 4
|
||||
addr -= uint32(s.pos + i)
|
||||
addr >>= 4
|
||||
norm &= ^(uint64(0x8fffff) << 13)
|
||||
norm |= uint64(addr&0x0fffff) << 13
|
||||
norm |= uint64(addr&0x100000) << (36 - 20)
|
||||
instr &= 1<<bitRes - 1
|
||||
instr |= norm << bitRes
|
||||
for j = 0; j < 6; j++ {
|
||||
buf[i+j+int(bytePos)] = byte(instr >> (8 * uint(j)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func bcjARMFilter(s *xzDecBCJ, buf []byte) int {
|
||||
var i int
|
||||
var addr uint32
|
||||
for i = 0; i+4 <= len(buf); i += 4 {
|
||||
if buf[i+3] == 0xeb {
|
||||
addr = uint32(buf[i]) | uint32(buf[i+1])<<8 |
|
||||
uint32(buf[i+2])<<16
|
||||
addr <<= 2
|
||||
addr -= uint32(s.pos + i + 8)
|
||||
addr >>= 2
|
||||
buf[i] = byte(addr)
|
||||
buf[i+1] = byte(addr >> 8)
|
||||
buf[i+2] = byte(addr >> 16)
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func bcjARMThumbFilter(s *xzDecBCJ, buf []byte) int {
|
||||
var i int
|
||||
var addr uint32
|
||||
for i = 0; i+4 <= len(buf); i += 2 {
|
||||
if buf[i+1]&0xf8 == 0xf0 && buf[i+3]&0xf8 == 0xf8 {
|
||||
addr = uint32(buf[i+1]&0x07)<<19 |
|
||||
uint32(buf[i])<<11 |
|
||||
uint32(buf[i+3]&0x07)<<8 |
|
||||
uint32(buf[i+2])
|
||||
addr <<= 1
|
||||
addr -= uint32(s.pos + i + 4)
|
||||
addr >>= 1
|
||||
buf[i+1] = byte(0xf0 | (addr>>19)&0x07)
|
||||
buf[i] = byte(addr >> 11)
|
||||
buf[i+3] = byte(0xf8 | (addr>>8)&0x07)
|
||||
buf[i+2] = byte(addr)
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func bcjSPARCFilter(s *xzDecBCJ, buf []byte) int {
|
||||
var i int
|
||||
var instr uint32
|
||||
for i = 0; i+4 <= len(buf); i += 4 {
|
||||
instr = getBE32(buf[i:])
|
||||
if instr>>22 == 0x100 || instr>>22 == 0x1ff {
|
||||
instr <<= 2
|
||||
instr -= uint32(s.pos + i)
|
||||
instr >>= 2
|
||||
instr = (0x40000000 - instr&0x400000) |
|
||||
0x40000000 | (instr & 0x3FFFFF)
|
||||
putBE32(instr, buf[i:])
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
/*
|
||||
* Apply the selected BCJ filter. Update *pos and s.pos to match the amount
|
||||
* of data that got filtered.
|
||||
*/
|
||||
func bcjApply(s *xzDecBCJ, buf []byte, pos *int) {
|
||||
var filtered int
|
||||
buf = buf[*pos:]
|
||||
switch s.typ {
|
||||
case idBCJX86:
|
||||
filtered = bcjX86Filter(s, buf)
|
||||
case idBCJPowerPC:
|
||||
filtered = bcjPowerPCFilter(s, buf)
|
||||
case idBCJIA64:
|
||||
filtered = bcjIA64Filter(s, buf)
|
||||
case idBCJARM:
|
||||
filtered = bcjARMFilter(s, buf)
|
||||
case idBCJARMThumb:
|
||||
filtered = bcjARMThumbFilter(s, buf)
|
||||
case idBCJSPARC:
|
||||
filtered = bcjSPARCFilter(s, buf)
|
||||
default:
|
||||
/* Never reached */
|
||||
}
|
||||
*pos += filtered
|
||||
s.pos += filtered
|
||||
}
|
||||
|
||||
/*
|
||||
* Flush pending filtered data from temp to the output buffer.
|
||||
* Move the remaining mixture of possibly filtered and unfiltered
|
||||
* data to the beginning of temp.
|
||||
*/
|
||||
func bcjFlush(s *xzDecBCJ, b *xzBuf) {
|
||||
var copySize int
|
||||
copySize = len(b.out) - b.outPos
|
||||
if copySize > s.temp.filtered {
|
||||
copySize = s.temp.filtered
|
||||
}
|
||||
copy(b.out[b.outPos:], s.temp.buf[:copySize])
|
||||
b.outPos += copySize
|
||||
s.temp.filtered -= copySize
|
||||
copy(s.temp.buf, s.temp.buf[copySize:])
|
||||
s.temp.buf = s.temp.buf[:len(s.temp.buf)-copySize]
|
||||
}
|
||||
|
||||
/*
|
||||
* Decode raw stream which has a BCJ filter as the first filter.
|
||||
*
|
||||
* The BCJ filter functions are primitive in sense that they process the
|
||||
* data in chunks of 1-16 bytes. To hide this issue, this function does
|
||||
* some buffering.
|
||||
*/
|
||||
func xzDecBCJRun(s *xzDecBCJ, b *xzBuf, chain func(*xzBuf) xzRet) xzRet {
|
||||
var outStart int
|
||||
/*
|
||||
* Flush pending already filtered data to the output buffer. Return
|
||||
* immediately if we couldn't flush everything, or if the next
|
||||
* filter in the chain had already returned xzStreamEnd.
|
||||
*/
|
||||
if s.temp.filtered > 0 {
|
||||
bcjFlush(s, b)
|
||||
if s.temp.filtered > 0 {
|
||||
return xzOK
|
||||
}
|
||||
if s.ret == xzStreamEnd {
|
||||
return xzStreamEnd
|
||||
}
|
||||
}
|
||||
/*
|
||||
* If we have more output space than what is currently pending in
|
||||
* temp, copy the unfiltered data from temp to the output buffer
|
||||
* and try to fill the output buffer by decoding more data from the
|
||||
* next filter in the chain. Apply the BCJ filter on the new data
|
||||
* in the output buffer. If everything cannot be filtered, copy it
|
||||
* to temp and rewind the output buffer position accordingly.
|
||||
*
|
||||
* This needs to be always run when len(temp.buf) == 0 to handle a special
|
||||
* case where the output buffer is full and the next filter has no
|
||||
* more output coming but hasn't returned xzStreamEnd yet.
|
||||
*/
|
||||
if len(s.temp.buf) < len(b.out)-b.outPos || len(s.temp.buf) == 0 {
|
||||
outStart = b.outPos
|
||||
copy(b.out[b.outPos:], s.temp.buf)
|
||||
b.outPos += len(s.temp.buf)
|
||||
s.ret = chain(b)
|
||||
if s.ret != xzStreamEnd && s.ret != xzOK {
|
||||
return s.ret
|
||||
}
|
||||
bcjApply(s, b.out[:b.outPos], &outStart)
|
||||
/*
|
||||
* As an exception, if the next filter returned xzStreamEnd,
|
||||
* we can do that too, since the last few bytes that remain
|
||||
* unfiltered are meant to remain unfiltered.
|
||||
*/
|
||||
if s.ret == xzStreamEnd {
|
||||
return xzStreamEnd
|
||||
}
|
||||
s.temp.buf = s.temp.bufArray[:b.outPos-outStart]
|
||||
b.outPos -= len(s.temp.buf)
|
||||
copy(s.temp.buf, b.out[b.outPos:])
|
||||
/*
|
||||
* If there wasn't enough input to the next filter to fill
|
||||
* the output buffer with unfiltered data, there's no point
|
||||
* to try decoding more data to temp.
|
||||
*/
|
||||
if b.outPos+len(s.temp.buf) < len(b.out) {
|
||||
return xzOK
|
||||
}
|
||||
}
|
||||
/*
|
||||
* We have unfiltered data in temp. If the output buffer isn't full
|
||||
* yet, try to fill the temp buffer by decoding more data from the
|
||||
* next filter. Apply the BCJ filter on temp. Then we hopefully can
|
||||
* fill the actual output buffer by copying filtered data from temp.
|
||||
* A mix of filtered and unfiltered data may be left in temp; it will
|
||||
* be taken care on the next call to this function.
|
||||
*/
|
||||
if b.outPos < len(b.out) {
|
||||
/* Make b.out temporarily point to s.temp. */
|
||||
s.out = b.out
|
||||
s.outPos = b.outPos
|
||||
b.out = s.temp.bufArray[:]
|
||||
b.outPos = len(s.temp.buf)
|
||||
s.ret = chain(b)
|
||||
s.temp.buf = s.temp.bufArray[:b.outPos]
|
||||
b.out = s.out
|
||||
b.outPos = s.outPos
|
||||
if s.ret != xzOK && s.ret != xzStreamEnd {
|
||||
return s.ret
|
||||
}
|
||||
bcjApply(s, s.temp.buf, &s.temp.filtered)
|
||||
/*
|
||||
* If the next filter returned xzStreamEnd, we mark that
|
||||
* everything is filtered, since the last unfiltered bytes
|
||||
* of the stream are meant to be left as is.
|
||||
*/
|
||||
if s.ret == xzStreamEnd {
|
||||
s.temp.filtered = len(s.temp.buf)
|
||||
}
|
||||
bcjFlush(s, b)
|
||||
if s.temp.filtered > 0 {
|
||||
return xzOK
|
||||
}
|
||||
}
|
||||
return s.ret
|
||||
}
|
||||
|
||||
/*
|
||||
* Allocate memory for BCJ decoders. xzDecBCJReset must be used before
|
||||
* calling xzDecBCJRun.
|
||||
*/
|
||||
func xzDecBCJCreate() *xzDecBCJ {
|
||||
return new(xzDecBCJ)
|
||||
}
|
||||
|
||||
/*
|
||||
* Decode the Filter ID of a BCJ filter and check the start offset is
|
||||
* valid. Returns xzOK if the given Filter ID and offset is
|
||||
* supported. Otherwise xzOptionsError is returned.
|
||||
*/
|
||||
func xzDecBCJReset(s *xzDecBCJ, id xzFilterID, offset int) xzRet {
|
||||
switch id {
|
||||
case idBCJX86:
|
||||
case idBCJPowerPC:
|
||||
case idBCJIA64:
|
||||
case idBCJARM:
|
||||
case idBCJARMThumb:
|
||||
case idBCJSPARC:
|
||||
default:
|
||||
/* Unsupported Filter ID */
|
||||
return xzOptionsError
|
||||
}
|
||||
// check offset is a multiple of alignment
|
||||
switch id {
|
||||
case idBCJPowerPC, idBCJARM, idBCJSPARC:
|
||||
if offset%4 != 0 {
|
||||
return xzOptionsError
|
||||
}
|
||||
case idBCJIA64:
|
||||
if offset%16 != 0 {
|
||||
return xzOptionsError
|
||||
}
|
||||
case idBCJARMThumb:
|
||||
if offset%2 != 0 {
|
||||
return xzOptionsError
|
||||
}
|
||||
}
|
||||
s.typ = id
|
||||
s.ret = xzOK
|
||||
s.pos = offset
|
||||
s.x86PrevMask = 0
|
||||
s.temp.filtered = 0
|
||||
s.temp.buf = nil
|
||||
return xzOK
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Delta decoder
|
||||
*
|
||||
* Author: Lasse Collin <lasse.collin@tukaani.org>
|
||||
*
|
||||
* Translation to Go: Michael Cross <https://github.com/xi2>
|
||||
*
|
||||
* This file has been put into the public domain.
|
||||
* You can do whatever you want with this file.
|
||||
*/
|
||||
|
||||
package xz
|
||||
|
||||
type xzDecDelta struct {
|
||||
delta [256]byte
|
||||
pos byte
|
||||
distance int // in range [1, 256]
|
||||
}
|
||||
|
||||
/*
|
||||
* Decode raw stream which has a delta filter as the first filter.
|
||||
*/
|
||||
func xzDecDeltaRun(s *xzDecDelta, b *xzBuf, chain func(*xzBuf) xzRet) xzRet {
|
||||
outStart := b.outPos
|
||||
ret := chain(b)
|
||||
for i := outStart; i < b.outPos; i++ {
|
||||
tmp := b.out[i] + s.delta[byte(s.distance+int(s.pos))]
|
||||
s.delta[s.pos] = tmp
|
||||
b.out[i] = tmp
|
||||
s.pos--
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
/*
|
||||
* Allocate memory for a delta decoder. xzDecDeltaReset must be used
|
||||
* before calling xzDecDeltaRun.
|
||||
*/
|
||||
func xzDecDeltaCreate() *xzDecDelta {
|
||||
return new(xzDecDelta)
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns xzOK if the given distance is valid. Otherwise
|
||||
* xzOptionsError is returned.
|
||||
*/
|
||||
func xzDecDeltaReset(s *xzDecDelta, distance int) xzRet {
|
||||
if distance < 1 || distance > 256 {
|
||||
return xzOptionsError
|
||||
}
|
||||
s.delta = [256]byte{}
|
||||
s.pos = 0
|
||||
s.distance = distance
|
||||
return xzOK
|
||||
}
|
||||
+1235
File diff suppressed because it is too large
Load Diff
+932
@@ -0,0 +1,932 @@
|
||||
/*
|
||||
* .xz Stream decoder
|
||||
*
|
||||
* Author: Lasse Collin <lasse.collin@tukaani.org>
|
||||
*
|
||||
* Translation to Go: Michael Cross <https://github.com/xi2>
|
||||
*
|
||||
* This file has been put into the public domain.
|
||||
* You can do whatever you want with this file.
|
||||
*/
|
||||
|
||||
package xz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"hash"
|
||||
"hash/crc32"
|
||||
"hash/crc64"
|
||||
)
|
||||
|
||||
/* from linux/lib/xz/xz_stream.h **************************************/
|
||||
|
||||
/*
|
||||
* See the .xz file format specification at
|
||||
* http://tukaani.org/xz/xz-file-format.txt
|
||||
* to understand the container format.
|
||||
*/
|
||||
const (
|
||||
streamHeaderSize = 12
|
||||
headerMagic = "\xfd7zXZ\x00"
|
||||
footerMagic = "YZ"
|
||||
)
|
||||
|
||||
/*
|
||||
* Variable-length integer can hold a 63-bit unsigned integer or a special
|
||||
* value indicating that the value is unknown.
|
||||
*/
|
||||
type vliType uint64
|
||||
|
||||
const (
|
||||
vliUnknown vliType = ^vliType(0)
|
||||
/* Maximum encoded size of a VLI */
|
||||
vliBytesMax = 8 * 8 / 7 // (Sizeof(vliType) * 8 / 7)
|
||||
)
|
||||
|
||||
/* from linux/lib/xz/xz_dec_stream.c **********************************/
|
||||
|
||||
/* Hash used to validate the Index field */
|
||||
type xzDecHash struct {
|
||||
unpadded vliType
|
||||
uncompressed vliType
|
||||
sha256 hash.Hash
|
||||
}
|
||||
|
||||
// type of xzDec.sequence
|
||||
type xzDecSeq int
|
||||
|
||||
const (
|
||||
seqStreamHeader xzDecSeq = iota
|
||||
seqBlockStart
|
||||
seqBlockHeader
|
||||
seqBlockUncompress
|
||||
seqBlockPadding
|
||||
seqBlockCheck
|
||||
seqIndex
|
||||
seqIndexPadding
|
||||
seqIndexCRC32
|
||||
seqStreamFooter
|
||||
)
|
||||
|
||||
// type of xzDec.index.sequence
|
||||
type xzDecIndexSeq int
|
||||
|
||||
const (
|
||||
seqIndexCount xzDecIndexSeq = iota
|
||||
seqIndexUnpadded
|
||||
seqIndexUncompressed
|
||||
)
|
||||
|
||||
/**
|
||||
* xzDec - Opaque type to hold the XZ decoder state
|
||||
*/
|
||||
type xzDec struct {
|
||||
/* Position in decMain */
|
||||
sequence xzDecSeq
|
||||
/* Position in variable-length integers and Check fields */
|
||||
pos int
|
||||
/* Variable-length integer decoded by decVLI */
|
||||
vli vliType
|
||||
/* Saved inPos and outPos */
|
||||
inStart int
|
||||
outStart int
|
||||
/* CRC32 checksum hash used in Index */
|
||||
crc32 hash.Hash
|
||||
/* Hashes used in Blocks */
|
||||
checkCRC32 hash.Hash
|
||||
checkCRC64 hash.Hash
|
||||
checkSHA256 hash.Hash
|
||||
/* for checkTypes CRC32/CRC64/SHA256, check is one of the above 3 hashes */
|
||||
check hash.Hash
|
||||
/* Embedded stream header struct containing CheckType */
|
||||
*Header
|
||||
/*
|
||||
* True if the next call to xzDecRun is allowed to return
|
||||
* xzBufError.
|
||||
*/
|
||||
allowBufError bool
|
||||
/* Information stored in Block Header */
|
||||
blockHeader struct {
|
||||
/*
|
||||
* Value stored in the Compressed Size field, or
|
||||
* vliUnknown if Compressed Size is not present.
|
||||
*/
|
||||
compressed vliType
|
||||
/*
|
||||
* Value stored in the Uncompressed Size field, or
|
||||
* vliUnknown if Uncompressed Size is not present.
|
||||
*/
|
||||
uncompressed vliType
|
||||
/* Size of the Block Header field */
|
||||
size int
|
||||
}
|
||||
/* Information collected when decoding Blocks */
|
||||
block struct {
|
||||
/* Observed compressed size of the current Block */
|
||||
compressed vliType
|
||||
/* Observed uncompressed size of the current Block */
|
||||
uncompressed vliType
|
||||
/* Number of Blocks decoded so far */
|
||||
count vliType
|
||||
/*
|
||||
* Hash calculated from the Block sizes. This is used to
|
||||
* validate the Index field.
|
||||
*/
|
||||
hash xzDecHash
|
||||
}
|
||||
/* Variables needed when verifying the Index field */
|
||||
index struct {
|
||||
/* Position in decIndex */
|
||||
sequence xzDecIndexSeq
|
||||
/* Size of the Index in bytes */
|
||||
size vliType
|
||||
/* Number of Records (matches block.count in valid files) */
|
||||
count vliType
|
||||
/*
|
||||
* Hash calculated from the Records (matches block.hash in
|
||||
* valid files).
|
||||
*/
|
||||
hash xzDecHash
|
||||
}
|
||||
/*
|
||||
* Temporary buffer needed to hold Stream Header, Block Header,
|
||||
* and Stream Footer. The Block Header is the biggest (1 KiB)
|
||||
* so we reserve space according to that. bufArray has to be aligned
|
||||
* to a multiple of four bytes; the variables before it
|
||||
* should guarantee this.
|
||||
*/
|
||||
temp struct {
|
||||
pos int
|
||||
buf []byte // slice buf will be backed by bufArray
|
||||
bufArray [1024]byte
|
||||
}
|
||||
// chain is the function (or to be more precise, closure) which
|
||||
// does the decompression and will call into the lzma2 and other
|
||||
// filter code as needed. It is constructed by decBlockHeader
|
||||
chain func(b *xzBuf) xzRet
|
||||
// lzma2 holds the state of the last filter (which must be LZMA2)
|
||||
lzma2 *xzDecLZMA2
|
||||
// pointers to allocated BCJ/Delta filters
|
||||
bcjs []*xzDecBCJ
|
||||
deltas []*xzDecDelta
|
||||
// number of currently in use BCJ/Delta filters from the above
|
||||
bcjsUsed int
|
||||
deltasUsed int
|
||||
}
|
||||
|
||||
/* Sizes of the Check field with different Check IDs */
|
||||
var checkSizes = [...]byte{
|
||||
0,
|
||||
4, 4, 4,
|
||||
8, 8, 8,
|
||||
16, 16, 16,
|
||||
32, 32, 32,
|
||||
64, 64, 64,
|
||||
}
|
||||
|
||||
/*
|
||||
* Fill s.temp by copying data starting from b.in[b.inPos]. Caller
|
||||
* must have set s.temp.pos to indicate how much data we are supposed
|
||||
* to copy into s.temp.buf. Return true once s.temp.pos has reached
|
||||
* len(s.temp.buf).
|
||||
*/
|
||||
func fillTemp(s *xzDec, b *xzBuf) bool {
|
||||
copySize := len(b.in) - b.inPos
|
||||
tempRemaining := len(s.temp.buf) - s.temp.pos
|
||||
if copySize > tempRemaining {
|
||||
copySize = tempRemaining
|
||||
}
|
||||
copy(s.temp.buf[s.temp.pos:], b.in[b.inPos:])
|
||||
b.inPos += copySize
|
||||
s.temp.pos += copySize
|
||||
if s.temp.pos == len(s.temp.buf) {
|
||||
s.temp.pos = 0
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/* Decode a variable-length integer (little-endian base-128 encoding) */
|
||||
func decVLI(s *xzDec, in []byte, inPos *int) xzRet {
|
||||
var byte byte
|
||||
if s.pos == 0 {
|
||||
s.vli = 0
|
||||
}
|
||||
for *inPos < len(in) {
|
||||
byte = in[*inPos]
|
||||
*inPos++
|
||||
s.vli |= vliType(byte&0x7f) << uint(s.pos)
|
||||
if byte&0x80 == 0 {
|
||||
/* Don't allow non-minimal encodings. */
|
||||
if byte == 0 && s.pos != 0 {
|
||||
return xzDataError
|
||||
}
|
||||
s.pos = 0
|
||||
return xzStreamEnd
|
||||
}
|
||||
s.pos += 7
|
||||
if s.pos == 7*vliBytesMax {
|
||||
return xzDataError
|
||||
}
|
||||
}
|
||||
return xzOK
|
||||
}
|
||||
|
||||
/*
|
||||
* Decode the Compressed Data field from a Block. Update and validate
|
||||
* the observed compressed and uncompressed sizes of the Block so that
|
||||
* they don't exceed the values possibly stored in the Block Header
|
||||
* (validation assumes that no integer overflow occurs, since vliType
|
||||
* is uint64). Update s.check if presence of the CRC32/CRC64/SHA256
|
||||
* field was indicated in Stream Header.
|
||||
*
|
||||
* Once the decoding is finished, validate that the observed sizes match
|
||||
* the sizes possibly stored in the Block Header. Update the hash and
|
||||
* Block count, which are later used to validate the Index field.
|
||||
*/
|
||||
func decBlock(s *xzDec, b *xzBuf) xzRet {
|
||||
var ret xzRet
|
||||
s.inStart = b.inPos
|
||||
s.outStart = b.outPos
|
||||
ret = s.chain(b)
|
||||
s.block.compressed += vliType(b.inPos - s.inStart)
|
||||
s.block.uncompressed += vliType(b.outPos - s.outStart)
|
||||
/*
|
||||
* There is no need to separately check for vliUnknown since
|
||||
* the observed sizes are always smaller than vliUnknown.
|
||||
*/
|
||||
if s.block.compressed > s.blockHeader.compressed ||
|
||||
s.block.uncompressed > s.blockHeader.uncompressed {
|
||||
return xzDataError
|
||||
}
|
||||
switch s.CheckType {
|
||||
case CheckCRC32, CheckCRC64, CheckSHA256:
|
||||
_, _ = s.check.Write(b.out[s.outStart:b.outPos])
|
||||
}
|
||||
if ret == xzStreamEnd {
|
||||
if s.blockHeader.compressed != vliUnknown &&
|
||||
s.blockHeader.compressed != s.block.compressed {
|
||||
return xzDataError
|
||||
}
|
||||
if s.blockHeader.uncompressed != vliUnknown &&
|
||||
s.blockHeader.uncompressed != s.block.uncompressed {
|
||||
return xzDataError
|
||||
}
|
||||
s.block.hash.unpadded +=
|
||||
vliType(s.blockHeader.size) + s.block.compressed
|
||||
s.block.hash.unpadded += vliType(checkSizes[s.CheckType])
|
||||
s.block.hash.uncompressed += s.block.uncompressed
|
||||
var buf [2 * 8]byte // 2*Sizeof(vliType)
|
||||
putLE64(uint64(s.block.hash.unpadded), buf[:])
|
||||
putLE64(uint64(s.block.hash.uncompressed), buf[8:])
|
||||
_, _ = s.block.hash.sha256.Write(buf[:])
|
||||
s.block.count++
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
/* Update the Index size and the CRC32 hash. */
|
||||
func indexUpdate(s *xzDec, b *xzBuf) {
|
||||
inUsed := b.inPos - s.inStart
|
||||
s.index.size += vliType(inUsed)
|
||||
_, _ = s.crc32.Write(b.in[s.inStart : s.inStart+inUsed])
|
||||
}
|
||||
|
||||
/*
|
||||
* Decode the Number of Records, Unpadded Size, and Uncompressed Size
|
||||
* fields from the Index field. That is, Index Padding and CRC32 are not
|
||||
* decoded by this function.
|
||||
*
|
||||
* This can return xzOK (more input needed), xzStreamEnd (everything
|
||||
* successfully decoded), or xzDataError (input is corrupt).
|
||||
*/
|
||||
func decIndex(s *xzDec, b *xzBuf) xzRet {
|
||||
var ret xzRet
|
||||
for {
|
||||
ret = decVLI(s, b.in, &b.inPos)
|
||||
if ret != xzStreamEnd {
|
||||
indexUpdate(s, b)
|
||||
return ret
|
||||
}
|
||||
switch s.index.sequence {
|
||||
case seqIndexCount:
|
||||
s.index.count = s.vli
|
||||
/*
|
||||
* Validate that the Number of Records field
|
||||
* indicates the same number of Records as
|
||||
* there were Blocks in the Stream.
|
||||
*/
|
||||
if s.index.count != s.block.count {
|
||||
return xzDataError
|
||||
}
|
||||
s.index.sequence = seqIndexUnpadded
|
||||
case seqIndexUnpadded:
|
||||
s.index.hash.unpadded += s.vli
|
||||
s.index.sequence = seqIndexUncompressed
|
||||
case seqIndexUncompressed:
|
||||
s.index.hash.uncompressed += s.vli
|
||||
var buf [2 * 8]byte // 2*Sizeof(vliType)
|
||||
putLE64(uint64(s.index.hash.unpadded), buf[:])
|
||||
putLE64(uint64(s.index.hash.uncompressed), buf[8:])
|
||||
_, _ = s.index.hash.sha256.Write(buf[:])
|
||||
s.index.count--
|
||||
s.index.sequence = seqIndexUnpadded
|
||||
}
|
||||
if !(s.index.count > 0) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return xzStreamEnd
|
||||
}
|
||||
|
||||
/*
|
||||
* Validate that the next 4 bytes match s.crc32.Sum(nil). s.pos must
|
||||
* be zero when starting to validate the first byte.
|
||||
*/
|
||||
func crcValidate(s *xzDec, b *xzBuf) xzRet {
|
||||
sum := s.crc32.Sum(nil)
|
||||
// CRC32 - reverse slice
|
||||
sum[0], sum[1], sum[2], sum[3] = sum[3], sum[2], sum[1], sum[0]
|
||||
for {
|
||||
if b.inPos == len(b.in) {
|
||||
return xzOK
|
||||
}
|
||||
if sum[s.pos] != b.in[b.inPos] {
|
||||
return xzDataError
|
||||
}
|
||||
b.inPos++
|
||||
s.pos++
|
||||
if !(s.pos < 4) {
|
||||
break
|
||||
}
|
||||
}
|
||||
s.crc32.Reset()
|
||||
s.pos = 0
|
||||
return xzStreamEnd
|
||||
}
|
||||
|
||||
/*
|
||||
* Validate that the next 4/8/32 bytes match s.check.Sum(nil). s.pos
|
||||
* must be zero when starting to validate the first byte.
|
||||
*/
|
||||
func checkValidate(s *xzDec, b *xzBuf) xzRet {
|
||||
sum := s.check.Sum(nil)
|
||||
if s.CheckType == CheckCRC32 || s.CheckType == CheckCRC64 {
|
||||
// CRC32/64 - reverse slice
|
||||
for i, j := 0, len(sum)-1; i < j; i, j = i+1, j-1 {
|
||||
sum[i], sum[j] = sum[j], sum[i]
|
||||
}
|
||||
}
|
||||
for {
|
||||
if b.inPos == len(b.in) {
|
||||
return xzOK
|
||||
}
|
||||
if sum[s.pos] != b.in[b.inPos] {
|
||||
return xzDataError
|
||||
}
|
||||
b.inPos++
|
||||
s.pos++
|
||||
if !(s.pos < len(sum)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
s.check.Reset()
|
||||
s.pos = 0
|
||||
return xzStreamEnd
|
||||
}
|
||||
|
||||
/*
|
||||
* Skip over the Check field when the Check ID is not supported.
|
||||
* Returns true once the whole Check field has been skipped over.
|
||||
*/
|
||||
func checkSkip(s *xzDec, b *xzBuf) bool {
|
||||
for s.pos < int(checkSizes[s.CheckType]) {
|
||||
if b.inPos == len(b.in) {
|
||||
return false
|
||||
}
|
||||
b.inPos++
|
||||
s.pos++
|
||||
}
|
||||
s.pos = 0
|
||||
return true
|
||||
}
|
||||
|
||||
/* polynomial table used in decStreamHeader below */
|
||||
var xzCRC64Table = crc64.MakeTable(crc64.ECMA)
|
||||
|
||||
/* Decode the Stream Header field (the first 12 bytes of the .xz Stream). */
|
||||
func decStreamHeader(s *xzDec) xzRet {
|
||||
if string(s.temp.buf[:len(headerMagic)]) != headerMagic {
|
||||
return xzFormatError
|
||||
}
|
||||
if crc32.ChecksumIEEE(s.temp.buf[len(headerMagic):len(headerMagic)+2]) !=
|
||||
getLE32(s.temp.buf[len(headerMagic)+2:]) {
|
||||
return xzDataError
|
||||
}
|
||||
if s.temp.buf[len(headerMagic)] != 0 {
|
||||
return xzOptionsError
|
||||
}
|
||||
/*
|
||||
* Of integrity checks, we support none (Check ID = 0),
|
||||
* CRC32 (Check ID = 1), CRC64 (Check ID = 4) and SHA256 (Check ID = 10)
|
||||
* However, we will accept other check types too, but then the check
|
||||
* won't be verified and a warning (xzUnsupportedCheck) will be given.
|
||||
*/
|
||||
s.CheckType = CheckID(s.temp.buf[len(headerMagic)+1])
|
||||
if s.CheckType > checkMax {
|
||||
return xzOptionsError
|
||||
}
|
||||
switch s.CheckType {
|
||||
case CheckNone:
|
||||
// CheckNone: no action needed
|
||||
case CheckCRC32:
|
||||
if s.checkCRC32 == nil {
|
||||
s.checkCRC32 = crc32.NewIEEE()
|
||||
} else {
|
||||
s.checkCRC32.Reset()
|
||||
}
|
||||
s.check = s.checkCRC32
|
||||
case CheckCRC64:
|
||||
if s.checkCRC64 == nil {
|
||||
s.checkCRC64 = crc64.New(xzCRC64Table)
|
||||
} else {
|
||||
s.checkCRC64.Reset()
|
||||
}
|
||||
s.check = s.checkCRC64
|
||||
case CheckSHA256:
|
||||
if s.checkSHA256 == nil {
|
||||
s.checkSHA256 = sha256.New()
|
||||
} else {
|
||||
s.checkSHA256.Reset()
|
||||
}
|
||||
s.check = s.checkSHA256
|
||||
default:
|
||||
return xzUnsupportedCheck
|
||||
}
|
||||
return xzOK
|
||||
}
|
||||
|
||||
/* Decode the Stream Footer field (the last 12 bytes of the .xz Stream) */
|
||||
func decStreamFooter(s *xzDec) xzRet {
|
||||
if string(s.temp.buf[10:10+len(footerMagic)]) != footerMagic {
|
||||
return xzDataError
|
||||
}
|
||||
if crc32.ChecksumIEEE(s.temp.buf[4:10]) != getLE32(s.temp.buf) {
|
||||
return xzDataError
|
||||
}
|
||||
/*
|
||||
* Validate Backward Size. Note that we never added the size of the
|
||||
* Index CRC32 field to s->index.size, thus we use s->index.size / 4
|
||||
* instead of s->index.size / 4 - 1.
|
||||
*/
|
||||
if s.index.size>>2 != vliType(getLE32(s.temp.buf[4:])) {
|
||||
return xzDataError
|
||||
}
|
||||
if s.temp.buf[8] != 0 || CheckID(s.temp.buf[9]) != s.CheckType {
|
||||
return xzDataError
|
||||
}
|
||||
/*
|
||||
* Use xzStreamEnd instead of xzOK to be more convenient
|
||||
* for the caller.
|
||||
*/
|
||||
return xzStreamEnd
|
||||
}
|
||||
|
||||
/* Decode the Block Header and initialize the filter chain. */
|
||||
func decBlockHeader(s *xzDec) xzRet {
|
||||
var ret xzRet
|
||||
/*
|
||||
* Validate the CRC32. We know that the temp buffer is at least
|
||||
* eight bytes so this is safe.
|
||||
*/
|
||||
crc := getLE32(s.temp.buf[len(s.temp.buf)-4:])
|
||||
s.temp.buf = s.temp.buf[:len(s.temp.buf)-4]
|
||||
if crc32.ChecksumIEEE(s.temp.buf) != crc {
|
||||
return xzDataError
|
||||
}
|
||||
s.temp.pos = 2
|
||||
/*
|
||||
* Catch unsupported Block Flags.
|
||||
*/
|
||||
if s.temp.buf[1]&0x3C != 0 {
|
||||
return xzOptionsError
|
||||
}
|
||||
/* Compressed Size */
|
||||
if s.temp.buf[1]&0x40 != 0 {
|
||||
if decVLI(s, s.temp.buf, &s.temp.pos) != xzStreamEnd {
|
||||
return xzDataError
|
||||
}
|
||||
if s.vli >= 1<<63-8 {
|
||||
// the whole block must stay smaller than 2^63 bytes
|
||||
// the block header cannot be smaller than 8 bytes
|
||||
return xzDataError
|
||||
}
|
||||
if s.vli == 0 {
|
||||
// compressed size must be non-zero
|
||||
return xzDataError
|
||||
}
|
||||
s.blockHeader.compressed = s.vli
|
||||
} else {
|
||||
s.blockHeader.compressed = vliUnknown
|
||||
}
|
||||
/* Uncompressed Size */
|
||||
if s.temp.buf[1]&0x80 != 0 {
|
||||
if decVLI(s, s.temp.buf, &s.temp.pos) != xzStreamEnd {
|
||||
return xzDataError
|
||||
}
|
||||
s.blockHeader.uncompressed = s.vli
|
||||
} else {
|
||||
s.blockHeader.uncompressed = vliUnknown
|
||||
}
|
||||
// get total number of filters (1-4)
|
||||
filterTotal := int(s.temp.buf[1]&0x03) + 1
|
||||
// slice to hold decoded filters
|
||||
filterList := make([]struct {
|
||||
id xzFilterID
|
||||
props uint32
|
||||
}, filterTotal)
|
||||
// decode the non-last filters which cannot be LZMA2
|
||||
for i := 0; i < filterTotal-1; i++ {
|
||||
/* Valid Filter Flags always take at least two bytes. */
|
||||
if len(s.temp.buf)-s.temp.pos < 2 {
|
||||
return xzDataError
|
||||
}
|
||||
s.temp.pos += 2
|
||||
switch id := xzFilterID(s.temp.buf[s.temp.pos-2]); id {
|
||||
case idDelta:
|
||||
// delta filter
|
||||
if s.temp.buf[s.temp.pos-1] != 0x01 {
|
||||
return xzOptionsError
|
||||
}
|
||||
/* Filter Properties contains distance - 1 */
|
||||
if len(s.temp.buf)-s.temp.pos < 1 {
|
||||
return xzDataError
|
||||
}
|
||||
props := uint32(s.temp.buf[s.temp.pos])
|
||||
s.temp.pos++
|
||||
filterList[i] = struct {
|
||||
id xzFilterID
|
||||
props uint32
|
||||
}{id: id, props: props}
|
||||
case idBCJX86, idBCJPowerPC, idBCJIA64,
|
||||
idBCJARM, idBCJARMThumb, idBCJSPARC:
|
||||
// bcj filter
|
||||
var props uint32
|
||||
switch s.temp.buf[s.temp.pos-1] {
|
||||
case 0x00:
|
||||
props = 0
|
||||
case 0x04:
|
||||
if len(s.temp.buf)-s.temp.pos < 4 {
|
||||
return xzDataError
|
||||
}
|
||||
props = getLE32(s.temp.buf[s.temp.pos:])
|
||||
s.temp.pos += 4
|
||||
default:
|
||||
return xzOptionsError
|
||||
}
|
||||
filterList[i] = struct {
|
||||
id xzFilterID
|
||||
props uint32
|
||||
}{id: id, props: props}
|
||||
default:
|
||||
return xzOptionsError
|
||||
}
|
||||
}
|
||||
/*
|
||||
* decode the last filter which must be LZMA2
|
||||
*/
|
||||
if len(s.temp.buf)-s.temp.pos < 2 {
|
||||
return xzDataError
|
||||
}
|
||||
/* Filter ID = LZMA2 */
|
||||
if xzFilterID(s.temp.buf[s.temp.pos]) != idLZMA2 {
|
||||
return xzOptionsError
|
||||
}
|
||||
s.temp.pos++
|
||||
/* Size of Properties = 1-byte Filter Properties */
|
||||
if s.temp.buf[s.temp.pos] != 0x01 {
|
||||
return xzOptionsError
|
||||
}
|
||||
s.temp.pos++
|
||||
/* Filter Properties contains LZMA2 dictionary size. */
|
||||
if len(s.temp.buf)-s.temp.pos < 1 {
|
||||
return xzDataError
|
||||
}
|
||||
props := uint32(s.temp.buf[s.temp.pos])
|
||||
s.temp.pos++
|
||||
filterList[filterTotal-1] = struct {
|
||||
id xzFilterID
|
||||
props uint32
|
||||
}{id: idLZMA2, props: props}
|
||||
/*
|
||||
* Process the filter list and create s.chain, going from last
|
||||
* filter (LZMA2) to first filter
|
||||
*
|
||||
* First, LZMA2.
|
||||
*/
|
||||
ret = xzDecLZMA2Reset(s.lzma2, byte(filterList[filterTotal-1].props))
|
||||
if ret != xzOK {
|
||||
return ret
|
||||
}
|
||||
s.chain = func(b *xzBuf) xzRet {
|
||||
return xzDecLZMA2Run(s.lzma2, b)
|
||||
}
|
||||
/*
|
||||
* Now the non-last filters
|
||||
*/
|
||||
for i := filterTotal - 2; i >= 0; i-- {
|
||||
switch id := filterList[i].id; id {
|
||||
case idDelta:
|
||||
// delta filter
|
||||
var delta *xzDecDelta
|
||||
if s.deltasUsed < len(s.deltas) {
|
||||
delta = s.deltas[s.deltasUsed]
|
||||
} else {
|
||||
delta = xzDecDeltaCreate()
|
||||
s.deltas = append(s.deltas, delta)
|
||||
}
|
||||
s.deltasUsed++
|
||||
ret = xzDecDeltaReset(delta, int(filterList[i].props)+1)
|
||||
if ret != xzOK {
|
||||
return ret
|
||||
}
|
||||
chain := s.chain
|
||||
s.chain = func(b *xzBuf) xzRet {
|
||||
return xzDecDeltaRun(delta, b, chain)
|
||||
}
|
||||
case idBCJX86, idBCJPowerPC, idBCJIA64,
|
||||
idBCJARM, idBCJARMThumb, idBCJSPARC:
|
||||
// bcj filter
|
||||
var bcj *xzDecBCJ
|
||||
if s.bcjsUsed < len(s.bcjs) {
|
||||
bcj = s.bcjs[s.bcjsUsed]
|
||||
} else {
|
||||
bcj = xzDecBCJCreate()
|
||||
s.bcjs = append(s.bcjs, bcj)
|
||||
}
|
||||
s.bcjsUsed++
|
||||
ret = xzDecBCJReset(bcj, id, int(filterList[i].props))
|
||||
if ret != xzOK {
|
||||
return ret
|
||||
}
|
||||
chain := s.chain
|
||||
s.chain = func(b *xzBuf) xzRet {
|
||||
return xzDecBCJRun(bcj, b, chain)
|
||||
}
|
||||
}
|
||||
}
|
||||
/* The rest must be Header Padding. */
|
||||
for s.temp.pos < len(s.temp.buf) {
|
||||
if s.temp.buf[s.temp.pos] != 0x00 {
|
||||
return xzOptionsError
|
||||
}
|
||||
s.temp.pos++
|
||||
}
|
||||
s.temp.pos = 0
|
||||
s.block.compressed = 0
|
||||
s.block.uncompressed = 0
|
||||
return xzOK
|
||||
}
|
||||
|
||||
func decMain(s *xzDec, b *xzBuf) xzRet {
|
||||
var ret xzRet
|
||||
/*
|
||||
* Store the start position for the case when we are in the middle
|
||||
* of the Index field.
|
||||
*/
|
||||
s.inStart = b.inPos
|
||||
for {
|
||||
switch s.sequence {
|
||||
case seqStreamHeader:
|
||||
/*
|
||||
* Stream Header is copied to s.temp, and then
|
||||
* decoded from there. This way if the caller
|
||||
* gives us only little input at a time, we can
|
||||
* still keep the Stream Header decoding code
|
||||
* simple. Similar approach is used in many places
|
||||
* in this file.
|
||||
*/
|
||||
if !fillTemp(s, b) {
|
||||
return xzOK
|
||||
}
|
||||
/*
|
||||
* If decStreamHeader returns
|
||||
* xzUnsupportedCheck, it is still possible
|
||||
* to continue decoding. Thus, update s.sequence
|
||||
* before calling decStreamHeader.
|
||||
*/
|
||||
s.sequence = seqBlockStart
|
||||
ret = decStreamHeader(s)
|
||||
if ret != xzOK {
|
||||
return ret
|
||||
}
|
||||
fallthrough
|
||||
case seqBlockStart:
|
||||
/* We need one byte of input to continue. */
|
||||
if b.inPos == len(b.in) {
|
||||
return xzOK
|
||||
}
|
||||
/* See if this is the beginning of the Index field. */
|
||||
if b.in[b.inPos] == 0 {
|
||||
s.inStart = b.inPos
|
||||
b.inPos++
|
||||
s.sequence = seqIndex
|
||||
break
|
||||
}
|
||||
/*
|
||||
* Calculate the size of the Block Header and
|
||||
* prepare to decode it.
|
||||
*/
|
||||
s.blockHeader.size = (int(b.in[b.inPos]) + 1) * 4
|
||||
s.temp.buf = s.temp.bufArray[:s.blockHeader.size]
|
||||
s.temp.pos = 0
|
||||
s.sequence = seqBlockHeader
|
||||
fallthrough
|
||||
case seqBlockHeader:
|
||||
if !fillTemp(s, b) {
|
||||
return xzOK
|
||||
}
|
||||
ret = decBlockHeader(s)
|
||||
if ret != xzOK {
|
||||
return ret
|
||||
}
|
||||
s.sequence = seqBlockUncompress
|
||||
fallthrough
|
||||
case seqBlockUncompress:
|
||||
ret = decBlock(s, b)
|
||||
if ret != xzStreamEnd {
|
||||
return ret
|
||||
}
|
||||
s.sequence = seqBlockPadding
|
||||
fallthrough
|
||||
case seqBlockPadding:
|
||||
/*
|
||||
* Size of Compressed Data + Block Padding
|
||||
* must be a multiple of four. We don't need
|
||||
* s->block.compressed for anything else
|
||||
* anymore, so we use it here to test the size
|
||||
* of the Block Padding field.
|
||||
*/
|
||||
for s.block.compressed&3 != 0 {
|
||||
if b.inPos == len(b.in) {
|
||||
return xzOK
|
||||
}
|
||||
if b.in[b.inPos] != 0 {
|
||||
return xzDataError
|
||||
}
|
||||
b.inPos++
|
||||
s.block.compressed++
|
||||
}
|
||||
s.sequence = seqBlockCheck
|
||||
fallthrough
|
||||
case seqBlockCheck:
|
||||
switch s.CheckType {
|
||||
case CheckCRC32, CheckCRC64, CheckSHA256:
|
||||
ret = checkValidate(s, b)
|
||||
if ret != xzStreamEnd {
|
||||
return ret
|
||||
}
|
||||
default:
|
||||
if !checkSkip(s, b) {
|
||||
return xzOK
|
||||
}
|
||||
}
|
||||
s.sequence = seqBlockStart
|
||||
case seqIndex:
|
||||
ret = decIndex(s, b)
|
||||
if ret != xzStreamEnd {
|
||||
return ret
|
||||
}
|
||||
s.sequence = seqIndexPadding
|
||||
fallthrough
|
||||
case seqIndexPadding:
|
||||
for (s.index.size+vliType(b.inPos-s.inStart))&3 != 0 {
|
||||
if b.inPos == len(b.in) {
|
||||
indexUpdate(s, b)
|
||||
return xzOK
|
||||
}
|
||||
if b.in[b.inPos] != 0 {
|
||||
return xzDataError
|
||||
}
|
||||
b.inPos++
|
||||
}
|
||||
/* Finish the CRC32 value and Index size. */
|
||||
indexUpdate(s, b)
|
||||
/* Compare the hashes to validate the Index field. */
|
||||
if !bytes.Equal(
|
||||
s.block.hash.sha256.Sum(nil), s.index.hash.sha256.Sum(nil)) {
|
||||
return xzDataError
|
||||
}
|
||||
s.sequence = seqIndexCRC32
|
||||
fallthrough
|
||||
case seqIndexCRC32:
|
||||
ret = crcValidate(s, b)
|
||||
if ret != xzStreamEnd {
|
||||
return ret
|
||||
}
|
||||
s.temp.buf = s.temp.bufArray[:streamHeaderSize]
|
||||
s.sequence = seqStreamFooter
|
||||
fallthrough
|
||||
case seqStreamFooter:
|
||||
if !fillTemp(s, b) {
|
||||
return xzOK
|
||||
}
|
||||
return decStreamFooter(s)
|
||||
}
|
||||
}
|
||||
/* Never reached */
|
||||
}
|
||||
|
||||
/**
|
||||
* xzDecRun - Run the XZ decoder
|
||||
* @s: Decoder state allocated using xzDecInit
|
||||
* @b: Input and output buffers
|
||||
*
|
||||
* See xzRet for details of return values.
|
||||
*
|
||||
* xzDecRun is a wrapper for decMain to handle some special cases.
|
||||
*
|
||||
* We must return xzBufError when it seems clear that we are not
|
||||
* going to make any progress anymore. This is to prevent the caller
|
||||
* from calling us infinitely when the input file is truncated or
|
||||
* otherwise corrupt. Since zlib-style API allows that the caller
|
||||
* fills the input buffer only when the decoder doesn't produce any
|
||||
* new output, we have to be careful to avoid returning xzBufError
|
||||
* too easily: xzBufError is returned only after the second
|
||||
* consecutive call to xzDecRun that makes no progress.
|
||||
*/
|
||||
func xzDecRun(s *xzDec, b *xzBuf) xzRet {
|
||||
inStart := b.inPos
|
||||
outStart := b.outPos
|
||||
ret := decMain(s, b)
|
||||
if ret == xzOK && inStart == b.inPos && outStart == b.outPos {
|
||||
if s.allowBufError {
|
||||
ret = xzBufError
|
||||
}
|
||||
s.allowBufError = true
|
||||
} else {
|
||||
s.allowBufError = false
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* xzDecInit - Allocate and initialize a XZ decoder state
|
||||
* @dictMax: Maximum size of the LZMA2 dictionary (history buffer) for
|
||||
* decoding. LZMA2 dictionary is always 2^n bytes
|
||||
* or 2^n + 2^(n-1) bytes (the latter sizes are less common
|
||||
* in practice), so other values for dictMax don't make sense.
|
||||
*
|
||||
* dictMax specifies the maximum allowed dictionary size that xzDecRun
|
||||
* may allocate once it has parsed the dictionary size from the stream
|
||||
* headers. This way excessive allocations can be avoided while still
|
||||
* limiting the maximum memory usage to a sane value to prevent running the
|
||||
* system out of memory when decompressing streams from untrusted sources.
|
||||
*
|
||||
* xzDecInit returns a pointer to an xzDec, which is ready to be used with
|
||||
* xzDecRun.
|
||||
*/
|
||||
func xzDecInit(dictMax uint32, header *Header) *xzDec {
|
||||
s := new(xzDec)
|
||||
s.crc32 = crc32.NewIEEE()
|
||||
s.Header = header
|
||||
s.block.hash.sha256 = sha256.New()
|
||||
s.index.hash.sha256 = sha256.New()
|
||||
s.lzma2 = xzDecLZMA2Create(dictMax)
|
||||
xzDecReset(s)
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* xzDecReset - Reset an already allocated decoder state
|
||||
* @s: Decoder state allocated using xzDecInit
|
||||
*
|
||||
* This function can be used to reset the decoder state without
|
||||
* reallocating memory with xzDecInit.
|
||||
*/
|
||||
func xzDecReset(s *xzDec) {
|
||||
s.sequence = seqStreamHeader
|
||||
s.allowBufError = false
|
||||
s.pos = 0
|
||||
s.crc32.Reset()
|
||||
s.check = nil
|
||||
s.CheckType = checkUnset
|
||||
s.block.compressed = 0
|
||||
s.block.uncompressed = 0
|
||||
s.block.count = 0
|
||||
s.block.hash.unpadded = 0
|
||||
s.block.hash.uncompressed = 0
|
||||
s.block.hash.sha256.Reset()
|
||||
s.index.sequence = seqIndexCount
|
||||
s.index.size = 0
|
||||
s.index.count = 0
|
||||
s.index.hash.unpadded = 0
|
||||
s.index.hash.uncompressed = 0
|
||||
s.index.hash.sha256.Reset()
|
||||
s.temp.pos = 0
|
||||
s.temp.buf = s.temp.bufArray[:streamHeaderSize]
|
||||
s.chain = nil
|
||||
s.bcjsUsed = 0
|
||||
s.deltasUsed = 0
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* XZ decompressor utility functions
|
||||
*
|
||||
* Author: Michael Cross <https://github.com/xi2>
|
||||
*
|
||||
* This file has been put into the public domain.
|
||||
* You can do whatever you want with this file.
|
||||
*/
|
||||
|
||||
package xz
|
||||
|
||||
func getLE32(buf []byte) uint32 {
|
||||
return uint32(buf[0]) |
|
||||
uint32(buf[1])<<8 |
|
||||
uint32(buf[2])<<16 |
|
||||
uint32(buf[3])<<24
|
||||
}
|
||||
|
||||
func getBE32(buf []byte) uint32 {
|
||||
return uint32(buf[0])<<24 |
|
||||
uint32(buf[1])<<16 |
|
||||
uint32(buf[2])<<8 |
|
||||
uint32(buf[3])
|
||||
}
|
||||
|
||||
func putLE32(val uint32, buf []byte) {
|
||||
buf[0] = byte(val)
|
||||
buf[1] = byte(val >> 8)
|
||||
buf[2] = byte(val >> 16)
|
||||
buf[3] = byte(val >> 24)
|
||||
return
|
||||
}
|
||||
|
||||
func putBE32(val uint32, buf []byte) {
|
||||
buf[0] = byte(val >> 24)
|
||||
buf[1] = byte(val >> 16)
|
||||
buf[2] = byte(val >> 8)
|
||||
buf[3] = byte(val)
|
||||
return
|
||||
}
|
||||
|
||||
func putLE64(val uint64, buf []byte) {
|
||||
buf[0] = byte(val)
|
||||
buf[1] = byte(val >> 8)
|
||||
buf[2] = byte(val >> 16)
|
||||
buf[3] = byte(val >> 24)
|
||||
buf[4] = byte(val >> 32)
|
||||
buf[5] = byte(val >> 40)
|
||||
buf[6] = byte(val >> 48)
|
||||
buf[7] = byte(val >> 56)
|
||||
return
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* XZ decompressor
|
||||
*
|
||||
* Authors: Lasse Collin <lasse.collin@tukaani.org>
|
||||
* Igor Pavlov <http://7-zip.org/>
|
||||
*
|
||||
* Translation to Go: Michael Cross <https://github.com/xi2>
|
||||
*
|
||||
* This file has been put into the public domain.
|
||||
* You can do whatever you want with this file.
|
||||
*/
|
||||
|
||||
package xz
|
||||
|
||||
/* from linux/include/linux/xz.h **************************************/
|
||||
|
||||
/**
|
||||
* xzRet - Return codes
|
||||
* @xzOK: Everything is OK so far. More input or more
|
||||
* output space is required to continue.
|
||||
* @xzStreamEnd: Operation finished successfully.
|
||||
* @xzUnSupportedCheck: Integrity check type is not supported. Decoding
|
||||
* is still possible by simply calling xzDecRun
|
||||
* again.
|
||||
* @xzMemlimitError: A bigger LZMA2 dictionary would be needed than
|
||||
* allowed by the dictMax argument given to
|
||||
* xzDecInit.
|
||||
* @xzFormatError: File format was not recognized (wrong magic
|
||||
* bytes).
|
||||
* @xzOptionsError: This implementation doesn't support the requested
|
||||
* compression options. In the decoder this means
|
||||
* that the header CRC32 matches, but the header
|
||||
* itself specifies something that we don't support.
|
||||
* @xzDataError: Compressed data is corrupt.
|
||||
* @xzBufError: Cannot make any progress.
|
||||
*
|
||||
* xzBufError is returned when two consecutive calls to XZ code cannot
|
||||
* consume any input and cannot produce any new output. This happens
|
||||
* when there is no new input available, or the output buffer is full
|
||||
* while at least one output byte is still pending. Assuming your code
|
||||
* is not buggy, you can get this error only when decoding a
|
||||
* compressed stream that is truncated or otherwise corrupt.
|
||||
*/
|
||||
type xzRet int
|
||||
|
||||
const (
|
||||
xzOK xzRet = iota
|
||||
xzStreamEnd
|
||||
xzUnsupportedCheck
|
||||
xzMemlimitError
|
||||
xzFormatError
|
||||
xzOptionsError
|
||||
xzDataError
|
||||
xzBufError
|
||||
)
|
||||
|
||||
/**
|
||||
* xzBuf - Passing input and output buffers to XZ code
|
||||
* @in: Input buffer.
|
||||
* @inPos: Current position in the input buffer. This must not exceed
|
||||
* input buffer size.
|
||||
* @out: Output buffer.
|
||||
* @outPos: Current position in the output buffer. This must not exceed
|
||||
* output buffer size.
|
||||
*
|
||||
* Only the contents of the output buffer from out[outPos] onward, and
|
||||
* the variables inPos and outPos are modified by the XZ code.
|
||||
*/
|
||||
type xzBuf struct {
|
||||
in []byte
|
||||
inPos int
|
||||
out []byte
|
||||
outPos int
|
||||
}
|
||||
|
||||
/* All XZ filter IDs */
|
||||
type xzFilterID int64
|
||||
|
||||
const (
|
||||
idDelta xzFilterID = 0x03
|
||||
idBCJX86 xzFilterID = 0x04
|
||||
idBCJPowerPC xzFilterID = 0x05
|
||||
idBCJIA64 xzFilterID = 0x06
|
||||
idBCJARM xzFilterID = 0x07
|
||||
idBCJARMThumb xzFilterID = 0x08
|
||||
idBCJSPARC xzFilterID = 0x09
|
||||
idLZMA2 xzFilterID = 0x21
|
||||
)
|
||||
|
||||
// CheckID is the type of the data integrity check in an XZ stream
|
||||
// calculated from the uncompressed data.
|
||||
type CheckID int
|
||||
|
||||
func (id CheckID) String() string {
|
||||
switch id {
|
||||
case CheckNone:
|
||||
return "None"
|
||||
case CheckCRC32:
|
||||
return "CRC32"
|
||||
case CheckCRC64:
|
||||
return "CRC64"
|
||||
case CheckSHA256:
|
||||
return "SHA256"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
CheckNone CheckID = 0x00
|
||||
CheckCRC32 CheckID = 0x01
|
||||
CheckCRC64 CheckID = 0x04
|
||||
CheckSHA256 CheckID = 0x0A
|
||||
checkMax CheckID = 0x0F
|
||||
checkUnset CheckID = -1
|
||||
)
|
||||
|
||||
// An XZ stream contains a stream header which holds information about
|
||||
// the stream. That information is exposed as fields of the
|
||||
// Reader. Currently it contains only the stream's data integrity
|
||||
// check type.
|
||||
type Header struct {
|
||||
CheckType CheckID // type of the stream's data integrity check
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// Package xz implements XZ decompression natively in Go.
|
||||
//
|
||||
// Usage
|
||||
//
|
||||
// For ease of use, this package is designed to have a similar API to
|
||||
// compress/gzip. See the examples for further details.
|
||||
//
|
||||
// Implementation
|
||||
//
|
||||
// This package is a translation from C to Go of XZ Embedded
|
||||
// (http://tukaani.org/xz/embedded.html) with enhancements made so as
|
||||
// to implement all mandatory and optional parts of the XZ file format
|
||||
// specification v1.0.4. It supports all filters and block check
|
||||
// types, supports multiple streams, and performs index verification
|
||||
// using SHA-256 as recommended by the specification.
|
||||
//
|
||||
// Speed
|
||||
//
|
||||
// On the author's Intel Ivybridge i5, decompression speed is about
|
||||
// half that of the standard XZ Utils (tested with a recent linux
|
||||
// kernel tarball).
|
||||
//
|
||||
// Thanks
|
||||
//
|
||||
// Thanks are due to Lasse Collin and Igor Pavlov, the authors of XZ
|
||||
// Embedded, on whose code package xz is based. It would not exist
|
||||
// without their decision to allow others to modify and reuse their
|
||||
// code.
|
||||
//
|
||||
// Bug reports
|
||||
//
|
||||
// For bug reports relating to this package please contact the author
|
||||
// through https://github.com/xi2/xz/issues, and not the authors of XZ
|
||||
// Embedded.
|
||||
package xz
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* Package xz Go Reader API
|
||||
*
|
||||
* Author: Michael Cross <https://github.com/xi2>
|
||||
*
|
||||
* This file has been put into the public domain.
|
||||
* You can do whatever you want with this file.
|
||||
*/
|
||||
|
||||
package xz
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Package specific errors.
|
||||
var (
|
||||
ErrUnsupportedCheck = errors.New("xz: integrity check type not supported")
|
||||
ErrMemlimit = errors.New("xz: LZMA2 dictionary size exceeds max")
|
||||
ErrFormat = errors.New("xz: file format not recognized")
|
||||
ErrOptions = errors.New("xz: compression options not supported")
|
||||
ErrData = errors.New("xz: data is corrupt")
|
||||
ErrBuf = errors.New("xz: data is truncated or corrupt")
|
||||
)
|
||||
|
||||
// DefaultDictMax is the default maximum dictionary size in bytes used
|
||||
// by the decoder. This value is sufficient to decompress files
|
||||
// created with XZ Utils "xz -9".
|
||||
const DefaultDictMax = 1 << 26 // 64 MiB
|
||||
|
||||
// inBufSize is the input buffer size used by the decoder.
|
||||
const inBufSize = 1 << 13 // 8 KiB
|
||||
|
||||
// A Reader is an io.Reader that can be used to retrieve uncompressed
|
||||
// data from an XZ file.
|
||||
//
|
||||
// In general, an XZ file can be a concatenation of other XZ
|
||||
// files. Reads from the Reader return the concatenation of the
|
||||
// uncompressed data of each.
|
||||
type Reader struct {
|
||||
Header
|
||||
r io.Reader // the wrapped io.Reader
|
||||
multistream bool // true if reader is in multistream mode
|
||||
rEOF bool // true after io.EOF received on r
|
||||
dEOF bool // true after decoder has completed
|
||||
padding int // bytes of stream padding read (or -1)
|
||||
in [inBufSize]byte // backing array for buf.in
|
||||
buf *xzBuf // decoder input/output buffers
|
||||
dec *xzDec // decoder state
|
||||
err error // the result of the last decoder call
|
||||
}
|
||||
|
||||
// NewReader creates a new Reader reading from r. The decompressor
|
||||
// will use an LZMA2 dictionary size up to dictMax bytes in
|
||||
// size. Passing a value of zero sets dictMax to DefaultDictMax. If
|
||||
// an individual XZ stream requires a dictionary size greater than
|
||||
// dictMax in order to decompress, Read will return ErrMemlimit.
|
||||
//
|
||||
// If NewReader is passed a value of nil for r then a Reader is
|
||||
// created such that all read attempts will return io.EOF. This is
|
||||
// useful if you just want to allocate memory for a Reader which will
|
||||
// later be initialized with Reset.
|
||||
//
|
||||
// Due to internal buffering, the Reader may read more data than
|
||||
// necessary from r.
|
||||
func NewReader(r io.Reader, dictMax uint32) (*Reader, error) {
|
||||
if dictMax == 0 {
|
||||
dictMax = DefaultDictMax
|
||||
}
|
||||
z := &Reader{
|
||||
r: r,
|
||||
multistream: true,
|
||||
padding: -1,
|
||||
buf: &xzBuf{},
|
||||
}
|
||||
if r == nil {
|
||||
z.rEOF, z.dEOF = true, true
|
||||
}
|
||||
z.dec = xzDecInit(dictMax, &z.Header)
|
||||
var err error
|
||||
if r != nil {
|
||||
_, err = z.Read(nil) // read stream header
|
||||
}
|
||||
return z, err
|
||||
}
|
||||
|
||||
// decode is a wrapper around xzDecRun that additionally handles
|
||||
// stream padding. It treats the padding as a kind of stream that
|
||||
// decodes to nothing.
|
||||
//
|
||||
// When decoding padding, z.padding >= 0
|
||||
// When decoding a real stream, z.padding == -1
|
||||
func (z *Reader) decode() (ret xzRet) {
|
||||
if z.padding >= 0 {
|
||||
// read all padding in input buffer
|
||||
for z.buf.inPos < len(z.buf.in) &&
|
||||
z.buf.in[z.buf.inPos] == 0 {
|
||||
z.buf.inPos++
|
||||
z.padding++
|
||||
}
|
||||
switch {
|
||||
case z.buf.inPos == len(z.buf.in) && z.rEOF:
|
||||
// case: out of padding. no more input data available
|
||||
if z.padding%4 != 0 {
|
||||
ret = xzDataError
|
||||
} else {
|
||||
ret = xzStreamEnd
|
||||
}
|
||||
case z.buf.inPos == len(z.buf.in):
|
||||
// case: read more padding next loop iteration
|
||||
ret = xzOK
|
||||
default:
|
||||
// case: out of padding. more input data available
|
||||
if z.padding%4 != 0 {
|
||||
ret = xzDataError
|
||||
} else {
|
||||
xzDecReset(z.dec)
|
||||
ret = xzStreamEnd
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ret = xzDecRun(z.dec, z.buf)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (z *Reader) Read(p []byte) (n int, err error) {
|
||||
// restore err
|
||||
err = z.err
|
||||
// set decoder output buffer to p
|
||||
z.buf.out = p
|
||||
z.buf.outPos = 0
|
||||
for {
|
||||
// update n
|
||||
n = z.buf.outPos
|
||||
// if last call to decoder ended with an error, return that error
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
// if decoder has finished, return with err == io.EOF
|
||||
if z.dEOF {
|
||||
err = io.EOF
|
||||
break
|
||||
}
|
||||
// if p full, return with err == nil, unless we have not yet
|
||||
// read the stream header with Read(nil)
|
||||
if n == len(p) && z.CheckType != checkUnset {
|
||||
break
|
||||
}
|
||||
// if needed, read more data from z.r
|
||||
if z.buf.inPos == len(z.buf.in) && !z.rEOF {
|
||||
rn, e := z.r.Read(z.in[:])
|
||||
if e != nil && e != io.EOF {
|
||||
// read error
|
||||
err = e
|
||||
break
|
||||
}
|
||||
if e == io.EOF {
|
||||
z.rEOF = true
|
||||
}
|
||||
// set new input buffer in z.buf
|
||||
z.buf.in = z.in[:rn]
|
||||
z.buf.inPos = 0
|
||||
}
|
||||
// decode more data
|
||||
ret := z.decode()
|
||||
switch ret {
|
||||
case xzOK:
|
||||
// no action needed
|
||||
case xzStreamEnd:
|
||||
if z.padding >= 0 {
|
||||
z.padding = -1
|
||||
if !z.multistream || z.rEOF {
|
||||
z.dEOF = true
|
||||
}
|
||||
} else {
|
||||
z.padding = 0
|
||||
}
|
||||
case xzUnsupportedCheck:
|
||||
err = ErrUnsupportedCheck
|
||||
case xzMemlimitError:
|
||||
err = ErrMemlimit
|
||||
case xzFormatError:
|
||||
err = ErrFormat
|
||||
case xzOptionsError:
|
||||
err = ErrOptions
|
||||
case xzDataError:
|
||||
err = ErrData
|
||||
case xzBufError:
|
||||
err = ErrBuf
|
||||
}
|
||||
// save err
|
||||
z.err = err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Multistream controls whether the reader is operating in multistream
|
||||
// mode.
|
||||
//
|
||||
// If enabled (the default), the Reader expects the input to be a
|
||||
// sequence of XZ streams, possibly interspersed with stream padding,
|
||||
// which it reads one after another. The effect is that the
|
||||
// concatenation of a sequence of XZ streams or XZ files is
|
||||
// treated as equivalent to the compressed result of the concatenation
|
||||
// of the sequence. This is standard behaviour for XZ readers.
|
||||
//
|
||||
// Calling Multistream(false) disables this behaviour; disabling the
|
||||
// behaviour can be useful when reading file formats that distinguish
|
||||
// individual XZ streams. In this mode, when the Reader reaches the
|
||||
// end of the stream, Read returns io.EOF. To start the next stream,
|
||||
// call z.Reset(nil) followed by z.Multistream(false). If there is no
|
||||
// next stream, z.Reset(nil) will return io.EOF.
|
||||
func (z *Reader) Multistream(ok bool) {
|
||||
z.multistream = ok
|
||||
}
|
||||
|
||||
// Reset, for non-nil values of io.Reader r, discards the Reader z's
|
||||
// state and makes it equivalent to the result of its original state
|
||||
// from NewReader, but reading from r instead. This permits reusing a
|
||||
// Reader rather than allocating a new one.
|
||||
//
|
||||
// If you wish to leave r unchanged use z.Reset(nil). This keeps r
|
||||
// unchanged and ensures internal buffering is preserved. If the
|
||||
// Reader was at the end of a stream it is then ready to read any
|
||||
// follow on streams. If there are no follow on streams z.Reset(nil)
|
||||
// returns io.EOF. If the Reader was not at the end of a stream then
|
||||
// z.Reset(nil) does nothing.
|
||||
func (z *Reader) Reset(r io.Reader) error {
|
||||
switch {
|
||||
case r == nil:
|
||||
z.multistream = true
|
||||
if !z.dEOF {
|
||||
return nil
|
||||
}
|
||||
if z.rEOF {
|
||||
return io.EOF
|
||||
}
|
||||
z.dEOF = false
|
||||
_, err := z.Read(nil) // read stream header
|
||||
return err
|
||||
default:
|
||||
z.r = r
|
||||
z.multistream = true
|
||||
z.rEOF = false
|
||||
z.dEOF = false
|
||||
z.padding = -1
|
||||
z.buf.in = nil
|
||||
z.buf.inPos = 0
|
||||
xzDecReset(z.dec)
|
||||
z.err = nil
|
||||
_, err := z.Read(nil) // read stream header
|
||||
return err
|
||||
}
|
||||
}
|
||||
Vendored
+3
@@ -83,6 +83,9 @@ github.com/nats-io/nuid
|
||||
github.com/novox/mesh-host/internal/declaration
|
||||
github.com/novox/mesh-host/rootsearch
|
||||
github.com/novox/mesh-host/validate
|
||||
# github.com/therootcompany/xz v1.0.1
|
||||
## explicit; go 1.15
|
||||
github.com/therootcompany/xz
|
||||
# golang.org/x/crypto v0.57.0
|
||||
## explicit; go 1.26.0
|
||||
golang.org/x/crypto/acme
|
||||
|
||||
Reference in New Issue
Block a user