@@ -9,6 +9,10 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
99const REPO_OWNER : & str = "hotdata-dev" ;
1010const REPO_NAME : & str = "hotdata-cli" ;
1111const CURRENT_VERSION : & str = env ! ( "CARGO_PKG_VERSION" ) ;
12+ /// Fully-qualified Homebrew formula (e.g. `hotdata-dev/tap/cli`). Pulled from
13+ /// `[package.metadata.hotdata]` in Cargo.toml via build.rs so the README,
14+ /// dist-workspace.toml, and this binary all agree on the same target.
15+ const HOMEBREW_FORMULA : & str = env ! ( "HOTDATA_HOMEBREW_FORMULA" ) ;
1216const CHECK_INTERVAL_SECS : u64 = 86_400 ;
1317const NETWORK_TIMEOUT_SECS : u64 = 5 ;
1418
@@ -134,8 +138,8 @@ pub fn maybe_print_update_notice() {
134138 return ;
135139 } ;
136140 let how = match detect_install_method ( ) {
137- InstallMethod :: Homebrew => "Run: brew upgrade hotdata" ,
138- InstallMethod :: Other => "Run: hotdata update" ,
141+ InstallMethod :: Homebrew => format ! ( "Run: brew upgrade {HOMEBREW_FORMULA}" ) ,
142+ InstallMethod :: Other => "Run: hotdata update" . to_string ( ) ,
139143 } ;
140144 eprintln ! (
141145 "{}" ,
@@ -151,7 +155,7 @@ pub fn run_update() {
151155
152156 if detect_install_method ( ) == InstallMethod :: Homebrew {
153157 println ! ( "hotdata was installed via Homebrew. Update with:" ) ;
154- println ! ( " {}" , "brew upgrade hotdata" . cyan( ) ) ;
158+ println ! ( " {}" , format! ( "brew upgrade {HOMEBREW_FORMULA}" ) . cyan( ) ) ;
155159 return ;
156160 }
157161
@@ -222,23 +226,23 @@ fn perform_update(version: &Version) -> Result<(), String> {
222226 lzma_rs:: xz_decompress ( & mut std:: io:: Cursor :: new ( & xz_bytes[ ..] ) , & mut tar_bytes)
223227 . map_err ( |e| format ! ( "xz decompress: {e}" ) ) ?;
224228
225- let tmp_dir = std:: env:: temp_dir ( ) . join ( format ! ( "hotdata-update-{}" , std:: process:: id( ) ) ) ;
226- if tmp_dir. exists ( ) {
227- let _ = fs:: remove_dir_all ( & tmp_dir) ;
228- }
229- fs:: create_dir_all ( & tmp_dir) . map_err ( |e| format ! ( "creating temp dir: {e}" ) ) ?;
229+ // `tempfile::TempDir` creates a randomly-named directory with 0700
230+ // permissions and removes it on drop. The random suffix prevents a
231+ // local attacker on a shared system from pre-planting a symlink at a
232+ // predictable path and redirecting the extraction to a directory
233+ // they control.
234+ let tmp_dir = tempfile:: TempDir :: new ( ) . map_err ( |e| format ! ( "creating temp dir: {e}" ) ) ?;
230235
231236 let mut archive = tar:: Archive :: new ( std:: io:: Cursor :: new ( & tar_bytes[ ..] ) ) ;
232237 archive
233- . unpack ( & tmp_dir)
238+ . unpack ( tmp_dir. path ( ) )
234239 . map_err ( |e| format ! ( "extract tar: {e}" ) ) ?;
235240
236241 // cargo-dist lays out the tarball as `<asset-stem>/hotdata` (the binary
237242 // sits at the top of a single directory matching the asset name without
238243 // its extension).
239- let new_binary = tmp_dir. join ( & asset_stem) . join ( "hotdata" ) ;
244+ let new_binary = tmp_dir. path ( ) . join ( & asset_stem) . join ( "hotdata" ) ;
240245 if !new_binary. exists ( ) {
241- let _ = fs:: remove_dir_all ( & tmp_dir) ;
242246 return Err ( format ! (
243247 "binary not found in archive at {}" ,
244248 new_binary. display( )
@@ -253,13 +257,10 @@ fn perform_update(version: &Version) -> Result<(), String> {
253257 let backup = current_exe. with_extension ( "old" ) ;
254258 let _ = fs:: remove_file ( & backup) ;
255259
256- let result = self_update:: Move :: from_source ( & new_binary)
260+ self_update:: Move :: from_source ( & new_binary)
257261 . replace_using_temp ( & backup)
258262 . to_dest ( & current_exe)
259- . map_err ( |e| format ! ( "replacing binary: {e}" ) ) ;
260-
261- let _ = fs:: remove_dir_all ( & tmp_dir) ;
262- result
263+ . map_err ( |e| format ! ( "replacing binary: {e}" ) )
263264}
264265
265266#[ cfg( test) ]
0 commit comments