Skip to content

Commit 96d9ad3

Browse files
authored
fix: digest verification logic, version handling and install cleanup (#182)
2 parents 04dcbfe + 270ef96 commit 96d9ad3

File tree

8 files changed

+937
-219
lines changed

8 files changed

+937
-219
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.0.1-alpha
45
## v1.0.0-alpha
56
### BREAKING CHANGES
67
This release written from scratch to simplify the code and maintainability.

my_unicorn/github_client.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import aiohttp
1111

1212
from .auth import GitHubAuthManager, auth_manager
13+
from .utils import extract_and_validate_version
1314

1415

1516
class GitHubAsset(TypedDict):
@@ -46,16 +47,28 @@ def __init__(self, owner: str, repo: str, session: aiohttp.ClientSession) -> Non
4647
self.session = session
4748

4849
def _normalize_version(self, tag_name: str) -> str:
49-
"""Normalize version by stripping 'v' prefix.
50+
"""Normalize version by extracting and sanitizing version string.
51+
52+
Handles various formats including package@version and v-prefixed versions.
5053
5154
Args:
52-
tag_name: Version tag that may have 'v' prefix
55+
tag_name: Version tag that may have 'v' prefix or package format
5356
5457
Returns:
55-
Version string without 'v' prefix
58+
Sanitized version string
5659
5760
"""
58-
return tag_name.lstrip("v") if tag_name else ""
61+
if not tag_name:
62+
return ""
63+
64+
# Use the comprehensive version extraction and validation
65+
normalized = extract_and_validate_version(tag_name)
66+
67+
# Fall back to original logic if extraction fails
68+
if normalized is None:
69+
return tag_name.lstrip("v")
70+
71+
return normalized
5972

6073
async def fetch_latest_release(self) -> GitHubReleaseDetails:
6174
"""Fetch the latest release information from GitHub API.

my_unicorn/strategies/install_catalog.py

Lines changed: 124 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from my_unicorn.download import IconAsset
1111

1212
from ..logger import get_logger
13+
from ..utils import extract_and_validate_version
1314
from .install_url import InstallationError, InstallStrategy, ValidationError
1415

1516
logger = get_logger(__name__)
@@ -193,9 +194,11 @@ async def _install_single_app(
193194

194195
# Verify download if requested
195196
if kwargs.get("verify_downloads", True):
196-
await self._perform_verification(
197+
verification_result = await self._perform_verification(
197198
download_path, appimage_asset, app_config, release_data
198199
)
200+
else:
201+
verification_result = None
199202

200203
# Move to install directory
201204
final_path = self.storage_service.move_to_install_dir(download_path)
@@ -220,9 +223,15 @@ async def _install_single_app(
220223
if app_config.get("icon"):
221224
icon_path = await self._setup_catalog_icon(app_config, app_name, icon_dir)
222225

223-
# Create application configuration
226+
# Create application configuration (pass verification result for config updates)
224227
self._create_app_config(
225-
app_name, final_path, app_config, release_data, icon_dir, appimage_asset
228+
app_name,
229+
final_path,
230+
app_config,
231+
release_data,
232+
icon_dir,
233+
appimage_asset,
234+
verification_result,
226235
)
227236

228237
# Create desktop entry to reflect any changes (icon, paths, etc.)
@@ -252,7 +261,7 @@ async def _install_single_app(
252261
"path": str(final_path),
253262
"name": final_path.name,
254263
"source": "catalog",
255-
"version": release_data.get("tag_name"),
264+
"version": extract_and_validate_version(release_data.get("tag_name", "")),
256265
"icon_path": str(icon_path) if icon_path else None,
257266
}
258267

@@ -363,7 +372,7 @@ async def _perform_verification(
363372
asset: dict[str, Any],
364373
app_config: dict[str, Any],
365374
release_data: dict[str, Any],
366-
) -> None:
375+
) -> dict[str, Any]:
367376
"""Perform download verification based on catalog configuration.
368377
369378
Args:
@@ -372,6 +381,9 @@ async def _perform_verification(
372381
app_config: Application configuration from catalog
373382
release_data: GitHub release data
374383
384+
Returns:
385+
Dictionary containing successful verification methods and updated config
386+
375387
Raises:
376388
InstallationError: If verification fails
377389
@@ -381,28 +393,53 @@ async def _perform_verification(
381393
# Get verification configuration from catalog
382394
verification_config = app_config.get("verification", {})
383395

384-
# Skip verification if configured
385-
if verification_config.get("skip", False):
386-
logger.debug("⏭️ Verification skipped (configured in catalog)")
387-
return
388-
389396
logger.debug(f"🔍 Starting verification for {path.name}")
390397
verifier = Verifier(path)
391398
verification_passed = False
399+
successful_methods = {}
400+
updated_verification_config = verification_config.copy()
401+
402+
# Detect available verification methods
403+
has_digest = bool(asset.get("digest"))
404+
has_checksum_file = bool(verification_config.get("checksum_file"))
405+
catalog_skip = verification_config.get("skip", False)
406+
407+
logger.debug(
408+
f" Available methods: digest={has_digest}, checksum_file={has_checksum_file}"
409+
)
410+
logger.debug(f" Catalog skip setting: {catalog_skip}")
392411

393-
# Try digest verification first if enabled and available
394-
if verification_config.get("digest", False) and asset.get("digest"):
412+
# Only skip if configured AND no strong verification methods available
413+
if catalog_skip and not has_digest and not has_checksum_file:
414+
logger.debug(
415+
"⏭️ Verification skipped (configured in catalog, no strong methods available)"
416+
)
417+
return {"successful_methods": {}, "updated_config": updated_verification_config}
418+
elif catalog_skip and (has_digest or has_checksum_file):
419+
logger.debug(
420+
"🔄 Overriding catalog skip setting - strong verification methods available"
421+
)
422+
# Update config to reflect that we're now using verification
423+
updated_verification_config["skip"] = False
424+
425+
# Try digest verification first if available (prioritize over catalog setting)
426+
if has_digest:
395427
try:
396-
logger.debug("🔐 Attempting digest verification")
428+
logger.debug("🔐 Attempting digest verification (from GitHub API)")
429+
if catalog_skip:
430+
logger.debug(" Note: Using digest despite catalog skip=true setting")
397431
verifier.verify_digest(asset["digest"])
398432
logger.debug("✅ Digest verification passed")
399433
verification_passed = True
434+
successful_methods["digest"] = asset["digest"]
435+
# Enable digest verification in config for future use
436+
updated_verification_config["digest"] = True
400437
except Exception as e:
401438
logger.error(f"❌ Digest verification failed: {e}")
402439
# Continue to try other verification methods
403440

404441
# Try checksum file verification if configured and digest didn't pass
405-
if not verification_passed and verification_config.get("checksum_file"):
442+
if not verification_passed and has_checksum_file:
406443
checksum_file = verification_config["checksum_file"]
407444
hash_type = verification_config.get("checksum_hash_type", "sha256")
408445

@@ -421,6 +458,11 @@ async def _perform_verification(
421458
)
422459
logger.debug("✅ Checksum file verification passed")
423460
verification_passed = True
461+
successful_methods["checksum_file"] = {
462+
"url": checksum_url,
463+
"hash_type": hash_type,
464+
"file": checksum_file,
465+
}
424466
except Exception as e:
425467
logger.error(f"❌ Checksum file verification failed: {e}")
426468
# Continue to basic file size check
@@ -439,15 +481,24 @@ async def _perform_verification(
439481
if not verification_passed:
440482
raise InstallationError("File verification failed")
441483

442-
# If we have verification methods configured but none passed, fail
443-
if (
444-
verification_config.get("digest", False)
445-
or verification_config.get("checksum_file")
446-
) and not verification_passed:
447-
raise InstallationError("Configured verification methods failed")
484+
# If we have strong verification methods available but none passed, fail
485+
if (has_digest or has_checksum_file) and not verification_passed:
486+
available_methods = []
487+
if has_digest:
488+
available_methods.append("digest")
489+
if has_checksum_file:
490+
available_methods.append("checksum_file")
491+
raise InstallationError(
492+
f"Available verification methods failed: {', '.join(available_methods)}"
493+
)
448494

449495
logger.debug("✅ Verification completed")
450496

497+
return {
498+
"successful_methods": successful_methods,
499+
"updated_config": updated_verification_config,
500+
}
501+
451502
async def _setup_catalog_icon(
452503
self, app_config: dict[str, Any], app_name: str, icon_dir: Path
453504
) -> Path | None:
@@ -497,6 +548,7 @@ def _create_app_config(
497548
release_data: dict[str, Any],
498549
icon_dir: Path,
499550
appimage_asset: dict[str, Any] | None = None,
551+
verification_result: dict[str, Any] | None = None,
500552
) -> None:
501553
"""Create application configuration after successful installation.
502554
@@ -507,6 +559,7 @@ def _create_app_config(
507559
release_data: GitHub release data
508560
icon_dir: Directory where icons are stored
509561
appimage_asset: AppImage asset info for digest
562+
verification_result: Result from verification with updated config
510563
511564
"""
512565
# Extract appimage config from catalog config
@@ -516,7 +569,8 @@ def _create_app_config(
516569
"config_version": "1.0.0",
517570
"source": "catalog",
518571
"appimage": {
519-
"version": release_data.get("tag_name", "unknown"),
572+
"version": extract_and_validate_version(release_data.get("tag_name", ""))
573+
or "unknown",
520574
"name": app_path.name,
521575
"rename": appimage_config.get("rename", app_name),
522576
"name_template": appimage_config.get("name_template", ""),
@@ -527,14 +581,8 @@ def _create_app_config(
527581
"owner": catalog_config.get("owner", ""),
528582
"repo": catalog_config.get("repo", ""),
529583
"github": catalog_config.get("github", {"repo": True, "prerelease": False}),
530-
"verification": catalog_config.get(
531-
"verification",
532-
{
533-
"digest": True,
534-
"skip": False,
535-
"checksum_file": "",
536-
"checksum_hash_type": "sha256",
537-
},
584+
"verification": self._get_updated_verification_config(
585+
catalog_config, verification_result
538586
),
539587
"icon": catalog_config.get("icon", {}),
540588
}
@@ -553,7 +601,53 @@ def _create_app_config(
553601
config["icon"]["path"] = str(icon_path)
554602

555603
self.catalog_manager.save_app_config(app_name, config)
556-
logger.debug(f"📝 Saved configuration for {app_name}")
604+
605+
# Log verification config updates if any
606+
if verification_result and verification_result.get("successful_methods"):
607+
methods = list(verification_result["successful_methods"].keys())
608+
logger.debug(
609+
f"📝 Saved configuration for {app_name} with updated verification: {', '.join(methods)}"
610+
)
611+
else:
612+
logger.debug(f"📝 Saved configuration for {app_name}")
613+
614+
def _get_updated_verification_config(
615+
self, catalog_config: dict[str, Any], verification_result: dict[str, Any] | None
616+
) -> dict[str, Any]:
617+
"""Get updated verification configuration based on successful verification methods.
618+
619+
Args:
620+
catalog_config: Original catalog configuration
621+
verification_result: Result from verification process
622+
623+
Returns:
624+
Updated verification configuration
625+
626+
"""
627+
# Start with catalog default or fallback
628+
verification_config = catalog_config.get(
629+
"verification",
630+
{
631+
"digest": True,
632+
"skip": False,
633+
"checksum_file": "",
634+
"checksum_hash_type": "sha256",
635+
},
636+
)
637+
638+
# Update based on verification results if available
639+
if verification_result and verification_result.get("updated_config"):
640+
updated_config = verification_result["updated_config"]
641+
verification_config.update(updated_config)
642+
643+
# Log what was updated
644+
if verification_result.get("successful_methods"):
645+
methods = list(verification_result["successful_methods"].keys())
646+
logger.debug(
647+
f"🔧 Updated verification config based on successful methods: {', '.join(methods)}"
648+
)
649+
650+
return verification_config
557651

558652
def _get_current_timestamp(self) -> str:
559653
"""Get current timestamp as ISO string.

0 commit comments

Comments
 (0)