···3838 return openSquashFS(file, offset)
3939 }
40404141- // Not at the expected offset — try scanning forward.
4141+ // Not at the expected offset; try scanning forward.
4242 if scanned, ok := scanForSquashFS(file, offset); ok {
4343 return openSquashFS(file, scanned)
4444 }
+22-10
internal/appherder/install.go
···2020 return "", fmt.Errorf("resolve AppImage path %q: %w", appimage, err)
2121 }
22222323+ // Verify before openAppImage: the DwarFS fallback executes the AppImage to
2424+ // extract it, so bad images must be refused first. Pinned-key check is
2525+ // deferred until the app name is known from the desktop file inside.
2626+ fingerprint, err := verifyAppImage(appimage, "", want)
2727+ if err != nil {
2828+ return "", err
2929+ }
3030+2331 fsys, closeAppImage, err := openAppImage(appimage)
2432 if err != nil {
2533 return "", err
···4149 icon := resolveIcon(fsys)
4250 appName = deriveAppName(desktop, desktopName, appimage)
43514444- // Verify integrity and signature before any filesystem writes, so a refused
4545- // AppImage installs nothing.
4646- pin, err := verifyAppImage(appimage, a.pinnedSigningKey(appName), want)
4747- if err != nil {
4848- return "", err
5252+ // Pinned-key check deferred from above; app name now known.
5353+ pinned := a.pinnedSigningKey(appName)
5454+ if pinned != "" {
5555+ if fingerprint == "" {
5656+ return "", fmt.Errorf("refusing unsigned AppImage: a signing key is pinned (%s); uninstall and reinstall to trust a different build", pinned)
5757+ }
5858+ if !strings.EqualFold(pinned, fingerprint) {
5959+ return "", fmt.Errorf("refusing AppImage: signing key changed (pinned %s, got %s); uninstall and reinstall to trust the new key", pinned, fingerprint)
6060+ }
4961 }
6262+ pin := fingerprint
50635164 // No desktop file inside the AppImage: synthesize a terminal launcher so
5265 // CLI apps still get a menu entry and are tracked by managedApps.
···114127 return a.install(tmpName, expectedChecksum{})
115128}
116129117117-// deriveAppName picks the canonical install name, matching GearLever so the
118118-// two tools land at the same path: the desktop entry's Name field (e.g.
119119-// "ES-DE" -> "esde"), then the desktop-file id, then the source filename.
130130+// deriveAppName picks the canonical install name: the desktop entry's Name
131131+// field (e.g. "ES-DE" -> "esde"), then the desktop-file id, then the source
132132+// filename.
120133func deriveAppName(desktop *desktopfile.File, desktopName string, appimagePath string) string {
121134 if desktop != nil {
122135 if name, ok := desktop.Get(desktopEntrySection, "Name"); ok && name != "" {
···130143}
131144132145// sanitizeAppName lowercases s, turns spaces into underscores, and drops any
133133-// character that isn't alphanumeric, underscore, or dot — GearLever's naming
134134-// rule.
146146+// character that isn't alphanumeric, underscore, or dot.
135147func sanitizeAppName(name string) string {
136148 name = strings.ToLower(name)
137149 name = strings.ReplaceAll(name, " ", "_")
+1-1
internal/appherder/list_test.go
···135135 t.Fatal(err)
136136 }
137137138138- // Desktop file with no Name= field — list should fall back to the filename.
138138+ // Desktop file with no Name= field; list should fall back to the filename.
139139 if err := os.WriteFile(filepath.Join(appimages, "noname.appimage"), []byte("x"), 0o644); err != nil {
140140 t.Fatal(err)
141141 }
+1-1
internal/appherder/sync.go
···151151}
152152153153// appImageBackedOrphans returns appids of unmanaged desktop entries whose
154154-// TryExec or Exec points at a missing file inside appimagesDir — launchers left
154154+// TryExec or Exec points at a missing file inside appimagesDir; launchers left
155155// by another tool after their AppImage was deleted.
156156func appImageBackedOrphans(applicationsDir, appimagesDir string) ([]string, error) {
157157 matches, err := filepath.Glob(filepath.Join(applicationsDir, "*.desktop"))