mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-11 13:16:53 +00:00
feat(BRIDGE-309): Update to the bridge updater logic corresponding to the version file restructure
This commit is contained in:
255
internal/updater/types_test.go
Normal file
255
internal/updater/types_test.go
Normal file
@ -0,0 +1,255 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_ReleaseCategory_UpdateEligible(t *testing.T) {
|
||||
// If release is beta only beta users can update
|
||||
require.True(t, EarlyAccessReleaseCategory.UpdateEligible(EarlyChannel))
|
||||
require.False(t, EarlyAccessReleaseCategory.UpdateEligible(StableChannel))
|
||||
|
||||
// If the release is stable and is the newest then both beta and stable users can update
|
||||
require.True(t, StableReleaseCategory.UpdateEligible(EarlyChannel))
|
||||
require.True(t, StableReleaseCategory.UpdateEligible(StableChannel))
|
||||
}
|
||||
|
||||
func Test_ReleaseCategory_JsonUnmarshal(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected ReleaseCategory
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
input: `{"ReleaseCategory": "EarlyAccess"}`,
|
||||
expected: EarlyAccessReleaseCategory,
|
||||
},
|
||||
{
|
||||
input: `{"ReleaseCategory": "Earlyaccess"}`,
|
||||
expected: EarlyAccessReleaseCategory,
|
||||
},
|
||||
{
|
||||
input: `{"ReleaseCategory": "earlyaccess"}`,
|
||||
expected: EarlyAccessReleaseCategory,
|
||||
},
|
||||
{
|
||||
input: `{"ReleaseCategory": " earlyaccess "}`,
|
||||
expected: EarlyAccessReleaseCategory,
|
||||
},
|
||||
{
|
||||
input: `{"ReleaseCategory": "Stable"}`,
|
||||
expected: StableReleaseCategory,
|
||||
},
|
||||
{
|
||||
input: `{"ReleaseCategory": "Stable "}`,
|
||||
expected: StableReleaseCategory,
|
||||
},
|
||||
{
|
||||
input: `{"ReleaseCategory": "stable"}`,
|
||||
expected: StableReleaseCategory,
|
||||
},
|
||||
{
|
||||
input: `{"ReleaseCategory": "invalid"}`,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
var data struct {
|
||||
ReleaseCategory ReleaseCategory
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
err := json.Unmarshal([]byte(test.input), &data)
|
||||
if err != nil && !test.wantErr {
|
||||
t.Errorf("json.Unmarshal() error = %v, wantErr %v", err, test.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if test.wantErr && err == nil {
|
||||
t.Errorf("expected err got nil")
|
||||
}
|
||||
|
||||
if !test.wantErr && data.ReleaseCategory != test.expected {
|
||||
t.Errorf("got %v, want %v", data.ReleaseCategory, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ReleaseCategory_JsonMarshal(t *testing.T) {
|
||||
tests := []struct {
|
||||
input struct {
|
||||
ReleaseCategory ReleaseCategory `json:"ReleaseCategory"`
|
||||
}
|
||||
expectedOutput string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
input: struct {
|
||||
ReleaseCategory ReleaseCategory `json:"ReleaseCategory"`
|
||||
}{ReleaseCategory: StableReleaseCategory},
|
||||
expectedOutput: `{"ReleaseCategory":"Stable"}`,
|
||||
},
|
||||
{
|
||||
input: struct {
|
||||
ReleaseCategory ReleaseCategory `json:"ReleaseCategory"`
|
||||
}{ReleaseCategory: EarlyAccessReleaseCategory},
|
||||
expectedOutput: `{"ReleaseCategory":"EarlyAccess"}`,
|
||||
},
|
||||
{
|
||||
input: struct {
|
||||
ReleaseCategory ReleaseCategory `json:"ReleaseCategory"`
|
||||
}{ReleaseCategory: 4},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
output, err := json.Marshal(test.input)
|
||||
|
||||
if test.wantErr {
|
||||
if err == nil && len(output) == 0 {
|
||||
t.Errorf("expected error or non-empty output for invalid category")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if string(output) != test.expectedOutput {
|
||||
t.Errorf("json.Marshal() = %v, want %v", string(output), test.expectedOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_FileIdentifier_JsonUnmarshal(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected FileIdentifier
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
input: `{"Identifier": "package"}`,
|
||||
expected: PackageIdentifier,
|
||||
},
|
||||
{
|
||||
input: `{"Identifier": "Package"}`,
|
||||
expected: PackageIdentifier,
|
||||
},
|
||||
{
|
||||
input: `{"Identifier": "pACKage"}`,
|
||||
expected: PackageIdentifier,
|
||||
},
|
||||
{
|
||||
input: `{"Identifier": "pACKage "}`,
|
||||
expected: PackageIdentifier,
|
||||
},
|
||||
{
|
||||
input: `{"Identifier": "installer"}`,
|
||||
expected: InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
input: `{"Identifier": "Installer"}`,
|
||||
expected: InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
input: `{"Identifier": "iNSTaller "}`,
|
||||
expected: InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
input: `{"Identifier": "error"}`,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Identifier FileIdentifier
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
err := json.Unmarshal([]byte(test.input), &data)
|
||||
if err != nil && !test.wantErr {
|
||||
t.Errorf("json.Unmarshal() error = %v, wantErr %v", err, test.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if test.wantErr && err == nil {
|
||||
t.Errorf("expected err got nil")
|
||||
}
|
||||
|
||||
if !test.wantErr && data.Identifier != test.expected {
|
||||
t.Errorf("got %v, want %v", data.Identifier, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_FileIdentifier_JsonMarshal(t *testing.T) {
|
||||
tests := []struct {
|
||||
input struct {
|
||||
Identifier FileIdentifier
|
||||
}
|
||||
expectedOutput string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
input: struct {
|
||||
Identifier FileIdentifier
|
||||
}{Identifier: PackageIdentifier},
|
||||
expectedOutput: `{"Identifier":"package"}`,
|
||||
},
|
||||
{
|
||||
input: struct {
|
||||
Identifier FileIdentifier
|
||||
}{Identifier: InstallerIdentifier},
|
||||
expectedOutput: `{"Identifier":"installer"}`,
|
||||
},
|
||||
{
|
||||
input: struct {
|
||||
Identifier FileIdentifier
|
||||
}{Identifier: 4},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
output, err := json.Marshal(test.input)
|
||||
|
||||
if test.wantErr {
|
||||
if err == nil && len(output) == 0 {
|
||||
t.Errorf("expected error or non-empty output for invalid identifier")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if string(output) != test.expectedOutput {
|
||||
t.Errorf("json.Marshal() = %v, want %v", string(output), test.expectedOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
135
internal/updater/types_version.go
Normal file
135
internal/updater/types_version.go
Normal file
@ -0,0 +1,135 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ReleaseCategory uint8
|
||||
type FileIdentifier uint8
|
||||
|
||||
const (
|
||||
EarlyAccessReleaseCategory ReleaseCategory = iota
|
||||
StableReleaseCategory
|
||||
)
|
||||
|
||||
const (
|
||||
PackageIdentifier FileIdentifier = iota
|
||||
InstallerIdentifier
|
||||
)
|
||||
|
||||
var (
|
||||
releaseCategoryName = map[uint8]string{ //nolint:gochecknoglobals
|
||||
0: "EarlyAccess",
|
||||
1: "Stable",
|
||||
}
|
||||
releaseCategoryValue = map[string]uint8{ //nolint:gochecknoglobals
|
||||
"earlyaccess": 0,
|
||||
"stable": 1,
|
||||
}
|
||||
fileIdentifierName = map[uint8]string{ //nolint:gochecknoglobals
|
||||
0: "package",
|
||||
1: "installer",
|
||||
}
|
||||
fileIdentifierValue = map[string]uint8{ //nolint:gochecknoglobals
|
||||
"package": 0,
|
||||
"installer": 1,
|
||||
}
|
||||
)
|
||||
|
||||
func ParseFileIdentifier(s string) (FileIdentifier, error) {
|
||||
s = strings.TrimSpace(strings.ToLower(s))
|
||||
val, ok := fileIdentifierValue[s]
|
||||
if !ok {
|
||||
return FileIdentifier(0), fmt.Errorf("%s is not a valid file identifier", s)
|
||||
}
|
||||
|
||||
return FileIdentifier(val), nil
|
||||
}
|
||||
|
||||
func (fi FileIdentifier) String() string {
|
||||
return fileIdentifierName[uint8(fi)]
|
||||
}
|
||||
|
||||
func (fi FileIdentifier) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(fi.String())
|
||||
}
|
||||
|
||||
func (fi *FileIdentifier) UnmarshalJSON(data []byte) (err error) {
|
||||
var fileIdentifier string
|
||||
if err := json.Unmarshal(data, &fileIdentifier); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsedFileIdentifier, err := ParseFileIdentifier(fileIdentifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*fi = parsedFileIdentifier
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseReleaseCategory(s string) (ReleaseCategory, error) {
|
||||
s = strings.TrimSpace(strings.ToLower(s))
|
||||
val, ok := releaseCategoryValue[s]
|
||||
if !ok {
|
||||
return ReleaseCategory(0), fmt.Errorf("%s is not a valid release category", s)
|
||||
}
|
||||
|
||||
return ReleaseCategory(val), nil
|
||||
}
|
||||
|
||||
func (rc ReleaseCategory) String() string {
|
||||
return releaseCategoryName[uint8(rc)]
|
||||
}
|
||||
|
||||
func (rc ReleaseCategory) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(rc.String())
|
||||
}
|
||||
|
||||
func (rc *ReleaseCategory) UnmarshalJSON(data []byte) (err error) {
|
||||
var releaseCat string
|
||||
if err := json.Unmarshal(data, &releaseCat); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsedCat, err := ParseReleaseCategory(releaseCat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*rc = parsedCat
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc ReleaseCategory) UpdateEligible(channel Channel) bool {
|
||||
if channel == StableChannel && rc == StableReleaseCategory {
|
||||
return true
|
||||
}
|
||||
|
||||
if channel == EarlyChannel && rc == EarlyAccessReleaseCategory || rc == StableReleaseCategory {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@ -29,13 +29,17 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/versioner"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const updateFileVersion = 1
|
||||
|
||||
var (
|
||||
ErrDownloadVerify = errors.New("failed to download or verify the update")
|
||||
ErrInstall = errors.New("failed to install the update")
|
||||
ErrUpdateAlreadyInstalled = errors.New("update is already installed")
|
||||
ErrVersionFileDownloadOrVerify = errors.New("failed to download or verify the version file")
|
||||
ErrReleaseUpdatePackageMissing = errors.New("release update package is missing")
|
||||
)
|
||||
|
||||
type Downloader interface {
|
||||
@ -53,6 +57,7 @@ type Updater struct {
|
||||
verifier *crypto.KeyRing
|
||||
product string
|
||||
platform string
|
||||
version uint
|
||||
}
|
||||
|
||||
func NewUpdater(ver *versioner.Versioner, verifier *crypto.KeyRing, product, platform string) *Updater {
|
||||
@ -62,10 +67,36 @@ func NewUpdater(ver *versioner.Versioner, verifier *crypto.KeyRing, product, pla
|
||||
verifier: verifier,
|
||||
product: product,
|
||||
platform: platform,
|
||||
version: updateFileVersion,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Updater) GetVersionInfo(ctx context.Context, downloader Downloader, channel Channel) (VersionInfo, error) {
|
||||
func (u *Updater) GetVersionInfoLegacy(ctx context.Context, downloader Downloader, channel Channel) (VersionInfoLegacy, error) {
|
||||
b, err := downloader.DownloadAndVerify(
|
||||
ctx,
|
||||
u.verifier,
|
||||
u.getVersionFileURLLegacy(),
|
||||
u.getVersionFileURLLegacy()+".sig",
|
||||
)
|
||||
if err != nil {
|
||||
return VersionInfoLegacy{}, fmt.Errorf("%w: %w", ErrVersionFileDownloadOrVerify, err)
|
||||
}
|
||||
|
||||
var versionMap VersionMap
|
||||
|
||||
if err := json.Unmarshal(b, &versionMap); err != nil {
|
||||
return VersionInfoLegacy{}, err
|
||||
}
|
||||
|
||||
version, ok := versionMap[channel]
|
||||
if !ok {
|
||||
return VersionInfoLegacy{}, errors.New("no updates available for this channel")
|
||||
}
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func (u *Updater) GetVersionInfo(ctx context.Context, downloader Downloader) (VersionInfo, error) {
|
||||
b, err := downloader.DownloadAndVerify(
|
||||
ctx,
|
||||
u.verifier,
|
||||
@ -76,21 +107,16 @@ func (u *Updater) GetVersionInfo(ctx context.Context, downloader Downloader, cha
|
||||
return VersionInfo{}, fmt.Errorf("%w: %w", ErrVersionFileDownloadOrVerify, err)
|
||||
}
|
||||
|
||||
var versionMap VersionMap
|
||||
var releases VersionInfo
|
||||
|
||||
if err := json.Unmarshal(b, &versionMap); err != nil {
|
||||
if err := json.Unmarshal(b, &releases); err != nil {
|
||||
return VersionInfo{}, err
|
||||
}
|
||||
|
||||
version, ok := versionMap[channel]
|
||||
if !ok {
|
||||
return VersionInfo{}, errors.New("no updates available for this channel")
|
||||
}
|
||||
|
||||
return version, nil
|
||||
return releases, nil
|
||||
}
|
||||
|
||||
func (u *Updater) InstallUpdate(ctx context.Context, downloader Downloader, update VersionInfo) error {
|
||||
func (u *Updater) InstallUpdateLegacy(ctx context.Context, downloader Downloader, update VersionInfoLegacy) error {
|
||||
if u.installer.IsAlreadyInstalled(update.Version) {
|
||||
return ErrUpdateAlreadyInstalled
|
||||
}
|
||||
@ -113,13 +139,64 @@ func (u *Updater) InstallUpdate(ctx context.Context, downloader Downloader, upda
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Updater) InstallUpdate(ctx context.Context, downloader Downloader, release Release) error {
|
||||
if u.installer.IsAlreadyInstalled(release.Version) {
|
||||
return ErrUpdateAlreadyInstalled
|
||||
}
|
||||
|
||||
// Find update package
|
||||
idx := slices.IndexFunc(release.File, func(file File) bool {
|
||||
return file.Identifier == PackageIdentifier
|
||||
})
|
||||
|
||||
if idx == -1 {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"release_version": release.Version,
|
||||
}).Error("Update release does not contain update package")
|
||||
return ErrReleaseUpdatePackageMissing
|
||||
}
|
||||
|
||||
releaseUpdatePackage := release.File[idx]
|
||||
|
||||
b, err := downloader.DownloadAndVerify(
|
||||
ctx,
|
||||
u.verifier,
|
||||
releaseUpdatePackage.URL,
|
||||
releaseUpdatePackage.URL+".sig",
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrDownloadVerify, err)
|
||||
}
|
||||
|
||||
if err := u.installer.InstallUpdate(release.Version, bytes.NewReader(b)); err != nil {
|
||||
logrus.WithError(err).Error("Failed to install update")
|
||||
return ErrInstall
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Updater) RemoveOldUpdates() error {
|
||||
return u.versioner.RemoveOldVersions()
|
||||
}
|
||||
|
||||
// getVersionFileURL returns the URL of the version file.
|
||||
// getVersionFileURLLegacy returns the URL of the version file.
|
||||
// For example:
|
||||
// - https://protonmail.com/download/bridge/version_linux.json
|
||||
func (u *Updater) getVersionFileURL() string {
|
||||
func (u *Updater) getVersionFileURLLegacy() string {
|
||||
return fmt.Sprintf("%v/%v/version_%v.json", Host, u.product, u.platform)
|
||||
}
|
||||
|
||||
// getVersionFileURL returns the URL of the version file.
|
||||
// For example:
|
||||
// - https://protonmail.com/download/windows/x86/v1/version.json
|
||||
// - https://protonmail.com/download/linux/x86/v1/version.json
|
||||
// - https://protonmail.com/download/darwin/universal/v1/version.json
|
||||
func (u *Updater) getVersionFileURL() string {
|
||||
switch u.platform {
|
||||
case "darwin":
|
||||
return fmt.Sprintf("%v/%v/%v/universal/v%v/version.json", Host, u.product, u.platform, u.version)
|
||||
default:
|
||||
return fmt.Sprintf("%v/%v/%v/x86/v%v/version.json", Host, u.product, u.platform, u.version)
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,10 +19,36 @@ package updater
|
||||
|
||||
import (
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater/versioncompare"
|
||||
)
|
||||
|
||||
// VersionInfo is information about one version of the app.
|
||||
type File struct {
|
||||
URL string `json:"Url"`
|
||||
Sha512CheckSum string `json:"Sha512CheckSum,omitempty"`
|
||||
Identifier FileIdentifier `json:"Identifier"`
|
||||
}
|
||||
|
||||
type Release struct {
|
||||
ReleaseCategory ReleaseCategory `json:"CategoryName"`
|
||||
Version *semver.Version
|
||||
SystemVersion versioncompare.SystemVersion `json:"SystemVersion,omitempty"`
|
||||
RolloutProportion float64
|
||||
MinAuto *semver.Version `json:"MinAuto,omitempty"`
|
||||
ReleaseNotesPage string
|
||||
LandingPage string
|
||||
File []File `json:"File"`
|
||||
}
|
||||
|
||||
func (rel Release) IsEmpty() bool {
|
||||
return rel.Version == nil && len(rel.File) == 0
|
||||
}
|
||||
|
||||
type VersionInfo struct {
|
||||
Releases []Release `json:"Releases"`
|
||||
}
|
||||
|
||||
// VersionInfoLegacy is information about one version of the app.
|
||||
type VersionInfoLegacy struct {
|
||||
// Version is the semantic version of the release.
|
||||
Version *semver.Version
|
||||
|
||||
@ -46,6 +72,10 @@ type VersionInfo struct {
|
||||
RolloutProportion float64
|
||||
}
|
||||
|
||||
func (verInfo VersionInfoLegacy) IsEmpty() bool {
|
||||
return verInfo.Version == nil && verInfo.ReleaseNotesPage == ""
|
||||
}
|
||||
|
||||
// VersionMap represents the structure of the version.json file.
|
||||
// It looks like this:
|
||||
//
|
||||
@ -79,4 +109,4 @@ type VersionInfo struct {
|
||||
// }
|
||||
// }.
|
||||
|
||||
type VersionMap map[Channel]VersionInfo
|
||||
type VersionMap map[Channel]VersionInfoLegacy
|
||||
|
||||
205
internal/updater/version_test.go
Normal file
205
internal/updater/version_test.go
Normal file
@ -0,0 +1,205 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater/versioncompare"
|
||||
)
|
||||
|
||||
var mockJSONData = `
|
||||
{
|
||||
"Releases": [
|
||||
{
|
||||
"CategoryName": "Stable",
|
||||
"Version": "2.1.0",
|
||||
"ReleaseDate": "2025-01-15T08:00:00Z",
|
||||
"File": [
|
||||
{
|
||||
"Url": "https://downloads.example.com/v2.1.0/MyApp-2.1.0.pkg",
|
||||
"Sha512CheckSum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"Identifier": "package"
|
||||
},
|
||||
{
|
||||
"Url": "https://downloads.example.com/v2.1.0/MyApp-2.1.0.dmg",
|
||||
"Sha512CheckSum": "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce",
|
||||
"Identifier": "installer"
|
||||
}
|
||||
],
|
||||
"RolloutProportion": 0.5,
|
||||
"MinAuto": "2.0.0",
|
||||
"Commit": "8f52d45c9f8c31aa391315ea24e40c4a7e0b2c1d",
|
||||
"ReleaseNotesPage": "https://example.com/releases/2.1.0/notes",
|
||||
"LandingPage": "https://example.com/releases/2.1.0"
|
||||
},
|
||||
{
|
||||
"CategoryName": "EarlyAccess",
|
||||
"Version": "2.2.0-beta.1",
|
||||
"ReleaseDate": "2025-01-20T10:00:00Z",
|
||||
"File": [
|
||||
{
|
||||
"Url": "https://downloads.example.com/beta/v2.2.0-beta.1/MyApp-2.2.0-beta.1.pkg",
|
||||
"Sha512CheckSum": "a9f0e44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"Identifier": "package"
|
||||
}
|
||||
],
|
||||
"SystemVersion": {
|
||||
"Minimum": "13"
|
||||
},
|
||||
"RolloutProportion": 0.25,
|
||||
"MinAuto": "2.1.0",
|
||||
"Commit": "3e72d45c9f8c31aa391315ea24e40c4a7e0b2c1d",
|
||||
"ReleaseNotesPage": "https://example.com/releases/2.2.0-beta.1/notes",
|
||||
"LandingPage": "https://example.com/releases/2.2.0-beta.1"
|
||||
},
|
||||
{
|
||||
"CategoryName": "Stable",
|
||||
"Version": "2.0.0",
|
||||
"ReleaseDate": "2024-12-01T09:00:00Z",
|
||||
"File": [
|
||||
{
|
||||
"Url": "https://downloads.example.com/v2.0.0/MyApp-2.0.0.pkg",
|
||||
"Sha512CheckSum": "b5f0e44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"Identifier": "package"
|
||||
},
|
||||
{
|
||||
"Url": "https://downloads.example.com/v2.0.0/MyApp-2.0.0.dmg",
|
||||
"Sha512CheckSum": "d583e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce",
|
||||
"Identifier": "installer"
|
||||
}
|
||||
],
|
||||
"SystemVersion": {
|
||||
"Maximum": "12.0.0",
|
||||
"Minimum": "1.0.0"
|
||||
},
|
||||
"RolloutProportion": 1.0,
|
||||
"MinAuto": "1.9.0",
|
||||
"Commit": "2a42d45c9f8c31aa391315ea24e40c4a7e0b2c1d",
|
||||
"ReleaseNotesPage": "https://example.com/releases/2.0.0/notes",
|
||||
"LandingPage": "https://example.com/releases/2.0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
var expectedVersionInfo = VersionInfo{
|
||||
Releases: []Release{
|
||||
{
|
||||
ReleaseCategory: StableReleaseCategory,
|
||||
Version: semver.MustParse("2.1.0"),
|
||||
RolloutProportion: 0.5,
|
||||
MinAuto: semver.MustParse("2.0.0"),
|
||||
File: []File{
|
||||
{
|
||||
URL: "https://downloads.example.com/v2.1.0/MyApp-2.1.0.pkg",
|
||||
Identifier: PackageIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "https://downloads.example.com/v2.1.0/MyApp-2.1.0.dmg",
|
||||
Identifier: InstallerIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ReleaseCategory: EarlyAccessReleaseCategory,
|
||||
Version: semver.MustParse("2.2.0-beta.1"),
|
||||
RolloutProportion: 0.25,
|
||||
MinAuto: semver.MustParse("2.1.0"),
|
||||
File: []File{
|
||||
{
|
||||
URL: "https://downloads.example.com/beta/v2.2.0-beta.1/MyApp-2.2.0-beta.1.pkg",
|
||||
Identifier: PackageIdentifier,
|
||||
},
|
||||
},
|
||||
SystemVersion: versioncompare.SystemVersion{Minimum: "13"},
|
||||
},
|
||||
{
|
||||
ReleaseCategory: StableReleaseCategory,
|
||||
Version: semver.MustParse("2.0.0"),
|
||||
RolloutProportion: 1.0,
|
||||
MinAuto: semver.MustParse("1.9.0"),
|
||||
SystemVersion: versioncompare.SystemVersion{Maximum: "12.0.0", Minimum: "1.0.0"},
|
||||
File: []File{
|
||||
{
|
||||
URL: "https://downloads.example.com/v2.0.0/MyApp-2.0.0.pkg",
|
||||
Identifier: PackageIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "https://downloads.example.com/v2.0.0/MyApp-2.0.0.dmg",
|
||||
Identifier: InstallerIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func Test_Releases_JsonParse(t *testing.T) {
|
||||
var versionInfo VersionInfo
|
||||
if err := json.Unmarshal([]byte(mockJSONData), &versionInfo); err != nil {
|
||||
t.Fatalf("Failed to parse JSON: %v", err)
|
||||
}
|
||||
|
||||
if len(expectedVersionInfo.Releases) != len(versionInfo.Releases) {
|
||||
t.Fatalf("expected %d releases, parsed %d releases", len(expectedVersionInfo.Releases), len(versionInfo.Releases))
|
||||
}
|
||||
|
||||
for i, expectedRelease := range expectedVersionInfo.Releases {
|
||||
release := versionInfo.Releases[i]
|
||||
|
||||
if release.ReleaseCategory != expectedRelease.ReleaseCategory {
|
||||
t.Errorf("Release %d: expected category %v, got %v", i, expectedRelease.ReleaseCategory, release.ReleaseCategory)
|
||||
}
|
||||
|
||||
if release.Version.String() != expectedRelease.Version.String() {
|
||||
t.Errorf("Release %d: expected version %s, got %s", i, expectedRelease.Version, release.Version)
|
||||
}
|
||||
|
||||
if release.RolloutProportion != expectedRelease.RolloutProportion {
|
||||
t.Errorf("Release %d: expected rollout proportion %f, got %f", i, expectedRelease.RolloutProportion, release.RolloutProportion)
|
||||
}
|
||||
|
||||
if expectedRelease.MinAuto != nil && release.MinAuto.String() != expectedRelease.MinAuto.String() {
|
||||
t.Errorf("Release %d: expected min auto %s, got %s", i, expectedRelease.MinAuto, release.MinAuto)
|
||||
}
|
||||
|
||||
if expectedRelease.SystemVersion.Minimum != release.SystemVersion.Minimum {
|
||||
t.Errorf("Release %d: expected system version minimum %s, got %s", i, expectedRelease.SystemVersion.Minimum, release.SystemVersion.Minimum)
|
||||
}
|
||||
|
||||
if expectedRelease.SystemVersion.Maximum != release.SystemVersion.Maximum {
|
||||
t.Errorf("Release %d: expected system version minimum %s, got %s", i, expectedRelease.SystemVersion.Maximum, release.SystemVersion.Maximum)
|
||||
}
|
||||
|
||||
if len(release.File) != len(expectedRelease.File) {
|
||||
t.Errorf("Release %d: expected %d files, got %d", i, len(expectedRelease.File), len(release.File))
|
||||
}
|
||||
|
||||
for j, expectedFile := range expectedRelease.File {
|
||||
file := release.File[j]
|
||||
if file.URL != expectedFile.URL {
|
||||
t.Errorf("Release %d, File %d: expected URL %s, got %s", i, j, expectedFile.URL, file.URL)
|
||||
}
|
||||
if file.Identifier != expectedFile.Identifier {
|
||||
t.Errorf("Release %d, File %d: expected Identifier %v, got %v", i, j, expectedFile.Identifier, file.Identifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
134
internal/updater/versioncompare/compare_darwin.go
Normal file
134
internal/updater/versioncompare/compare_darwin.go
Normal file
@ -0,0 +1,134 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build darwin
|
||||
|
||||
package versioncompare
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/elastic/go-sysinfo/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (sysVer SystemVersion) IsHostVersionEligible(log *logrus.Entry, host types.Host, getHostOSVersion func(host types.Host) string) (bool, error) {
|
||||
if sysVer.Minimum == "" && sysVer.Maximum == "" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// We use getHostOSVersion simply for testing; It's passed via Bridge.
|
||||
var hostVersion string
|
||||
if getHostOSVersion == nil {
|
||||
hostVersion = host.Info().OS.Version
|
||||
} else {
|
||||
hostVersion = getHostOSVersion(host)
|
||||
}
|
||||
|
||||
log.Debugf("Checking host OS and update system version requirements. Host: %s; Maximum: %s; Minimum: %s",
|
||||
hostVersion, sysVer.Maximum, sysVer.Minimum)
|
||||
|
||||
hostVersionArr := strings.Split(hostVersion, ".")
|
||||
if len(hostVersionArr) == 0 || hostVersion == "" {
|
||||
return true, fmt.Errorf("could not get host version: %v", hostVersion)
|
||||
}
|
||||
|
||||
hostVersionArrInt := make([]int, len(hostVersionArr))
|
||||
for i := 0; i < len(hostVersionArr); i++ {
|
||||
hostNum, err := strconv.Atoi(hostVersionArr[i])
|
||||
if err != nil {
|
||||
// If we receive an alphanumeric version - we should continue with the update and stop checking for
|
||||
// OS version requirements.
|
||||
return true, fmt.Errorf("invalid host version number: %s - %s", hostVersionArr[i], hostVersion)
|
||||
}
|
||||
hostVersionArrInt[i] = hostNum
|
||||
}
|
||||
|
||||
if sysVer.Minimum != "" {
|
||||
pass, err := compareMinimumVersion(hostVersionArrInt, sysVer.Minimum)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !pass {
|
||||
return false, fmt.Errorf("host version is below minimum: hostVersion %v - minimumVersion %v", hostVersion, sysVer.Minimum)
|
||||
}
|
||||
}
|
||||
|
||||
if sysVer.Maximum != "" {
|
||||
pass, err := compareMaximumVersion(hostVersionArrInt, sysVer.Maximum)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !pass {
|
||||
return false, fmt.Errorf("host version is above maximum version: hostVersion %v - minimumVersion %v", hostVersion, sysVer.Maximum)
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func compareMinimumVersion(hostVersionArr []int, minVersion string) (bool, error) {
|
||||
minVersionArr := strings.Split(minVersion, ".")
|
||||
iterationDepth := min(len(hostVersionArr), len(minVersionArr))
|
||||
|
||||
for i := 0; i < iterationDepth; i++ {
|
||||
hostNum := hostVersionArr[i]
|
||||
|
||||
minNum, err := strconv.Atoi(minVersionArr[i])
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid minimum version number: %s - %s", minVersionArr[i], minVersion)
|
||||
}
|
||||
|
||||
if hostNum < minNum {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if hostNum > minNum {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil // minVersion is inclusive
|
||||
}
|
||||
|
||||
func compareMaximumVersion(hostVersionArr []int, maxVersion string) (bool, error) {
|
||||
maxVersionArr := strings.Split(maxVersion, ".")
|
||||
iterationDepth := min(len(maxVersionArr), len(hostVersionArr))
|
||||
|
||||
for i := 0; i < iterationDepth; i++ {
|
||||
hostNum := hostVersionArr[i]
|
||||
|
||||
maxNum, err := strconv.Atoi(maxVersionArr[i])
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid maximum version number: %s - %s", maxVersionArr[i], maxVersion)
|
||||
}
|
||||
|
||||
if hostNum > maxNum {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if hostNum < maxNum {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil // maxVersion is inclusive
|
||||
}
|
||||
105
internal/updater/versioncompare/compare_darwin_test.go
Normal file
105
internal/updater/versioncompare/compare_darwin_test.go
Normal file
@ -0,0 +1,105 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build darwin
|
||||
|
||||
package versioncompare
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/elastic/go-sysinfo"
|
||||
"github.com/elastic/go-sysinfo/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_IsHost_EligibleDarwin(t *testing.T) {
|
||||
host, err := sysinfo.Host()
|
||||
require.NoError(t, err)
|
||||
|
||||
testData := []struct {
|
||||
sysVer SystemVersion
|
||||
getHostOsVersionFn func(host types.Host) string
|
||||
shouldContinue bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
sysVer: SystemVersion{Minimum: "9.5", Maximum: "12.0"},
|
||||
getHostOsVersionFn: func(_ types.Host) string { return "10.0" },
|
||||
shouldContinue: true,
|
||||
},
|
||||
{
|
||||
sysVer: SystemVersion{Minimum: "9.5.5.5", Maximum: "10.1.1.0"},
|
||||
getHostOsVersionFn: func(_ types.Host) string { return "10.0" },
|
||||
shouldContinue: true,
|
||||
},
|
||||
{
|
||||
sysVer: SystemVersion{Minimum: "10.0.1", Maximum: "12.0"},
|
||||
getHostOsVersionFn: func(_ types.Host) string { return "10.0" },
|
||||
shouldContinue: true,
|
||||
},
|
||||
{
|
||||
sysVer: SystemVersion{Minimum: "11.0", Maximum: "12.0"},
|
||||
getHostOsVersionFn: func(_ types.Host) string { return "10.0" },
|
||||
shouldContinue: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
sysVer: SystemVersion{Minimum: "11.1.0", Maximum: "12.0.0"},
|
||||
getHostOsVersionFn: func(_ types.Host) string { return "11.0.0" },
|
||||
shouldContinue: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
sysVer: SystemVersion{Minimum: "10.0", Maximum: "12.0"},
|
||||
getHostOsVersionFn: func(_ types.Host) string { return "12.0" },
|
||||
shouldContinue: true,
|
||||
},
|
||||
{
|
||||
sysVer: SystemVersion{Minimum: "11.1.0", Maximum: "12.0.0"},
|
||||
getHostOsVersionFn: func(_ types.Host) string { return "" },
|
||||
shouldContinue: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
sysVer: SystemVersion{Minimum: "11.1.0", Maximum: "12.0.0"},
|
||||
getHostOsVersionFn: func(_ types.Host) string { return "a.b.c" },
|
||||
shouldContinue: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
sysVer: SystemVersion{},
|
||||
getHostOsVersionFn: func(_ types.Host) string { return "1.2.3" },
|
||||
shouldContinue: true,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testData {
|
||||
l := logrus.WithField("test", "test")
|
||||
shouldContinue, err := test.sysVer.IsHostVersionEligible(l, host, test.getHostOsVersionFn)
|
||||
|
||||
if test.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
require.Equal(t, test.shouldContinue, shouldContinue)
|
||||
}
|
||||
}
|
||||
31
internal/updater/versioncompare/compare_linux.go
Normal file
31
internal/updater/versioncompare/compare_linux.go
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build linux
|
||||
|
||||
package versioncompare
|
||||
|
||||
import (
|
||||
"github.com/elastic/go-sysinfo/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// IsHostVersionEligible - Checks whether host OS version is eligible for update. Defaults to true on Linux.
|
||||
func (sysVer SystemVersion) IsHostVersionEligible(log *logrus.Entry, _ types.Host, _ func(host types.Host) string) (bool, error) {
|
||||
log.Info("Checking host OS version on Linux. Defaulting to true.")
|
||||
return true, nil
|
||||
}
|
||||
31
internal/updater/versioncompare/compare_windows.go
Normal file
31
internal/updater/versioncompare/compare_windows.go
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package versioncompare
|
||||
|
||||
import (
|
||||
"github.com/elastic/go-sysinfo/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// IsHostVersionEligible - Checks whether host OS version is eligible for update. Defaults to true on Linux.
|
||||
func (sysVer SystemVersion) IsHostVersionEligible(log *logrus.Entry, _ types.Host, _ func(host types.Host) string) (bool, error) {
|
||||
log.Info("Checking host OS version on Windows. Defaulting to true.")
|
||||
return true, nil
|
||||
}
|
||||
29
internal/updater/versioncompare/types.go
Normal file
29
internal/updater/versioncompare/types.go
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package versioncompare
|
||||
|
||||
import "fmt"
|
||||
|
||||
type SystemVersion struct {
|
||||
Minimum string `json:"Minimum,omitempty"`
|
||||
Maximum string `json:"Maximum,omitempty"`
|
||||
}
|
||||
|
||||
func (sysVer SystemVersion) String() string {
|
||||
return fmt.Sprintf("SystemVersion: Maximum %s, Minimum %s", sysVer.Maximum, sysVer.Minimum)
|
||||
}
|
||||
Reference in New Issue
Block a user