diff --git a/base/updater/doc.go b/base/updater/doc.go deleted file mode 100644 index 829a5bd3..00000000 --- a/base/updater/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package updater is an update registry that manages updates and versions. -package updater diff --git a/base/updater/export.go b/base/updater/export.go deleted file mode 100644 index 51889bde..00000000 --- a/base/updater/export.go +++ /dev/null @@ -1,15 +0,0 @@ -package updater - -// // Export exports the list of resources. -// func (reg *ResourceRegistry) Export() map[string]*Resource { -// reg.RLock() -// defer reg.RUnlock() - -// // copy the map -// copiedResources := make(map[string]*Resource) -// for key, val := range reg.resources { -// copiedResources[key] = val.Export() -// } - -// return copiedResources -// } diff --git a/base/updater/fetch.go b/base/updater/fetch.go deleted file mode 100644 index 89456774..00000000 --- a/base/updater/fetch.go +++ /dev/null @@ -1,347 +0,0 @@ -package updater - -// import ( -// "bytes" -// "context" -// "errors" -// "fmt" -// "hash" -// "io" -// "net/http" -// "net/url" -// "os" -// "path" -// "path/filepath" -// "time" - -// "github.com/safing/jess/filesig" -// "github.com/safing/jess/lhash" -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/utils/renameio" -// ) - -// func (reg *ResourceRegistry) fetchFile(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error { -// // backoff when retrying -// if tries > 0 { -// select { -// case <-ctx.Done(): -// return nil // module is shutting down -// case <-time.After(time.Duration(tries*tries) * time.Second): -// } -// } - -// // check destination dir -// dirPath := filepath.Dir(rv.storagePath()) -// err := reg.storageDir.EnsureAbsPath(dirPath) -// if err != nil { -// return fmt.Errorf("could not create updates folder: %s", dirPath) -// } - -// // If verification is enabled, download signature first. -// var ( -// verifiedHash *lhash.LabeledHash -// sigFileData []byte -// ) -// if rv.resource.VerificationOptions != nil { -// verifiedHash, sigFileData, err = reg.fetchAndVerifySigFile( -// ctx, client, -// rv.resource.VerificationOptions, -// rv.versionedSigPath(), rv.SigningMetadata(), -// tries, -// ) -// if err != nil { -// switch rv.resource.VerificationOptions.DownloadPolicy { -// case SignaturePolicyRequire: -// return fmt.Errorf("signature verification failed: %w", err) -// case SignaturePolicyWarn: -// log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) -// case SignaturePolicyDisable: -// log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) -// } -// } -// } - -// // open file for writing -// atomicFile, err := renameio.TempFile(reg.tmpDir.Path, rv.storagePath()) -// if err != nil { -// return fmt.Errorf("could not create temp file for download: %w", err) -// } -// defer atomicFile.Cleanup() //nolint:errcheck // ignore error for now, tmp dir will be cleaned later again anyway - -// // start file download -// resp, downloadURL, err := reg.makeRequest(ctx, client, rv.versionedPath(), tries) -// if err != nil { -// return err -// } -// defer func() { -// _ = resp.Body.Close() -// }() - -// // Write to the hasher at the same time, if needed. -// var hasher hash.Hash -// var writeDst io.Writer = atomicFile -// if verifiedHash != nil { -// hasher = verifiedHash.Algorithm().RawHasher() -// writeDst = io.MultiWriter(hasher, atomicFile) -// } - -// // Download and write file. -// n, err := io.Copy(writeDst, resp.Body) -// if err != nil { -// return fmt.Errorf("failed to download %q: %w", downloadURL, err) -// } -// if resp.ContentLength != n { -// return fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength) -// } - -// // Before file is finalized, check if hash, if available. -// if hasher != nil { -// downloadDigest := hasher.Sum(nil) -// if verifiedHash.EqualRaw(downloadDigest) { -// log.Infof("%s: verified signature of %s", reg.Name, downloadURL) -// } else { -// switch rv.resource.VerificationOptions.DownloadPolicy { -// case SignaturePolicyRequire: -// return errors.New("file does not match signed checksum") -// case SignaturePolicyWarn: -// log.Warningf("%s: checksum does not match file from %s", reg.Name, downloadURL) -// case SignaturePolicyDisable: -// log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL) -// } - -// // Reset hasher to signal that the sig should not be written. -// hasher = nil -// } -// } - -// // Write signature file, if we have one and if verification succeeded. -// if len(sigFileData) > 0 && hasher != nil { -// sigFilePath := rv.storagePath() + filesig.Extension -// err := os.WriteFile(sigFilePath, sigFileData, 0o0644) //nolint:gosec -// if err != nil { -// switch rv.resource.VerificationOptions.DownloadPolicy { -// case SignaturePolicyRequire: -// return fmt.Errorf("failed to write signature file %s: %w", sigFilePath, err) -// case SignaturePolicyWarn: -// log.Warningf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err) -// case SignaturePolicyDisable: -// log.Debugf("%s: failed to write signature file %s: %s", reg.Name, sigFilePath, err) -// } -// } -// } - -// // finalize file -// err = atomicFile.CloseAtomicallyReplace() -// if err != nil { -// return fmt.Errorf("%s: failed to finalize file %s: %w", reg.Name, rv.storagePath(), err) -// } -// // set permissions -// if !onWindows { -// // TODO: only set executable files to 0755, set other to 0644 -// err = os.Chmod(rv.storagePath(), 0o0755) //nolint:gosec // See TODO above. -// if err != nil { -// log.Warningf("%s: failed to set permissions on downloaded file %s: %s", reg.Name, rv.storagePath(), err) -// } -// } - -// log.Debugf("%s: fetched %s and stored to %s", reg.Name, downloadURL, rv.storagePath()) -// return nil -// } - -// func (reg *ResourceRegistry) fetchMissingSig(ctx context.Context, client *http.Client, rv *ResourceVersion, tries int) error { -// // backoff when retrying -// if tries > 0 { -// select { -// case <-ctx.Done(): -// return nil // module is shutting down -// case <-time.After(time.Duration(tries*tries) * time.Second): -// } -// } - -// // Check destination dir. -// dirPath := filepath.Dir(rv.storagePath()) -// err := reg.storageDir.EnsureAbsPath(dirPath) -// if err != nil { -// return fmt.Errorf("could not create updates folder: %s", dirPath) -// } - -// // Download and verify the missing signature. -// verifiedHash, sigFileData, err := reg.fetchAndVerifySigFile( -// ctx, client, -// rv.resource.VerificationOptions, -// rv.versionedSigPath(), rv.SigningMetadata(), -// tries, -// ) -// if err != nil { -// switch rv.resource.VerificationOptions.DownloadPolicy { -// case SignaturePolicyRequire: -// return fmt.Errorf("signature verification failed: %w", err) -// case SignaturePolicyWarn: -// log.Warningf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) -// case SignaturePolicyDisable: -// log.Debugf("%s: failed to verify downloaded signature of %s: %s", reg.Name, rv.versionedPath(), err) -// } -// return nil -// } - -// // Check if the signature matches the resource file. -// ok, err := verifiedHash.MatchesFile(rv.storagePath()) -// if err != nil { -// switch rv.resource.VerificationOptions.DownloadPolicy { -// case SignaturePolicyRequire: -// return fmt.Errorf("error while verifying resource file: %w", err) -// case SignaturePolicyWarn: -// log.Warningf("%s: error while verifying resource file %s", reg.Name, rv.storagePath()) -// case SignaturePolicyDisable: -// log.Debugf("%s: error while verifying resource file %s", reg.Name, rv.storagePath()) -// } -// return nil -// } -// if !ok { -// switch rv.resource.VerificationOptions.DownloadPolicy { -// case SignaturePolicyRequire: -// return errors.New("resource file does not match signed checksum") -// case SignaturePolicyWarn: -// log.Warningf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath()) -// case SignaturePolicyDisable: -// log.Debugf("%s: checksum does not match resource file from %s", reg.Name, rv.storagePath()) -// } -// return nil -// } - -// // Write signature file. -// err = os.WriteFile(rv.storageSigPath(), sigFileData, 0o0644) //nolint:gosec -// if err != nil { -// switch rv.resource.VerificationOptions.DownloadPolicy { -// case SignaturePolicyRequire: -// return fmt.Errorf("failed to write signature file %s: %w", rv.storageSigPath(), err) -// case SignaturePolicyWarn: -// log.Warningf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err) -// case SignaturePolicyDisable: -// log.Debugf("%s: failed to write signature file %s: %s", reg.Name, rv.storageSigPath(), err) -// } -// } - -// log.Debugf("%s: fetched %s and stored to %s", reg.Name, rv.versionedSigPath(), rv.storageSigPath()) -// return nil -// } - -// func (reg *ResourceRegistry) fetchAndVerifySigFile(ctx context.Context, client *http.Client, verifOpts *VerificationOptions, sigFilePath string, requiredMetadata map[string]string, tries int) (*lhash.LabeledHash, []byte, error) { -// // Download signature file. -// resp, _, err := reg.makeRequest(ctx, client, sigFilePath, tries) -// if err != nil { -// return nil, nil, err -// } -// defer func() { -// _ = resp.Body.Close() -// }() -// sigFileData, err := io.ReadAll(resp.Body) -// if err != nil { -// return nil, nil, err -// } - -// // Extract all signatures. -// sigs, err := filesig.ParseSigFile(sigFileData) -// switch { -// case len(sigs) == 0 && err != nil: -// return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) -// case len(sigs) == 0: -// return nil, nil, errors.New("no signatures found in signature file") -// case err != nil: -// return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) -// } - -// // Verify all signatures. -// var verifiedHash *lhash.LabeledHash -// for _, sig := range sigs { -// fd, err := filesig.VerifyFileData( -// sig, -// requiredMetadata, -// verifOpts.TrustStore, -// ) -// if err != nil { -// return nil, sigFileData, err -// } - -// // Save or check verified hash. -// if verifiedHash == nil { -// verifiedHash = fd.FileHash() -// } else if !fd.FileHash().Equal(verifiedHash) { -// // Return an error if two valid hashes mismatch. -// // For simplicity, all hash algorithms must be the same for now. -// return nil, sigFileData, errors.New("file hashes from different signatures do not match") -// } -// } - -// return verifiedHash, sigFileData, nil -// } - -// func (reg *ResourceRegistry) fetchData(ctx context.Context, client *http.Client, downloadPath string, tries int) (fileData []byte, downloadedFrom string, err error) { -// // backoff when retrying -// if tries > 0 { -// select { -// case <-ctx.Done(): -// return nil, "", nil // module is shutting down -// case <-time.After(time.Duration(tries*tries) * time.Second): -// } -// } - -// // start file download -// resp, downloadURL, err := reg.makeRequest(ctx, client, downloadPath, tries) -// if err != nil { -// return nil, downloadURL, err -// } -// defer func() { -// _ = resp.Body.Close() -// }() - -// // download and write file -// buf := bytes.NewBuffer(make([]byte, 0, resp.ContentLength)) -// n, err := io.Copy(buf, resp.Body) -// if err != nil { -// return nil, downloadURL, fmt.Errorf("failed to download %q: %w", downloadURL, err) -// } -// if resp.ContentLength != n { -// return nil, downloadURL, fmt.Errorf("failed to finish download of %q: written %d out of %d bytes", downloadURL, n, resp.ContentLength) -// } - -// return buf.Bytes(), downloadURL, nil -// } - -// func (reg *ResourceRegistry) makeRequest(ctx context.Context, client *http.Client, downloadPath string, tries int) (resp *http.Response, downloadURL string, err error) { -// // parse update URL -// updateBaseURL := reg.UpdateURLs[tries%len(reg.UpdateURLs)] -// u, err := url.Parse(updateBaseURL) -// if err != nil { -// return nil, "", fmt.Errorf("failed to parse update URL %q: %w", updateBaseURL, err) -// } -// // add download path -// u.Path = path.Join(u.Path, downloadPath) -// // compile URL -// downloadURL = u.String() - -// // create request -// req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, http.NoBody) -// if err != nil { -// return nil, "", fmt.Errorf("failed to create request for %q: %w", downloadURL, err) -// } - -// // set user agent -// if reg.UserAgent != "" { -// req.Header.Set("User-Agent", reg.UserAgent) -// } - -// // start request -// resp, err = client.Do(req) -// if err != nil { -// return nil, "", fmt.Errorf("failed to make request to %q: %w", downloadURL, err) -// } - -// // check return code -// if resp.StatusCode != http.StatusOK { -// _ = resp.Body.Close() -// return nil, "", fmt.Errorf("failed to fetch %q: %d %s", downloadURL, resp.StatusCode, resp.Status) -// } - -// return resp, downloadURL, err -// } diff --git a/base/updater/file.go b/base/updater/file.go deleted file mode 100644 index 758f2a7b..00000000 --- a/base/updater/file.go +++ /dev/null @@ -1,148 +0,0 @@ -package updater - -import ( - -// semver "github.com/hashicorp/go-version" - -) - -// File represents a file from the update system. -// type File struct { -// resource *Resource -// version *ResourceVersion -// notifier *notifier -// versionedPath string -// storagePath string -// } - -// // Identifier returns the identifier of the file. -// func (file *File) Identifier() string { -// return file.resource.Identifier -// } - -// // Version returns the version of the file. -// func (file *File) Version() string { -// return file.version.VersionNumber -// } - -// // SemVer returns the semantic version of the file. -// func (file *File) SemVer() *semver.Version { -// return file.version.semVer -// } - -// // EqualsVersion normalizes the given version and checks equality with semver. -// func (file *File) EqualsVersion(version string) bool { -// return file.version.EqualsVersion(version) -// } - -// // Path returns the absolute filepath of the file. -// func (file *File) Path() string { -// return file.storagePath -// } - -// // SigningMetadata returns the metadata to be included in signatures. -// func (file *File) SigningMetadata() map[string]string { -// return map[string]string{ -// "id": file.Identifier(), -// "version": file.Version(), -// } -// } - -// Verify verifies the given file. -// func (file *File) Verify() ([]*filesig.FileData, error) { -// // Check if verification is configured. -// if file.resource.VerificationOptions == nil { -// return nil, ErrVerificationNotConfigured -// } - -// // Verify file. -// fileData, err := filesig.VerifyFile( -// file.storagePath, -// file.storagePath+filesig.Extension, -// file.SigningMetadata(), -// file.resource.VerificationOptions.TrustStore, -// ) -// if err != nil { -// switch file.resource.VerificationOptions.DiskLoadPolicy { -// case SignaturePolicyRequire: -// return nil, err -// case SignaturePolicyWarn: -// log.Warningf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err) -// case SignaturePolicyDisable: -// log.Debugf("%s: failed to verify %s: %s", file.resource.registry.Name, file.storagePath, err) -// } -// } - -// return fileData, nil -// } - -// Blacklist notifies the update system that this file is somehow broken, and should be ignored from now on, until restarted. -// func (file *File) Blacklist() error { -// return file.resource.Blacklist(file.version.VersionNumber) -// } - -// markActiveWithLocking marks the file as active, locking the resource in the process. -// func (file *File) markActiveWithLocking() { -// file.resource.Lock() -// defer file.resource.Unlock() - -// // update last used version -// if file.resource.ActiveVersion != file.version { -// log.Debugf("updater: setting active version of resource %s from %s to %s", file.resource.Identifier, file.resource.ActiveVersion, file.version.VersionNumber) -// file.resource.ActiveVersion = file.version -// } -// } - -// Unpacker describes the function that is passed to -// File.Unpack. It receives a reader to the compressed/packed -// file and should return a reader that provides -// unpacked file contents. If the returned reader implements -// io.Closer it's close method is invoked when an error -// or io.EOF is returned from Read(). -// type Unpacker func(io.Reader) (io.Reader, error) - -// Unpack returns the path to the unpacked version of file and -// unpacks it on demand using unpacker. -// func (file *File) Unpack(suffix string, unpacker Unpacker) (string, error) { -// path := strings.TrimSuffix(file.Path(), suffix) - -// if suffix == "" { -// path += "-unpacked" -// } - -// _, err := os.Stat(path) -// if err == nil { -// return path, nil -// } - -// if !errors.Is(err, fs.ErrNotExist) { -// return "", err -// } - -// f, err := os.Open(file.Path()) -// if err != nil { -// return "", err -// } -// defer func() { -// _ = f.Close() -// }() - -// r, err := unpacker(f) -// if err != nil { -// return "", err -// } - -// ioErr := utils.CreateAtomic(path, r, &utils.AtomicFileOptions{ -// TempDir: file.resource.registry.TmpDir().Path, -// }) - -// if c, ok := r.(io.Closer); ok { -// if err := c.Close(); err != nil && ioErr == nil { -// // if ioErr is already set we ignore the error from -// // closing the unpacker. -// ioErr = err -// } -// } - -// return path, ioErr -// } diff --git a/base/updater/filename.go b/base/updater/filename.go deleted file mode 100644 index 69e9db00..00000000 --- a/base/updater/filename.go +++ /dev/null @@ -1,57 +0,0 @@ -package updater - -import ( - "path" - "regexp" - "strings" -) - -var ( - fileVersionRegex = regexp.MustCompile(`_v[0-9]+-[0-9]+-[0-9]+(-[a-z]+)?`) - rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(-[a-z]+)?$`) -) - -// GetIdentifierAndVersion splits the given file path into its identifier and version. -func GetIdentifierAndVersion(versionedPath string) (identifier, version string, ok bool) { - dirPath, filename := path.Split(versionedPath) - - // Extract version from filename. - rawVersion := fileVersionRegex.FindString(filename) - if rawVersion == "" { - // No version present in file, making it invalid. - return "", "", false - } - - // Trim the `_v` that gets caught by the regex and - // replace `-` with `.` to get the version string. - version = strings.Replace(strings.TrimLeft(rawVersion, "_v"), "-", ".", 2) - - // Put the filename back together without version. - i := strings.Index(filename, rawVersion) - if i < 0 { - // extracted version not in string (impossible) - return "", "", false - } - filename = filename[:i] + filename[i+len(rawVersion):] - - // Put the full path back together and return it. - // `dirPath + filename` is guaranteed by path.Split() - return dirPath + filename, version, true -} - -// GetVersionedPath combines the identifier and version and returns it as a file path. -func GetVersionedPath(identifier, version string) (versionedPath string) { - identifierPath, filename := path.Split(identifier) - - // Split the filename where the version should go. - splittedFilename := strings.SplitN(filename, ".", 2) - // Replace `.` with `-` for the filename format. - transformedVersion := strings.Replace(version, ".", "-", 2) - - // Put everything back together and return it. - versionedPath = identifierPath + splittedFilename[0] + "_v" + transformedVersion - if len(splittedFilename) > 1 { - versionedPath += "." + splittedFilename[1] - } - return versionedPath -} diff --git a/base/updater/filename_test.go b/base/updater/filename_test.go deleted file mode 100644 index cf5fb922..00000000 --- a/base/updater/filename_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package updater - -import ( - "regexp" - "testing" - - "github.com/stretchr/testify/assert" -) - -func testRegexMatch(t *testing.T, testRegex *regexp.Regexp, testString string, shouldMatch bool) { - t.Helper() - - if testRegex.MatchString(testString) != shouldMatch { - if shouldMatch { - t.Errorf("regex %s should match %s", testRegex, testString) - } else { - t.Errorf("regex %s should not match %s", testRegex, testString) - } - } -} - -func testRegexFind(t *testing.T, testRegex *regexp.Regexp, testString string, shouldMatch bool) { - t.Helper() - - if (testRegex.FindString(testString) != "") != shouldMatch { - if shouldMatch { - t.Errorf("regex %s should find %s", testRegex, testString) - } else { - t.Errorf("regex %s should not find %s", testRegex, testString) - } - } -} - -func testVersionTransformation(t *testing.T, testFilename, testIdentifier, testVersion string) { - t.Helper() - - identifier, version, ok := GetIdentifierAndVersion(testFilename) - if !ok { - t.Errorf("failed to get identifier and version of %s", testFilename) - } - assert.Equal(t, testIdentifier, identifier, "identifier does not match") - assert.Equal(t, testVersion, version, "version does not match") - - versionedPath := GetVersionedPath(testIdentifier, testVersion) - assert.Equal(t, testFilename, versionedPath, "filename (versioned path) does not match") -} - -func TestRegexes(t *testing.T) { - t.Parallel() - - testRegexMatch(t, rawVersionRegex, "0.1.2", true) - testRegexMatch(t, rawVersionRegex, "0.1.2-beta", true) - testRegexMatch(t, rawVersionRegex, "0.1.2-staging", true) - testRegexMatch(t, rawVersionRegex, "12.13.14", true) - - testRegexMatch(t, rawVersionRegex, "v0.1.2", false) - testRegexMatch(t, rawVersionRegex, "0.", false) - testRegexMatch(t, rawVersionRegex, "0.1", false) - testRegexMatch(t, rawVersionRegex, "0.1.", false) - testRegexMatch(t, rawVersionRegex, ".1.2", false) - testRegexMatch(t, rawVersionRegex, ".1.", false) - testRegexMatch(t, rawVersionRegex, "012345", false) - - testRegexFind(t, fileVersionRegex, "/path/to/file_v0-0-0", true) - testRegexFind(t, fileVersionRegex, "/path/to/file_v1-2-3", true) - testRegexFind(t, fileVersionRegex, "/path/to/file_v1-2-3.exe", true) - - testRegexFind(t, fileVersionRegex, "/path/to/file-v1-2-3", false) - testRegexFind(t, fileVersionRegex, "/path/to/file_v1.2.3", false) - testRegexFind(t, fileVersionRegex, "/path/to/file_1-2-3", false) - testRegexFind(t, fileVersionRegex, "/path/to/file_v1-2", false) - testRegexFind(t, fileVersionRegex, "/path/to/file-v1-2-3", false) - - testVersionTransformation(t, "/path/to/file_v0-0-0", "/path/to/file", "0.0.0") - testVersionTransformation(t, "/path/to/file_v1-2-3", "/path/to/file", "1.2.3") - testVersionTransformation(t, "/path/to/file_v1-2-3-beta", "/path/to/file", "1.2.3-beta") - testVersionTransformation(t, "/path/to/file_v1-2-3-staging", "/path/to/file", "1.2.3-staging") - testVersionTransformation(t, "/path/to/file_v1-2-3.exe", "/path/to/file.exe", "1.2.3") - testVersionTransformation(t, "/path/to/file_v1-2-3-staging.exe", "/path/to/file.exe", "1.2.3-staging") -} diff --git a/base/updater/get.go b/base/updater/get.go deleted file mode 100644 index 365cb736..00000000 --- a/base/updater/get.go +++ /dev/null @@ -1,87 +0,0 @@ -package updater - -import ( - "errors" -) - -// Errors returned by the updater package. -var ( - ErrNotFound = errors.New("the requested file could not be found") - ErrNotAvailableLocally = errors.New("the requested file is not available locally") - ErrVerificationNotConfigured = errors.New("verification not configured for this resource") -) - -// GetFile returns the selected (mostly newest) file with the given -// identifier or an error, if it fails. -// func (reg *ResourceRegistry) GetFile(identifier string) (*File, error) { -// return nil, fmt.Errorf("invalid file: %s", identifier) -// reg.RLock() -// res, ok := reg.resources[identifier] -// reg.RUnlock() -// if !ok { -// return nil, ErrNotFound -// } - -// file := res.GetFile() -// // check if file is available locally -// if file.version.Available { -// file.markActiveWithLocking() - -// // Verify file, if configured. -// _, err := file.Verify() -// if err != nil && !errors.Is(err, ErrVerificationNotConfigured) { -// // TODO: If verification is required, try deleting the resource and downloading it again. -// return nil, fmt.Errorf("failed to verify file: %w", err) -// } - -// return file, nil -// } - -// // check if online -// if !reg.Online { -// return nil, ErrNotAvailableLocally -// } - -// // check download dir -// err := reg.tmpDir.Ensure() -// if err != nil { -// return nil, fmt.Errorf("could not prepare tmp directory for download: %w", err) -// } - -// // Start registry operation. -// reg.state.StartOperation(StateFetching) -// defer reg.state.EndOperation() - -// // download file -// log.Tracef("%s: starting download of %s", reg.Name, file.versionedPath) -// client := &http.Client{} -// for tries := range 5 { -// err = reg.fetchFile(context.TODO(), client, file.version, tries) -// if err != nil { -// log.Tracef("%s: failed to download %s: %s, retrying (%d)", reg.Name, file.versionedPath, err, tries+1) -// } else { -// file.markActiveWithLocking() - -// // TODO: We just download the file - should we verify it again? -// return file, nil -// } -// } -// log.Warningf("%s: failed to download %s: %s", reg.Name, file.versionedPath, err) -// return nil, err -// } - -// GetVersion returns the selected version of the given identifier. -// The returned resource version may not be modified. -// func (reg *ResourceRegistry) GetVersion(identifier string) (*ResourceVersion, error) { -// reg.RLock() -// res, ok := reg.resources[identifier] -// reg.RUnlock() -// if !ok { -// return nil, ErrNotFound -// } - -// res.Lock() -// defer res.Unlock() - -// return res.SelectedVersion, nil -// } diff --git a/base/updater/indexes.go b/base/updater/indexes.go deleted file mode 100644 index 81a373a3..00000000 --- a/base/updater/indexes.go +++ /dev/null @@ -1,109 +0,0 @@ -package updater - -import ( - "encoding/json" - "errors" - "fmt" - "time" -) - -const ( - baseIndexExtension = ".json" - v2IndexExtension = ".v2.json" -) - -// Index describes an index file pulled by the updater. -type Index struct { - // Path is the path to the index file - // on the update server. - Path string - - // Channel holds the release channel name of the index. - // It must match the filename without extension. - Channel string - - // PreRelease signifies that all versions of this index should be marked as - // pre-releases, no matter if the versions actually have a pre-release tag or - // not. - PreRelease bool - - // AutoDownload specifies whether new versions should be automatically downloaded. - AutoDownload bool - - // LastRelease holds the time of the last seen release of this index. - LastRelease time.Time -} - -// IndexFile represents an index file. -type IndexFile struct { - Channel string - Published time.Time - - Releases map[string]string -} - -var ( - // ErrIndexChecksumMismatch is returned when an index does not match its - // signed checksum. - ErrIndexChecksumMismatch = errors.New("index checksum does mot match signature") - - // ErrIndexFromFuture is returned when an index is parsed with a - // Published timestamp that lies in the future. - ErrIndexFromFuture = errors.New("index is from the future") - - // ErrIndexIsOlder is returned when an index is parsed with an older - // Published timestamp than the current Published timestamp. - ErrIndexIsOlder = errors.New("index is older than the current one") - - // ErrIndexChannelMismatch is returned when an index is parsed with a - // different channel that the expected one. - ErrIndexChannelMismatch = errors.New("index does not match the expected channel") -) - -// ParseIndexFile parses an index file and checks if it is valid. -func ParseIndexFile(indexData []byte, channel string, lastIndexRelease time.Time) (*IndexFile, error) { - // Load into struct. - indexFile := &IndexFile{} - err := json.Unmarshal(indexData, indexFile) - if err != nil { - return nil, fmt.Errorf("failed to parse signed index data: %w", err) - } - - // Fallback to old format if there are no releases and no channel is defined. - // TODO: Remove in v1 - if len(indexFile.Releases) == 0 && indexFile.Channel == "" { - return loadOldIndexFormat(indexData, channel) - } - - // Check the index metadata. - switch { - case !indexFile.Published.IsZero() && time.Now().Before(indexFile.Published): - return indexFile, ErrIndexFromFuture - - case !indexFile.Published.IsZero() && - !lastIndexRelease.IsZero() && - lastIndexRelease.After(indexFile.Published): - return indexFile, ErrIndexIsOlder - - case channel != "" && - indexFile.Channel != "" && - channel != indexFile.Channel: - return indexFile, ErrIndexChannelMismatch - } - - return indexFile, nil -} - -func loadOldIndexFormat(indexData []byte, channel string) (*IndexFile, error) { - releases := make(map[string]string) - err := json.Unmarshal(indexData, &releases) - if err != nil { - return nil, err - } - - return &IndexFile{ - Channel: channel, - // Do NOT define `Published`, as this would break the "is newer" check. - Releases: releases, - }, nil -} diff --git a/base/updater/indexes_test.go b/base/updater/indexes_test.go deleted file mode 100644 index a85046cd..00000000 --- a/base/updater/indexes_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package updater - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -var ( - oldFormat = `{ - "all/ui/modules/assets.zip": "0.3.0", - "all/ui/modules/portmaster.zip": "0.2.4", - "linux_amd64/core/portmaster-core": "0.8.13" -}` - - newFormat = `{ - "Channel": "stable", - "Published": "2022-01-02T00:00:00Z", - "Releases": { - "all/ui/modules/assets.zip": "0.3.0", - "all/ui/modules/portmaster.zip": "0.2.4", - "linux_amd64/core/portmaster-core": "0.8.13" - } -}` - - formatTestChannel = "stable" - formatTestReleases = map[string]string{ - "all/ui/modules/assets.zip": "0.3.0", - "all/ui/modules/portmaster.zip": "0.2.4", - "linux_amd64/core/portmaster-core": "0.8.13", - } -) - -func TestIndexParsing(t *testing.T) { - t.Parallel() - - lastRelease, err := time.Parse(time.RFC3339, "2022-01-01T00:00:00Z") - if err != nil { - t.Fatal(err) - } - - oldIndexFile, err := ParseIndexFile([]byte(oldFormat), formatTestChannel, lastRelease) - if err != nil { - t.Fatal(err) - } - - newIndexFile, err := ParseIndexFile([]byte(newFormat), formatTestChannel, lastRelease) - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, formatTestChannel, oldIndexFile.Channel, "channel should be the same") - assert.Equal(t, formatTestChannel, newIndexFile.Channel, "channel should be the same") - assert.Equal(t, formatTestReleases, oldIndexFile.Releases, "releases should be the same") - assert.Equal(t, formatTestReleases, newIndexFile.Releases, "releases should be the same") -} diff --git a/base/updater/notifier.go b/base/updater/notifier.go deleted file mode 100644 index 75de01e4..00000000 --- a/base/updater/notifier.go +++ /dev/null @@ -1,33 +0,0 @@ -package updater - -// import ( -// "github.com/tevino/abool" -// ) - -// type notifier struct { -// upgradeAvailable *abool.AtomicBool -// notifyChannel chan struct{} -// } - -// func newNotifier() *notifier { -// return ¬ifier{ -// upgradeAvailable: abool.NewBool(false), -// notifyChannel: make(chan struct{}), -// } -// } - -// func (n *notifier) markAsUpgradeable() { -// if n.upgradeAvailable.SetToIf(false, true) { -// close(n.notifyChannel) -// } -// } - -// // UpgradeAvailable returns whether an upgrade is available for this file. -// func (file *File) UpgradeAvailable() bool { -// return file.notifier.upgradeAvailable.IsSet() -// } - -// // WaitForAvailableUpgrade blocks (selectable) until an upgrade for this file is available. -// func (file *File) WaitForAvailableUpgrade() <-chan struct{} { -// return file.notifier.notifyChannel -// } diff --git a/base/updater/registry.go b/base/updater/registry.go deleted file mode 100644 index 3d4861bf..00000000 --- a/base/updater/registry.go +++ /dev/null @@ -1,270 +0,0 @@ -package updater - -// import ( -// "errors" -// "fmt" -// "os" -// "path/filepath" -// "runtime" -// "strings" -// "sync" - -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/utils" -// ) - -// const ( -// onWindows = runtime.GOOS == "windows" -// ) - -// ResourceRegistry is a registry for managing update resources. -// type ResourceRegistry struct { -// sync.RWMutex - -// Name string -// storageDir *utils.DirStructure -// tmpDir *utils.DirStructure -// indexes []*Index -// state *RegistryState - -// resources map[string]*Resource -// UpdateURLs []string -// UserAgent string -// MandatoryUpdates []string -// AutoUnpack []string - -// // Verification holds a map of VerificationOptions assigned to their -// // applicable identifier path prefix. -// // Use an empty string to denote the default. -// // Use empty options to disable verification for a path prefix. -// Verification map[string]*VerificationOptions - -// // UsePreReleases signifies that pre-releases should be used when selecting a -// // version. Even if false, a pre-release version will still be used if it is -// // defined as the current version by an index. -// UsePreReleases bool - -// // DevMode specifies if a local 0.0.0 version should be always chosen, when available. -// DevMode bool - -// // Online specifies if resources may be downloaded if not available locally. -// Online bool - -// // StateNotifyFunc may be set to receive any changes to the registry state. -// // The specified function may lock the state, but may not block or take a -// // lot of time. -// StateNotifyFunc func(*RegistryState) -// } - -// // AddIndex adds a new index to the resource registry. -// // The order is important, as indexes added later will override the current -// // release from earlier indexes. -// func (reg *ResourceRegistry) AddIndex(idx Index) { -// reg.Lock() -// defer reg.Unlock() - -// // Get channel name from path. -// idx.Channel = strings.TrimSuffix( -// filepath.Base(idx.Path), filepath.Ext(idx.Path), -// ) - -// reg.indexes = append(reg.indexes, &idx) -// } - -// // PreInitUpdateState sets the initial update state of the registry before initialization. -// func (reg *ResourceRegistry) PreInitUpdateState(s UpdateState) error { -// if reg.state != nil { -// return errors.New("registry already initialized") -// } - -// reg.state = &RegistryState{ -// Updates: s, -// } -// return nil -// } - -// // Initialize initializes a raw registry struct and makes it ready for usage. -// func (reg *ResourceRegistry) Initialize(storageDir *utils.DirStructure) error { -// // check if storage dir is available -// err := storageDir.Ensure() -// if err != nil { -// return err -// } - -// // set default name -// if reg.Name == "" { -// reg.Name = "updater" -// } - -// // initialize private attributes -// reg.storageDir = storageDir -// reg.tmpDir = storageDir.ChildDir("tmp", 0o0700) -// reg.resources = make(map[string]*Resource) -// if reg.state == nil { -// reg.state = &RegistryState{} -// } -// reg.state.ID = StateReady -// reg.state.reg = reg - -// // remove tmp dir to delete old entries -// err = reg.Cleanup() -// if err != nil { -// log.Warningf("%s: failed to remove tmp dir: %s", reg.Name, err) -// } - -// // (re-)create tmp dir -// err = reg.tmpDir.Ensure() -// if err != nil { -// log.Warningf("%s: failed to create tmp dir: %s", reg.Name, err) -// } - -// // Check verification options. -// if reg.Verification != nil { -// for prefix, opts := range reg.Verification { -// // Check if verification is disable for this prefix. -// if opts == nil { -// continue -// } - -// // If enabled, a trust store is required. -// if opts.TrustStore == nil { -// return fmt.Errorf("verification enabled for prefix %q, but no trust store configured", prefix) -// } - -// // DownloadPolicy must be equal or stricter than DiskLoadPolicy. -// if opts.DiskLoadPolicy < opts.DownloadPolicy { -// return errors.New("verification download policy must be equal or stricter than the disk load policy") -// } - -// // Warn if all policies are disabled. -// if opts.DownloadPolicy == SignaturePolicyDisable && -// opts.DiskLoadPolicy == SignaturePolicyDisable { -// log.Warningf("%s: verification enabled for prefix %q, but all policies set to disable", reg.Name, prefix) -// } -// } -// } - -// return nil -// } - -// // StorageDir returns the main storage dir of the resource registry. -// func (reg *ResourceRegistry) StorageDir() *utils.DirStructure { -// return reg.storageDir -// } - -// // TmpDir returns the temporary working dir of the resource registry. -// func (reg *ResourceRegistry) TmpDir() *utils.DirStructure { -// return reg.tmpDir -// } - -// // SetDevMode sets the development mode flag. -// func (reg *ResourceRegistry) SetDevMode(on bool) { -// reg.Lock() -// defer reg.Unlock() - -// reg.DevMode = on -// } - -// // SetUsePreReleases sets the UsePreReleases flag. -// func (reg *ResourceRegistry) SetUsePreReleases(yes bool) { -// reg.Lock() -// defer reg.Unlock() - -// reg.UsePreReleases = yes -// } - -// // AddResource adds a resource to the registry. Does _not_ select new version. -// func (reg *ResourceRegistry) AddResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error { -// reg.Lock() -// defer reg.Unlock() - -// err := reg.addResource(identifier, version, index, available, currentRelease, preRelease) -// return err -// } - -// func (reg *ResourceRegistry) addResource(identifier, version string, index *Index, available, currentRelease, preRelease bool) error { -// res, ok := reg.resources[identifier] -// if !ok { -// res = reg.newResource(identifier) -// reg.resources[identifier] = res -// } -// res.Index = index - -// return res.AddVersion(version, available, currentRelease, preRelease) -// } - -// // AddResources adds resources to the registry. Errors are logged, the last one is returned. Despite errors, non-failing resources are still added. Does _not_ select new versions. -// func (reg *ResourceRegistry) AddResources(versions map[string]string, index *Index, available, currentRelease, preRelease bool) error { -// reg.Lock() -// defer reg.Unlock() - -// // add versions and their flags to registry -// var lastError error -// for identifier, version := range versions { -// lastError = reg.addResource(identifier, version, index, available, currentRelease, preRelease) -// if lastError != nil { -// log.Warningf("%s: failed to add resource %s: %s", reg.Name, identifier, lastError) -// } -// } - -// return lastError -// } - -// // SelectVersions selects new resource versions depending on the current registry state. -// func (reg *ResourceRegistry) SelectVersions() { -// reg.RLock() -// defer reg.RUnlock() - -// for _, res := range reg.resources { -// res.Lock() -// res.selectVersion() -// res.Unlock() -// } -// } - -// // GetSelectedVersions returns a list of the currently selected versions. -// func (reg *ResourceRegistry) GetSelectedVersions() (versions map[string]string) { -// reg.RLock() -// defer reg.RUnlock() - -// for _, res := range reg.resources { -// res.Lock() -// versions[res.Identifier] = res.SelectedVersion.VersionNumber -// res.Unlock() -// } - -// return -// } - -// // Purge deletes old updates, retaining a certain amount, specified by the keep -// // parameter. Will at least keep 2 updates per resource. -// func (reg *ResourceRegistry) Purge(keep int) { -// reg.RLock() -// defer reg.RUnlock() - -// for _, res := range reg.resources { -// res.Purge(keep) -// } -// } - -// // ResetResources removes all resources from the registry. -// func (reg *ResourceRegistry) ResetResources() { -// reg.Lock() -// defer reg.Unlock() - -// reg.resources = make(map[string]*Resource) -// } - -// // ResetIndexes removes all indexes from the registry. -// func (reg *ResourceRegistry) ResetIndexes() { -// reg.Lock() -// defer reg.Unlock() - -// reg.indexes = make([]*Index, 0, len(reg.indexes)) -// } - -// // Cleanup removes temporary files. -// func (reg *ResourceRegistry) Cleanup() error { -// // delete download tmp dir -// return os.RemoveAll(reg.tmpDir.Path) -// } diff --git a/base/updater/registry_test.go b/base/updater/registry_test.go deleted file mode 100644 index a8978f68..00000000 --- a/base/updater/registry_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package updater - -import ( - "os" - "testing" - - "github.com/safing/portmaster/base/utils" -) - -var registry *ResourceRegistry - -func TestMain(m *testing.M) { - // setup - tmpDir, err := os.MkdirTemp("", "ci-portmaster-") - if err != nil { - panic(err) - } - registry = &ResourceRegistry{ - UsePreReleases: true, - DevMode: true, - Online: true, - } - err = registry.Initialize(utils.NewDirStructure(tmpDir, 0o0777)) - if err != nil { - panic(err) - } - - // run - // call flag.Parse() here if TestMain uses flags - ret := m.Run() - - // teardown - _ = os.RemoveAll(tmpDir) - os.Exit(ret) -} diff --git a/base/updater/resource.go b/base/updater/resource.go deleted file mode 100644 index 9180d0c9..00000000 --- a/base/updater/resource.go +++ /dev/null @@ -1,582 +0,0 @@ -package updater - -// import ( -// "errors" -// "io/fs" -// "os" -// "path/filepath" -// "sort" -// "strings" -// "sync" - -// semver "github.com/hashicorp/go-version" - -// "github.com/safing/jess/filesig" -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/utils" -// ) - -// var devVersion *semver.Version - -// func init() { -// var err error -// devVersion, err = semver.NewVersion("0") -// if err != nil { -// panic(err) -// } -// } - -// Resource represents a resource (via an identifier) and multiple file versions. -// type Resource struct { -// sync.Mutex -// registry *ResourceRegistry -// notifier *notifier - -// // Identifier is the unique identifier for that resource. -// // It forms a file path using a forward-slash as the -// // path separator. -// Identifier string - -// // Versions holds all available resource versions. -// Versions []*ResourceVersion - -// // ActiveVersion is the last version of the resource -// // that someone requested using GetFile(). -// ActiveVersion *ResourceVersion - -// // SelectedVersion is newest, selectable version of -// // that resource that is available. A version -// // is selectable if it's not blacklisted by the user. -// // Note that it's not guaranteed that the selected version -// // is available locally. In that case, GetFile will attempt -// // to download the latest version from the updates servers -// // specified in the resource registry. -// SelectedVersion *ResourceVersion - -// // VerificationOptions holds the verification options for this resource. -// VerificationOptions *VerificationOptions - -// // Index holds a reference to the index this resource was last defined in. -// // Will be nil if resource was only found on disk. -// Index *Index -// } - -// ResourceVersion represents a single version of a resource. -// type ResourceVersion struct { -// resource *Resource - -// // VersionNumber is the string representation of the resource -// // version. -// VersionNumber string -// semVer *semver.Version - -// // Available indicates if this version is available locally. -// Available bool - -// // SigAvailable indicates if the signature of this version is available locally. -// SigAvailable bool - -// // CurrentRelease indicates that this is the current release that should be -// // selected, if possible. -// CurrentRelease bool - -// // PreRelease indicates that this version is pre-release. -// PreRelease bool - -// // Blacklisted may be set to true if this version should -// // be skipped and not used. This is useful if the version -// // is known to be broken. -// Blacklisted bool -// } - -// func (rv *ResourceVersion) String() string { -// return rv.VersionNumber -// } - -// // SemVer returns the semantic version of the resource. -// func (rv *ResourceVersion) SemVer() *semver.Version { -// return rv.semVer -// } - -// EqualsVersion normalizes the given version and checks equality with semver. -// func (rv *ResourceVersion) EqualsVersion(version string) bool { -// cmpSemVer, err := semver.NewVersion(version) -// if err != nil { -// return false -// } - -// return rv.semVer.Equal(cmpSemVer) -// } - -// // isSelectable returns true if the version represented by rv is selectable. -// // A version is selectable if it's not blacklisted and either already locally -// // available or ready to be downloaded. -// func (rv *ResourceVersion) isSelectable() bool { -// switch { -// case rv.Blacklisted: -// // Should not be used. -// return false -// case rv.Available: -// // Is available locally, use! -// return true -// case !rv.resource.registry.Online: -// // Cannot download, because registry is set to offline. -// return false -// case rv.resource.Index == nil: -// // Cannot download, because resource is not part of an index. -// return false -// case !rv.resource.Index.AutoDownload: -// // Cannot download, because index may not automatically download. -// return false -// default: -// // Is not available locally, but we are allowed to download it on request! -// return true -// } -// } - -// isBetaVersionNumber checks if rv is marked as a beta version by checking -// the version string. It does not honor the BetaRelease field of rv! -// func (rv *ResourceVersion) isBetaVersionNumber() bool { //nolint:unused -// // "b" suffix check if for backwards compatibility -// // new versions should use the pre-release suffix as -// // declared by https://semver.org -// // i.e. 1.2.3-beta -// switch rv.semVer.Prerelease() { -// case "b", "beta": -// return true -// default: -// return false -// } -// } - -// Export makes a copy of the resource with only the exposed information. -// Attributes are copied and safe to access. -// Any ResourceVersion must not be modified. -// func (res *Resource) Export() *Resource { -// res.Lock() -// defer res.Unlock() - -// // Copy attibutes. -// export := &Resource{ -// Identifier: res.Identifier, -// Versions: make([]*ResourceVersion, len(res.Versions)), -// ActiveVersion: res.ActiveVersion, -// SelectedVersion: res.SelectedVersion, -// } -// // Copy Versions slice. -// copy(export.Versions, res.Versions) - -// return export -// } - -// // Len is the number of elements in the collection. -// // It implements sort.Interface for ResourceVersion. -// func (res *Resource) Len() int { -// return len(res.Versions) -// } - -// // Less reports whether the element with index i should -// // sort before the element with index j. -// // It implements sort.Interface for ResourceVersions. -// func (res *Resource) Less(i, j int) bool { -// return res.Versions[i].semVer.GreaterThan(res.Versions[j].semVer) -// } - -// // Swap swaps the elements with indexes i and j. -// // It implements sort.Interface for ResourceVersions. -// func (res *Resource) Swap(i, j int) { -// res.Versions[i], res.Versions[j] = res.Versions[j], res.Versions[i] -// } - -// // available returns whether any version of the resource is available. -// func (res *Resource) available() bool { -// for _, rv := range res.Versions { -// if rv.Available { -// return true -// } -// } -// return false -// } - -// // inUse returns true if the resource is currently in use. -// func (res *Resource) inUse() bool { -// return res.ActiveVersion != nil -// } - -// // AnyVersionAvailable returns true if any version of -// // res is locally available. -// func (res *Resource) AnyVersionAvailable() bool { -// res.Lock() -// defer res.Unlock() - -// return res.available() -// } - -// func (reg *ResourceRegistry) newResource(identifier string) *Resource { -// return &Resource{ -// registry: reg, -// Identifier: identifier, -// Versions: make([]*ResourceVersion, 0, 1), -// VerificationOptions: reg.GetVerificationOptions(identifier), -// } -// } - -// // AddVersion adds a resource version to a resource. -// func (res *Resource) AddVersion(version string, available, currentRelease, preRelease bool) error { -// res.Lock() -// defer res.Unlock() - -// // reset current release flags -// if currentRelease { -// for _, rv := range res.Versions { -// rv.CurrentRelease = false -// } -// } - -// var rv *ResourceVersion -// // check for existing version -// for _, possibleMatch := range res.Versions { -// if possibleMatch.VersionNumber == version { -// rv = possibleMatch -// break -// } -// } - -// // create new version if none found -// if rv == nil { -// // parse to semver -// sv, err := semver.NewVersion(version) -// if err != nil { -// return err -// } - -// rv = &ResourceVersion{ -// resource: res, -// VersionNumber: sv.String(), // Use normalized version. -// semVer: sv, -// } -// res.Versions = append(res.Versions, rv) -// } - -// // set flags -// if available { -// rv.Available = true - -// // If available and signatures are enabled for this resource, check if the -// // signature is available. -// if res.VerificationOptions != nil && utils.PathExists(rv.storageSigPath()) { -// rv.SigAvailable = true -// } -// } -// if currentRelease { -// rv.CurrentRelease = true -// } -// if preRelease || rv.semVer.Prerelease() != "" { -// rv.PreRelease = true -// } - -// return nil -// } - -// // GetFile returns the selected version as a *File. -// func (res *Resource) GetFile() *File { -// res.Lock() -// defer res.Unlock() - -// // check for notifier -// if res.notifier == nil { -// // create new notifier -// res.notifier = newNotifier() -// } - -// // check if version is selected -// if res.SelectedVersion == nil { -// res.selectVersion() -// } - -// // create file -// return &File{ -// resource: res, -// version: res.SelectedVersion, -// notifier: res.notifier, -// versionedPath: res.SelectedVersion.versionedPath(), -// storagePath: res.SelectedVersion.storagePath(), -// } -// } - -// //nolint:gocognit // function already kept as simple as possible -// func (res *Resource) selectVersion() { -// sort.Sort(res) - -// // export after we finish -// var fallback bool -// defer func() { -// if fallback { -// log.Tracef("updater: selected version %s (as fallback) for resource %s", res.SelectedVersion, res.Identifier) -// } else { -// log.Debugf("updater: selected version %s for resource %s", res.SelectedVersion, res.Identifier) -// } - -// if res.inUse() && -// res.SelectedVersion != res.ActiveVersion && // new selected version does not match previously selected version -// res.notifier != nil { - -// res.notifier.markAsUpgradeable() -// res.notifier = nil - -// log.Debugf("updater: active version of %s is %s, update available", res.Identifier, res.ActiveVersion.VersionNumber) -// } -// }() - -// if len(res.Versions) == 0 { -// // TODO: find better way to deal with an empty version slice (which should not happen) -// res.SelectedVersion = nil -// return -// } - -// // Target selection - -// // 1) Dev release if dev mode is active and ignore blacklisting -// if res.registry.DevMode { -// // Get last version, as this will be v0.0.0, if available. -// rv := res.Versions[len(res.Versions)-1] -// // Check if it's v0.0.0. -// if rv.semVer.Equal(devVersion) && rv.Available { -// res.SelectedVersion = rv -// return -// } -// } - -// // 2) Find the current release. This may be also be a pre-release. -// for _, rv := range res.Versions { -// if rv.CurrentRelease { -// if rv.isSelectable() { -// res.SelectedVersion = rv -// return -// } -// // There can only be once current release, -// // so we can abort after finding one. -// break -// } -// } - -// // 3) If UsePreReleases is set, find any newest version. -// if res.registry.UsePreReleases { -// for _, rv := range res.Versions { -// if rv.isSelectable() { -// res.SelectedVersion = rv -// return -// } -// } -// } - -// // 4) Find the newest stable version. -// for _, rv := range res.Versions { -// if !rv.PreRelease && rv.isSelectable() { -// res.SelectedVersion = rv -// return -// } -// } - -// // 5) Default to newest. -// res.SelectedVersion = res.Versions[0] -// fallback = true -// } - -// // Blacklist blacklists the specified version and selects a new version. -// func (res *Resource) Blacklist(version string) error { -// res.Lock() -// defer res.Unlock() - -// // count available and valid versions -// valid := 0 -// for _, rv := range res.Versions { -// if rv.semVer.Equal(devVersion) { -// continue // ignore dev versions -// } -// if !rv.Blacklisted { -// valid++ -// } -// } -// if valid <= 1 { -// return errors.New("cannot blacklist last version") // last one, cannot blacklist! -// } - -// // find version and blacklist -// for _, rv := range res.Versions { -// if rv.VersionNumber == version { -// // blacklist and update -// rv.Blacklisted = true -// res.selectVersion() -// return nil -// } -// } - -// return errors.New("could not find version") -// } - -// // Purge deletes old updates, retaining a certain amount, specified by -// // the keep parameter. Purge will always keep at least 2 versions so -// // specifying a smaller keep value will have no effect. -// func (res *Resource) Purge(keepExtra int) { //nolint:gocognit -// res.Lock() -// defer res.Unlock() - -// // If there is any blacklisted version within the resource, pause purging. -// // In this case we may need extra available versions beyond what would be -// // available after purging. -// for _, rv := range res.Versions { -// if rv.Blacklisted { -// log.Debugf( -// "%s: pausing purging of resource %s, as it contains blacklisted items", -// res.registry.Name, -// rv.resource.Identifier, -// ) -// return -// } -// } - -// // Safeguard the amount of extra version to keep. -// if keepExtra < 2 { -// keepExtra = 2 -// } - -// // Search for purge boundary. -// var purgeBoundary int -// var skippedActiveVersion bool -// var skippedSelectedVersion bool -// var skippedStableVersion bool -// boundarySearch: -// for i, rv := range res.Versions { -// // Check if required versions are already skipped. -// switch { -// case !skippedActiveVersion && res.ActiveVersion != nil: -// // Skip versions until the active version, if it's set. -// case !skippedSelectedVersion && res.SelectedVersion != nil: -// // Skip versions until the selected version, if it's set. -// case !skippedStableVersion: -// // Skip versions until the stable version. -// default: -// // All required version skipped, set purge boundary. -// purgeBoundary = i + keepExtra -// break boundarySearch -// } - -// // Check if current instance is a required version. -// if rv == res.ActiveVersion { -// skippedActiveVersion = true -// } -// if rv == res.SelectedVersion { -// skippedSelectedVersion = true -// } -// if !rv.PreRelease { -// skippedStableVersion = true -// } -// } - -// // Check if there is anything to purge at all. -// if purgeBoundary <= keepExtra || purgeBoundary >= len(res.Versions) { -// return -// } - -// // Purge everything beyond the purge boundary. -// for _, rv := range res.Versions[purgeBoundary:] { -// // Only remove if resource file is actually available. -// if !rv.Available { -// continue -// } - -// // Remove resource file. -// storagePath := rv.storagePath() -// err := os.Remove(storagePath) -// if err != nil { -// if !errors.Is(err, fs.ErrNotExist) { -// log.Warningf("%s: failed to purge resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) -// } -// } else { -// log.Tracef("%s: purged resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) -// } - -// // Remove resource signature file. -// err = os.Remove(rv.storageSigPath()) -// if err != nil { -// if !errors.Is(err, fs.ErrNotExist) { -// log.Warningf("%s: failed to purge resource signature %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) -// } -// } else { -// log.Tracef("%s: purged resource signature %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) -// } - -// // Remove unpacked version of resource. -// ext := filepath.Ext(storagePath) -// if ext == "" { -// // Nothing to do if file does not have an extension. -// continue -// } -// unpackedPath := strings.TrimSuffix(storagePath, ext) - -// // Remove if it exists, or an error occurs on access. -// _, err = os.Stat(unpackedPath) -// if err == nil || !errors.Is(err, fs.ErrNotExist) { -// err = os.Remove(unpackedPath) -// if err != nil { -// log.Warningf("%s: failed to purge unpacked resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err) -// } else { -// log.Tracef("%s: purged unpacked resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber) -// } -// } -// } - -// // remove entries of deleted files -// res.Versions = res.Versions[purgeBoundary:] -// } - -// // SigningMetadata returns the metadata to be included in signatures. -// func (rv *ResourceVersion) SigningMetadata() map[string]string { -// return map[string]string{ -// "id": rv.resource.Identifier, -// "version": rv.VersionNumber, -// } -// } - -// // GetFile returns the version as a *File. -// // It locks the resource for doing so. -// func (rv *ResourceVersion) GetFile() *File { -// rv.resource.Lock() -// defer rv.resource.Unlock() - -// // check for notifier -// if rv.resource.notifier == nil { -// // create new notifier -// rv.resource.notifier = newNotifier() -// } - -// // create file -// return &File{ -// resource: rv.resource, -// version: rv, -// notifier: rv.resource.notifier, -// versionedPath: rv.versionedPath(), -// storagePath: rv.storagePath(), -// } -// } - -// // versionedPath returns the versioned identifier. -// func (rv *ResourceVersion) versionedPath() string { -// return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber) -// } - -// // versionedSigPath returns the versioned identifier of the file signature. -// func (rv *ResourceVersion) versionedSigPath() string { -// return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber) + filesig.Extension -// } - -// // storagePath returns the absolute storage path. -// func (rv *ResourceVersion) storagePath() string { -// return filepath.Join(rv.resource.registry.storageDir.Path, filepath.FromSlash(rv.versionedPath())) -// } - -// // storageSigPath returns the absolute storage path of the file signature. -// func (rv *ResourceVersion) storageSigPath() string { -// return rv.storagePath() + filesig.Extension -// } diff --git a/base/updater/resource_test.go b/base/updater/resource_test.go deleted file mode 100644 index ceb51e9f..00000000 --- a/base/updater/resource_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package updater - -import ( - "fmt" - "testing" - - semver "github.com/hashicorp/go-version" - "github.com/stretchr/testify/assert" -) - -func TestVersionSelection(t *testing.T) { - t.Parallel() - - res := registry.newResource("test/a") - - err := res.AddVersion("1.2.2", true, false, false) - if err != nil { - t.Fatal(err) - } - err = res.AddVersion("1.2.3", true, false, false) - if err != nil { - t.Fatal(err) - } - err = res.AddVersion("1.2.4-beta", true, false, false) - if err != nil { - t.Fatal(err) - } - err = res.AddVersion("1.2.4-staging", true, false, false) - if err != nil { - t.Fatal(err) - } - err = res.AddVersion("1.2.5", false, false, false) - if err != nil { - t.Fatal(err) - } - err = res.AddVersion("1.2.6-beta", false, false, false) - if err != nil { - t.Fatal(err) - } - err = res.AddVersion("0", true, false, false) - if err != nil { - t.Fatal(err) - } - - registry.UsePreReleases = true - registry.DevMode = true - registry.Online = true - res.Index = &Index{AutoDownload: true} - - res.selectVersion() - if res.SelectedVersion.VersionNumber != "0.0.0" { - t.Errorf("selected version should be 0.0.0, not %s", res.SelectedVersion.VersionNumber) - } - - registry.DevMode = false - res.selectVersion() - if res.SelectedVersion.VersionNumber != "1.2.6-beta" { - t.Errorf("selected version should be 1.2.6-beta, not %s", res.SelectedVersion.VersionNumber) - } - - registry.UsePreReleases = false - res.selectVersion() - if res.SelectedVersion.VersionNumber != "1.2.5" { - t.Errorf("selected version should be 1.2.5, not %s", res.SelectedVersion.VersionNumber) - } - - registry.Online = false - res.selectVersion() - if res.SelectedVersion.VersionNumber != "1.2.3" { - t.Errorf("selected version should be 1.2.3, not %s", res.SelectedVersion.VersionNumber) - } - - f123 := res.GetFile() - f123.markActiveWithLocking() - - err = res.Blacklist("1.2.3") - if err != nil { - t.Fatal(err) - } - if res.SelectedVersion.VersionNumber != "1.2.2" { - t.Errorf("selected version should be 1.2.2, not %s", res.SelectedVersion.VersionNumber) - } - - if !f123.UpgradeAvailable() { - t.Error("upgrade should be available (flag)") - } - select { - case <-f123.WaitForAvailableUpgrade(): - default: - t.Error("upgrade should be available (chan)") - } - - t.Logf("resource: %+v", res) - for _, rv := range res.Versions { - t.Logf("version %s: %+v", rv.VersionNumber, rv) - } -} - -func TestVersionParsing(t *testing.T) { - t.Parallel() - - assert.Equal(t, "1.2.3", parseVersion("1.2.3")) - assert.Equal(t, "1.2.0", parseVersion("1.2.0")) - assert.Equal(t, "0.2.0", parseVersion("0.2.0")) - assert.Equal(t, "0.0.0", parseVersion("0")) - assert.Equal(t, "1.2.3-b", parseVersion("1.2.3-b")) - assert.Equal(t, "1.2.3-b", parseVersion("1.2.3b")) - assert.Equal(t, "1.2.3-beta", parseVersion("1.2.3-beta")) - assert.Equal(t, "1.2.3-beta", parseVersion("1.2.3beta")) - assert.Equal(t, "1.2.3", parseVersion("01.02.03")) -} - -func parseVersion(v string) string { - sv, err := semver.NewVersion(v) - if err != nil { - return fmt.Sprintf("failed to parse version: %s", err) - } - return sv.String() -} diff --git a/base/updater/signing.go b/base/updater/signing.go deleted file mode 100644 index 85f4eb6f..00000000 --- a/base/updater/signing.go +++ /dev/null @@ -1,49 +0,0 @@ -package updater - -// import ( -// "strings" - -// "github.com/safing/jess" -// ) - -// // VerificationOptions holds options for verification of files. -// type VerificationOptions struct { -// TrustStore jess.TrustStore -// DownloadPolicy SignaturePolicy -// DiskLoadPolicy SignaturePolicy -// } - -// // GetVerificationOptions returns the verification options for the given identifier. -// func (reg *ResourceRegistry) GetVerificationOptions(identifier string) *VerificationOptions { -// if reg.Verification == nil { -// return nil -// } - -// var ( -// longestPrefix = -1 -// bestMatch *VerificationOptions -// ) -// for prefix, opts := range reg.Verification { -// if len(prefix) > longestPrefix && strings.HasPrefix(identifier, prefix) { -// longestPrefix = len(prefix) -// bestMatch = opts -// } -// } - -// return bestMatch -// } - -// // SignaturePolicy defines behavior in case of errors. -// type SignaturePolicy uint8 - -// // Signature Policies. -// const ( -// // SignaturePolicyRequire fails on any error. -// SignaturePolicyRequire = iota - -// // SignaturePolicyWarn only warns on errors. -// SignaturePolicyWarn - -// // SignaturePolicyDisable only downloads signatures, but does not verify them. -// SignaturePolicyDisable -// ) diff --git a/base/updater/state.go b/base/updater/state.go deleted file mode 100644 index 8cbada6f..00000000 --- a/base/updater/state.go +++ /dev/null @@ -1,180 +0,0 @@ -package updater - -// import ( -// "sort" -// "sync" -// "time" - -// "github.com/safing/portmaster/base/utils" -// ) - -// // Registry States. -// const ( -// StateReady = "ready" // Default idle state. -// StateChecking = "checking" // Downloading indexes. -// StateDownloading = "downloading" // Downloading updates. -// StateFetching = "fetching" // Fetching a single file. -// ) - -// // RegistryState describes the registry state. -// type RegistryState struct { -// sync.Mutex -// reg *ResourceRegistry - -// // ID holds the ID of the state the registry is currently in. -// ID string - -// // Details holds further information about the current state. -// Details any - -// // Updates holds generic information about the current status of pending -// // and recently downloaded updates. -// Updates UpdateState - -// // operationLock locks the operation of any state changing operation. -// // This is separate from the registry lock, which locks access to the -// // registry struct. -// operationLock sync.Mutex -// } - -// // StateDownloadingDetails holds details of the downloading state. -// type StateDownloadingDetails struct { -// // Resources holds the resource IDs that are being downloaded. -// Resources []string - -// // FinishedUpTo holds the index of Resources that is currently being -// // downloaded. Previous resources have finished downloading. -// FinishedUpTo int -// } - -// // UpdateState holds generic information about the current status of pending -// // and recently downloaded updates. -// type UpdateState struct { -// // LastCheckAt holds the time of the last update check. -// LastCheckAt *time.Time -// // LastCheckError holds the error of the last check. -// LastCheckError error -// // PendingDownload holds the resources that are pending download. -// PendingDownload []string - -// // LastDownloadAt holds the time when resources were downloaded the last time. -// LastDownloadAt *time.Time -// // LastDownloadError holds the error of the last download. -// LastDownloadError error -// // LastDownload holds the resources that we downloaded the last time updates -// // were downloaded. -// LastDownload []string - -// // LastSuccessAt holds the time of the last successful update (check). -// LastSuccessAt *time.Time -// } - -// // GetState returns the current registry state. -// // The returned data must not be modified. -// func (reg *ResourceRegistry) GetState() RegistryState { -// reg.state.Lock() -// defer reg.state.Unlock() - -// return RegistryState{ -// ID: reg.state.ID, -// Details: reg.state.Details, -// Updates: reg.state.Updates, -// } -// } - -// // StartOperation starts an operation. -// func (s *RegistryState) StartOperation(id string) bool { -// defer s.notify() - -// s.operationLock.Lock() - -// s.Lock() -// defer s.Unlock() - -// s.ID = id -// return true -// } - -// // UpdateOperationDetails updates the details of an operation. -// // The supplied struct should be a copy and must not be changed after calling -// // this function. -// func (s *RegistryState) UpdateOperationDetails(details any) { -// defer s.notify() - -// s.Lock() -// defer s.Unlock() - -// s.Details = details -// } - -// // EndOperation ends an operation. -// func (s *RegistryState) EndOperation() { -// defer s.notify() -// defer s.operationLock.Unlock() - -// s.Lock() -// defer s.Unlock() - -// s.ID = StateReady -// s.Details = nil -// } - -// // ReportUpdateCheck reports an update check to the registry state. -// func (s *RegistryState) ReportUpdateCheck(pendingDownload []string, failed error) { -// defer s.notify() - -// sort.Strings(pendingDownload) - -// s.Lock() -// defer s.Unlock() - -// now := time.Now() -// s.Updates.LastCheckAt = &now -// s.Updates.LastCheckError = failed -// s.Updates.PendingDownload = pendingDownload - -// if failed == nil { -// s.Updates.LastSuccessAt = &now -// } -// } - -// // ReportDownloads reports downloaded updates to the registry state. -// func (s *RegistryState) ReportDownloads(downloaded []string, failed error) { -// defer s.notify() - -// sort.Strings(downloaded) - -// s.Lock() -// defer s.Unlock() - -// now := time.Now() -// s.Updates.LastDownloadAt = &now -// s.Updates.LastDownloadError = failed -// s.Updates.LastDownload = downloaded - -// // Remove downloaded resources from the pending list. -// if len(s.Updates.PendingDownload) > 0 { -// newPendingDownload := make([]string, 0, len(s.Updates.PendingDownload)) -// for _, pending := range s.Updates.PendingDownload { -// if !utils.StringInSlice(downloaded, pending) { -// newPendingDownload = append(newPendingDownload, pending) -// } -// } -// s.Updates.PendingDownload = newPendingDownload -// } - -// if failed == nil { -// s.Updates.LastSuccessAt = &now -// } -// } - -// func (s *RegistryState) notify() { -// switch { -// case s.reg == nil: -// return -// case s.reg.StateNotifyFunc == nil: -// return -// } - -// s.reg.StateNotifyFunc(s) -// } diff --git a/base/updater/storage.go b/base/updater/storage.go deleted file mode 100644 index 8cfe8444..00000000 --- a/base/updater/storage.go +++ /dev/null @@ -1,272 +0,0 @@ -package updater - -// import ( -// "context" -// "errors" -// "fmt" -// "io/fs" -// "net/http" -// "os" -// "path/filepath" -// "strings" - -// "github.com/safing/jess/filesig" -// "github.com/safing/jess/lhash" -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/utils" -// ) - -// // ScanStorage scans root within the storage dir and adds found -// // resources to the registry. If an error occurred, it is logged -// // and the last error is returned. Everything that was found -// // despite errors is added to the registry anyway. Leave root -// // empty to scan the full storage dir. -// func (reg *ResourceRegistry) ScanStorage(root string) error { -// var lastError error - -// // prep root -// if root == "" { -// root = reg.storageDir.Path -// } else { -// var err error -// root, err = filepath.Abs(root) -// if err != nil { -// return err -// } -// if !strings.HasPrefix(root, reg.storageDir.Path) { -// return errors.New("supplied scan root path not within storage") -// } -// } - -// // walk fs -// _ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { -// // skip tmp dir (including errors trying to read it) -// if strings.HasPrefix(path, reg.tmpDir.Path) { -// return filepath.SkipDir -// } - -// // handle walker error -// if err != nil { -// lastError = fmt.Errorf("%s: could not read %s: %w", reg.Name, path, err) -// log.Warning(lastError.Error()) -// return nil -// } - -// // Ignore file signatures. -// if strings.HasSuffix(path, filesig.Extension) { -// return nil -// } - -// // get relative path to storage -// relativePath, err := filepath.Rel(reg.storageDir.Path, path) -// if err != nil { -// lastError = fmt.Errorf("%s: could not get relative path of %s: %w", reg.Name, path, err) -// log.Warning(lastError.Error()) -// return nil -// } - -// // convert to identifier and version -// relativePath = filepath.ToSlash(relativePath) -// identifier, version, ok := GetIdentifierAndVersion(relativePath) -// if !ok { -// // file does not conform to format -// return nil -// } - -// // fully ignore directories that also have an identifier - these will be unpacked resources -// if info.IsDir() { -// return filepath.SkipDir -// } - -// // save -// err = reg.AddResource(identifier, version, nil, true, false, false) -// if err != nil { -// lastError = fmt.Errorf("%s: could not get add resource %s v%s: %w", reg.Name, identifier, version, err) -// log.Warning(lastError.Error()) -// } -// return nil -// }) - -// return lastError -// } - -// // LoadIndexes loads the current release indexes from disk -// // or will fetch a new version if not available and the -// // registry is marked as online. -// func (reg *ResourceRegistry) LoadIndexes(ctx context.Context) error { -// var firstErr error -// client := &http.Client{} -// for _, idx := range reg.getIndexes() { -// err := reg.loadIndexFile(idx) -// if err == nil { -// log.Debugf("%s: loaded index %s", reg.Name, idx.Path) -// } else if reg.Online { -// // try to download the index file if a local disk version -// // does not exist or we don't have permission to read it. -// if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) { -// err = reg.downloadIndex(ctx, client, idx) -// } -// } - -// if err != nil && firstErr == nil { -// firstErr = err -// } -// } - -// return firstErr -// } - -// // getIndexes returns a copy of the index. -// // The indexes itself are references. -// func (reg *ResourceRegistry) getIndexes() []*Index { -// reg.RLock() -// defer reg.RUnlock() - -// indexes := make([]*Index, len(reg.indexes)) -// copy(indexes, reg.indexes) -// return indexes -// } - -// func (reg *ResourceRegistry) loadIndexFile(idx *Index) error { -// indexPath := filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)) -// indexData, err := os.ReadFile(indexPath) -// if err != nil { -// return fmt.Errorf("failed to read index file %s: %w", idx.Path, err) -// } - -// // Verify signature, if enabled. -// if verifOpts := reg.GetVerificationOptions(idx.Path); verifOpts != nil { -// // Load and check signature. -// verifiedHash, _, err := reg.loadAndVerifySigFile(verifOpts, indexPath+filesig.Extension) -// if err != nil { -// switch verifOpts.DiskLoadPolicy { -// case SignaturePolicyRequire: -// return fmt.Errorf("failed to verify signature of index %s: %w", idx.Path, err) -// case SignaturePolicyWarn: -// log.Warningf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err) -// case SignaturePolicyDisable: -// log.Debugf("%s: failed to verify signature of index %s: %s", reg.Name, idx.Path, err) -// } -// } - -// // Check if signature checksum matches the index data. -// if err == nil && !verifiedHash.Matches(indexData) { -// switch verifOpts.DiskLoadPolicy { -// case SignaturePolicyRequire: -// return fmt.Errorf("index file %s does not match signature", idx.Path) -// case SignaturePolicyWarn: -// log.Warningf("%s: index file %s does not match signature", reg.Name, idx.Path) -// case SignaturePolicyDisable: -// log.Debugf("%s: index file %s does not match signature", reg.Name, idx.Path) -// } -// } -// } - -// // Parse the index file. -// indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease) -// if err != nil { -// return fmt.Errorf("failed to parse index file %s: %w", idx.Path, err) -// } - -// // Update last seen release. -// idx.LastRelease = indexFile.Published - -// // Warn if there aren't any releases in the index. -// if len(indexFile.Releases) == 0 { -// log.Debugf("%s: index %s has no releases", reg.Name, idx.Path) -// return nil -// } - -// // Add index releases to available resources. -// err = reg.AddResources(indexFile.Releases, idx, false, true, idx.PreRelease) -// if err != nil { -// log.Warningf("%s: failed to add resource: %s", reg.Name, err) -// } -// return nil -// } - -// func (reg *ResourceRegistry) loadAndVerifySigFile(verifOpts *VerificationOptions, sigFilePath string) (*lhash.LabeledHash, []byte, error) { -// // Load signature file. -// sigFileData, err := os.ReadFile(sigFilePath) -// if err != nil { -// return nil, nil, fmt.Errorf("failed to read signature file: %w", err) -// } - -// // Extract all signatures. -// sigs, err := filesig.ParseSigFile(sigFileData) -// switch { -// case len(sigs) == 0 && err != nil: -// return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) -// case len(sigs) == 0: -// return nil, nil, errors.New("no signatures found in signature file") -// case err != nil: -// return nil, nil, fmt.Errorf("failed to parse signature file: %w", err) -// } - -// // Verify all signatures. -// var verifiedHash *lhash.LabeledHash -// for _, sig := range sigs { -// fd, err := filesig.VerifyFileData( -// sig, -// nil, -// verifOpts.TrustStore, -// ) -// if err != nil { -// return nil, sigFileData, err -// } - -// // Save or check verified hash. -// if verifiedHash == nil { -// verifiedHash = fd.FileHash() -// } else if !fd.FileHash().Equal(verifiedHash) { -// // Return an error if two valid hashes mismatch. -// // For simplicity, all hash algorithms must be the same for now. -// return nil, sigFileData, errors.New("file hashes from different signatures do not match") -// } -// } - -// return verifiedHash, sigFileData, nil -// } - -// // CreateSymlinks creates a directory structure with unversioned symlinks to the given updates list. -// func (reg *ResourceRegistry) CreateSymlinks(symlinkRoot *utils.DirStructure) error { -// err := os.RemoveAll(symlinkRoot.Path) -// if err != nil { -// return fmt.Errorf("failed to wipe symlink root: %w", err) -// } - -// err = symlinkRoot.Ensure() -// if err != nil { -// return fmt.Errorf("failed to create symlink root: %w", err) -// } - -// reg.RLock() -// defer reg.RUnlock() - -// for _, res := range reg.resources { -// if res.SelectedVersion == nil { -// return fmt.Errorf("no selected version available for %s", res.Identifier) -// } - -// targetPath := res.SelectedVersion.storagePath() -// linkPath := filepath.Join(symlinkRoot.Path, filepath.FromSlash(res.Identifier)) -// linkPathDir := filepath.Dir(linkPath) - -// err = symlinkRoot.EnsureAbsPath(linkPathDir) -// if err != nil { -// return fmt.Errorf("failed to create dir for link: %w", err) -// } - -// relativeTargetPath, err := filepath.Rel(linkPathDir, targetPath) -// if err != nil { -// return fmt.Errorf("failed to get relative target path: %w", err) -// } - -// err = os.Symlink(relativeTargetPath, linkPath) -// if err != nil { -// return fmt.Errorf("failed to link %s: %w", res.Identifier, err) -// } -// } - -// return nil -// } diff --git a/base/updater/storage_test.go b/base/updater/storage_test.go deleted file mode 100644 index 2e4122fa..00000000 --- a/base/updater/storage_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package updater - -/* -func testLoadLatestScope(t *testing.T, basePath, filePath, expectedIdentifier, expectedVersion string) { - fullPath := filepath.Join(basePath, filePath) - - // create dir - dirPath := filepath.Dir(fullPath) - err := os.MkdirAll(dirPath, 0755) - if err != nil { - t.Fatalf("could not create test dir: %s\n", err) - return - } - - // touch file - err = os.WriteFile(fullPath, []byte{}, 0644) - if err != nil { - t.Fatalf("could not create test file: %s\n", err) - return - } - - // run loadLatestScope - latest, err := ScanForLatest(basePath, true) - if err != nil { - t.Errorf("could not update latest: %s\n", err) - return - } - for key, val := range latest { - localUpdates[key] = val - } - - // test result - version, ok := localUpdates[expectedIdentifier] - if !ok { - t.Errorf("identifier %s not in map", expectedIdentifier) - t.Errorf("current map: %v", localUpdates) - } - if version != expectedVersion { - t.Errorf("unexpected version for %s: %s", filePath, version) - } -} - -func TestLoadLatestScope(t *testing.T) { - - updatesLock.Lock() - defer updatesLock.Unlock() - - tmpDir, err := os.MkdirTemp("", "testing_") - if err != nil { - t.Fatalf("could not create test dir: %s\n", err) - return - } - defer os.RemoveAll(tmpDir) - - testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-3.zip", "all/ui/assets.zip", "1.2.3") - testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-4b.zip", "all/ui/assets.zip", "1.2.4b") - testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-5.zip", "all/ui/assets.zip", "1.2.5") - testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-3-4.zip", "all/ui/assets.zip", "1.3.4") - testLoadLatestScope(t, tmpDir, "all/ui/assets_v2-3-4.zip", "all/ui/assets.zip", "2.3.4") - testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-3.zip", "all/ui/assets.zip", "2.3.4") - testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-4.zip", "all/ui/assets.zip", "2.3.4") - testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-3-4.zip", "all/ui/assets.zip", "2.3.4") - testLoadLatestScope(t, tmpDir, "os_platform/portmaster/portmaster_v1-2-3", "os_platform/portmaster/portmaster", "1.2.3") - testLoadLatestScope(t, tmpDir, "os_platform/portmaster/portmaster_v2-1-1", "os_platform/portmaster/portmaster", "2.1.1") - testLoadLatestScope(t, tmpDir, "os_platform/portmaster/portmaster_v1-2-3", "os_platform/portmaster/portmaster", "2.1.1") - -} -*/ diff --git a/base/updater/unpacking.go b/base/updater/unpacking.go deleted file mode 100644 index 5b157695..00000000 --- a/base/updater/unpacking.go +++ /dev/null @@ -1,195 +0,0 @@ -package updater - -// import ( -// "archive/zip" -// "compress/gzip" -// "errors" -// "fmt" -// "io" -// "io/fs" -// "os" -// "path" -// "path/filepath" -// "strings" - -// "github.com/hashicorp/go-multierror" - -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/utils" -// ) - -// // MaxUnpackSize specifies the maximum size that will be unpacked. -// const MaxUnpackSize = 1000000000 // 1GB - -// // UnpackGZIP unpacks a GZIP compressed reader r -// // and returns a new reader. It's suitable to be -// // used with registry.GetPackedFile. -// func UnpackGZIP(r io.Reader) (io.Reader, error) { -// return gzip.NewReader(r) -// } - -// // UnpackResources unpacks all resources defined in the AutoUnpack list. -// func (reg *ResourceRegistry) UnpackResources() error { -// reg.RLock() -// defer reg.RUnlock() - -// var multierr *multierror.Error -// for _, res := range reg.resources { -// if utils.StringInSlice(reg.AutoUnpack, res.Identifier) { -// err := res.UnpackArchive() -// if err != nil { -// multierr = multierror.Append( -// multierr, -// fmt.Errorf("%s: %w", res.Identifier, err), -// ) -// } -// } -// } - -// return multierr.ErrorOrNil() -// } - -// const ( -// zipSuffix = ".zip" -// ) - -// // UnpackArchive unpacks the archive the resource refers to. The contents are -// // unpacked into a directory with the same name as the file, excluding the -// // suffix. If the destination folder already exists, it is assumed that the -// // contents have already been correctly unpacked. -// func (res *Resource) UnpackArchive() error { -// res.Lock() -// defer res.Unlock() - -// // Only unpack selected versions. -// if res.SelectedVersion == nil { -// return nil -// } - -// switch { -// case strings.HasSuffix(res.Identifier, zipSuffix): -// return res.unpackZipArchive() -// default: -// return fmt.Errorf("unsupported file type for unpacking") -// } -// } - -// func (res *Resource) unpackZipArchive() error { -// // Get file and directory paths. -// archiveFile := res.SelectedVersion.storagePath() -// destDir := strings.TrimSuffix(archiveFile, zipSuffix) -// tmpDir := filepath.Join( -// res.registry.tmpDir.Path, -// filepath.FromSlash(strings.TrimSuffix( -// path.Base(res.SelectedVersion.versionedPath()), -// zipSuffix, -// )), -// ) - -// // Check status of destination. -// dstStat, err := os.Stat(destDir) -// switch { -// case errors.Is(err, fs.ErrNotExist): -// // The destination does not exist, continue with unpacking. -// case err != nil: -// return fmt.Errorf("cannot access destination for unpacking: %w", err) -// case !dstStat.IsDir(): -// return fmt.Errorf("destination for unpacking is blocked by file: %s", dstStat.Name()) -// default: -// // Archive already seems to be unpacked. -// return nil -// } - -// // Create the tmp directory for unpacking. -// err = res.registry.tmpDir.EnsureAbsPath(tmpDir) -// if err != nil { -// return fmt.Errorf("failed to create tmp dir for unpacking: %w", err) -// } - -// // Defer clean up of directories. -// defer func() { -// // Always clean up the tmp dir. -// _ = os.RemoveAll(tmpDir) -// // Cleanup the destination in case of an error. -// if err != nil { -// _ = os.RemoveAll(destDir) -// } -// }() - -// // Open the archive for reading. -// var archiveReader *zip.ReadCloser -// archiveReader, err = zip.OpenReader(archiveFile) -// if err != nil { -// return fmt.Errorf("failed to open zip reader: %w", err) -// } -// defer func() { -// _ = archiveReader.Close() -// }() - -// // Save all files to the tmp dir. -// for _, file := range archiveReader.File { -// err = copyFromZipArchive( -// file, -// filepath.Join(tmpDir, filepath.FromSlash(file.Name)), -// ) -// if err != nil { -// return fmt.Errorf("failed to extract archive file %s: %w", file.Name, err) -// } -// } - -// // Make the final move. -// err = os.Rename(tmpDir, destDir) -// if err != nil { -// return fmt.Errorf("failed to move the extracted archive from %s to %s: %w", tmpDir, destDir, err) -// } - -// // Fix permissions on the destination dir. -// err = res.registry.storageDir.EnsureAbsPath(destDir) -// if err != nil { -// return fmt.Errorf("failed to apply directory permissions on %s: %w", destDir, err) -// } - -// log.Infof("%s: unpacked %s", res.registry.Name, res.SelectedVersion.versionedPath()) -// return nil -// } - -// func copyFromZipArchive(archiveFile *zip.File, dstPath string) error { -// // If file is a directory, create it and continue. -// if archiveFile.FileInfo().IsDir() { -// err := os.Mkdir(dstPath, archiveFile.Mode()) -// if err != nil { -// return fmt.Errorf("failed to create directory %s: %w", dstPath, err) -// } -// return nil -// } - -// // Open archived file for reading. -// fileReader, err := archiveFile.Open() -// if err != nil { -// return fmt.Errorf("failed to open file in archive: %w", err) -// } -// defer func() { -// _ = fileReader.Close() -// }() - -// // Open destination file for writing. -// dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, archiveFile.Mode()) -// if err != nil { -// return fmt.Errorf("failed to open destination file %s: %w", dstPath, err) -// } -// defer func() { -// _ = dstFile.Close() -// }() - -// // Copy full file from archive to dst. -// if _, err := io.CopyN(dstFile, fileReader, MaxUnpackSize); err != nil { -// // EOF is expected here as the archive is likely smaller -// // thane MaxUnpackSize -// if errors.Is(err, io.EOF) { -// return nil -// } -// return err -// } - -// return nil -// } diff --git a/base/updater/updating.go b/base/updater/updating.go deleted file mode 100644 index 3c76304a..00000000 --- a/base/updater/updating.go +++ /dev/null @@ -1,359 +0,0 @@ -package updater - -// import ( -// "context" -// "fmt" -// "net/http" -// "os" -// "path" -// "path/filepath" -// "strings" - -// "golang.org/x/exp/slices" - -// "github.com/safing/jess/filesig" -// "github.com/safing/jess/lhash" -// "github.com/safing/portmaster/base/log" -// "github.com/safing/portmaster/base/utils" -// ) - -// // UpdateIndexes downloads all indexes. An error is only returned when all -// // indexes fail to update. -// func (reg *ResourceRegistry) UpdateIndexes(ctx context.Context) error { -// var lastErr error -// var anySuccess bool - -// // Start registry operation. -// reg.state.StartOperation(StateChecking) -// defer reg.state.EndOperation() - -// client := &http.Client{} -// for _, idx := range reg.getIndexes() { -// if err := reg.downloadIndex(ctx, client, idx); err != nil { -// lastErr = err -// log.Warningf("%s: failed to update index %s: %s", reg.Name, idx.Path, err) -// } else { -// anySuccess = true -// } -// } - -// // If all indexes failed to update, fail. -// if !anySuccess { -// err := fmt.Errorf("failed to update all indexes, last error was: %w", lastErr) -// reg.state.ReportUpdateCheck(nil, err) -// return err -// } - -// // Get pending resources and update status. -// pendingResourceVersions, _ := reg.GetPendingDownloads(true, false) -// reg.state.ReportUpdateCheck( -// humanInfoFromResourceVersions(pendingResourceVersions), -// nil, -// ) - -// return nil -// } - -// func (reg *ResourceRegistry) downloadIndex(ctx context.Context, client *http.Client, idx *Index) error { -// var ( -// // Index. -// indexErr error -// indexData []byte -// downloadURL string - -// // Signature. -// sigErr error -// verifiedHash *lhash.LabeledHash -// sigFileData []byte -// verifOpts = reg.GetVerificationOptions(idx.Path) -// ) - -// // Upgrade to v2 index if verification is enabled. -// downloadIndexPath := idx.Path -// if verifOpts != nil { -// downloadIndexPath = strings.TrimSuffix(downloadIndexPath, baseIndexExtension) + v2IndexExtension -// } - -// // Download new index and signature. -// for tries := range 3 { -// // Index and signature need to be fetched together, so that they are -// // fetched from the same source. One source should always have a matching -// // index and signature. Backup sources may be behind a little. -// // If the signature verification fails, another source should be tried. - -// // Get index data. -// indexData, downloadURL, indexErr = reg.fetchData(ctx, client, downloadIndexPath, tries) -// if indexErr != nil { -// log.Debugf("%s: failed to fetch index %s: %s", reg.Name, downloadURL, indexErr) -// continue -// } - -// // Get signature and verify it. -// if verifOpts != nil { -// verifiedHash, sigFileData, sigErr = reg.fetchAndVerifySigFile( -// ctx, client, -// verifOpts, downloadIndexPath+filesig.Extension, nil, -// tries, -// ) -// if sigErr != nil { -// log.Debugf("%s: failed to verify signature of %s: %s", reg.Name, downloadURL, sigErr) -// continue -// } - -// // Check if the index matches the verified hash. -// if verifiedHash.Matches(indexData) { -// log.Infof("%s: verified signature of %s", reg.Name, downloadURL) -// } else { -// sigErr = ErrIndexChecksumMismatch -// log.Debugf("%s: checksum does not match file from %s", reg.Name, downloadURL) -// continue -// } -// } - -// break -// } -// if indexErr != nil { -// return fmt.Errorf("failed to fetch index %s: %w", downloadIndexPath, indexErr) -// } -// if sigErr != nil { -// return fmt.Errorf("failed to fetch or verify index %s signature: %w", downloadIndexPath, sigErr) -// } - -// // Parse the index file. -// indexFile, err := ParseIndexFile(indexData, idx.Channel, idx.LastRelease) -// if err != nil { -// return fmt.Errorf("failed to parse index %s: %w", idx.Path, err) -// } - -// // Add index data to registry. -// if len(indexFile.Releases) > 0 { -// // Check if all resources are within the indexes' authority. -// authoritativePath := path.Dir(idx.Path) + "/" -// if authoritativePath == "./" { -// // Fix path for indexes at the storage root. -// authoritativePath = "" -// } -// cleanedData := make(map[string]string, len(indexFile.Releases)) -// for key, version := range indexFile.Releases { -// if strings.HasPrefix(key, authoritativePath) { -// cleanedData[key] = version -// } else { -// log.Warningf("%s: index %s oversteps it's authority by defining version for %s", reg.Name, idx.Path, key) -// } -// } - -// // add resources to registry -// err = reg.AddResources(cleanedData, idx, false, true, idx.PreRelease) -// if err != nil { -// log.Warningf("%s: failed to add resources: %s", reg.Name, err) -// } -// } else { -// log.Debugf("%s: index %s is empty", reg.Name, idx.Path) -// } - -// // Check if dest dir exists. -// indexDir := filepath.FromSlash(path.Dir(idx.Path)) -// err = reg.storageDir.EnsureRelPath(indexDir) -// if err != nil { -// log.Warningf("%s: failed to ensure directory for updated index %s: %s", reg.Name, idx.Path, err) -// } - -// // Index files must be readable by portmaster-staert with user permissions in order to load the index. -// err = os.WriteFile( //nolint:gosec -// filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)), -// indexData, 0o0644, -// ) -// if err != nil { -// log.Warningf("%s: failed to save updated index %s: %s", reg.Name, idx.Path, err) -// } - -// // Write signature file, if we have one. -// if len(sigFileData) > 0 { -// err = os.WriteFile( //nolint:gosec -// filepath.Join(reg.storageDir.Path, filepath.FromSlash(idx.Path)+filesig.Extension), -// sigFileData, 0o0644, -// ) -// if err != nil { -// log.Warningf("%s: failed to save updated index signature %s: %s", reg.Name, idx.Path+filesig.Extension, err) -// } -// } - -// log.Infof("%s: updated index %s with %d entries", reg.Name, idx.Path, len(indexFile.Releases)) -// return nil -// } - -// // DownloadUpdates checks if updates are available and downloads updates of used components. -// func (reg *ResourceRegistry) DownloadUpdates(ctx context.Context, includeManual bool) error { -// // Start registry operation. -// reg.state.StartOperation(StateDownloading) -// defer reg.state.EndOperation() - -// // Get pending updates. -// toUpdate, missingSigs := reg.GetPendingDownloads(includeManual, true) -// downloadDetailsResources := humanInfoFromResourceVersions(toUpdate) -// reg.state.UpdateOperationDetails(&StateDownloadingDetails{ -// Resources: downloadDetailsResources, -// }) - -// // nothing to update -// if len(toUpdate) == 0 && len(missingSigs) == 0 { -// log.Infof("%s: everything up to date", reg.Name) -// return nil -// } - -// // check download dir -// if err := reg.tmpDir.Ensure(); err != nil { -// return fmt.Errorf("could not prepare tmp directory for download: %w", err) -// } - -// // download updates -// log.Infof("%s: starting to download %d updates", reg.Name, len(toUpdate)) -// client := &http.Client{} -// var reportError error - -// for i, rv := range toUpdate { -// log.Infof( -// "%s: downloading update [%d/%d]: %s version %s", -// reg.Name, -// i+1, len(toUpdate), -// rv.resource.Identifier, rv.VersionNumber, -// ) -// var err error -// for tries := range 3 { -// err = reg.fetchFile(ctx, client, rv, tries) -// if err == nil { -// // Update resource version state. -// rv.resource.Lock() -// rv.Available = true -// if rv.resource.VerificationOptions != nil { -// rv.SigAvailable = true -// } -// rv.resource.Unlock() - -// break -// } -// } -// if err != nil { -// reportError := fmt.Errorf("failed to download %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err) -// log.Warningf("%s: %s", reg.Name, reportError) -// } - -// reg.state.UpdateOperationDetails(&StateDownloadingDetails{ -// Resources: downloadDetailsResources, -// FinishedUpTo: i + 1, -// }) -// } - -// if len(missingSigs) > 0 { -// log.Infof("%s: downloading %d missing signatures", reg.Name, len(missingSigs)) - -// for _, rv := range missingSigs { -// var err error -// for tries := range 3 { -// err = reg.fetchMissingSig(ctx, client, rv, tries) -// if err == nil { -// // Update resource version state. -// rv.resource.Lock() -// rv.SigAvailable = true -// rv.resource.Unlock() - -// break -// } -// } -// if err != nil { -// reportError := fmt.Errorf("failed to download missing sig of %s version %s: %w", rv.resource.Identifier, rv.VersionNumber, err) -// log.Warningf("%s: %s", reg.Name, reportError) -// } -// } -// } - -// reg.state.ReportDownloads( -// downloadDetailsResources, -// reportError, -// ) -// log.Infof("%s: finished downloading updates", reg.Name) - -// return nil -// } - -// // DownloadUpdates checks if updates are available and downloads updates of used components. - -// // GetPendingDownloads returns the list of pending downloads. -// // If manual is set, indexes with AutoDownload=false will be checked. -// // If auto is set, indexes with AutoDownload=true will be checked. -// func (reg *ResourceRegistry) GetPendingDownloads(manual, auto bool) (resources, sigs []*ResourceVersion) { -// reg.RLock() -// defer reg.RUnlock() - -// // create list of downloads -// var toUpdate []*ResourceVersion -// var missingSigs []*ResourceVersion - -// for _, res := range reg.resources { -// func() { -// res.Lock() -// defer res.Unlock() - -// // Skip resources without index or indexes that should not be reported -// // according to parameters. -// switch { -// case res.Index == nil: -// // Cannot download if resource is not part of an index. -// return -// case manual && !res.Index.AutoDownload: -// // Manual update report and index is not auto-download. -// case auto && res.Index.AutoDownload: -// // Auto update report and index is auto-download. -// default: -// // Resource should not be reported. -// return -// } - -// // Skip resources we don't need. -// switch { -// case res.inUse(): -// // Update if resource is in use. -// case res.available(): -// // Update if resource is available locally, ie. was used in the past. -// case utils.StringInSlice(reg.MandatoryUpdates, res.Identifier): -// // Update is set as mandatory. -// default: -// // Resource does not need to be updated. -// return -// } - -// // Go through all versions until we find versions that need updating. -// for _, rv := range res.Versions { -// switch { -// case !rv.CurrentRelease: -// // We are not interested in older releases. -// case !rv.Available: -// // File not available locally, download! -// toUpdate = append(toUpdate, rv) -// case !rv.SigAvailable && res.VerificationOptions != nil: -// // File signature is not available and verification is enabled, download signature! -// missingSigs = append(missingSigs, rv) -// } -// } -// }() -// } - -// slices.SortFunc(toUpdate, func(a, b *ResourceVersion) int { -// return strings.Compare(a.resource.Identifier, b.resource.Identifier) -// }) -// slices.SortFunc(missingSigs, func(a, b *ResourceVersion) int { -// return strings.Compare(a.resource.Identifier, b.resource.Identifier) -// }) - -// return toUpdate, missingSigs -// } - -// func humanInfoFromResourceVersions(resourceVersions []*ResourceVersion) []string { -// identifiers := make([]string, len(resourceVersions)) - -// for i, rv := range resourceVersions { -// identifiers[i] = fmt.Sprintf("%s v%s", rv.resource.Identifier, rv.VersionNumber) -// } - -// return identifiers -// } diff --git a/service/updates/registry.go b/service/updates/registry.go index 35dc67be..4a797947 100644 --- a/service/updates/registry.go +++ b/service/updates/registry.go @@ -6,7 +6,6 @@ import ( "io" "os" "path/filepath" - "runtime" semver "github.com/hashicorp/go-version" "github.com/safing/portmaster/base/log" @@ -112,19 +111,6 @@ func (r *Registry) performUpgrade(downloadDir string, indexFile string) error { } else { log.Debugf("updates: %s moved", artifact.Filename) } - - // Special case for linux. - // When installed the portmaster ui path is `/usr/bin/portmaster`. During update the ui will be placed in `/usr/lib/portmaster/portmaster` - // After an update the original binary should be deleted and replaced by symlink - // `/usr/bin/portmaster` -> `/usr/lib/portmaster/portmaster` - if runtime.GOOS == "linux" && artifact.Filename == "portmaster" && artifact.Platform == currentPlatform { - err = r.makeSymlinkForUI() - if err != nil { - log.Errorf("failed to create symlink for the ui: %s", err) - } else { - log.Infof("updates: ui symlink successfully created") - } - } } log.Infof("updates: update complete") @@ -212,16 +198,6 @@ func (r *Registry) CleanOldFiles() error { return nil } -func (r *Registry) makeSymlinkForUI() error { - portmasterBinPath := "/usr/bin/portmaster" - _ = os.Remove(portmasterBinPath) - err := os.Symlink(filepath.Join(r.dir, "portmaster"), portmasterBinPath) - if err != nil { - return fmt.Errorf("failed to create symlink: %w", err) - } - return nil -} - type File struct { id string path string