You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
188 lines
6.5 KiB
188 lines
6.5 KiB
// Copyright (C) 2024 Simon Quigley <tsimonq2@ubuntu.com>
|
|
//
|
|
// This program 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.
|
|
//
|
|
// This program 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.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/snapcore/snapd/snap"
|
|
"github.com/snapcore/snapd/store"
|
|
"golang.org/x/crypto/sha3"
|
|
)
|
|
|
|
// verifyChecksum calculates the SHA3-384 checksum of a file and compares it with the expected checksum.
|
|
func verifyChecksum(filePath, expectedChecksum string) (bool, error) {
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to open file for checksum verification: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
hash := sha3.New384()
|
|
if _, err := io.Copy(hash, file); err != nil {
|
|
return false, fmt.Errorf("failed to calculate checksum: %w", err)
|
|
}
|
|
|
|
calculatedChecksum := hex.EncodeToString(hash.Sum(nil))
|
|
return strings.EqualFold(calculatedChecksum, expectedChecksum), nil
|
|
}
|
|
|
|
// extractRevisionFromFile extracts the revision number from a file name by splitting at the last underscore.
|
|
func extractRevisionFromFile(fileName string) string {
|
|
lastUnderscore := strings.LastIndex(fileName, "_")
|
|
if lastUnderscore == -1 {
|
|
return ""
|
|
}
|
|
revisionWithSuffix := fileName[lastUnderscore+1:]
|
|
// Handle both .snap and .assert suffixes
|
|
if strings.HasSuffix(revisionWithSuffix, ".snap") {
|
|
return strings.TrimSuffix(revisionWithSuffix, ".snap")
|
|
} else if strings.HasSuffix(revisionWithSuffix, ".assert") {
|
|
return strings.TrimSuffix(revisionWithSuffix, ".assert")
|
|
}
|
|
return revisionWithSuffix
|
|
}
|
|
|
|
// fileExists checks if a file exists and is not a directory before we use it
|
|
func fileExists(filename string) bool {
|
|
info, err := os.Stat(filename)
|
|
if os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
return !info.IsDir()
|
|
}
|
|
|
|
// parseSnapInfo extracts snap details from the assertion file and returns the snap's current revision.
|
|
func parseSnapInfo(assertFilePath, snapName string) store.CurrentSnap {
|
|
currentSnap := store.CurrentSnap{InstanceName: snapName}
|
|
file, err := os.Open(assertFilePath)
|
|
if err != nil {
|
|
verboseLog("Failed to read assertion file: %v\n", err)
|
|
return currentSnap
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if strings.HasPrefix(line, "snap-id:") {
|
|
currentSnap.SnapID = strings.TrimSpace(strings.SplitN(line, ":", 2)[1])
|
|
}
|
|
if strings.HasPrefix(line, "snap-revision:") {
|
|
revisionStr := strings.TrimSpace(strings.SplitN(line, ":", 2)[1])
|
|
revision, err := strconv.Atoi(revisionStr)
|
|
if err != nil {
|
|
verboseLog("Failed to parse snap-revision for snap %s: %v", snapName, err)
|
|
continue
|
|
}
|
|
currentSnap.Revision.N = revision
|
|
}
|
|
}
|
|
|
|
// Validate that required fields are present
|
|
if currentSnap.SnapID == "" || currentSnap.Revision.N == 0 {
|
|
verboseLog("Incomplete snap info in assertion file: %s", assertFilePath)
|
|
}
|
|
|
|
return currentSnap
|
|
}
|
|
|
|
// isSnapInCurrentSnaps checks if a snap is already in currentSnaps
|
|
func isSnapInCurrentSnaps(snapName string) (bool, snap.Revision) {
|
|
for _, snap := range currentSnaps {
|
|
if snap.InstanceName == snapName {
|
|
return true, snap.Revision
|
|
}
|
|
}
|
|
return false, snap.Revision{}
|
|
}
|
|
|
|
func removeSnapFromCurrentSnaps(snapName string, revision snap.Revision) {
|
|
for i := 0; i < len(currentSnaps); i++ {
|
|
if currentSnaps[i].Revision == revision {
|
|
// Remove the element at index i
|
|
currentSnaps = append(currentSnaps[:i], currentSnaps[i+1:]...)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// initializeDirectories ensures that the snaps and assertions directories exist
|
|
func initializeDirectories(snapsDir, assertionsDir string) {
|
|
if err := os.MkdirAll(snapsDir, 0755); err != nil {
|
|
log.Fatalf("Failed to create snaps directory: %v", err)
|
|
}
|
|
if err := os.MkdirAll(assertionsDir, 0755); err != nil {
|
|
log.Fatalf("Failed to create assertions directory: %v", err)
|
|
}
|
|
}
|
|
|
|
// getCurrentSnapInfo retrieves current snap information from assertions
|
|
func getCurrentSnapInfo(assertionsDir, snapName string) (*store.CurrentSnap, error) {
|
|
assertionFiles, err := filepath.Glob(filepath.Join(assertionsDir, fmt.Sprintf("%s_*.assert", snapName)))
|
|
if err != nil || len(assertionFiles) == 0 {
|
|
return nil, fmt.Errorf("no assertion file found for snap: %s", snapName)
|
|
}
|
|
|
|
assertionFile := assertionFiles[0]
|
|
currentSnap := parseSnapInfo(assertionFile, snapName)
|
|
if currentSnap.SnapID == "" || currentSnap.Revision.N == 0 {
|
|
return nil, fmt.Errorf("incomplete snap info in assertion file for snap: %s", snapName)
|
|
}
|
|
verboseLog("Found snap info for %s: SnapID: %s, Revision: %d", snapName, currentSnap.SnapID, currentSnap.Revision.N)
|
|
return ¤tSnap, nil
|
|
}
|
|
|
|
// verifySnapIntegrity verifies the integrity of a snap file
|
|
func verifySnapIntegrity(filePath, expectedChecksum string) bool {
|
|
checksumMatches, err := verifyChecksum(filePath, expectedChecksum)
|
|
if err != nil {
|
|
verboseLog("Checksum verification failed for %s: %v", filePath, err)
|
|
return false
|
|
}
|
|
return checksumMatches
|
|
}
|
|
|
|
// Get the raw VERSION_ID from /etc/os-release to use for branch detection
|
|
func getVersionID() (string, error) {
|
|
file, err := os.Open("/etc/os-release")
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to open /etc/os-release: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if strings.HasPrefix(line, "VERSION_ID=") {
|
|
// Remove the prefix and any surrounding quotes
|
|
versionID := strings.Trim(strings.SplitN(line, "=", 2)[1], `"`)
|
|
return versionID, nil
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return "", fmt.Errorf("error reading /etc/os-release: %w", err)
|
|
}
|
|
|
|
return "", fmt.Errorf("VERSION_ID not found in /etc/os-release")
|
|
}
|