Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
28 changes: 28 additions & 0 deletions internal/serde/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2026 Amutable GmbH

package serde

import (
"errors"
"fmt"
)

// ErrMissingField is returned by [parseMapKey] if the key is missing entirely.
var ErrMissingField = errors.New("missing required field")

// ParseMapKey takes the value from the map with the given key, parses it into
// the given slot, and drops it from the original map. This is quite handy for
// detecting unsupported fields in an ergonomic way when parsing maps from
// encodings like JSON or TOML.
func ParseMapKey[T any](data map[string]any, key string, slot *T) error {
if valAny, ok := data[key]; !ok {
return fmt.Errorf("%w %q", ErrMissingField, key)
} else if val, ok := valAny.(T); !ok {
return fmt.Errorf("field %q has incorrect value type: %v (%T) is not a %T", key, valAny, valAny, *new(T))
} else { //nolint:revive // variable chaining makes this uglier vis-a-vis indent-error-flow
*slot = val
delete(data, key)
return nil
}
}
Loading
Loading