| // Copyright 2024 The Update Framework Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License |
| // |
| // SPDX-License-Identifier: Apache-2.0 |
| // |
| |
| package updater |
| |
| import ( |
| "encoding/hex" |
| "errors" |
| "fmt" |
| "io" |
| "net/http" |
| "net/url" |
| "os" |
| "path/filepath" |
| "regexp" |
| "strconv" |
| "strings" |
| "time" |
| |
| "github.com/theupdateframework/go-tuf/v2/metadata" |
| "github.com/theupdateframework/go-tuf/v2/metadata/config" |
| "github.com/theupdateframework/go-tuf/v2/metadata/trustedmetadata" |
| ) |
| |
| // Client update workflow implementation |
| // |
| // The "Updater" provides an implementation of the TUF client workflow (ref. https://theupdateframework.github.io/specification/latest/#detailed-client-workflow). |
| // "Updater" provides an API to query available targets and to download them in a |
| // secure manner: All downloaded files are verified by signed metadata. |
| // High-level description of "Updater" functionality: |
| // - Initializing an "Updater" loads and validates the trusted local root |
| // metadata: This root metadata is used as the source of trust for all other |
| // metadata. |
| // - Refresh() can optionally be called to update and load all top-level |
| // metadata as described in the specification, using both locally cached |
| // metadata and metadata downloaded from the remote repository. If refresh is |
| // not done explicitly, it will happen automatically during the first target |
| // info lookup. |
| // - Updater can be used to download targets. For each target: |
| // - GetTargetInfo() is first used to find information about a |
| // specific target. This will load new targets metadata as needed (from |
| // local cache or remote repository). |
| // - FindCachedTarget() can optionally be used to check if a |
| // target file is already locally cached. |
| // - DownloadTarget() downloads a target file and ensures it is |
| // verified correct by the metadata. |
| type Updater struct { |
| trusted *trustedmetadata.TrustedMetadata |
| cfg *config.UpdaterConfig |
| } |
| |
| type roleParentTuple struct { |
| Role string |
| Parent string |
| } |
| |
| // New creates a new Updater instance and loads trusted root metadata |
| func New(config *config.UpdaterConfig) (*Updater, error) { |
| // make sure the trusted root metadata and remote URL were provided |
| if len(config.LocalTrustedRoot) == 0 || len(config.RemoteMetadataURL) == 0 { |
| return nil, fmt.Errorf("no initial trusted root metadata or remote URL provided") |
| } |
| // create a new trusted metadata instance using the trusted root.json |
| trustedMetadataSet, err := trustedmetadata.New(config.LocalTrustedRoot) |
| if err != nil { |
| return nil, err |
| } |
| // create an updater instance |
| updater := &Updater{ |
| cfg: config, |
| trusted: trustedMetadataSet, // save trusted metadata set |
| } |
| // ensure paths exist, doesn't do anything if caching is disabled |
| err = updater.cfg.EnsurePathsExist() |
| if err != nil { |
| return nil, err |
| } |
| // persist the initial root metadata to the local metadata folder |
| err = updater.persistMetadata(metadata.ROOT, updater.cfg.LocalTrustedRoot) |
| if err != nil { |
| return nil, err |
| } |
| // all okay, return the updater instance |
| return updater, nil |
| } |
| |
| // Refresh loads and possibly refreshes top-level metadata. |
| // Downloads, verifies, and loads metadata for the top-level roles in the |
| // specified order (root -> timestamp -> snapshot -> targets) implementing |
| // all the checks required in the TUF client workflow. |
| // A Refresh() can be done only once during the lifetime of an Updater. |
| // If Refresh() has not been explicitly called before the first |
| // GetTargetInfo() call, it will be done implicitly at that time. |
| // The metadata for delegated roles is not updated by Refresh(): |
| // that happens on demand during GetTargetInfo(). However, if the |
| // repository uses consistent snapshots (ref. https://theupdateframework.github.io/specification/latest/#consistent-snapshots), |
| // then all metadata downloaded by the Updater will use the same consistent repository state. |
| // |
| // If UnsafeLocalMode is set, no network interaction is performed, only |
| // the cached files on disk are used. If the cached data is not complete, |
| // this call will fail. |
| func (update *Updater) Refresh() error { |
| if update.cfg.UnsafeLocalMode { |
| return update.unsafeLocalRefresh() |
| } |
| return update.onlineRefresh() |
| } |
| |
| // onlineRefresh implements the TUF client workflow as described for |
| // the Refresh function. |
| func (update *Updater) onlineRefresh() error { |
| err := update.loadRoot() |
| if err != nil { |
| return err |
| } |
| err = update.loadTimestamp() |
| if err != nil { |
| return err |
| } |
| err = update.loadSnapshot() |
| if err != nil { |
| return err |
| } |
| _, err = update.loadTargets(metadata.TARGETS, metadata.ROOT) |
| if err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // unsafeLoadRefresh tries to load the persisted metadata already cached |
| // on disk. Note that this is an usafe function, and does deviate from the |
| // TUF specification section 5.3 to 5.7 (update phases). |
| // The metadata on disk are verified against the provided root though, |
| // and expiration dates are verified. |
| func (update *Updater) unsafeLocalRefresh() error { |
| // Root is already loaded |
| // load timestamp |
| var p = filepath.Join(update.cfg.LocalMetadataDir, metadata.TIMESTAMP) |
| data, err := update.loadLocalMetadata(p) |
| if err != nil { |
| return err |
| } |
| _, err = update.trusted.UpdateTimestamp(data) |
| if err != nil { |
| return err |
| } |
| |
| // load snapshot |
| p = filepath.Join(update.cfg.LocalMetadataDir, metadata.SNAPSHOT) |
| data, err = update.loadLocalMetadata(p) |
| if err != nil { |
| return err |
| } |
| _, err = update.trusted.UpdateSnapshot(data, false) |
| if err != nil { |
| return err |
| } |
| |
| // targets |
| p = filepath.Join(update.cfg.LocalMetadataDir, metadata.TARGETS) |
| data, err = update.loadLocalMetadata(p) |
| if err != nil { |
| return err |
| } |
| // verify and load the new target metadata |
| _, err = update.trusted.UpdateDelegatedTargets(data, metadata.TARGETS, metadata.ROOT) |
| if err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| // GetTargetInfo returns metadata.TargetFiles instance with information |
| // for targetPath. The return value can be used as an argument to |
| // DownloadTarget() and FindCachedTarget(). |
| // If Refresh() has not been called before calling |
| // GetTargetInfo(), the refresh will be done implicitly. |
| // As a side-effect this method downloads all the additional (delegated |
| // targets) metadata it needs to return the target information. |
| func (update *Updater) GetTargetInfo(targetPath string) (*metadata.TargetFiles, error) { |
| // do a Refresh() in case there's no trusted targets.json yet |
| if update.trusted.Targets[metadata.TARGETS] == nil { |
| err := update.Refresh() |
| if err != nil { |
| return nil, err |
| } |
| } |
| return update.preOrderDepthFirstWalk(targetPath) |
| } |
| |
| // DownloadTarget downloads the target file specified by targetFile |
| func (update *Updater) DownloadTarget(targetFile *metadata.TargetFiles, filePath, targetBaseURL string) (string, []byte, error) { |
| log := metadata.GetLogger() |
| |
| var err error |
| if filePath == "" { |
| filePath, err = update.generateTargetFilePath(targetFile) |
| if err != nil { |
| return "", nil, err |
| } |
| } |
| if targetBaseURL == "" { |
| if update.cfg.RemoteTargetsURL == "" { |
| return "", nil, &metadata.ErrValue{Msg: "targetBaseURL must be set in either DownloadTarget() or the Updater struct"} |
| } |
| targetBaseURL = ensureTrailingSlash(update.cfg.RemoteTargetsURL) |
| } else { |
| targetBaseURL = ensureTrailingSlash(targetBaseURL) |
| } |
| |
| targetFilePath := targetFile.Path |
| targetRemotePath := targetFilePath |
| consistentSnapshot := update.trusted.Root.Signed.ConsistentSnapshot |
| if consistentSnapshot && update.cfg.PrefixTargetsWithHash { |
| hashes := "" |
| // get first hex value of hashes |
| for _, v := range targetFile.Hashes { |
| hashes = hex.EncodeToString(v) |
| break |
| } |
| baseName := filepath.Base(targetFilePath) |
| dirName, ok := strings.CutSuffix(targetFilePath, "/"+baseName) |
| if !ok { |
| // <hash>.<target-name> |
| targetRemotePath = fmt.Sprintf("%s.%s", hashes, baseName) |
| } else { |
| // <dir-prefix>/<hash>.<target-name> |
| targetRemotePath = fmt.Sprintf("%s/%s.%s", dirName, hashes, baseName) |
| } |
| } |
| fullURL := fmt.Sprintf("%s%s", targetBaseURL, targetRemotePath) |
| data, err := update.cfg.Fetcher.DownloadFile(fullURL, targetFile.Length, time.Second*15) |
| if err != nil { |
| return "", nil, err |
| } |
| err = targetFile.VerifyLengthHashes(data) |
| if err != nil { |
| return "", nil, err |
| } |
| |
| // do not persist the target file if cache is disabled |
| if !update.cfg.DisableLocalCache { |
| err = os.WriteFile(filePath, data, 0644) |
| if err != nil { |
| return "", nil, err |
| } |
| } |
| log.Info("Downloaded target", "path", targetFile.Path) |
| return filePath, data, nil |
| } |
| |
| // FindCachedTarget checks whether a local file is an up to date target |
| func (update *Updater) FindCachedTarget(targetFile *metadata.TargetFiles, filePath string) (string, []byte, error) { |
| var err error |
| targetFilePath := "" |
| // do not look for cached target file if cache is disabled |
| if update.cfg.DisableLocalCache { |
| return "", nil, nil |
| } |
| // get its path if not provided |
| if filePath == "" { |
| targetFilePath, err = update.generateTargetFilePath(targetFile) |
| if err != nil { |
| return "", nil, err |
| } |
| } else { |
| targetFilePath = filePath |
| } |
| // get file content |
| data, err := readFile(targetFilePath) |
| if err != nil { |
| // do not want to return err, instead we say that there's no cached target available |
| return "", nil, nil |
| } |
| // verify if the length and hashes of this target file match the expected values |
| err = targetFile.VerifyLengthHashes(data) |
| if err != nil { |
| // do not want to return err, instead we say that there's no cached target available |
| return "", nil, nil |
| } |
| // if all okay, return its path |
| return targetFilePath, data, nil |
| } |
| |
| // loadTimestamp load local and remote timestamp metadata |
| func (update *Updater) loadTimestamp() error { |
| log := metadata.GetLogger() |
| // try to read local timestamp |
| data, err := update.loadLocalMetadata(filepath.Join(update.cfg.LocalMetadataDir, metadata.TIMESTAMP)) |
| if err != nil { |
| // this means there's no existing local timestamp so we should proceed downloading it without the need to UpdateTimestamp |
| log.Info("Local timestamp does not exist") |
| } else { |
| // local timestamp exists, let's try to verify it and load it to the trusted metadata set |
| _, err := update.trusted.UpdateTimestamp(data) |
| if err != nil { |
| if errors.Is(err, &metadata.ErrRepository{}) { |
| // local timestamp is not valid, proceed downloading from remote; note that this error type includes several other subset errors |
| log.Info("Local timestamp is not valid") |
| } else { |
| // another error |
| return err |
| } |
| } |
| log.Info("Local timestamp is valid") |
| // all okay, local timestamp exists and it is valid, nevertheless proceed with downloading from remote |
| } |
| // load from remote (whether local load succeeded or not) |
| data, err = update.downloadMetadata(metadata.TIMESTAMP, update.cfg.TimestampMaxLength, "") |
| if err != nil { |
| return err |
| } |
| // try to verify and load the newly downloaded timestamp |
| _, err = update.trusted.UpdateTimestamp(data) |
| if err != nil { |
| if errors.Is(err, &metadata.ErrEqualVersionNumber{}) { |
| // if the new timestamp version is the same as current, discard the |
| // new timestamp; this is normal and it shouldn't raise any error |
| return nil |
| } else { |
| // another error |
| return err |
| } |
| } |
| // proceed with persisting the new timestamp |
| err = update.persistMetadata(metadata.TIMESTAMP, data) |
| if err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // loadSnapshot load local (and if needed remote) snapshot metadata |
| func (update *Updater) loadSnapshot() error { |
| log := metadata.GetLogger() |
| // try to read local snapshot |
| data, err := update.loadLocalMetadata(filepath.Join(update.cfg.LocalMetadataDir, metadata.SNAPSHOT)) |
| if err != nil { |
| // this means there's no existing local snapshot so we should proceed downloading it without the need to UpdateSnapshot |
| log.Info("Local snapshot does not exist") |
| } else { |
| // successfully read a local snapshot metadata, so let's try to verify and load it to the trusted metadata set |
| _, err = update.trusted.UpdateSnapshot(data, true) |
| if err != nil { |
| // this means snapshot verification/loading failed |
| if errors.Is(err, &metadata.ErrRepository{}) { |
| // local snapshot is not valid, proceed downloading from remote; note that this error type includes several other subset errors |
| log.Info("Local snapshot is not valid") |
| } else { |
| // another error |
| return err |
| } |
| } else { |
| // this means snapshot verification/loading succeeded |
| log.Info("Local snapshot is valid: not downloading new one") |
| return nil |
| } |
| } |
| // local snapshot does not exist or is invalid, update from remote |
| log.Info("Failed to load local snapshot") |
| if update.trusted.Timestamp == nil { |
| return fmt.Errorf("trusted timestamp not set") |
| } |
| // extract the snapshot meta from the trusted timestamp metadata |
| snapshotMeta := update.trusted.Timestamp.Signed.Meta[fmt.Sprintf("%s.json", metadata.SNAPSHOT)] |
| // extract the length of the snapshot metadata to be downloaded |
| length := snapshotMeta.Length |
| if length == 0 { |
| length = update.cfg.SnapshotMaxLength |
| } |
| // extract which snapshot version should be downloaded in case of consistent snapshots |
| version := "" |
| if update.trusted.Root.Signed.ConsistentSnapshot { |
| version = strconv.FormatInt(snapshotMeta.Version, 10) |
| } |
| // download snapshot metadata |
| data, err = update.downloadMetadata(metadata.SNAPSHOT, length, version) |
| if err != nil { |
| return err |
| } |
| // verify and load the new snapshot |
| _, err = update.trusted.UpdateSnapshot(data, false) |
| if err != nil { |
| return err |
| } |
| // persist the new snapshot |
| err = update.persistMetadata(metadata.SNAPSHOT, data) |
| if err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // loadTargets load local (and if needed remote) metadata for roleName |
| func (update *Updater) loadTargets(roleName, parentName string) (*metadata.Metadata[metadata.TargetsType], error) { |
| log := metadata.GetLogger() |
| // avoid loading "roleName" more than once during "GetTargetInfo" |
| role, ok := update.trusted.Targets[roleName] |
| if ok { |
| return role, nil |
| } |
| // try to read local targets |
| data, err := update.loadLocalMetadata(filepath.Join(update.cfg.LocalMetadataDir, roleName)) |
| if err != nil { |
| // this means there's no existing local target file so we should proceed downloading it without the need to UpdateDelegatedTargets |
| log.Info("Local role does not exist", "role", roleName) |
| } else { |
| // successfully read a local targets metadata, so let's try to verify and load it to the trusted metadata set |
| delegatedTargets, err := update.trusted.UpdateDelegatedTargets(data, roleName, parentName) |
| if err != nil { |
| // this means targets verification/loading failed |
| if errors.Is(err, &metadata.ErrRepository{}) { |
| // local target file is not valid, proceed downloading from remote; note that this error type includes several other subset errors |
| log.Info("Local role is not valid", "role", roleName) |
| } else { |
| // another error |
| return nil, err |
| } |
| } else { |
| // this means targets verification/loading succeeded |
| log.Info("Local role is valid: not downloading new one", "role", roleName) |
| return delegatedTargets, nil |
| } |
| } |
| // local "roleName" does not exist or is invalid, update from remote |
| log.Info("Failed to load local role", "role", roleName) |
| if update.trusted.Snapshot == nil { |
| return nil, fmt.Errorf("trusted snapshot not set") |
| } |
| // extract the targets meta from the trusted snapshot metadata |
| metaInfo := update.trusted.Snapshot.Signed.Meta[fmt.Sprintf("%s.json", roleName)] |
| // extract the length of the target metadata to be downloaded |
| length := metaInfo.Length |
| if length == 0 { |
| length = update.cfg.TargetsMaxLength |
| } |
| // extract which target metadata version should be downloaded in case of consistent snapshots |
| version := "" |
| if update.trusted.Root.Signed.ConsistentSnapshot { |
| version = strconv.FormatInt(metaInfo.Version, 10) |
| } |
| // download targets metadata |
| data, err = update.downloadMetadata(roleName, length, version) |
| if err != nil { |
| return nil, err |
| } |
| // verify and load the new target metadata |
| delegatedTargets, err := update.trusted.UpdateDelegatedTargets(data, roleName, parentName) |
| if err != nil { |
| return nil, err |
| } |
| // persist the new target metadata |
| err = update.persistMetadata(roleName, data) |
| if err != nil { |
| return nil, err |
| } |
| return delegatedTargets, nil |
| } |
| |
| // loadRoot load remote root metadata. Sequentially load and |
| // persist on local disk every newer root metadata version |
| // available on the remote |
| func (update *Updater) loadRoot() error { |
| // calculate boundaries |
| lowerBound := update.trusted.Root.Signed.Version + 1 |
| upperBound := lowerBound + update.cfg.MaxRootRotations |
| |
| // loop until we find the latest available version of root (download -> verify -> load -> persist) |
| for nextVersion := lowerBound; nextVersion < upperBound; nextVersion++ { |
| data, err := update.downloadMetadata(metadata.ROOT, update.cfg.RootMaxLength, strconv.FormatInt(nextVersion, 10)) |
| if err != nil { |
| // downloading the root metadata failed for some reason |
| var tmpErr *metadata.ErrDownloadHTTP |
| if errors.As(err, &tmpErr) { |
| if tmpErr.StatusCode != http.StatusNotFound && tmpErr.StatusCode != http.StatusForbidden { |
| // unexpected HTTP status code |
| return err |
| } |
| // 404/403 means current root is newest available, so we can stop the loop and move forward |
| break |
| } |
| // some other error ocurred |
| return err |
| } else { |
| // downloading root metadata succeeded, so let's try to verify and load it |
| _, err = update.trusted.UpdateRoot(data) |
| if err != nil { |
| return err |
| } |
| // persist root metadata to disk |
| err = update.persistMetadata(metadata.ROOT, data) |
| if err != nil { |
| return err |
| } |
| } |
| } |
| return nil |
| } |
| |
| // preOrderDepthFirstWalk interrogates the tree of target delegations |
| // in order of appearance (which implicitly order trustworthiness), |
| // and returns the matching target found in the most trusted role. |
| func (update *Updater) preOrderDepthFirstWalk(targetFilePath string) (*metadata.TargetFiles, error) { |
| log := metadata.GetLogger() |
| // list of delegations to be interrogated. A (role, parent role) pair |
| // is needed to load and verify the delegated targets metadata |
| delegationsToVisit := []roleParentTuple{{ |
| Role: metadata.TARGETS, |
| Parent: metadata.ROOT, |
| }} |
| visitedRoleNames := map[string]bool{} |
| // pre-order depth-first traversal of the graph of target delegations |
| for len(visitedRoleNames) <= update.cfg.MaxDelegations && len(delegationsToVisit) > 0 { |
| // pop the role name from the top of the stack |
| delegation := delegationsToVisit[len(delegationsToVisit)-1] |
| delegationsToVisit = delegationsToVisit[:len(delegationsToVisit)-1] |
| // skip any visited current role to prevent cycles |
| _, ok := visitedRoleNames[delegation.Role] |
| if ok { |
| log.Info("Skipping visited current role", "role", delegation.Role) |
| continue |
| } |
| // the metadata for delegation.Role must be downloaded/updated before |
| // its targets, delegations, and child roles can be inspected |
| targets, err := update.loadTargets(delegation.Role, delegation.Parent) |
| if err != nil { |
| return nil, err |
| } |
| target, ok := targets.Signed.Targets[targetFilePath] |
| if ok { |
| log.Info("Found target in current role", "role", delegation.Role) |
| return target, nil |
| } |
| // after pre-order check, add current role to set of visited roles |
| visitedRoleNames[delegation.Role] = true |
| if targets.Signed.Delegations != nil { |
| var childRolesToVisit []roleParentTuple |
| // note that this may be a slow operation if there are many |
| // delegated roles |
| roles := targets.Signed.Delegations.GetRolesForTarget(targetFilePath) |
| for _, rolesForTarget := range roles { |
| log.Info("Adding child role", "role", rolesForTarget.Name) |
| childRolesToVisit = append(childRolesToVisit, roleParentTuple{Role: rolesForTarget.Name, Parent: delegation.Role}) |
| if rolesForTarget.Terminating { |
| log.Info("Not backtracking to other roles") |
| delegationsToVisit = []roleParentTuple{} |
| break |
| } |
| } |
| // push childRolesToVisit in reverse order of appearance |
| // onto delegationsToVisit. Roles are popped from the end of |
| // the list |
| reverseSlice(childRolesToVisit) |
| delegationsToVisit = append(delegationsToVisit, childRolesToVisit...) |
| } |
| } |
| if len(delegationsToVisit) > 0 { |
| log.Info("Too many roles left to visit for max allowed delegations", |
| "roles-left", len(delegationsToVisit), |
| "allowed-delegations", update.cfg.MaxDelegations) |
| } |
| // if this point is reached then target is not found, return nil |
| return nil, fmt.Errorf("target %s not found", targetFilePath) |
| } |
| |
| // persistMetadata writes metadata to disk atomically to avoid data loss |
| func (update *Updater) persistMetadata(roleName string, data []byte) error { |
| log := metadata.GetLogger() |
| // do not persist the metadata if we have disabled local caching |
| if update.cfg.DisableLocalCache { |
| return nil |
| } |
| // caching enabled, proceed with persisting the metadata locally |
| fileName := filepath.Join(update.cfg.LocalMetadataDir, fmt.Sprintf("%s.json", url.QueryEscape(roleName))) |
| // create a temporary file |
| file, err := os.CreateTemp(update.cfg.LocalMetadataDir, "tuf_tmp") |
| if err != nil { |
| return err |
| } |
| defer file.Close() |
| // write the data content to the temporary file |
| err = os.WriteFile(file.Name(), data, 0644) |
| if err != nil { |
| // delete the temporary file if there was an error while writing |
| errRemove := os.Remove(file.Name()) |
| if errRemove != nil { |
| log.Info("Failed to delete temporary file", "name", file.Name()) |
| } |
| return err |
| } |
| |
| // can't move/rename an open file on windows, so close it first |
| err = file.Close() |
| if err != nil { |
| return err |
| } |
| // if all okay, rename the temporary file to the desired one |
| err = os.Rename(file.Name(), fileName) |
| if err != nil { |
| return err |
| } |
| read, err := os.ReadFile(fileName) |
| if err != nil { |
| return err |
| } |
| if string(read) != string(data) { |
| return fmt.Errorf("failed to persist metadata") |
| } |
| return nil |
| } |
| |
| // downloadMetadata download a metadata file and return it as bytes |
| func (update *Updater) downloadMetadata(roleName string, length int64, version string) ([]byte, error) { |
| urlPath := ensureTrailingSlash(update.cfg.RemoteMetadataURL) |
| // build urlPath |
| if version == "" { |
| urlPath = fmt.Sprintf("%s%s.json", urlPath, url.QueryEscape(roleName)) |
| } else { |
| urlPath = fmt.Sprintf("%s%s.%s.json", urlPath, version, url.QueryEscape(roleName)) |
| } |
| return update.cfg.Fetcher.DownloadFile(urlPath, length, time.Second*15) |
| } |
| |
| // generateTargetFilePath generates path from TargetFiles |
| func (update *Updater) generateTargetFilePath(tf *metadata.TargetFiles) (string, error) { |
| // LocalTargetsDir can be omitted if caching is disabled |
| if update.cfg.LocalTargetsDir == "" && !update.cfg.DisableLocalCache { |
| return "", &metadata.ErrValue{Msg: "LocalTargetsDir must be set if filepath is not given"} |
| } |
| // Use URL encoded target path as filename |
| return filepath.Join(update.cfg.LocalTargetsDir, url.QueryEscape(tf.Path)), nil |
| } |
| |
| // loadLocalMetadata reads a local <roleName>.json file and returns its bytes |
| func (update *Updater) loadLocalMetadata(roleName string) ([]byte, error) { |
| return readFile(fmt.Sprintf("%s.json", roleName)) |
| } |
| |
| // GetTopLevelTargets returns the top-level target files |
| func (update *Updater) GetTopLevelTargets() map[string]*metadata.TargetFiles { |
| return update.trusted.Targets[metadata.TARGETS].Signed.Targets |
| } |
| |
| // GetTrustedMetadataSet returns the trusted metadata set |
| func (update *Updater) GetTrustedMetadataSet() trustedmetadata.TrustedMetadata { |
| return *update.trusted |
| } |
| |
| // UnsafeSetRefTime sets the reference time that the updater uses. |
| // This should only be done in tests. |
| // Using this function is useful when testing time-related behavior in go-tuf. |
| func (update *Updater) UnsafeSetRefTime(t time.Time) { |
| update.trusted.RefTime = t |
| } |
| |
| func IsWindowsPath(path string) bool { |
| match, _ := regexp.MatchString(`^[a-zA-Z]:\\`, path) |
| return match |
| } |
| |
| // ensureTrailingSlash ensures url ends with a slash |
| func ensureTrailingSlash(url string) string { |
| if IsWindowsPath(url) { |
| slash := string(filepath.Separator) |
| if strings.HasSuffix(url, slash) { |
| return url |
| } |
| return url + slash |
| } |
| if strings.HasSuffix(url, "/") { |
| return url |
| } |
| return url + "/" |
| } |
| |
| // reverseSlice reverses the elements in a generic type of slice |
| func reverseSlice[S ~[]E, E any](s S) { |
| for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { |
| s[i], s[j] = s[j], s[i] |
| } |
| } |
| |
| // readFile reads the content of a file and return its bytes |
| func readFile(name string) ([]byte, error) { |
| in, err := os.Open(name) |
| if err != nil { |
| return nil, err |
| } |
| defer in.Close() |
| data, err := io.ReadAll(in) |
| if err != nil { |
| return nil, err |
| } |
| return data, nil |
| } |