1+ import hashlib
12import json
23import os
34import shutil
1415from .service import restart_service , service_running
1516
1617_STABLE_URL = "https://github.com/HorizonUnix/UXTU4Linux/releases/latest/download/UXTU4Linux.zip"
17- _BETA_URL = "https://github.com/HorizonUnix/UXTU4Linux/releases/download/U4L-Beta/UXTU4Linux.zip"
18+ _BETA_URL = "https://github.com/HorizonUnix/UXTU4Linux/releases/download/U4L-Beta/UXTU4Linux.zip"
19+ _STABLE_SHA256_URL = "https://github.com/HorizonUnix/UXTU4Linux/releases/latest/download/UXTU4Linux.zip.sha256"
20+ _BETA_SHA256_URL = "https://github.com/HorizonUnix/UXTU4Linux/releases/download/U4L-Beta/UXTU4Linux.zip.sha256"
21+
22+ _SHA256_URL : dict [str , str ] = {
23+ _STABLE_URL : _STABLE_SHA256_URL ,
24+ _BETA_URL : _BETA_SHA256_URL ,
25+ }
1826
1927
2028def _ver_tuple (v : str ) -> tuple :
@@ -27,12 +35,29 @@ def _ver_tuple(v: str) -> tuple:
2735 return (0 ,)
2836
2937
30- def _release_url (url : str ) -> str :
38+ def _get_release_tag (url : str ) -> str :
3139 return url .rstrip ("/" ).split ("/" )[- 1 ]
3240
3341
42+ def _sha256_file (path : str ) -> str :
43+ h = hashlib .sha256 ()
44+ with open (path , "rb" ) as f :
45+ for chunk in iter (lambda : f .read (1024 * 1024 ), b"" ):
46+ h .update (chunk )
47+ return h .hexdigest ()
48+
49+
50+ def _fetch_expected_sha256 (sha_url : str ) -> str :
51+ with urllib .request .urlopen (sha_url , timeout = 10 ) as resp :
52+ raw = resp .read ().decode ("utf-8" , errors = "replace" ).strip ()
53+ expected = raw .split ()[0 ].lower ()
54+ if len (expected ) != 64 or not all (c in "0123456789abcdef" for c in expected ):
55+ raise RuntimeError (f"Invalid checksum format from { sha_url } " )
56+ return expected
57+
58+
3459def get_latest_version () -> str :
35- tag = _release_url (urllib .request .urlopen (cfg .LATEST_VER_URL , timeout = 5 ).geturl ())
60+ tag = _get_release_tag (urllib .request .urlopen (cfg .LATEST_VER_URL , timeout = 5 ).geturl ())
3661 if not tag or not tag .lstrip ("v" )[:1 ].isdigit ():
3762 raise ValueError (f"Unexpected version tag: { tag !r} " )
3863 return tag
@@ -44,7 +69,7 @@ def get_changelog() -> str:
4469 return data .get ("body" , "No changelog available." )
4570
4671
47- def get_beta_commit () -> str | None :
72+ def get_beta_tag_sha () -> str | None :
4873 req = urllib .request .Request (
4974 "https://api.github.com/repos/HorizonUnix/UXTU4Linux/git/ref/tags/U4L-Beta" ,
5075 headers = {"Accept" : "application/vnd.github+json" },
@@ -54,11 +79,15 @@ def get_beta_commit() -> str | None:
5479 return sha [:7 ] if sha else None
5580
5681
82+ def get_beta_commit () -> str | None :
83+ return get_beta_tag_sha ()
84+
85+
5786def _do_update (url : str = _STABLE_URL ) -> None :
5887
5988 module_file = Path (__file__ ).resolve ()
60- assets_dir = str (module_file .parent .parent )
61- src_dir = str (module_file .parent .parent .parent )
89+ assets_dir = str (module_file .parent .parent )
90+ src_dir = str (module_file .parent .parent .parent )
6291 install_dir = str (module_file .parent .parent .parent .parent )
6392
6493 zip_path = os .path .join (install_dir , "UXTU4Linux.zip" )
@@ -69,7 +98,15 @@ def _do_update(url: str = _STABLE_URL) -> None:
6998 presets_src = os .path .join (assets_dir , "custom.json" )
7099
71100 def _sudo (* args : str ) -> int :
72- return subprocess .run (["sudo" , * args ]).returncode
101+ result = subprocess .run (["sudo" , * args ], capture_output = True , text = True )
102+ if result .returncode != 0 :
103+ cmd = " " .join (["sudo" , * args ])
104+ print (f" Command failed ({ result .returncode } ): { cmd } " )
105+ if result .stderr and result .stderr .strip ():
106+ print (f" stderr: { result .stderr .strip ()} " )
107+ if result .stdout and result .stdout .strip ():
108+ print (f" stdout: { result .stdout .strip ()} " )
109+ return result .returncode
73110
74111 try :
75112 if os .path .exists (config_src ):
@@ -80,6 +117,19 @@ def _sudo(*args: str) -> int:
80117 print (" Downloading update..." )
81118 urllib .request .urlretrieve (url , zip_path )
82119
120+ sha_url = _SHA256_URL .get (url )
121+ if sha_url :
122+ try :
123+ print (" Verifying integrity..." )
124+ expected = _fetch_expected_sha256 (sha_url )
125+ actual = _sha256_file (zip_path )
126+ if actual != expected :
127+ raise RuntimeError (
128+ f"Integrity check failed (expected { expected } , got { actual } )."
129+ )
130+ except urllib .error .URLError :
131+ print (" Warning: checksum file not available — skipping integrity check." )
132+
83133 print (" Extracting..." )
84134 with zipfile .ZipFile (zip_path , "r" ) as zf :
85135 zf .extractall (new_folder )
@@ -200,4 +250,4 @@ def check_updates() -> None:
200250 "It may be unstable and is intended for testing only."
201251 )
202252 if not confirm ("Continue?" , subtitle = subtitle ):
203- sys .exit ("Quitting." )
253+ sys .exit ("Quitting." )
0 commit comments