|
1 | 1 | use std::{io::Seek, path::PathBuf}; |
2 | 2 |
|
3 | | -use anyhow::{ensure, Context}; |
| 3 | +use anyhow::{Context, ensure}; |
4 | 4 | use clap::{Args, Parser, Subcommand, ValueEnum}; |
5 | 5 | use futures_util::TryStreamExt; |
6 | 6 | use tokio::io::AsyncWriteExt; |
7 | 7 | use tracing::level_filters::LevelFilter; |
8 | 8 | use wasm_pkg_client::{ |
9 | | - caching::{CachingClient, FileCache}, |
10 | 9 | Client, PublishOpts, |
| 10 | + caching::{CachingClient, FileCache}, |
11 | 11 | }; |
12 | 12 | use wasm_pkg_common::{ |
13 | 13 | self, |
@@ -311,21 +311,61 @@ impl GetArgs { |
311 | 311 | tracing::debug!(?release, "Fetched release details"); |
312 | 312 |
|
313 | 313 | let output_trailing_slash = self.output.as_os_str().to_string_lossy().ends_with('/'); |
314 | | - let parent_dir = if output_trailing_slash { |
315 | | - let parent_dir = self.output.as_path(); |
316 | | - if !tokio::fs::try_exists(parent_dir).await? { |
317 | | - tokio::fs::create_dir_all(parent_dir).await |
318 | | - .context("Failed to create output dir")? |
| 314 | + // SAFETY: to avoid TOCTOU bugs, we open the directory and keep it open |
| 315 | + let _parent_dir: tokio::fs::File; |
| 316 | + let parent_dir_path = if output_trailing_slash { |
| 317 | + let parent_dir_path = self.output.as_path(); |
| 318 | + if !tokio::fs::try_exists(parent_dir_path).await? { |
| 319 | + tokio::fs::create_dir_all(parent_dir_path) |
| 320 | + .await |
| 321 | + .context("Failed to create output dir")?; |
| 322 | + _parent_dir = tokio::fs::OpenOptions::new() |
| 323 | + .read(true) |
| 324 | + .write(true) |
| 325 | + .create_new(true) |
| 326 | + .open(parent_dir_path) |
| 327 | + .await |
| 328 | + .with_context(|| { |
| 329 | + format!("failed to create new dir @ [{}]", parent_dir_path.display()) |
| 330 | + })?; |
| 331 | + parent_dir_path |
| 332 | + } else { |
| 333 | + _parent_dir = tokio::fs::OpenOptions::new() |
| 334 | + .read(true) |
| 335 | + .write(true) |
| 336 | + .create(false) |
| 337 | + .open(parent_dir_path) |
| 338 | + .await |
| 339 | + .with_context(|| { |
| 340 | + format!( |
| 341 | + "failed to open existing dir @ [{}]", |
| 342 | + parent_dir_path.display() |
| 343 | + ) |
| 344 | + })?; |
| 345 | + parent_dir_path |
319 | 346 | } |
320 | | - parent_dir |
321 | 347 | } else { |
322 | | - self.output |
| 348 | + let parent_dir_path = self |
| 349 | + .output |
323 | 350 | .parent() |
324 | | - .context("Failed to resolve output parent dir")? |
| 351 | + .context("Failed to resolve non-trailing-slash output parent dir")?; |
| 352 | + _parent_dir = tokio::fs::OpenOptions::new() |
| 353 | + .read(true) |
| 354 | + .write(true) |
| 355 | + .create(false) |
| 356 | + .open(parent_dir_path) |
| 357 | + .await |
| 358 | + .with_context(|| { |
| 359 | + format!( |
| 360 | + "failed to open existing parent dir @ [{}]", |
| 361 | + parent_dir_path.display() |
| 362 | + ) |
| 363 | + })?; |
| 364 | + parent_dir_path |
325 | 365 | }; |
326 | 366 |
|
327 | 367 | let (tmp_file, tmp_path) = |
328 | | - tempfile::NamedTempFile::with_prefix_in(".wkg-get", parent_dir)?.into_parts(); |
| 368 | + tempfile::NamedTempFile::with_prefix_in(".wkg-get", parent_dir_path)?.into_parts(); |
329 | 369 | tracing::debug!(?tmp_path, "Created temporary file"); |
330 | 370 |
|
331 | 371 | let mut content_stream = client.get_content(&package, &release).await?; |
|
0 commit comments