@@ -21,6 +21,7 @@ import (
2121 "encoding/json"
2222 "errors"
2323 "fmt"
24+ "maps"
2425 "os"
2526 "path/filepath"
2627 "regexp"
@@ -35,6 +36,14 @@ import (
3536// components or is otherwise invalid for use as a directory name.
3637var ErrInvalidRepoName = errors .New ("invalid repository name" )
3738
39+ // ErrMissingRepoURL is returned when a repository listed in the map file has no
40+ // usable URL, i.e. an empty URL list or an empty first URL.
41+ var ErrMissingRepoURL = errors .New ("repository has no URL configured" )
42+
43+ // ErrUnknownMappingRepo is returned when a mapping in the map file references a
44+ // repository that is not declared in the top-level repositories object.
45+ var ErrUnknownMappingRepo = errors .New ("mapping references an unknown repository" )
46+
3847// validRepoNamePattern defines the allowed characters for repository names.
3948// Names must start with an alphanumeric character and may contain alphanumeric
4049// characters, dots, hyphens, and underscores. This prevents path traversal
@@ -113,19 +122,23 @@ func NewConfig(repoMap []byte, roots map[string][]byte) (*MultiRepoConfig, error
113122
114123// New returns a multi-repository TUF client. All repositories described in the provided map file are initialized too
115124func New (config * MultiRepoConfig ) (* MultiRepoClient , error ) {
125+ if config == nil {
126+ return nil , fmt .Errorf ("no multi-repository config provided" )
127+ }
128+
129+ // validate the map file before initializing anything, so that a malformed map
130+ // file is reported as such instead of surfacing later as an obscure
131+ // initialization failure or a panic during target lookup
132+ if err := validateRepoMap (config .RepoMap ); err != nil {
133+ return nil , err
134+ }
135+
116136 // create a multi repo client instance
117137 client := & MultiRepoClient {
118138 Config : config ,
119139 TUFClients : map [string ]* updater.Updater {},
120140 }
121141
122- // validate repository names before using them as filesystem paths
123- for repoName := range config .RepoMap .Repositories {
124- if err := validateRepoName (repoName ); err != nil {
125- return nil , fmt .Errorf ("repository %q: %w" , repoName , err )
126- }
127- }
128-
129142 // create TUF clients for each repository listed in the map file
130143 if err := client .initTUFClients (); err != nil {
131144 return nil , err
@@ -138,13 +151,9 @@ func (client *MultiRepoClient) initTUFClients() error {
138151 log := metadata .GetLogger ()
139152
140153 // loop through each repository listed in the map file and initialize it
154+ // note: the map file has already been validated by validateRepoMap, so each
155+ // repository is guaranteed to have a usable URL at index 0
141156 for repoName , repoURL := range client .Config .RepoMap .Repositories {
142-
143- // Make sure we have at least one repo URL
144- if len (repoURL ) == 0 || repoURL [0 ] == "" {
145- return fmt .Errorf ("repository %q has no URL configured" , repoName )
146- }
147-
148157 log .Info ("Initializing" , "name" , repoName , "url" , repoURL [0 ])
149158
150159 // get the trusted root file from the location specified in the map file relevant to its path
@@ -393,6 +402,50 @@ func (cfg *MultiRepoConfig) EnsurePathsExist() error {
393402 return nil
394403}
395404
405+ // validateRepoMap checks that a map file is internally consistent before any
406+ // repository is initialized. It enforces that every repository name is safe to
407+ // use as a filesystem path, that every repository has a usable URL, and that
408+ // every repository referenced by a mapping is actually declared.
409+ //
410+ // Validating up front keeps New atomic: it either returns a fully initialized
411+ // client or fails without having created cache directories for a subset of the
412+ // repositories. It also makes the reported error deterministic, which map
413+ // iteration order alone would not guarantee.
414+ func validateRepoMap (repoMap * MultiRepoMapType ) error {
415+ if repoMap == nil {
416+ return fmt .Errorf ("no repository map provided" )
417+ }
418+
419+ // sort the repository names so that a map file with more than one problem
420+ // always reports the same error rather than a random one
421+ for _ , repoName := range slices .Sorted (maps .Keys (repoMap .Repositories )) {
422+ if err := validateRepoName (repoName ); err != nil {
423+ return fmt .Errorf ("repository %q: %w" , repoName , err )
424+ }
425+
426+ // only the first URL is used, as the client supports a single mirror per
427+ // repository for the time being
428+ if repoURL := repoMap .Repositories [repoName ]; len (repoURL ) == 0 || repoURL [0 ] == "" {
429+ return fmt .Errorf ("repository %q: %w" , repoName , ErrMissingRepoURL )
430+ }
431+ }
432+
433+ // every repository named in a mapping must have a corresponding TUF client,
434+ // otherwise GetTargetInfo would dereference a nil client during target lookup
435+ for i , eachMap := range repoMap .Mapping {
436+ if eachMap == nil {
437+ return fmt .Errorf ("mapping at index %d is null" , i )
438+ }
439+ for _ , repoName := range eachMap .Repositories {
440+ if _ , ok := repoMap .Repositories [repoName ]; ! ok {
441+ return fmt .Errorf ("mapping at index %d: %w - %s" , i , ErrUnknownMappingRepo , repoName )
442+ }
443+ }
444+ }
445+
446+ return nil
447+ }
448+
396449// validateRepoName checks that a repository name is safe to use as a directory
397450// component. Repository names must start with an alphanumeric character and
398451// contain only alphanumeric characters, dots, hyphens, and underscores.
0 commit comments