Skip to content
Draft
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
6 changes: 5 additions & 1 deletion cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,11 @@ pub(crate) struct PasswordArgs {
help = "Password of archive. If password is not given it's asked from the tty"
)]
pub(crate) password: Option<Option<String>>,
#[arg(long, value_name = "FILE", help = "Read password from specified file")]
#[arg(
long,
value_name = "FILE",
help = "Read the password from the specified file. Only the first non-empty line is used, and trailing newlines are ignored."
)]
pub(crate) password_file: Option<PathBuf>,
}

Expand Down
15 changes: 12 additions & 3 deletions cli/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,21 @@ pub(crate) mod strip;
pub mod update;
pub mod xattr;

use crate::cli::{CipherAlgorithmArgs, Cli, Commands, GlobalContext, PasswordArgs};
use std::{fs, io};
use crate::{
cli::{CipherAlgorithmArgs, Cli, Commands, GlobalContext, PasswordArgs},
utils::fs,
};
use std::io;

fn ask_password(args: PasswordArgs) -> io::Result<Option<Vec<u8>>> {
if let Some(path) = args.password_file {
return Ok(Some(fs::read(path)?));
return match fs::read_first_non_empty_line(path)? {
Some(password) => Ok(Some(password.into_bytes())),
None => Err(io::Error::new(
io::ErrorKind::InvalidData,
"password is empty",
)),
};
};
Ok(match args.password {
Some(Some(password)) => {
Expand Down
33 changes: 32 additions & 1 deletion cli/src/utils/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ pub(crate) use file_id::HardlinkResolver;
pub(crate) use nodump::is_nodump;
pub(crate) use owner::*;
pub(crate) use pna::fs::*;
use std::{fs, io, path::Path};
use std::{
fs,
io::{self, BufRead},
path::Path,
};

/// Returns the current process umask.
///
Expand Down Expand Up @@ -162,3 +166,30 @@ pub(crate) fn file_create(path: impl AsRef<Path>, overwrite: bool) -> io::Result
fs::File::create_new(path)
}
}

/// Returns the first non-empty line from a file as UTF-8, if any.
///
/// Empty lines are skipped. The trailing line terminator (`\n` or `\r\n`)
/// is not included in the returned string.
///
/// # Returns
///
/// - `Ok(Some(line))` if a non-empty line was found.
/// - `Ok(None)` if the file contains no non-empty lines.
/// - `Err(e)` if an I/O error occurs while opening or reading the file.
///
/// # Errors
///
/// Propagates any I/O error that occurs while opening or reading the file.
#[inline]
pub(crate) fn read_first_non_empty_line<P: AsRef<Path>>(path: P) -> io::Result<Option<String>> {
let file = fs::File::open(path)?;
let reader = io::BufReader::new(file);
for line in reader.lines() {
let line = line?;
if !line.is_empty() {
return Ok(Some(line));
}
}
Ok(None)
}
Loading