Skip to content

Commit 995a92c

Browse files
authored
feat: add .AppImage.DIGEST verification support (#197)
2 parents 1348764 + 60d43cd commit 995a92c

File tree

6 files changed

+145
-33
lines changed

6 files changed

+145
-33
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Changelog
22
All notable changes to this project will be documented in this file.
33

4+
## v1.4.0-alpha
45
## v1.3.0-alpha
56
## v1.2.0-alpha
67
### CHANGES

my_unicorn/github_client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class GitHubReleaseFetcher:
5858
r".*\.sum$",
5959
r".*\.hash$",
6060
r".*\.digest$",
61+
r".*\.DIGEST$",
6162
r".*appimage\.sha256$",
6263
r".*appimage\.sha512$",
6364
]

my_unicorn/services/verification_service.py

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,77 @@ def _build_checksum_url(
312312
f"https://github.com/{owner}/{repo}/releases/download/{tag_name}/{checksum_file}"
313313
)
314314

315+
def _prioritize_checksum_files(
316+
self,
317+
checksum_files: list[ChecksumFileInfo],
318+
target_filename: str,
319+
) -> list[ChecksumFileInfo]:
320+
"""Prioritize checksum files to try the most relevant one first.
321+
322+
For a target file like 'app.AppImage', this will prioritize:
323+
1. Exact match: 'app.AppImage.DIGEST'
324+
2. Platform-specific: 'app.AppImage.sha256'
325+
3. Generic files: 'checksums.txt', etc.
326+
327+
Args:
328+
checksum_files: List of detected checksum files
329+
target_filename: Name of the file being verified
330+
331+
Returns:
332+
Reordered list with most relevant checksum files first
333+
334+
"""
335+
if not checksum_files:
336+
return checksum_files
337+
338+
logger.debug(
339+
"🔍 Prioritizing %d checksum files for target: %s",
340+
len(checksum_files),
341+
target_filename,
342+
)
343+
344+
def get_priority(checksum_file: ChecksumFileInfo) -> tuple[int, str]:
345+
"""Get priority score for checksum file (lower = higher priority)."""
346+
filename = checksum_file.filename
347+
348+
# Priority 1: Exact match (e.g., app.AppImage.DIGEST)
349+
if (
350+
filename == f"{target_filename}.DIGEST"
351+
or filename == f"{target_filename}.digest"
352+
):
353+
logger.debug(" 📌 Priority 1 (exact .DIGEST): %s", filename)
354+
return (1, filename)
355+
356+
# Priority 2: Platform-specific hash files (e.g., app.AppImage.sha256)
357+
target_extensions = [".sha256", ".sha512", ".sha1", ".md5"]
358+
for ext in target_extensions:
359+
if filename == f"{target_filename}{ext}":
360+
logger.debug(" 📌 Priority 2 (platform-specific): %s", filename)
361+
return (2, filename)
362+
363+
# Priority 3: YAML files (usually most comprehensive)
364+
if checksum_file.format_type == "yaml":
365+
logger.debug(" 📌 Priority 3 (YAML): %s", filename)
366+
return (3, filename)
367+
368+
# Priority 4: Other .DIGEST files (might contain multiple files)
369+
if filename.lower().endswith((".digest",)):
370+
logger.debug(" 📌 Priority 4 (other .DIGEST): %s", filename)
371+
return (4, filename)
372+
373+
# Priority 5: Generic checksum files
374+
logger.debug(" 📌 Priority 5 (generic): %s", filename)
375+
return (5, filename)
376+
377+
# Sort by priority (lower number = higher priority)
378+
prioritized = sorted(checksum_files, key=get_priority)
379+
380+
logger.debug(" 📋 Final priority order:")
381+
for i, cf in enumerate(prioritized, 1):
382+
logger.debug(" %d. %s", i, cf.filename)
383+
384+
return prioritized
385+
315386
async def verify_file(
316387
self,
317388
file_path: Path,
@@ -384,9 +455,12 @@ async def verify_file(
384455
# Enable digest verification in config for future use
385456
updated_config["digest"] = True
386457

387-
# Try checksum file verification for all detected files
458+
# Try checksum file verification with smart prioritization
388459
if checksum_files:
389-
for i, checksum_file in enumerate(checksum_files):
460+
# Prioritize checksum files to try the most likely match first
461+
prioritized_files = self._prioritize_checksum_files(checksum_files, file_path.name)
462+
463+
for i, checksum_file in enumerate(prioritized_files):
390464
method_key = f"checksum_file_{i}" if i > 0 else "checksum_file"
391465

392466
checksum_result = await self._verify_checksum_file(

my_unicorn/strategies/install_url.py

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
)
1616
from ..icon import IconManager
1717
from ..logger import get_logger
18+
from ..services.verification_service import VerificationService
1819
from ..utils import extract_and_validate_version
19-
from ..verify import Verifier
2020
from .install import InstallationError, InstallStrategy, ValidationError
2121

2222
logger = get_logger(__name__)
@@ -161,7 +161,7 @@ async def _install_single_repo(
161161
# Verify download if requested
162162
if kwargs.get("verify_downloads", True):
163163
await self._perform_verification(
164-
download_path, appimage_asset, owner, repo_name
164+
download_path, appimage_asset, release_data, owner, repo_name
165165
)
166166

167167
# Move to install directory
@@ -258,13 +258,19 @@ async def _install_single_repo(
258258
}
259259

260260
async def _perform_verification(
261-
self, path: Path, asset: GitHubAsset, owner: str, repo_name: str
261+
self,
262+
path: Path,
263+
asset: GitHubAsset,
264+
release_data: GitHubReleaseDetails,
265+
owner: str,
266+
repo_name: str,
262267
) -> None:
263-
"""Perform download verification using available methods.
268+
"""Perform download verification using VerificationService with optimization.
264269
265270
Args:
266271
path: Path to downloaded file
267272
asset: GitHub asset information
273+
release_data: Full GitHub release data with all assets
268274
owner: Repository owner
269275
repo_name: Repository name
270276
@@ -273,35 +279,63 @@ async def _perform_verification(
273279
274280
"""
275281
logger.debug("🔍 Starting verification for %s", path.name)
276-
verifier = Verifier(path)
277-
verification_passed = False
278282

279-
# Try digest verification first if available
280-
if asset.get("digest"):
281-
try:
282-
logger.debug("🔐 Attempting digest verification")
283-
verifier.verify_digest(asset["digest"])
284-
logger.debug("✅ Digest verification passed")
285-
verification_passed = True
286-
except Exception as e:
287-
logger.warning("⚠️ Digest verification failed: %s", e)
283+
# Use VerificationService for comprehensive verification including optimized checksum files
284+
verification_service = VerificationService(self.download_service)
285+
286+
# Convert GitHubReleaseDetails assets to the format expected by verification service
287+
all_assets = []
288+
for release_asset in release_data["assets"]:
289+
all_assets.append(
290+
{
291+
"name": release_asset.get("name", ""),
292+
"size": release_asset.get("size", 0),
293+
"browser_download_url": release_asset.get("browser_download_url", ""),
294+
"digest": release_asset.get("digest", ""),
295+
}
296+
)
297+
298+
# Create verification config
299+
config = {
300+
"skip": False,
301+
"checksum_file": None, # Let it auto-detect with prioritization
302+
"checksum_hash_type": "sha256",
303+
"digest_enabled": bool(asset.get("digest")),
304+
}
305+
306+
# Convert asset to expected format
307+
asset_data = {
308+
"name": asset.get("name", ""),
309+
"size": asset.get("size", 0),
310+
"browser_download_url": asset.get("browser_download_url", ""),
311+
"digest": asset.get("digest", ""),
312+
}
288313

289-
# Always perform basic file size verification
290314
try:
291-
expected_size = asset.get("size", 0)
292-
if expected_size > 0:
293-
if not self.download_service.verify_file_size(path, expected_size):
294-
if not verification_passed:
295-
raise InstallationError("File size verification failed")
296-
else:
297-
logger.warning("⚠️ File size verification failed, but digest passed")
298-
else:
299-
logger.debug("✅ File size verification passed")
300-
else:
301-
logger.debug("⚠️ No expected file size available")
315+
# Perform comprehensive verification with optimized checksum file prioritization
316+
result = await verification_service.verify_file(
317+
file_path=path,
318+
asset=asset_data,
319+
config=config,
320+
owner=owner,
321+
repo=repo_name,
322+
tag_name=release_data["version"],
323+
app_name=repo_name,
324+
assets=all_assets,
325+
)
326+
327+
if not result.passed:
328+
raise InstallationError(
329+
"File verification failed - no verification methods succeeded"
330+
)
331+
332+
# Log verification methods used
333+
methods_used = list(result.methods.keys())
334+
logger.debug("✅ Verification passed using methods: %s", ", ".join(methods_used))
335+
302336
except Exception as e:
303-
if not verification_passed:
304-
raise InstallationError(f"File verification failed: {e}")
337+
logger.error("❌ Verification failed: %s", e)
338+
raise InstallationError(f"File verification failed: {e}")
305339

306340
logger.debug("✅ Verification completed")
307341

my_unicorn/verify.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ def _parse_traditional_checksum_file(
385385
logger.debug(" Full filename in checksum: %s", file_name)
386386
return hash_value
387387

388-
logger.warning("⚠️ Target file %s not found in traditional checksum file", filename)
388+
logger.info("⚠️ Target file %s not found in traditional checksum file", filename)
389389
logger.debug(" Found %d entries:", len(found_entries))
390390
for hash_val, full_name, base_name in found_entries:
391391
logger.debug(" • %s (full: %s) -> %s...", base_name, full_name, hash_val[:16])
@@ -422,6 +422,8 @@ def _detect_hash_type_from_filename(self, filename: str) -> HashType:
422422
return "sha1"
423423
elif "md5" in filename_lower:
424424
return "md5"
425+
elif filename_lower.endswith(".digest"):
426+
return "sha256" # .DIGEST files typically use sha256
425427
else:
426428
return "sha256" # Default fallback
427429

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = 'my-unicorn'
3-
version = '1.3.0-alpha'
3+
version = '1.4.0-alpha'
44
maintainers = [{ name = "Cyber-Syntax" }]
55
license = { text = "GPL-3.0-or-later" }
66
description = 'It downloads/updates appimages via GitHub API. It also validates the appimage with SHA256 and SHA512.'

0 commit comments

Comments
 (0)