Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: build

on:
push:
branches: [main]
branches: [main, dev]
pull_request:
workflow_dispatch:

Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Install/PKGBUILD
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Maintainer: Bryson Kelly <brysonak@protonmail (dot) com>
pkgname=bufusb-cli
_binname=bufusb
pkgver=0.2.5
pkgver=0.2.6
pkgrel=1
_srcdir="bufusb-$pkgver"
pkgdesc="A fast, safe bootable USB image flasher"
Expand Down
2 changes: 1 addition & 1 deletion Install/install.iss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#define BufAppVersion "0.2.5"
#define BufAppVersion "0.2.6"
#ifndef Arch
#define Arch "x64"
#endif
Expand Down
2 changes: 1 addition & 1 deletion buf-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bufusb"
version = "0.2.5"
version = "0.2.6"
edition = "2021"
description = "A fast, safe bootable USB flasher"

Expand Down
144 changes: 78 additions & 66 deletions buf-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@

use anyhow::{bail, Context, Result};
use clap::{ArgAction, Parser};
use libbuf::Mode;
use libbuf::{say, Mode};
use log::{debug, error, info, warn};

#[derive(Parser, Debug)]
#[command(
name = "bufusb",
version = "0.2.5",
long_version = "0.2.5\n Copyright (C) 2026 Bryson Kelly\n This program comes with ABSOLUTELY NO WARRANTY; for details, visit: https://github.com/brysonak/bufusb/blob/main/LICENSE\n This is free software, and you are welcome to redistribute it\n under certain conditions.",
version = "0.2.6",
long_version = "0.2.6\n Copyright (C) 2026 Bryson Kelly\n This program comes with ABSOLUTELY NO WARRANTY; for details, visit: https://github.com/brysonak/bufusb/blob/main/LICENSE\n This is free software, and you are welcome to redistribute it\n under certain conditions.",
author = "Bryson Kelly",
about = "A fast, safe bootable USB image flasher",
long_about = None,
Expand Down Expand Up @@ -120,7 +120,7 @@ struct Cli {
#[arg(
long = "log-path",
value_name = "PATH",
help = "Write the log file to this path (default path is the HOME/user directory)"
help = "Write the log file to this path (default: a timestamped file in the per-user log directory, see docs)"
)]
log_path: Option<String>,

Expand All @@ -136,45 +136,45 @@ struct Cli {
fn main() {
let cli = Cli::parse();

if let Err(e) = run(cli) {
error!("{:#}", e);
eprintln!("\n Error: {:#}\n", e);
if cli.no_logging && cli.log_path.is_some() {
eprintln!("error: --log-path and --no-logging cannot be used together");
std::process::exit(1);
}

let custom_log_path = cli.log_path.as_deref().map(|p| {
std::env::current_dir().map(|d| d.join(p)).unwrap_or_else(|_| p.into())
});

let file_logging = !cli.no_logging && (!cli.list || custom_log_path.is_some());
let log_path = libbuf::init_logger(file_logging, cli.verbose, custom_log_path);
if let Some(ref path) = log_path {
println!("Logging to: {}", path.display());
}
libbuf::logger::log_context();
debug!("Parsed CLI args: {:?}", cli);

let start = std::time::Instant::now();
let result = run(cli, log_path.as_deref());
match result {
Ok(()) => info!("Finished OK after {:.1?}", start.elapsed()),
Err(e) => {
error!("{:#}", e);
info!("Failed after {:.1?}", start.elapsed());
if let Some(p) = log_path {
eprintln!("The full log is at {}", p.display());
}
std::process::exit(1);
}
}
}

fn run(cli: Cli) -> Result<()> {
fn run(cli: Cli, log_path: Option<&std::path::Path>) -> Result<()> {
if cli.list {
let devices = libbuf::list_drives()?;
libbuf::print_device_table(&devices);
return Ok(());
}

if cli.no_logging && cli.log_path.is_some() {
bail!("Fatal: --log-path and --no-logging cannot be run at the same time. Stopping...");
}

// Absolute before elevation
let custom_log_path = cli
.log_path
.as_deref()
.map(|p| std::env::current_dir().map(|d| d.join(p)))
.transpose()
.context("Could not resolve --log-path")?;

let log_path = libbuf::init_logger(!cli.no_logging, cli.verbose, custom_log_path.clone())
.unwrap_or_else(|e| {
eprintln!("Warning: could not initialise logger: {}", e);
None
});

if let Some(ref path) = log_path {
println!("Logging to: {}", path.display());
}

info!("bufusb started");
debug!("Parsed CLI args: {:?}", cli);

let source = cli
.source
.clone()
Expand All @@ -188,9 +188,7 @@ fn run(cli: Cli) -> Result<()> {
// Parse --mode early. If the user asked for both dd and copy at once, stop before we prompt for elevation or touch the device
let requested = parse_modes(&cli.mode)?;
if requested.len() >= 2 {
error!("User requested both dd and copy modes at once");
eprintln!("Fatal: Cannot use both dd and copy modes at the same time, stopping...");
std::process::exit(1);
bail!("Fatal: Cannot use both dd and copy modes at the same time, stopping...");
}
let requested: Option<Mode> = requested.first().copied();

Expand All @@ -217,9 +215,11 @@ fn run(cli: Cli) -> Result<()> {
.with_context(|| format!("Could not resolve target path: {}", target))?
.to_string_lossy()
.into_owned();
info!("Source: {}", source);
info!("Target: {}", target);

if !libbuf::is_privileged() {
warn!("Not running as root/Administrator");
info!("Not running as root/Administrator, relaunching elevated");
let mut argv = vec![
"--source".to_string(), source.clone(),
"--target".to_string(), target.clone(),
Expand All @@ -233,7 +233,7 @@ fn run(cli: Cli) -> Result<()> {
if cli.offset != 0 {
argv.extend(["--offset".to_string(), cli.offset.to_string()]);
}
if let Some(ref p) = custom_log_path {
if let Some(p) = log_path {
argv.extend(["--log-path".to_string(), p.to_string_lossy().into_owned()]);
}
if let Some(ref l) = cli.label {
Expand All @@ -246,31 +246,55 @@ fn run(cli: Cli) -> Result<()> {
libbuf::elevate_or_warn(&argv)?;
}

log_target_device(&target);

// Sniff the image and settle on a single mode
let caps = libbuf::ImageCaps::sniff(std::path::Path::new(&source))
.with_context(|| format!("Could not read source header: {}", source))?;
info!(
"Source image: {} bytes, {:?}",
std::fs::metadata(&source).map(|m| m.len()).unwrap_or(0),
caps
);
let mode = match requested {
Some(m) => {
if libbuf::mode::mode_risky(m, caps) && !cli.force {
let risky = libbuf::mode::mode_risky(m, caps);
info!("Write mode: {} (forced with --mode, mismatched with image: {})", m, risky);
if risky && !cli.force {
confirm_risky_mode()?;
}
m
}
// No --mode given, auto-detect. Hybrids default to copy, dd only raw
// images get dd, extract-only ISOs get copy
None => libbuf::mode::auto(caps)?,
// No --mode given, auto-detect. Hybrids and raw images get dd, extract-only ISOs get copy
None => {
let m = libbuf::mode::auto(caps)?;
info!("Write mode: {} (auto-detected)", m);
m
}
};
info!("Write mode: {}", mode);

match mode {
Mode::Dd => write_dd(&cli, &source, &target),
Mode::Copy => write_copy(&cli, &source, &target, caps),
}
}

fn log_target_device(target: &str) {
match libbuf::list_drives() {
Ok(drives) => match drives.iter().find(|d| d.path.eq_ignore_ascii_case(target)) {
Some(d) => info!(
"Target device: {} | {} | {} ({} bytes) | removable={}",
d.path, d.model, d.size_human, d.size_bytes, d.removable
),
None => info!("Target {} is not in the drive list (image file, or a filtered device)", target),
},
Err(e) => info!("Could not enumerate drives to describe the target: {:#}", e),
}
}

fn write_dd(cli: &Cli, source: &str, target: &str) -> Result<()> {
if cli.label.is_some() {
println!("Warning: Flag(s) --label is not usable in dd mode, ignoring...");
warn!("--label is not usable in dd mode, ignoring");
}

let block_size = parse_size(&cli.block_size)
Expand All @@ -289,31 +313,24 @@ fn write_dd(cli: &Cli, source: &str, target: &str) -> Result<()> {
offset: cli.offset,
};

println!("\n Validating source and target...");
say!("\n Validating source and target...");
let (source_size, target_file) = libbuf::validate(&params)?;
println!(" Validation passed.");
info!("Validation passed, source size {} bytes", source_size);
say!(" Validation passed.");

if cli.dry_run {
println!("\n --dry-run: all checks passed. Nothing was written.\n");
info!("Dry-run complete, exiting without writing");
say!("\n --dry-run: all checks passed. Nothing was written.\n");
return Ok(());
}

if !cli.force {
confirm(source, target, source_size)?;
} else {
warn!("--force set, skipping confirmation prompt");
println!("\n --force: skipping confirmation, {} -> {}", source, target);
say!("\n --force: skipping confirmation, {} -> {}", source, target);
}

println!("\n Writing {} -> {}...\n", source, target);
info!("Starting write: {} -> {}", source, target);

say!("\n Writing {} -> {}...\n", source, target);
libbuf::write(&params, source_size, target_file)?;

info!("Write completed successfully");
println!("Write completed successfully.");
say!("Write completed successfully.");

Ok(())
}
Expand All @@ -335,10 +352,7 @@ fn write_copy(cli: &Cli, source: &str, target: &str, caps: libbuf::ImageCaps) ->
irrelevant.push("--block-size");
}
if !irrelevant.is_empty() {
println!(
"Warning: Flag(s) {} is not usable in copy mode, ignoring...",
irrelevant.join(", ")
);
warn!("{} not usable in copy mode, ignoring", irrelevant.join(", "));
}

if cli.dry_run {
Expand All @@ -349,13 +363,10 @@ fn write_copy(cli: &Cli, source: &str, target: &str, caps: libbuf::ImageCaps) ->
let iso_len = std::fs::metadata(source).map(|m| m.len()).unwrap_or(0);
confirm(source, target, iso_len)?;
} else {
warn!("--force set, skipping confirmation prompt");
println!("\n --force: skipping confirmation, copy {} -> {}", source, target);
say!("\n --force: skipping confirmation, copy {} -> {}", source, target);
}

println!("\n Copying {} -> {} (ISO mode)...\n", source, target);
info!("Starting copy: {} -> {}", source, target);

say!("\n Copying {} -> {} (ISO mode)...\n", source, target);
libbuf::copy::run(source, target, false, cli.label.as_deref())
}

Expand Down Expand Up @@ -400,6 +411,7 @@ fn confirm_risky_mode() -> Result<()> {

let mut input = String::new();
io::stdin().read_line(&mut input)?;
info!("Risky mode prompt answered {:?}", input.trim());
if input.trim().to_ascii_lowercase() != "y" {
bail!("Aborted by user.");
}
Expand Down
Loading
Loading