Validate byte-preserving record fields as base64 strings
This commit is contained in:
@@ -42,7 +42,9 @@ endpoint bytes uses version 2 with the same `evidence` object and an additional
|
|||||||
`evidence_bytes` object. Each entry in `evidence_bytes` uses an evidence field
|
`evidence_bytes` object. Each entry in `evidence_bytes` uses an evidence field
|
||||||
name and a standard base64 string containing its exact bytes. That entry
|
name and a standard base64 string containing its exact bytes. That entry
|
||||||
supplies the field's value when loading; the writer omits its lossy string form
|
supplies the field's value when loading; the writer omits its lossy string form
|
||||||
from `evidence`. Unknown field names are ignored. This representation applies
|
from `evidence`. Invalid known-field types or base64 encodings produce a
|
||||||
|
record-format error with the available decoding cause. Unknown field names are
|
||||||
|
ignored. This representation applies
|
||||||
to every evidence field and preserves the same identity as typed Go evidence.
|
to every evidence field and preserves the same identity as typed Go evidence.
|
||||||
Older CLIs that support only version 1 report an unsupported version for these
|
Older CLIs that support only version 1 report an unsupported version for these
|
||||||
byte-containing records; use a CLI supporting version 2 to load them.
|
byte-containing records; use a CLI supporting version 2 to load them.
|
||||||
|
|||||||
@@ -140,7 +140,11 @@ mismatch tests confirm that no remote tag query occurs.
|
|||||||
without relying on diagnostic output.
|
without relying on diagnostic output.
|
||||||
|
|
||||||
1. Add record encoding and storage helpers under `internal/publish`. Use a JSON
|
1. Add record encoding and storage helpers under `internal/publish`. Use a JSON
|
||||||
envelope with integer `format_version: 1` and an `evidence` object. Its fields
|
envelope with integer `format_version` and an `evidence` object. Ordinary
|
||||||
|
UTF-8 evidence uses version 1; non-UTF-8 fields use version 2 and a separate
|
||||||
|
`evidence_bytes` object of base64 strings, preserving exact supported bytes.
|
||||||
|
See the [canonical format](../integrations/publication-records.md#private-attempt-record).
|
||||||
|
The evidence fields
|
||||||
are `repository_dir`, `module_dir`, `module_path`, `version`, `tag`,
|
are `repository_dir`, `module_dir`, `module_path`, `version`, `tag`,
|
||||||
`candidate_commit`, `tag_object`, `endpoint`, `remote`, and `recorded_at`.
|
`candidate_commit`, `tag_object`, `endpoint`, `remote`, and `recorded_at`.
|
||||||
Store the exact endpoint. Fields other than the envelope version/object can
|
Store the exact endpoint. Fields other than the envelope version/object can
|
||||||
@@ -150,7 +154,10 @@ without relying on diagnostic output.
|
|||||||
malformed JSON, and structural type errors are `record_format` failures.
|
malformed JSON, and structural type errors are `record_format` failures.
|
||||||
Accept additional fields within a supported version. Semantic evidence
|
Accept additional fields within a supported version. Semantic evidence
|
||||||
completeness is evaluated by reconciliation, so a readable partial record
|
completeness is evaluated by reconciliation, so a readable partial record
|
||||||
remains usable for discovery and any classification it can support.
|
remains usable for discovery and any classification it can support. Decode
|
||||||
|
known version-2 byte fields as strings, then decode standard base64. Invalid
|
||||||
|
structural types and malformed base64 return `record_format` with their
|
||||||
|
available causes. Additional unknown fields retain their existing handling.
|
||||||
3. Resolve the default directory with
|
3. Resolve the default directory with
|
||||||
`git rev-parse --path-format=absolute --git-common-dir`, then append
|
`git rev-parse --path-format=absolute --git-common-dir`, then append
|
||||||
`releasekit/publication-attempts`. Use Git metadata discovery rather than
|
`releasekit/publication-attempts`. Use Git metadata discovery rather than
|
||||||
@@ -192,6 +199,11 @@ ignored, and unignored override. Use injected failures at the small file/Git
|
|||||||
boundary when platform permissions cannot reliably induce a failure. Verify
|
boundary when platform permissions cannot reliably induce a failure. Verify
|
||||||
that private records retain synthetic credentials while diagnostics redact them.
|
that private records retain synthetic credentials while diagnostics redact them.
|
||||||
|
|
||||||
|
**R17 follow-up completed:** Known byte fields validate their string representation
|
||||||
|
before base64 decoding. Tests cover valid/empty strings, numeric arrays, other
|
||||||
|
structural errors, and retained JSON/base64 causes. Existing all-field byte
|
||||||
|
round trips and original-versus-decoy Git endpoint observations remain covered.
|
||||||
|
|
||||||
## Stage 3: Add the facade-owned recovery interfaces
|
## Stage 3: Add the facade-owned recovery interfaces
|
||||||
|
|
||||||
**Completed:** 2026-11-02
|
**Completed:** 2026-11-02
|
||||||
@@ -522,6 +534,8 @@ contracts and a short consumer recovery workflow.
|
|||||||
explicit push readiness and fresh later invocations.
|
explicit push readiness and fresh later invocations.
|
||||||
R16 covers setup failure classifications and human/JSON guidance without
|
R16 covers setup failure classifications and human/JSON guidance without
|
||||||
querying an invalid or substituted endpoint.
|
querying an invalid or substituted endpoint.
|
||||||
|
R17 adds byte-field structural and encoding failures at the codec boundary,
|
||||||
|
reusing the existing partial-record and lossless-observation fixtures.
|
||||||
2. Establish read-only behavior through resulting refs, worktree/branch state,
|
2. Establish read-only behavior through resulting refs, worktree/branch state,
|
||||||
saved records, and controlled observations of candidate/hosted boundaries.
|
saved records, and controlled observations of candidate/hosted boundaries.
|
||||||
Reuse lower-level classification and codec tests. Tests use local remotes,
|
Reuse lower-level classification and codec tests. Tests use local remotes,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Publication reconciliation remediation
|
# Publication reconciliation remediation
|
||||||
|
|
||||||
Status: open. R01–R13 are addressed and individually verified. R14–R17 record
|
Status: all corrections are implemented and individually verified; final
|
||||||
remaining findings from the subsequent stage-by-stage review of `363f7cd`.
|
assembled verification is pending. R01–R13 retain their completion evidence.
|
||||||
Address the two handoff defects first, then the classification and format gaps.
|
R14–R17 describe the subsequent stage-by-stage review of `363f7cd` and its fixes.
|
||||||
|
|
||||||
R01–R13 describe commits `4dff808` through `599ad81`, against planning baseline
|
R01–R13 describe commits `4dff808` through `599ad81`, against planning baseline
|
||||||
`447b42a`. Their audit evidence and defect descriptions refer to that reviewed
|
`447b42a`. Their audit evidence and defect descriptions refer to that reviewed
|
||||||
@@ -734,6 +734,12 @@ reaching a substituted endpoint when a rewrite mismatch is detected.
|
|||||||
|
|
||||||
## R17: Validate the documented version-2 byte-field representation — P3
|
## R17: Validate the documented version-2 byte-field representation — P3
|
||||||
|
|
||||||
|
**Completed:** Known byte fields load through explicit JSON string validation and
|
||||||
|
standard base64 decoding. Tests passed for valid/empty strings, numeric arrays,
|
||||||
|
other structural types, malformed base64, and retained decoding causes, alongside
|
||||||
|
existing partial/unknown-field, version-1, and lossless endpoint regressions.
|
||||||
|
Stage 2/8 instructions and the canonical format guide are updated.
|
||||||
|
|
||||||
**Relevant code:** `internal/publish/record.go:48–59`;
|
**Relevant code:** `internal/publish/record.go:48–59`;
|
||||||
[private record format](../integrations/publication-records.md#private-attempt-record).
|
[private record format](../integrations/publication-records.md#private-attempt-record).
|
||||||
Affects Stage 2 and its Stage 8 format coverage.
|
Affects Stage 2 and its Stage 8 format coverage.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package publish
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -52,8 +53,15 @@ func LoadEvidence(path string) (Evidence, error) {
|
|||||||
}
|
}
|
||||||
for _, field := range evidenceFields(&evidence) {
|
for _, field := range evidenceFields(&evidence) {
|
||||||
if raw, ok := fields[field.name]; ok {
|
if raw, ok := fields[field.name]; ok {
|
||||||
var bytes []byte
|
var encoded *string
|
||||||
if err := json.Unmarshal(raw, &bytes); err != nil {
|
if err := json.Unmarshal(raw, &encoded); err != nil {
|
||||||
|
return Evidence{}, &RecoveryError{Kind: "record_format", RecordPath: path, cause: fmt.Errorf("decode record field %s bytes %q: %w", field.name, path, err)}
|
||||||
|
}
|
||||||
|
if encoded == nil {
|
||||||
|
return Evidence{}, &RecoveryError{Kind: "record_format", RecordPath: path, cause: fmt.Errorf("record field %s bytes %q must be a base64 string", field.name, path)}
|
||||||
|
}
|
||||||
|
bytes, err := base64.StdEncoding.DecodeString(*encoded)
|
||||||
|
if err != nil {
|
||||||
return Evidence{}, &RecoveryError{Kind: "record_format", RecordPath: path, cause: fmt.Errorf("decode record field %s bytes %q: %w", field.name, path, err)}
|
return Evidence{}, &RecoveryError{Kind: "record_format", RecordPath: path, cause: fmt.Errorf("decode record field %s bytes %q: %w", field.name, path, err)}
|
||||||
}
|
}
|
||||||
*field.value = string(bytes)
|
*field.value = string(bytes)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package publish
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -14,6 +15,50 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestLoadEvidenceValidatesKnownByteFieldsAsBase64Strings(t *testing.T) {
|
||||||
|
for _, test := range []struct{ name, value, want, failure string }{
|
||||||
|
{"valid", `"djEuMi4z"`, "v1.2.3", ""},
|
||||||
|
{"empty", `""`, "", ""},
|
||||||
|
{"malformed base64", `"not base64!"`, "", "base64"},
|
||||||
|
{"numeric array", `[118,49,46,50,46,51]`, "", "type"},
|
||||||
|
{"object", `{}`, "", "type"},
|
||||||
|
{"boolean", `true`, "", "type"},
|
||||||
|
{"number", `123`, "", "type"},
|
||||||
|
{"null", `null`, "", "schema"},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "record.json")
|
||||||
|
contents := `{"format_version":2,"evidence":{},"evidence_bytes":{"tag":` + test.value + `}}`
|
||||||
|
if err := os.WriteFile(path, []byte(contents), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
loaded, err := LoadEvidence(path)
|
||||||
|
if test.failure == "" {
|
||||||
|
if err != nil || loaded.Tag != test.want {
|
||||||
|
t.Fatalf("byte string = %#v, %v", loaded, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var typed *RecoveryError
|
||||||
|
if !errors.As(err, &typed) || typed.Kind != "record_format" || typed.Unwrap() == nil {
|
||||||
|
t.Fatalf("byte format failure = %v", err)
|
||||||
|
}
|
||||||
|
switch test.failure {
|
||||||
|
case "type":
|
||||||
|
var cause *json.UnmarshalTypeError
|
||||||
|
if !errors.As(err, &cause) {
|
||||||
|
t.Fatalf("lost JSON type cause: %v", err)
|
||||||
|
}
|
||||||
|
case "base64":
|
||||||
|
var cause base64.CorruptInputError
|
||||||
|
if !errors.As(err, &cause) {
|
||||||
|
t.Fatalf("lost base64 cause: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRecordsRoundTripNonUTF8Evidence(t *testing.T) {
|
func TestRecordsRoundTripNonUTF8Evidence(t *testing.T) {
|
||||||
repo := recordRepo(t)
|
repo := recordRepo(t)
|
||||||
raw := "path-\xff"
|
raw := "path-\xff"
|
||||||
|
|||||||
Reference in New Issue
Block a user