Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
539a1c16cd | ||
|
|
01fc9e1d73 | ||
|
|
10f76cafc4 |
@@ -6,5 +6,3 @@
|
|||||||
/mesh-controller
|
/mesh-controller
|
||||||
/postgres-provisioner
|
/postgres-provisioner
|
||||||
/redis-provisioner
|
/redis-provisioner
|
||||||
# And where `go build` in cmd/mesh-controller leaves it (novox/hq issue 233).
|
|
||||||
/cmd/mesh-controller/mesh-controller
|
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ RUN CGO_ENABLED=0 go build -trimpath -ldflags '-s -w' -o /mesh-builder ./cmd/mes
|
|||||||
FROM ${ALPINE_BASE}
|
FROM ${ALPINE_BASE}
|
||||||
# git to clone what it is asked to build, and the docker client to build and push it. The daemon
|
# 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
|
# 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
|
# rather than running one inside itself.
|
||||||
# (novox/hq ADR 0316): Go's standard library reads gzip and bzip2 and not xz.
|
RUN apk add --no-cache git docker-cli
|
||||||
RUN apk add --no-cache git docker-cli xz
|
|
||||||
COPY --from=build /mesh-builder /usr/local/bin/mesh-builder
|
COPY --from=build /mesh-builder /usr/local/bin/mesh-builder
|
||||||
ENTRYPOINT ["/usr/local/bin/mesh-builder"]
|
ENTRYPOINT ["/usr/local/bin/mesh-builder"]
|
||||||
|
|||||||
+15
-67
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/novox/mesh-controller/internal/broker"
|
"github.com/novox/mesh-controller/internal/broker"
|
||||||
"io"
|
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -55,9 +54,6 @@ func assignWith(ctx context.Context, open *stores, node string, opts assignOptio
|
|||||||
if err := refusedMovingTheController(modules); err != nil {
|
if err := refusedMovingTheController(modules); err != nil {
|
||||||
return "", err
|
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
|
// 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).
|
// be taken without ever having been previewed (novox/hq ADR 0100).
|
||||||
ctx, release, err := holdNodes(ctx, open, []string{node})
|
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 {
|
for _, line := range settled {
|
||||||
answer += "\n " + line
|
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
|
// 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
|
// 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
|
// 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.
|
// no credential yet; kept when it has one, so re-assigning rotates nothing.
|
||||||
var issued []string
|
|
||||||
for _, module := range added {
|
for _, module := range added {
|
||||||
if line := issueOnAssign(ctx, open, node, module); line != "" {
|
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
|
plan, _, err := planFor(ctx, open, node)
|
||||||
// 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
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Kept, and still refused. Both halves are the answer, and the rest of the mesh is still
|
// 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.
|
// 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 {
|
if len(modules) == 0 {
|
||||||
return "", fmt.Errorf("unassign %s names no module", node)
|
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})
|
ctx, release, err := holdNodes(ctx, open, []string{node})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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",
|
answer := fmt.Sprintf("%s no longer runs %s — run `push %s` to make it so",
|
||||||
node, strings.Join(modules, ", "), node)
|
node, strings.Join(modules, ", "), node)
|
||||||
after := resolvedAfter(ctx, open, shelf, node)
|
var left []string
|
||||||
for _, line := range unheldChangeTo(shelf, node, assigned, after.modules, after.unheld) {
|
for _, a := range assigned {
|
||||||
answer += "\n " + line
|
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
|
for _, line := range unheldChange(shelf, node, assigned, left) {
|
||||||
// mesh's reads said it, on standard error and once per other machine.
|
answer += "\n " + line
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// What it leaves behind that is irreplaceable is kept and retired, never removed (novox/hq ADR 0233).
|
// 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) {
|
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
|
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,
|
// 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
|
// 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.
|
// command API and the controller seat's verbs as they do from the command line.
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/novox/mesh-controller/internal/conditions"
|
|
||||||
"github.com/novox/mesh-controller/internal/inventory"
|
|
||||||
)
|
|
||||||
|
|
||||||
// kindApplyFailing is S21's kind: a machine whose declarations have been refused, or applied only in part,
|
|
||||||
// for longer than one apply interval (novox/hq issue 233).
|
|
||||||
const kindApplyFailing = "apply-failing"
|
|
||||||
|
|
||||||
// applyEvery is how often a node-engine applies what it was last told when nothing new arrives: its
|
|
||||||
// ReconcileEvery. A failure still there after one more of them is not one the next apply clears.
|
|
||||||
const applyEvery = 5 * time.Minute
|
|
||||||
|
|
||||||
// packageManagerKey is how a node-engine names a package manager that does not answer among what failed
|
|
||||||
// (mesh-host's probe.go): "the package manager (pacman)".
|
|
||||||
const packageManagerKey = "the package manager ("
|
|
||||||
|
|
||||||
// watchApplyFailing is S21: **a machine that does not apply what it was sent is said by the mesh, not only
|
|
||||||
// to someone who asks for `status`** (novox/hq issue 233, issue 187's family). A workstation refused every
|
|
||||||
// declaration for an hour and a half because its package manager did not answer, and the mesh's only word
|
|
||||||
// on it was a line in `status`. Raised once the machine's first report of this failure is older than one apply
|
|
||||||
// interval **while the machine is heard** — judged by time, not by how many reports said it: a node-engine
|
|
||||||
// says a re-apply only when how it went changed, so the same failure is one report however long it lasts.
|
|
||||||
// Names the refusal's or the failure's words; cleared by the clean apply the node-engine then says. Not while
|
|
||||||
// the machine is silent, asleep or not known: S1 says silent, and asleep is not lost.
|
|
||||||
func watchApplyFailing(f *signalFacts) []conditions.Observation {
|
|
||||||
heard := map[string]bool{}
|
|
||||||
for _, m := range f.machines {
|
|
||||||
if !m.asleep() && !m.lastHeard.IsZero() && f.now.Sub(m.lastHeard) <= heartbeatBound(m.every) {
|
|
||||||
heard[m.name] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var out []conditions.Observation
|
|
||||||
for _, d := range f.doing {
|
|
||||||
if !d.Wrong() || d.Since == nil || f.now.Sub(*d.Since) <= applyEvery || !heard[d.Node] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, applyFailing(d, f.now))
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// applyFailing is one machine's S21 condition.
|
|
||||||
func applyFailing(d inventory.Doing, now time.Time) conditions.Observation {
|
|
||||||
m := d.Node
|
|
||||||
since := d.Since.UTC().Format("2006-01-02 15:04 MST")
|
|
||||||
o := conditions.Observation{Scope: conditions.ScopeMachine, ID: m, Kind: kindApplyFailing, Machine: m,
|
|
||||||
Severity: conditions.Warning, Said: doingWords(d),
|
|
||||||
Resolved: m + " applies what it is sent again"}
|
|
||||||
how := "refused its declaration whole"
|
|
||||||
if d.Outcome != inventory.OutcomeRefused {
|
|
||||||
how = "applied its declaration only in part"
|
|
||||||
}
|
|
||||||
o.Summary = fmt.Sprintf("%s has %s since %s (bound %s): %s", m, how, since, ago(applyEvery), failedNames(d))
|
|
||||||
o.Headline = m + " is not applying its update"
|
|
||||||
o.Explanation = fmt.Sprintf("%s has %s for %s, and trying again has not helped. The details say what fails.",
|
|
||||||
m, plainHow(d), humanDuration(now.Sub(*d.Since)))
|
|
||||||
if tool := unansweringTool(d); tool != "" {
|
|
||||||
o.Summary = fmt.Sprintf("%s: %s does not answer, so no package reaches it; everything else it was sent "+
|
|
||||||
"is applied, since %s (bound %s)", m, tool, since, ago(applyEvery))
|
|
||||||
o.Headline = fmt.Sprintf("%s cannot install packages", m)
|
|
||||||
o.Explanation = fmt.Sprintf("The package manager on %s (%s) has not answered for %s, so no package reaches "+
|
|
||||||
"it. Everything else is still applied, so an update that repairs it can land; if none does, it needs a "+
|
|
||||||
"look on the machine.", m, tool, humanDuration(now.Sub(*d.Since)))
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
// plainHow is how a machine failed, as the operator reads it.
|
|
||||||
func plainHow(d inventory.Doing) string {
|
|
||||||
if d.Outcome == inventory.OutcomeRefused {
|
|
||||||
return "refused every update"
|
|
||||||
}
|
|
||||||
return "applied its updates only in part"
|
|
||||||
}
|
|
||||||
|
|
||||||
// unansweringTool is the package manager a machine said does not answer, or "".
|
|
||||||
func unansweringTool(d inventory.Doing) string {
|
|
||||||
for _, r := range d.Failed {
|
|
||||||
if rest, ok := strings.CutPrefix(r.ID, packageManagerKey); ok {
|
|
||||||
return strings.TrimSuffix(rest, ")")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// failedNames is what failed, by name: the refusal, or the failed resources' ids.
|
|
||||||
func failedNames(d inventory.Doing) string {
|
|
||||||
if d.Outcome == inventory.OutcomeRefused {
|
|
||||||
return "refused"
|
|
||||||
}
|
|
||||||
ids := make([]string, 0, len(d.Failed))
|
|
||||||
for _, r := range d.Failed {
|
|
||||||
ids = append(ids, r.ID)
|
|
||||||
}
|
|
||||||
sort.Strings(ids)
|
|
||||||
return "failed " + strings.Join(ids, ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// doingWords is the machine's own words on what failed: the evidence, kept inside the mesh.
|
|
||||||
func doingWords(d inventory.Doing) string {
|
|
||||||
if d.Outcome == inventory.OutcomeRefused {
|
|
||||||
return "refused: " + oneLine(d.Refused)
|
|
||||||
}
|
|
||||||
var parts []string
|
|
||||||
for _, r := range d.Failed {
|
|
||||||
parts = append(parts, r.ID+": "+oneLine(r.Error))
|
|
||||||
}
|
|
||||||
sort.Strings(parts)
|
|
||||||
return strings.Join(parts, "; ")
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/novox/mesh-controller/internal/conditions"
|
|
||||||
"github.com/novox/mesh-controller/internal/inventory"
|
|
||||||
)
|
|
||||||
|
|
||||||
// unansweringPackageManager is anchor's last report as the workstation's read on 2026-10-04, once its
|
|
||||||
// node-engine applies the rest (novox/hq issue 233): the package failed and the package manager did not answer,
|
|
||||||
// the same failure on times reports since for before now.
|
|
||||||
func unansweringPackageManager(now time.Time, forHowLong time.Duration, times int) inventory.Doing {
|
|
||||||
since := now.Add(-forHowLong)
|
|
||||||
return inventory.Doing{Node: "anchor", Outcome: inventory.OutcomeFailed, At: now.Add(-time.Second),
|
|
||||||
Since: &since, Times: times, Applied: 41, Failed: []inventory.FailedResource{
|
|
||||||
{ID: "apply", Error: `applying "pacman.base": nothing can be said about "base": this is the arch host and ` +
|
|
||||||
"pacman does not answer here. Either this machine is not Arch, or its package database is broken: " +
|
|
||||||
"pacman exited 1: error: config file /etc/pacman.conf could not be read: No such file or directory"},
|
|
||||||
{ID: "the package manager (pacman)", Error: "pacman does not answer here, so every package in this " +
|
|
||||||
"declaration fails and the rest of it is applied (novox/hq issue 233): pacman exited 1: error: config " +
|
|
||||||
"file /etc/pacman.conf could not be read: No such file or directory"}}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **A machine whose package manager does not answer is raised by the mesh itself, naming the tool and the
|
|
||||||
// probe's words** (novox/hq issue 233): the words in the evidence, the path kept out of the summary.
|
|
||||||
func TestAPackageManagerThatDoesNotAnswerIsRaisedNamingItsWords(t *testing.T) {
|
|
||||||
now := time.Date(2026, 10, 4, 14, 0, 0, 0, time.UTC)
|
|
||||||
f := calm(now)
|
|
||||||
f.doing = []inventory.Doing{unansweringPackageManager(now, 90*time.Minute, 18)}
|
|
||||||
got := watchApplyFailing(f)
|
|
||||||
if len(got) != 1 {
|
|
||||||
t.Fatalf("raised %d conditions, want one: %+v", len(got), got)
|
|
||||||
}
|
|
||||||
o := got[0]
|
|
||||||
if o.Key() != "machine.anchor.apply-failing" || o.Machine != "anchor" {
|
|
||||||
t.Errorf("the condition is not the machine's: %s", o.Key())
|
|
||||||
}
|
|
||||||
if !strings.Contains(o.Summary, "pacman does not answer") || !strings.Contains(o.Summary, "since 2026-10-04 12:30") {
|
|
||||||
t.Errorf("the summary does not name the tool and since when: %s", o.Summary)
|
|
||||||
}
|
|
||||||
if strings.Contains(o.Summary, "/etc/") {
|
|
||||||
t.Errorf("the summary carries a path, which belongs in the evidence: %s", o.Summary)
|
|
||||||
}
|
|
||||||
if !strings.Contains(o.Said, "could not be read") {
|
|
||||||
t.Errorf("the evidence does not carry the probe's words: %s", o.Said)
|
|
||||||
}
|
|
||||||
if !strings.Contains(o.Headline, "cannot install packages") || o.Explanation == "" || o.Resolved == "" {
|
|
||||||
t.Errorf("the plain words do not say it: %+v", o)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **A whole refusal is raised too**, with the node-engine's words: what the workstation's report said before the
|
|
||||||
// node-engine applied the rest.
|
|
||||||
func TestARefusalThatGoesOnIsRaised(t *testing.T) {
|
|
||||||
now := time.Date(2026, 10, 4, 14, 0, 0, 0, time.UTC)
|
|
||||||
since := now.Add(-time.Hour)
|
|
||||||
f := calm(now)
|
|
||||||
f.doing = []inventory.Doing{{Node: "anchor", Outcome: inventory.OutcomeRefused, Since: &since, Times: 12,
|
|
||||||
Refused: "this is the arch host and pacman does not answer here", At: now.Add(-time.Minute)}}
|
|
||||||
got := watchApplyFailing(f)
|
|
||||||
if len(got) != 1 || !strings.Contains(got[0].Summary, "refused its declaration whole") ||
|
|
||||||
!strings.Contains(got[0].Said, "pacman does not answer") {
|
|
||||||
t.Fatalf("a refusal that went on for an hour was not raised with its words: %+v", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **Judged by time while the machine is heard, not by how many reports said it** (review of novox/hq issue 233):
|
|
||||||
// a node-engine says the same failure once, so one report an apply interval old is a fault that goes on; one
|
|
||||||
// younger is not yet; and a silent or sleeping machine is S1's.
|
|
||||||
func TestOneReportOlderThanAnApplyIsRaisedAndAQuietMachineIsNot(t *testing.T) {
|
|
||||||
now := time.Date(2026, 10, 4, 14, 0, 0, 0, time.UTC)
|
|
||||||
f := calm(now)
|
|
||||||
f.doing = []inventory.Doing{unansweringPackageManager(now, 6*time.Minute, 1)}
|
|
||||||
if got := watchApplyFailing(f); len(got) != 1 {
|
|
||||||
t.Errorf("one report of a failure six minutes old was not raised: %+v", got)
|
|
||||||
}
|
|
||||||
f.doing = []inventory.Doing{unansweringPackageManager(now, 4*time.Minute, 1)}
|
|
||||||
if got := watchApplyFailing(f); len(got) != 0 {
|
|
||||||
t.Errorf("a failure four minutes old was raised before one apply could clear it: %+v", got)
|
|
||||||
}
|
|
||||||
f.doing = []inventory.Doing{unansweringPackageManager(now, time.Hour, 1)}
|
|
||||||
f.machines[0].lastHeard = now.Add(-time.Hour)
|
|
||||||
if got := watchApplyFailing(f); len(got) != 0 {
|
|
||||||
t.Errorf("a silent machine was raised for its last report: %+v", got)
|
|
||||||
}
|
|
||||||
f = calm(now)
|
|
||||||
f.doing = []inventory.Doing{unansweringPackageManager(now, time.Hour, 1)}
|
|
||||||
f.doing[0].Node = "a-machine-the-mesh-does-not-know"
|
|
||||||
if got := watchApplyFailing(f); len(got) != 0 {
|
|
||||||
t.Errorf("a machine not heard of was raised: %+v", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **A failed re-apply followed by a clean one clears S21** (review of novox/hq issue 233): the node-engine says
|
|
||||||
// the clean re-apply, the store keeps it as applied with no failure since, and the condition the failure raised
|
|
||||||
// is cleared through the keeper on the next tick.
|
|
||||||
func TestACleanReapplyAfterAFailedOneClearsTheCondition(t *testing.T) {
|
|
||||||
now := time.Date(2026, 10, 4, 14, 0, 0, 0, time.UTC)
|
|
||||||
store := conditions.NewInMemory()
|
|
||||||
k := conditions.NewKeeper(t.Context(), conditions.Options{Store: store, History: store,
|
|
||||||
Teller: &conditions.Told{}, Now: func() time.Time { return now }})
|
|
||||||
defer k.Close(context.Background())
|
|
||||||
w := &watchdogs{keeper: k, started: now.Add(-time.Hour)}
|
|
||||||
|
|
||||||
failing := calm(now)
|
|
||||||
failing.doing = []inventory.Doing{unansweringPackageManager(now, 20*time.Minute, 1)}
|
|
||||||
w.see(t.Context(), failing)
|
|
||||||
open, err := k.Open(t.Context())
|
|
||||||
if err != nil || len(open) != 1 || open[0].Kind != kindApplyFailing {
|
|
||||||
t.Fatalf("the failed re-apply raised %+v (%v), want apply-failing", open, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
clean := calm(now.Add(5 * time.Minute))
|
|
||||||
clean.doing = nil // the clean re-apply recorded: NotDoingWhatTheyWereTold no longer lists the machine
|
|
||||||
w.see(t.Context(), clean)
|
|
||||||
if open, _ := k.Open(t.Context()); len(open) != 0 {
|
|
||||||
t.Fatalf("the condition stayed open after the clean re-apply: %+v", open)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/novox/mesh-controller/internal/catalogue"
|
|
||||||
)
|
|
||||||
|
|
||||||
// An own secret issued outside the mesh that nobody gave is never made (novox/hq issue 401, ADR 0228). Seen on
|
|
||||||
// 2026-10-10: the send made a random value for the mounts module's smb-credentials and placed it, and the module read
|
|
||||||
// it as a credential given and wrong. The send now makes none, places none, and places a value only once a person
|
|
||||||
// gave one — and a value the mesh made for it before is not placed either.
|
|
||||||
|
|
||||||
// anAssistant is a module with a secret the mesh makes and one only an outside party issues.
|
|
||||||
func anAssistant() catalogue.Manifest {
|
|
||||||
return catalogue.Manifest{Module: "assistant", Version: "1",
|
|
||||||
OwnSecrets: catalogue.OwnSecrets{
|
|
||||||
"password": {Path: "/var/lib/mesh/assistant/password", Taken: catalogue.TakenAtStart},
|
|
||||||
"vendor-key": {Path: "/var/lib/mesh/assistant/vendor-key", Taken: catalogue.TakenAtStart, IssuedBy: catalogue.IssuedOutside},
|
|
||||||
},
|
|
||||||
Resources: []map[string]any{
|
|
||||||
{"id": "state", "type": "directory", "path": "/var/lib/mesh/assistant", "mode": "0700"},
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sentTo composes what a push sends to a machine, on the send path.
|
|
||||||
func sentTo(t *testing.T, open *stores, node string) []map[string]any {
|
|
||||||
t.Helper()
|
|
||||||
plan, settings, err := planFor(t.Context(), open, node)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
declared, err := declarationFor(t.Context(), open, node, plan, settings)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("the send to %s did not compose: %v", node, err)
|
|
||||||
}
|
|
||||||
return declared.Resources
|
|
||||||
}
|
|
||||||
|
|
||||||
func placedAt(resources []map[string]any, path string) map[string]any {
|
|
||||||
for _, r := range resources {
|
|
||||||
if r["path"] == path && r["sealed"] != nil {
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTheSendMakesNoValueForAnOutsideSecretNobodyGave(t *testing.T) {
|
|
||||||
open := aMesh(t)
|
|
||||||
ctx := t.Context()
|
|
||||||
register(t, open, anAssistant())
|
|
||||||
if _, err := assign(ctx, open, "laptop", "assistant"); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sent := sentTo(t, open, "laptop")
|
|
||||||
if placedAt(sent, "/var/lib/mesh/assistant/password") == nil {
|
|
||||||
t.Fatal("the secret the mesh makes was not placed")
|
|
||||||
}
|
|
||||||
if got := placedAt(sent, "/var/lib/mesh/assistant/vendor-key"); got != nil {
|
|
||||||
t.Fatalf("a secret nobody gave was placed: %v", got)
|
|
||||||
}
|
|
||||||
if _, held, err := open.inventory.ModuleSecretIfIssued(ctx, "laptop", "assistant", "vendor-key"); err != nil || held {
|
|
||||||
t.Fatalf("the send made a value for a secret issued outside the mesh (held %v, %v)", held, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A value the mesh made before this rule is not one a person gave: it is not placed.
|
|
||||||
if _, err := open.inventory.SecretForModule(ctx, "laptop", "assistant", "vendor-key"); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if got := placedAt(sentTo(t, open, "laptop"), "/var/lib/mesh/assistant/vendor-key"); got != nil {
|
|
||||||
t.Fatalf("a value the mesh made for a secret issued outside was placed: %v", got)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given, it is placed.
|
|
||||||
if err := open.inventory.AcceptSecretForModule(ctx, "laptop", "assistant", "vendor-key", "the vendor's key"); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
given, ok, err := open.inventory.GivenOwnSecret(ctx, "laptop", "assistant", "vendor-key")
|
|
||||||
if err != nil || !ok || !given.Current {
|
|
||||||
t.Fatalf("the value given is not read as given: %+v %v %v", given, ok, err)
|
|
||||||
}
|
|
||||||
placed := placedAt(sentTo(t, open, "laptop"), "/var/lib/mesh/assistant/vendor-key")
|
|
||||||
if placed == nil || placed["sealed"] != given.Sealed {
|
|
||||||
t.Fatalf("the value given is placed as %v", placed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The read paths answer as the send does: a question about the machine composes without the secret nobody gave, and
|
|
||||||
// asking writes nothing.
|
|
||||||
func TestAQuestionComposesWithoutAnOutsideSecretNobodyGave(t *testing.T) {
|
|
||||||
open := aMesh(t)
|
|
||||||
ctx := t.Context()
|
|
||||||
register(t, open, anAssistant())
|
|
||||||
if _, err := assign(ctx, open, "laptop", "assistant"); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
pushedBy(t, open, "laptop") // the mesh's own secret is made by the send
|
|
||||||
plan, settings, err := planFor(ctx, open, "laptop")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
gens, err := generators(ctx, open)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, choosing := range []Choosing{Reading, Foreseeing} {
|
|
||||||
got, err := declarationWith(ctx, open, "laptop", plan, settings, gens, choosing)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%v: %v", choosing, err)
|
|
||||||
}
|
|
||||||
if placedAt(got.Resources, "/var/lib/mesh/assistant/vendor-key") != nil || len(got.foreseen) != 0 {
|
|
||||||
t.Fatalf("%v: a secret nobody gave is composed (foreseen %v)", choosing, got.foreseen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, held, err := open.inventory.ModuleSecretIfIssued(ctx, "laptop", "assistant", "vendor-key"); err != nil || held {
|
|
||||||
t.Fatalf("a question made a value (held %v, %v)", held, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -81,13 +81,6 @@ var plainWordings = map[string]func(conditions.Observation) words{
|
|||||||
"It usually catches up by itself.", m, m),
|
"It usually catches up by itself.", m, m),
|
||||||
Resolved: m + " applied the change"}
|
Resolved: m + " applied the change"}
|
||||||
}),
|
}),
|
||||||
kindApplyFailing: worded(func(o conditions.Observation) words {
|
|
||||||
m := machineOr(o, "a machine")
|
|
||||||
return words{Headline: m + " is not applying its update",
|
|
||||||
Explanation: fmt.Sprintf("%s has refused or only partly applied what the mesh sent it, again and again. "+
|
|
||||||
"The details say what fails.", conditions.Capital(m)),
|
|
||||||
Resolved: m + " applies what it is sent again"}
|
|
||||||
}),
|
|
||||||
"tools-silent": worded(func(o conditions.Observation) words {
|
"tools-silent": worded(func(o conditions.Observation) words {
|
||||||
m := machineOr(o, "a machine")
|
m := machineOr(o, "a machine")
|
||||||
return words{Headline: "Tools on " + m + " are not answering",
|
return words{Headline: "Tools on " + m + " are not answering",
|
||||||
|
|||||||
+22
-94
@@ -292,7 +292,7 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// Said, not skipped: a machine dropped here offers nothing and holds nothing as far
|
// 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).
|
// 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
|
continue
|
||||||
}
|
}
|
||||||
firstHeld = append(firstHeld, got.Claims...)
|
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
|
// 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.
|
// design — that is not the machine being dropped, it is the view being partial.
|
||||||
if exclude == "" {
|
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
|
continue
|
||||||
}
|
}
|
||||||
@@ -347,35 +347,6 @@ func theRestOfTheMesh(ctx context.Context, inv *inventory.Inventory,
|
|||||||
return world, nil
|
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.
|
// declarationFor is everything a node would be sent.
|
||||||
//
|
//
|
||||||
// One place, because there were three and one of them was written before credentials existed and
|
// One place, because there were three and one of them was written before credentials existed and
|
||||||
@@ -590,13 +561,6 @@ func busCredentialIssued(ctx context.Context, inv *inventory.Inventory, node, mo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// givenUnderAnOldKey is the refusal for a value a person gave — a family's member, or an own secret issued outside
|
|
||||||
// the mesh — sealed to a key the machine no longer holds: the mesh cannot make another, so it is given again.
|
|
||||||
func givenUnderAnOldKey(module, node, name string) error {
|
|
||||||
return fmt.Errorf("%s on %s holds %q, which was given to the mesh rather than made by it, and %s has "+
|
|
||||||
"since generated a new sealing key. The mesh cannot make another; give it again", module, node, name, node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// renderingFor is everything a node's declaration is composed with, and the node's record.
|
// renderingFor is everything a node's declaration is composed with, and the node's record.
|
||||||
func renderingFor(ctx context.Context, open *stores, node string,
|
func renderingFor(ctx context.Context, open *stores, node string,
|
||||||
plan catalogue.Resolution, settings catalogue.SettingsBy,
|
plan catalogue.Resolution, settings catalogue.SettingsBy,
|
||||||
@@ -715,7 +679,10 @@ func renderingFor(ctx context.Context, open *stores, node string,
|
|||||||
}
|
}
|
||||||
if !g.Current {
|
if !g.Current {
|
||||||
if choosing == Allocating {
|
if choosing == Allocating {
|
||||||
return catalogue.Rendering{}, inventory.Node{}, givenUnderAnOldKey(m.Module, node, g.Name)
|
return catalogue.Rendering{}, inventory.Node{}, fmt.Errorf(
|
||||||
|
"%s on %s holds %q, which was given to the mesh rather than made by it, and %s has "+
|
||||||
|
"since generated a new sealing key. The mesh cannot make another; give it again",
|
||||||
|
m.Module, node, g.Name, node)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -725,31 +692,7 @@ func renderingFor(ctx context.Context, open *stores, node string,
|
|||||||
needed[m.Module][g.Name] = g.Sealed
|
needed[m.Module][g.Name] = g.Sealed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for name, own := range m.OwnSecrets.Plain() {
|
for name := range m.OwnSecrets.Plain() {
|
||||||
// **An own secret issued outside the mesh is never made** (novox/hq issue 401, ADR 0228; ADR 0283 for
|
|
||||||
// a family): only a value a person gave is placed. One not given is nothing — no file, and an empty
|
|
||||||
// value where a file of the module names it — so the module can tell it was not given, where a value
|
|
||||||
// the mesh made looked given and wrong. A value the mesh made for it before is not one either.
|
|
||||||
if own.IssuedBy == catalogue.IssuedOutside {
|
|
||||||
g, given, err := inv.GivenOwnSecret(ctx, node, m.Module, name)
|
|
||||||
if err != nil {
|
|
||||||
return catalogue.Rendering{}, inventory.Node{}, err
|
|
||||||
}
|
|
||||||
if !given {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !g.Current {
|
|
||||||
if choosing == Allocating {
|
|
||||||
return catalogue.Rendering{}, inventory.Node{}, givenUnderAnOldKey(m.Module, node, name)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if needed[m.Module] == nil {
|
|
||||||
needed[m.Module] = map[string]string{}
|
|
||||||
}
|
|
||||||
needed[m.Module][name] = g.Sealed
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Minted on the send path and only read on every other. Making one is an insert, and
|
// Minted on the send path and only read on every other. Making one is an insert, and
|
||||||
// a question that writes is a question that can block against the machine it is about.
|
// a question that writes is a question that can block against the machine it is about.
|
||||||
var sealed string
|
var sealed string
|
||||||
@@ -1808,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
|
// 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.
|
// 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 {
|
func unheldChange(shelf map[string]catalogue.Manifest, node string, before, after []string) []string {
|
||||||
return unheldChangeTo(shelf, node, before, after, unheldOver(shelf, node, after))
|
judge := func(names []string) map[string]catalogue.Unheld {
|
||||||
}
|
var set []catalogue.Manifest
|
||||||
|
for _, n := range names {
|
||||||
// unheldOver is the node's unmet seat dependencies judged over a set of module names alone.
|
if m, known := shelf[n]; known {
|
||||||
func unheldOver(shelf map[string]catalogue.Manifest, node string, names []string) []catalogue.Unheld {
|
set = append(set, m)
|
||||||
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)
|
was, now := judge(before), judge(after)
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
var lines []string
|
var lines []string
|
||||||
for _, k := range sortedNames(is) {
|
for _, k := range sortedNames(now) {
|
||||||
if _, already := was[k]; !already {
|
if _, already := was[k]; !already {
|
||||||
lines = append(lines, "but "+is[k].String())
|
lines = append(lines, "but "+now[k].String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, k := range sortedNames(was) {
|
for _, k := range sortedNames(was) {
|
||||||
if _, still := is[k]; still {
|
if _, still := now[k]; still {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
u := was[k]
|
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
|
|
||||||
}
|
|
||||||
@@ -254,16 +254,6 @@ var signalsTable = []signalRow{
|
|||||||
newest: func(f *signalFacts) time.Time {
|
newest: func(f *signalFacts) time.Time {
|
||||||
return newestOf(f.bus.waits, func(w busWaitFacts) time.Time { return w.since })
|
return newestOf(f.bus.waits, func(w busWaitFacts) time.Time { return w.since })
|
||||||
}},
|
}},
|
||||||
{Row: "S21", Signal: "a clean apply", Emitter: "node-engine", Trigger: "each report of a declaration refused or applied only in part (novox/hq issue 233)",
|
|
||||||
Bound: "one apply interval (5 min) past the first report of the same failure, judged by time while the machine " +
|
|
||||||
"is heard — the node-engine says a re-apply only when how it went changed; not while it is silent or " +
|
|
||||||
"asleep; naming the refusal, or the package manager that does not answer with its words; cleared by the " +
|
|
||||||
"clean apply it then says",
|
|
||||||
Kind: kindApplyFailing, Severity: conditions.Warning, Phase: 2,
|
|
||||||
needs: func(f *signalFacts) error { return f.doingErr }, watch: watchApplyFailing,
|
|
||||||
newest: func(f *signalFacts) time.Time {
|
|
||||||
return newestOf(f.doing, func(d inventory.Doing) time.Time { return d.At })
|
|
||||||
}},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// watchFacts is S14: the snapshot a merge check is fed is older than its bound, or none was kept since
|
// watchFacts is S14: the snapshot a merge check is fed is older than its bound, or none was kept since
|
||||||
|
|||||||
@@ -186,14 +186,6 @@ var suppressions = map[string]suppression{
|
|||||||
},
|
},
|
||||||
past: func(f *signalFacts) { f.refusedSends = []inventory.Send{refusedSend(f.now.Add(-time.Second))} },
|
past: func(f *signalFacts) { f.refusedSends = []inventory.Send{refusedSend(f.now.Add(-time.Second))} },
|
||||||
},
|
},
|
||||||
// A machine whose package manager does not answer (novox/hq issue 233): said once its first report of the
|
|
||||||
// failure is older than one apply interval while it is heard. Inside: exactly five minutes old.
|
|
||||||
"S21": {
|
|
||||||
inside: func(f *signalFacts) { f.doing = []inventory.Doing{unansweringPackageManager(f.now, 5*time.Minute, 1)} },
|
|
||||||
past: func(f *signalFacts) {
|
|
||||||
f.doing = []inventory.Doing{unansweringPackageManager(f.now, 5*time.Minute+time.Second, 1)}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Twice by hand within a fortnight is a healer wanted; once, or the first of two a day too old, is not.
|
// Twice by hand within a fortnight is a healer wanted; once, or the first of two a day too old, is not.
|
||||||
"S15": {
|
"S15": {
|
||||||
inside: func(f *signalFacts) {
|
inside: func(f *signalFacts) {
|
||||||
|
|||||||
@@ -344,58 +344,3 @@ func TestAnyKeyOfAModuleWithATrustedMergeableFileIsRefusedThroughAVerb(t *testin
|
|||||||
t.Fatalf("a file marked untrusted through the verb: %v", err)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -59,10 +59,6 @@ type signalFacts struct {
|
|||||||
// from, each naming its sender (S20, novox/hq issue 234).
|
// from, each naming its sender (S20, novox/hq issue 234).
|
||||||
refusedSends []inventory.Send
|
refusedSends []inventory.Send
|
||||||
refusedSendsErr error
|
refusedSendsErr error
|
||||||
// doing is every machine whose last report was not a clean apply, with since when it has said the same
|
|
||||||
// failure (S21, novox/hq issue 233).
|
|
||||||
doing []inventory.Doing
|
|
||||||
doingErr error
|
|
||||||
|
|
||||||
loop loopFacts
|
loop loopFacts
|
||||||
loopErr error
|
loopErr error
|
||||||
@@ -373,7 +369,6 @@ func (w *watchdogs) gather(ctx context.Context) *signalFacts {
|
|||||||
}
|
}
|
||||||
f.handActs, f.handActsErr = w.gatherHandActs(ctx, now)
|
f.handActs, f.handActsErr = w.gatherHandActs(ctx, now)
|
||||||
f.refusedSends, f.refusedSendsErr = inv.RefusedSendsSince(ctx, now.Add(-generationRefusalsSaid))
|
f.refusedSends, f.refusedSendsErr = inv.RefusedSendsSince(ctx, now.Add(-generationRefusalsSaid))
|
||||||
f.doing, f.doingErr = inv.NotDoingWhatTheyWereTold(ctx)
|
|
||||||
f.facts.taken, _, f.facts.began, f.facts.err = exportedFacts.last()
|
f.facts.taken, _, f.facts.began, f.facts.err = exportedFacts.last()
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ require (
|
|||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
github.com/novox/mesh-host v0.0.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/crypto v0.57.0
|
||||||
golang.org/x/net v0.58.0
|
golang.org/x/net v0.58.0
|
||||||
golang.org/x/sys v0.48.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.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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
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 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -14,13 +14,13 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/novox/mesh-controller/internal/catalogue"
|
"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
|
// 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)
|
buffered := bufio.NewReader(f)
|
||||||
magic, _ := buffered.Peek(6)
|
magic, _ := buffered.Peek(6)
|
||||||
var stream io.Reader
|
var stream io.Reader
|
||||||
var wait func() error
|
|
||||||
switch {
|
switch {
|
||||||
case bytes.HasPrefix(magic, []byte{0x1f, 0x8b}):
|
case bytes.HasPrefix(magic, []byte{0x1f, 0x8b}):
|
||||||
zipped, err := gzip.NewReader(buffered)
|
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")):
|
case bytes.HasPrefix(magic, []byte("BZh")):
|
||||||
stream = bzip2.NewReader(buffered)
|
stream = bzip2.NewReader(buffered)
|
||||||
case bytes.HasPrefix(magic, []byte{0xfd, '7', 'z', 'X', 'Z', 0x00}):
|
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).
|
// Read in Go (github.com/therootcompany/xz, a port of xz-embedded), never by a tool the build seat's image
|
||||||
if _, err := exec.LookPath("xz"); err != nil {
|
// may not carry: the seat's image is the catalogue's build-agent recipe, which installs git and docker-cli
|
||||||
return errors.New("it is compressed with xz, and this build seat has no xz to read it")
|
// 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")
|
zipped, err := xz.NewReader(buffered, xzDictMax)
|
||||||
cmd.Stdin = buffered
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
out, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
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 {
|
stream = zipped
|
||||||
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()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
default:
|
default:
|
||||||
return errors.New("it is not a tar archive compressed with gzip, bzip2 or xz; a single file is kept " +
|
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`")
|
"whole by naming it in `file`")
|
||||||
}
|
}
|
||||||
if err := untar(stream, into, within); err != nil {
|
return untar(stream, into, within)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
// untar writes a tar stream's files and directories within `within` into a directory.
|
||||||
func untar(stream io.Reader, into, within string) error {
|
func untar(stream io.Reader, into, within string) error {
|
||||||
reader := tar.NewReader(stream)
|
reader := tar.NewReader(stream)
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import (
|
|||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash/crc32"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -15,6 +17,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -383,3 +386,84 @@ func TestARedirectOffHttpsIsNotFollowed(t *testing.T) {
|
|||||||
t.Fatalf("%v", err)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -645,11 +645,6 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
|||||||
sealed = ForeseenSealed
|
sealed = ForeseenSealed
|
||||||
*foreseen = append(*foreseen, m.Module+"/"+name)
|
*foreseen = append(*foreseen, m.Module+"/"+name)
|
||||||
}
|
}
|
||||||
if sealed == "" && m.OwnSecrets[name].IssuedBy == IssuedOutside {
|
|
||||||
// **Not given, and the mesh never makes it** (novox/hq issue 401, ADR 0228): no file, as for a
|
|
||||||
// family's member (ADR 0283), so the module reads its absence as not given.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if sealed == "" {
|
if sealed == "" {
|
||||||
// Declared and not made. Refused rather than skipped: a module whose own
|
// Declared and not made. Refused rather than skipped: a module whose own
|
||||||
// credential is silently absent starts, fails to authenticate, and the reason is
|
// credential is silently absent starts, fails to authenticate, and the reason is
|
||||||
@@ -1007,7 +1002,7 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
|||||||
// content is where a setting lands, so a placeholder may only exist once the setting
|
// content is where a setting lands, so a placeholder may only exist once the setting
|
||||||
// has been put in — filling secrets first would look at content that is not yet what
|
// has been put in — filling secrets first would look at content that is not yet what
|
||||||
// the machine receives.
|
// the machine receives.
|
||||||
if err := intoFile(copied, sealed, outsideNotGiven(m, with.Needed[m.Module]), m.Module); err != nil {
|
if err := intoFile(copied, sealed, m.Module); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// **Substituted here, not on the machine.** A bound value is not secret — the mesh
|
// **Substituted here, not on the machine.** A bound value is not secret — the mesh
|
||||||
@@ -2436,9 +2431,6 @@ func secretsReadBy(resource map[string]any, m Manifest, given map[string]string)
|
|||||||
}
|
}
|
||||||
paths := map[string]string{}
|
paths := map[string]string{}
|
||||||
for name, own := range m.OwnSecrets.Plain() {
|
for name, own := range m.OwnSecrets.Plain() {
|
||||||
if own.IssuedBy == IssuedOutside && given[name] == "" {
|
|
||||||
continue // not given, so not placed: nothing to restart on (novox/hq issue 401)
|
|
||||||
}
|
|
||||||
paths[name] = own.Path
|
paths[name] = own.Path
|
||||||
}
|
}
|
||||||
for name, sealed := range given {
|
for name, sealed := range given {
|
||||||
|
|||||||
@@ -1718,7 +1718,6 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
|||||||
// facts about the catalogue and are checked at registration (CatalogueProblems).
|
// facts about the catalogue and are checked at registration (CatalogueProblems).
|
||||||
problems = append(problems, declaredSeatProblems(m)...)
|
problems = append(problems, declaredSeatProblems(m)...)
|
||||||
problems = append(problems, RunsAsProblems(m)...)
|
problems = append(problems, RunsAsProblems(m)...)
|
||||||
problems = append(problems, ReadsHolderProblems(m)...)
|
|
||||||
if m.Computed != "" && len(m.Resources) > 0 {
|
if m.Computed != "" && len(m.Resources) > 0 {
|
||||||
// One or the other. A module that both ships files and has them computed would leave
|
// 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.
|
// 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, invokeProblems(m)...)
|
||||||
problems = append(problems, replacesProblems(m)...)
|
problems = append(problems, replacesProblems(m)...)
|
||||||
problems = append(problems, UnitSettingProblems(m)...)
|
problems = append(problems, UnitSettingProblems(m)...)
|
||||||
problems = append(problems, OwnerSettingProblems(m)...)
|
|
||||||
problems = append(problems, endpointNameProblems(m)...)
|
problems = append(problems, endpointNameProblems(m)...)
|
||||||
problems = append(problems, RouteProblems(m)...)
|
problems = append(problems, RouteProblems(m)...)
|
||||||
for _, port := range m.Guards {
|
for _, port := range m.Guards {
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
package catalogue
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// An own secret issued outside the mesh that nobody gave (novox/hq issue 401, ADR 0228): the mesh makes no value for it,
|
|
||||||
// so the composition places no file, a file of the module that names it says empty there, and nothing restarts on it.
|
|
||||||
// Given, it is placed and filled like any own secret.
|
|
||||||
|
|
||||||
func outsideManifest(t *testing.T) Manifest {
|
|
||||||
t.Helper()
|
|
||||||
m, err := ParseManifest([]byte(`{"module":"assistant","version":"1",
|
|
||||||
"own-secrets":{
|
|
||||||
"password":{"path":"/var/lib/assistant/password.secret","taken":"at-start"},
|
|
||||||
"vendor-key":{"path":"/var/lib/assistant/vendor-key.secret","taken":"at-start","issued-by":"outside"}},
|
|
||||||
"resources":[
|
|
||||||
{"id":"env","type":"file","path":"/var/lib/assistant/env","mode":"0600",
|
|
||||||
"content":"PASSWORD=${secret:password}\nVENDOR_KEY=${secret:vendor-key}\n"},
|
|
||||||
{"id":"server","type":"container","name":"assistant","image":"assistant:1",
|
|
||||||
"env-file":["/var/lib/assistant/env"],
|
|
||||||
"volumes":["/var/lib/assistant/vendor-key.secret:/run/vendor-key:ro"],
|
|
||||||
"secrets-in-environment":"reads its settings from the environment only"}]}`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func resourceAt(out []map[string]any, path string) map[string]any {
|
|
||||||
for _, r := range out {
|
|
||||||
if r["path"] == path {
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func resourceNamed(out []map[string]any, name string) map[string]any {
|
|
||||||
for _, r := range out {
|
|
||||||
if r["name"] == name {
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAnOutsideSecretNotGivenIsNoFileAndAFileNamingItSaysEmpty(t *testing.T) {
|
|
||||||
r := Resolution{Node: "server", Modules: []Manifest{outsideManifest(t)}}
|
|
||||||
out, err := r.Declaration(Rendering{Needed: map[string]map[string]string{"assistant": {"password": "sealed-password"}}})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("a module whose outside secret nobody gave did not compose: %v", err)
|
|
||||||
}
|
|
||||||
if got := resourceAt(out, "/var/lib/assistant/vendor-key.secret"); got != nil {
|
|
||||||
t.Fatalf("a secret nobody gave was placed: %v", got)
|
|
||||||
}
|
|
||||||
env := resourceAt(out, "/var/lib/assistant/env")
|
|
||||||
if env == nil {
|
|
||||||
t.Fatal("the module's own file is not composed")
|
|
||||||
}
|
|
||||||
if content := env["content"].(string); content != "PASSWORD=${secret:password}\nVENDOR_KEY=\n" {
|
|
||||||
t.Fatalf("the file names the secret nobody gave as %q", content)
|
|
||||||
}
|
|
||||||
secrets, _ := env["secrets"].(map[string]any)
|
|
||||||
if len(secrets) != 1 || secrets["password"] != "sealed-password" {
|
|
||||||
t.Fatalf("the file is filled from %v", secrets)
|
|
||||||
}
|
|
||||||
server := resourceNamed(out, "assistant")
|
|
||||||
if on := strings.Join(stringsIn(server["restart-on"]), " "); strings.Contains(on, "vendor-key") {
|
|
||||||
t.Fatalf("the container restarts on a secret nobody gave: %q", on)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAnOutsideSecretGivenIsPlacedAndFilled(t *testing.T) {
|
|
||||||
r := Resolution{Node: "server", Modules: []Manifest{outsideManifest(t)}}
|
|
||||||
out, err := r.Declaration(Rendering{Needed: map[string]map[string]string{"assistant": {
|
|
||||||
"password": "sealed-password", "vendor-key": "sealed-vendor-key"}}})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
placed := resourceAt(out, "/var/lib/assistant/vendor-key.secret")
|
|
||||||
if placed == nil || placed["sealed"] != "sealed-vendor-key" {
|
|
||||||
t.Fatalf("the secret given is placed as %v", placed)
|
|
||||||
}
|
|
||||||
env := resourceAt(out, "/var/lib/assistant/env")
|
|
||||||
if content := env["content"].(string); content != "PASSWORD=${secret:password}\nVENDOR_KEY=${secret:vendor-key}\n" {
|
|
||||||
t.Fatalf("the file was changed: %q", content)
|
|
||||||
}
|
|
||||||
if secrets, _ := env["secrets"].(map[string]any); secrets["vendor-key"] != "sealed-vendor-key" {
|
|
||||||
t.Fatalf("the file is filled from %v", secrets)
|
|
||||||
}
|
|
||||||
server := resourceNamed(out, "assistant")
|
|
||||||
if on := strings.Join(stringsIn(server["restart-on"]), " "); !strings.Contains(on, "assistant."+NeedID("vendor-key")) {
|
|
||||||
t.Fatalf("the container does not restart on the secret given: %q", on)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A secret the mesh makes is still refused when it was not made: only an outside one is left out.
|
|
||||||
func TestAnOwnSecretTheMeshMakesIsStillRefusedWhenNotMade(t *testing.T) {
|
|
||||||
r := Resolution{Node: "server", Modules: []Manifest{outsideManifest(t)}}
|
|
||||||
_, err := r.Declaration(Rendering{})
|
|
||||||
if nm, ok := err.(*NotMadeError); !ok || nm.Name != "password" {
|
|
||||||
t.Fatalf("a secret the mesh makes, not made: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -303,10 +303,6 @@ func (r Resolution) ownRuntimes(with Rendering) ([]map[string]any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if len(words) > 0 {
|
||||||
body, err := json.Marshal(map[string]map[string]string{m.Module: words})
|
body, err := json.Marshal(map[string]map[string]string{m.Module: words})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -310,9 +310,6 @@ var defaultSeats = append([]Seat{
|
|||||||
// node-mounts writes a mount and an automount unit per share on a machine that asks (shares.go).
|
// 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: NFSServerSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0263", Serves: nfsServerVerbs()},
|
||||||
{Name: MountsSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0263", Serves: mountsVerbs()},
|
{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.
|
// The graphical session's roles (novox/hq ADR 0208), last because they are a workstation's.
|
||||||
graphicalSessionSeats()...)
|
graphicalSessionSeats()...)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
|||||||
delivered[s.Delivers] = s.Name
|
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
|
// into node-uplink (novox/hq ADR 0223); thirty-seven
|
||||||
// since the retired node-dns-resolver went (novox/hq ADR 0220); thirty-eight with
|
// 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);
|
// 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,
|
// 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
|
// 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.
|
// mesh-build-machine row goes, when no registered manifest claims it.
|
||||||
if len(Seats()) != 41 {
|
if len(Seats()) != 40 {
|
||||||
t.Errorf("the mesh defines %d seats rather than 41; the set is closed, so a change here is "+
|
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())
|
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,11 +156,7 @@ func secretFilesOf(resources []map[string]any) map[string]bool {
|
|||||||
// A name the module never declared is refused here rather than on the machine. The host would
|
// A name the module never declared is refused here rather than on the machine. The host would
|
||||||
// refuse it too — but it would do so having already been handed a declaration, which reads as the
|
// refuse it too — but it would do so having already been handed a declaration, which reads as the
|
||||||
// mesh sending something broken, and the name it could not find is a manifest's typo.
|
// mesh sending something broken, and the name it could not find is a manifest's typo.
|
||||||
//
|
func intoFile(resource map[string]any, sealed map[string]string, module string) error {
|
||||||
// **An own secret issued outside the mesh that nobody gave is filled with nothing** (novox/hq issue 401): the mesh
|
|
||||||
// never makes one (ADR 0228), and a file is where the module reads it, so the place says empty — the module sees it
|
|
||||||
// was not given, where a value the mesh made looked given and wrong. notGiven names those secrets.
|
|
||||||
func intoFile(resource map[string]any, sealed map[string]string, notGiven map[string]bool, module string) error {
|
|
||||||
if fmt.Sprint(resource["type"]) != "file" {
|
if fmt.Sprint(resource["type"]) != "file" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -175,10 +171,6 @@ func intoFile(resource map[string]any, sealed map[string]string, notGiven map[st
|
|||||||
into := map[string]any{}
|
into := map[string]any{}
|
||||||
for _, name := range used {
|
for _, name := range used {
|
||||||
value := sealed[name]
|
value := sealed[name]
|
||||||
if value == "" && notGiven[name] {
|
|
||||||
content = strings.ReplaceAll(content, "${secret:"+name+"}", "")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if value == "" {
|
if value == "" {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"%s has a file that says ${secret:%s}, and %s has no secret of its own by that "+
|
"%s has a file that says ${secret:%s}, and %s has no secret of its own by that "+
|
||||||
@@ -187,25 +179,10 @@ func intoFile(resource map[string]any, sealed map[string]string, notGiven map[st
|
|||||||
}
|
}
|
||||||
into[name] = value
|
into[name] = value
|
||||||
}
|
}
|
||||||
resource["content"] = content
|
resource["secrets"] = into
|
||||||
if len(into) > 0 {
|
|
||||||
resource["secrets"] = into
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// outsideNotGiven is each own secret of a module issued outside the mesh that this composition holds no value for
|
|
||||||
// (novox/hq issue 401): what intoFile fills with nothing, and what nothing restarts on.
|
|
||||||
func outsideNotGiven(m Manifest, given map[string]string) map[string]bool {
|
|
||||||
out := map[string]bool{}
|
|
||||||
for name, own := range m.OwnSecrets.Plain() {
|
|
||||||
if own.IssuedBy == IssuedOutside && given[name] == "" {
|
|
||||||
out[name] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// namesOr says what a module could have written, because the answer to "that name is wrong" is
|
// namesOr says what a module could have written, because the answer to "that name is wrong" is
|
||||||
// almost always one of two or three right ones.
|
// almost always one of two or three right ones.
|
||||||
func namesOr(sealed map[string]string) string {
|
func namesOr(sealed map[string]string) string {
|
||||||
|
|||||||
@@ -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"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"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
|
// (novox/hq issues 122, 134). ADR 0112 names the operator as one of the four providers; this is the
|
||||||
// operator answering.
|
// operator answering.
|
||||||
//
|
//
|
||||||
// `${setting:<key>}` in a file's content, in a service's unit name and in a directory's owner (a uid or
|
// `${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,
|
||||||
// gid only, issue 279) 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
|
// 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**,
|
// `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
|
// 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" {
|
if fmt.Sprint(resource["type"]) == "service" {
|
||||||
return settingIntoUnit(resource, layers, module)
|
return settingIntoUnit(resource, layers, module)
|
||||||
}
|
}
|
||||||
if fmt.Sprint(resource["type"]) == "directory" {
|
|
||||||
return settingIntoOwner(resource, layers, module)
|
|
||||||
}
|
|
||||||
if fmt.Sprint(resource["type"]) != "file" {
|
if fmt.Sprint(resource["type"]) != "file" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -148,107 +143,6 @@ func settingIntoUnit(resource map[string]any, layers []Layer, module string) err
|
|||||||
return nil
|
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) {
|
func settingValue(layers []Layer, key string) (any, bool) {
|
||||||
var value any
|
var value any
|
||||||
set := false
|
set := false
|
||||||
@@ -277,25 +171,9 @@ func orNoSettings(layers []Layer) string {
|
|||||||
return "; set today: " + strings.Join(keys, ", ")
|
return "; set today: " + strings.Join(keys, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// settingKeysUsedBy is every key a module's files, units, directory owners, contributions and served
|
// settingKeysUsedBy is every key a module's files, contributions and served facts ask for, so a
|
||||||
// facts ask for, so a setting that lands in one is not called stray.
|
// setting that lands in one is not called stray.
|
||||||
func settingKeysUsedBy(m Manifest) map[string]bool {
|
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{}
|
used := map[string]bool{}
|
||||||
note := func(s string) {
|
note := func(s string) {
|
||||||
for _, k := range settingsUsed(s) {
|
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,
|
// 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
|
// 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.
|
// 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.
|
// 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"
|
const TrustedField = "trusted"
|
||||||
|
|
||||||
// TerminalKeys are the settings keys of a module that are set at the controller's terminal alone: places and
|
// 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
|
// 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`, and every setting a directory's owner asks for. Places and accesses first, then the rest sorted.
|
// asks for unless it says `"trusted": false`. Places and accesses first, then the rest sorted.
|
||||||
func TerminalKeys(m Manifest) []string {
|
func TerminalKeys(m Manifest) []string {
|
||||||
keys := map[string]bool{}
|
keys := map[string]bool{}
|
||||||
for _, served := range m.Serves {
|
for _, served := range m.Serves {
|
||||||
@@ -60,13 +57,6 @@ func TerminalKeys(m Manifest) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, r := range m.Resources {
|
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" {
|
if fmt.Sprint(r["type"]) != "file" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ type filler struct {
|
|||||||
// seatInto, machineInto and contributionsInto. A pass that comes to read another field adds it here,
|
// 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.
|
// or the sweep refuses the placeholder it would have filled.
|
||||||
var fillers = []filler{
|
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: "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: "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"}},
|
{namespace: "secret", patterns: []*regexp.Regexp{placeholder}, fields: map[string][]string{"file": {"content"}},
|
||||||
|
|||||||
@@ -325,8 +325,6 @@ func (i *Inventory) ModuleSecretIfIssued(
|
|||||||
return sealed, true, nil
|
return sealed, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecretForModule makes what is not held, so it is never asked for an own secret issued outside the mesh: the
|
|
||||||
// composition reads only a value a person gave for one (GivenOwnSecret; novox/hq issue 401, ADR 0228).
|
|
||||||
func (i *Inventory) SecretForModule(ctx context.Context, node, module, name string) (string, error) {
|
func (i *Inventory) SecretForModule(ctx context.Context, node, module, name string) (string, error) {
|
||||||
key, err := i.SealingKeyOf(ctx, node)
|
key, err := i.SealingKeyOf(ctx, node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -657,35 +655,6 @@ func (i *Inventory) GivenMembers(ctx context.Context, node, module, family strin
|
|||||||
return out, rows.Err()
|
return out, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GivenOwnSecret is an own secret issued outside the mesh as a person gave it on a machine (novox/hq issue 401, ADR
|
|
||||||
// 0228): its value sealed to the machine and whether that is the key the machine holds now. Not given is false: a
|
|
||||||
// value the mesh made for it is not one a person gave, and is never read as one; a machine with no sealing key
|
|
||||||
// holds none either.
|
|
||||||
func (i *Inventory) GivenOwnSecret(ctx context.Context, node, module, name string) (GivenMember, bool, error) {
|
|
||||||
key, err := i.SealingKeyOf(ctx, node)
|
|
||||||
if err != nil || key == "" {
|
|
||||||
return GivenMember{}, false, err
|
|
||||||
}
|
|
||||||
record, err := i.NodeByName(ctx, node)
|
|
||||||
if err != nil {
|
|
||||||
return GivenMember{}, false, err
|
|
||||||
}
|
|
||||||
g := GivenMember{Name: name}
|
|
||||||
var against string
|
|
||||||
err = i.store.Pool().QueryRow(ctx,
|
|
||||||
`select sealed, node_key from module_secret
|
|
||||||
where node = $1 and module = $2 and name = $3 and origin = 'accepted'`,
|
|
||||||
record.ID, module, name).Scan(&g.Sealed, &against)
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return GivenMember{}, false, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return GivenMember{}, false, err
|
|
||||||
}
|
|
||||||
g.Current = against == key
|
|
||||||
return g, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GivenOwnSecrets is every own secret of a module a person gave on a machine, by name, with when (novox/hq ADR
|
// GivenOwnSecrets is every own secret of a module a person gave on a machine, by name, with when (novox/hq ADR
|
||||||
// 0283): what the controller checks a module's wait for a secret against. A value the mesh made is not one.
|
// 0283): what the controller checks a module's wait for a secret against. A value the mesh made is not one.
|
||||||
func (i *Inventory) GivenOwnSecrets(ctx context.Context, node, module string) (map[string]time.Time, error) {
|
func (i *Inventory) GivenOwnSecrets(ctx context.Context, node, module string) (map[string]time.Time, error) {
|
||||||
|
|||||||
Vendored
-7
@@ -22,10 +22,3 @@
|
|||||||
./cmd/mesh-controller TestAWalkEndedByCancelOrClearIsSaidEnded
|
./cmd/mesh-controller TestAWalkEndedByCancelOrClearIsSaidEnded
|
||||||
./cmd/mesh-controller TestAWalkRestartedByARebuildIsSaidStarted
|
./cmd/mesh-controller TestAWalkRestartedByARebuildIsSaidStarted
|
||||||
./cmd/mesh-controller TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain
|
./cmd/mesh-controller TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain
|
||||||
./cmd/mesh-controller TestReplay237
|
|
||||||
./cmd/mesh-controller TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer
|
|
||||||
./cmd/mesh-controller TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt
|
|
||||||
./cmd/mesh-controller TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal
|
|
||||||
./cmd/mesh-controller TestADirectoryOwnersKeyIsRefusedThroughAVerb
|
|
||||||
./cmd/mesh-controller TestTheSendMakesNoValueForAnOutsideSecretNobodyGave
|
|
||||||
./cmd/mesh-controller TestAQuestionComposesWithoutAnOutsideSecretNobodyGave
|
|
||||||
|
|||||||
+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/internal/declaration
|
||||||
github.com/novox/mesh-host/rootsearch
|
github.com/novox/mesh-host/rootsearch
|
||||||
github.com/novox/mesh-host/validate
|
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
|
# golang.org/x/crypto v0.57.0
|
||||||
## explicit; go 1.26.0
|
## explicit; go 1.26.0
|
||||||
golang.org/x/crypto/acme
|
golang.org/x/crypto/acme
|
||||||
|
|||||||
Reference in New Issue
Block a user