Skip to content
6 changes: 5 additions & 1 deletion cmd/quarry-client/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,11 @@ func proxyTargetFile(rw http.ResponseWriter, req *http.Request) (Err error) {
// TODO(uapi16): Once we get UAPI.16 support into systemd, we can serve a
// manifest that lists every URL candidate as an alternative contents source
// (as "quarry-client list --uapi-16" does) and drop this shim entirely.
for url, err := range infoExt.FetchURLs(&info.Repo.DataRootURL.URL) {
dataRootURL, err := info.Repo.DataURL()
if err != nil {
return fmt.Errorf("bad repo %s definition: cannot compute target url: %w", info.Repo.Name, err)
}
for url, err := range infoExt.FetchURLs(dataRootURL) {
if err != nil {
return fmt.Errorf("bad target data in repo %s for target %s: cannot compute target url: %w", info.Repo.Name, targetPath, err)
}
Expand Down
6 changes: 5 additions & 1 deletion cmd/quarry-client/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,11 @@ func (o *uapi16ListFormatter) Begin(ctx context.Context, client *tufclient.Clien
}

func (o *uapi16ListFormatter) Output(_ context.Context, target *tufclient.TargetInfo) error {
file, err := uapi16ext.FromTargetFile(target.TargetFiles, &target.Repo.DataRootURL.URL)
dataRootURL, err := target.Repo.DataURL()
if err != nil {
return err
}
file, err := uapi16ext.FromTargetFile(target.TargetFiles, dataRootURL)
if err != nil {
return err
}
Expand Down
7 changes: 3 additions & 4 deletions cmd/quarry-client/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,8 @@ const (
emptyHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
)

// testRepo returns a [config.Repository] for a dummy repository. The config
// has to be parsed because the URL fields are not exported types.
func testRepo(t *testing.T) *config.Repository {
// testRepo returns the [tufext.Repository] for a dummy repository config.
func testRepo(t *testing.T) *tufext.Repository {
t.Helper()
cfg, err := config.Parse(strings.NewReader(`
config_version = 1
Expand All @@ -49,7 +48,7 @@ data_root_url = "https://example.com/data"
require.NoError(t, err)
repo, ok := cfg.Repos["test-repo"]
require.True(t, ok, "test-repo should be in the parsed config")
return repo
return repo.AsRepository()
}

// testClient returns a client with no repositories configured. That is enough
Expand Down
23 changes: 15 additions & 8 deletions cmd/quarry-client/utils_tuf.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"go.amutable.dev/quarry/internal/expand"
"go.amutable.dev/quarry/internal/third_party/funchelpers"
"go.amutable.dev/quarry/internal/tufclient"
"go.amutable.dev/quarry/internal/tufclient/config"
"go.amutable.dev/quarry/internal/tufext"
)

Expand Down Expand Up @@ -86,7 +85,7 @@ func pprintHashes(wtr io.Writer, prefix string, hashes tufmetadata.Hashes) {
}
}

func pprintTargetFile(wtr io.Writer, prefix string, repo *config.Repository, target *tufmetadata.TargetFiles) {
func pprintTargetFile(wtr io.Writer, prefix string, repo *tufext.Repository, target *tufmetadata.TargetFiles) {
targetExt := tufext.TargetFilesExt(target)

mustFprintf(wtr, "%s%s:\n", prefix, target.Path)
Expand All @@ -98,11 +97,15 @@ func pprintTargetFile(wtr io.Writer, prefix string, repo *config.Repository, tar
mustFprintf(wtr, "%sInline data: %d bytes\n", prefix, len(data))
}
mustFprintf(wtr, "%sURL(s):\n", prefix)
for url, err := range targetExt.FetchURLs(&repo.DataRootURL.URL) {
if err != nil {
mustFprintf(wtr, "%s - <invalid target url: %v>\n", prefix, err)
if dataRootURL, err := repo.DataURL(); err != nil {
mustFprintf(wtr, "%s - <invalid repo data url: %v>\n", prefix, err)
} else {
for url, err := range targetExt.FetchURLs(dataRootURL) {
if err != nil {
mustFprintf(wtr, "%s - <invalid target url: %v>\n", prefix, err)
}
mustFprintf(wtr, "%s - %s\n", prefix, url)
}
mustFprintf(wtr, "%s - %s\n", prefix, url)
}
mustFprintf(wtr, "%sSize: %d\n", prefix, target.Length)
pprintHashes(wtr, prefix, target.Hashes)
Expand All @@ -113,7 +116,7 @@ func pprintTargetFile(wtr io.Writer, prefix string, repo *config.Repository, tar
// TODO(ext): UnrecognisedFields
}

func expandTargetFile(wtr io.Writer, fmtStr string, repo *config.Repository, target *tufmetadata.TargetFiles) error {
func expandTargetFile(wtr io.Writer, fmtStr string, repo *tufext.Repository, target *tufmetadata.TargetFiles) error {
targetExt := tufext.TargetFilesExt(target)

expander := expand.NewExpansions().
Expand All @@ -131,7 +134,11 @@ func expandTargetFile(wtr io.Writer, fmtStr string, repo *config.Repository, tar
} else if data != nil {
return "data:;base64," + base64.StdEncoding.EncodeToString(data), nil
}
for url, err := range targetExt.FetchURLs(&repo.DataRootURL.URL) {
dataRootURL, err := repo.DataURL()
if err != nil {
return "", err
}
for url, err := range targetExt.FetchURLs(dataRootURL) {
var urlStr string
if url != nil {
urlStr = url.String()
Expand Down
10 changes: 10 additions & 0 deletions internal/generics/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2026 Amutable GmbH

package generics

// TakeError can be used to wrap a function that returns (T, erorr) to extract
// just the error in one line.
func TakeError[T any](_ T, err error) error {
return err
}
12 changes: 6 additions & 6 deletions internal/jsonutils/jsonext.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import (
// when given a top-level structure that doesn't support UnrecognizedFields.
var ErrNotExtensible = errors.New("json structure not extensible")

// reParseJSON takes an arbitrary object and then re-parses as though it were
// ReparseJSON takes an arbitrary object and then re-parses as though it were
// JSON for the given type parameter. This is necessary to "cast" pre-parsed
// any interfaces into something strongly typed.
func reParseJSON[T any](data any) (T, error) {
func ReparseJSON[T any](data any) (T, error) {
var (
encoded []byte
err error
Expand All @@ -39,7 +39,7 @@ func GetExtensionJSON[T any](extStruct any, field string) (*T, error) {
// Get the set of structure fields as json.RawMessage so we can re-parse
// them slightly more efficiently and without triggering parsing errors for
// other fields.
structFields, err := reParseJSON[map[string]json.RawMessage](extStruct)
structFields, err := ReparseJSON[map[string]json.RawMessage](extStruct)
if err != nil {
return nil, fmt.Errorf("%w: re-parse %T as generic struct: %w", ErrNotExtensible, extStruct, err)
}
Expand Down Expand Up @@ -67,23 +67,23 @@ func SetExtensionJSON[W any](extStruct *W, field string, value any) (json.RawMes
if err != nil {
return nil, fmt.Errorf("marshal value %T: %w", value, err)
}
structFields, err := reParseJSON[map[string]json.RawMessage](*extStruct)
structFields, err := ReparseJSON[map[string]json.RawMessage](*extStruct)
if err != nil {
return nil, fmt.Errorf("%w: re-parse %T as generic struct: %w", ErrNotExtensible, *new(W), err)
}

var oldExtBytes json.RawMessage
oldExtBytes, structFields[field] = structFields[field], json.RawMessage(extBytes)

newExtStruct, err := reParseJSON[W](structFields)
newExtStruct, err := ReparseJSON[W](structFields)
if err != nil {
return nil, fmt.Errorf("%w: re-parse generic struct to %T: %w", ErrNotExtensible, *new(W), err)
}

// Make sure that round-tripping the new extension structure through
// encoding still includes the same extension fields. If not, then the
// struct doesn't support extensions of this form.
roundTripStructFields, err := reParseJSON[map[string]json.RawMessage](newExtStruct)
roundTripStructFields, err := ReparseJSON[map[string]json.RawMessage](newExtStruct)
if err != nil {
return nil, fmt.Errorf("%w: re-parse modified %T as generic struct: %w", ErrNotExtensible, *new(W), err)
}
Expand Down
6 changes: 6 additions & 0 deletions internal/serde/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2026 Amutable GmbH

// Package serde provides a poor-mans scheme for serialising and deserialising
// data in different formats.
package serde
39 changes: 39 additions & 0 deletions internal/serde/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2026 Amutable GmbH

package serde

import (
"errors"
"fmt"
)

// ErrMissingField is returned (usually wrapped) when a required field is
// missing entirely. [Validator] implementations should wrap it when reporting
// missing required fields.
var ErrMissingField = errors.New("missing required field")

// UnknownTypeError is returned when the tag of a [Union] value does not match
// any of its variants.
type UnknownTypeError struct {
Union string // [UnionOptions.Name]
Tag string
}

func (err *UnknownTypeError) Error() string {
return fmt.Sprintf("unsupported %s type %q", err.Union, err.Tag)
}

// UnknownFieldError is returned when a variant of a [Union] is given a field
// that it does not declare. Only the first unknown field is reported.
type UnknownFieldError struct {
Union string // [UnionOptions.Name]
Tag string
// Field is the name of the unknown field (for TOML, this is the full
// dotted key of the field).
Field string
}

func (err *UnknownFieldError) Error() string {
return fmt.Sprintf("%s %q has unsupported field %q", err.Union, err.Tag, err.Field)
}
Loading
Loading