feat!: add file-format header, configurable chunks, integration tests
Introduce a self-describing on-disk format and use it to address several
shortcomings of the 0.9 file layout, where the file simply began with a
raw 19-byte STREAM nonce prefix and used a hardcoded 64 KiB chunk size.
What changed for users
----------------------
* fcry files now start with a 16-byte header: magic ("fcry"), version,
algorithm id, flags, reserved byte, plaintext chunk_size (u32 LE),
KDF id + params, then the 19-byte nonce prefix. The full encoded
header is bound as AAD to every chunk, so tampering with chunk_size,
algorithm id, nonce prefix, or any future KDF parameter causes
authentication failure on every chunk -- not just the first.
* New `--chunk-size` CLI flag (encryption only). The decryptor reads
the chunk size from the header, so files encrypted with a non-default
size decrypt without the user having to remember it.
* Default plaintext chunk size raised from 64 KiB to 1 MiB.
* Bad input is now reported as an error instead of panicking: empty
ciphertext, truncated final chunk, wrong magic, bad version, zero
chunk_size, unknown algorithm id, and short --raw-key all return a
non-zero exit status with a diagnostic on stderr.
* Empty plaintext now produces a valid (authenticated) empty
ciphertext instead of panicking; the decryptor verifies it.
* `main` exits with status 1 on error (previously it printed and
returned 0).
This is a breaking change to the file format: 0.9.x files have no magic
or header and cannot be read by 0.10.x. Version bumped to 0.10.0.
Why this approach
-----------------
The header-as-AAD pattern is the standard way to make file-format
metadata tamper-evident without a separate signature: any bit-flip in
the header propagates into every chunk's authentication tag check, so
an attacker cannot, for example, change chunk_size to mis-frame the
stream or downgrade the algorithm id.
Storing chunk_size in the header (rather than fixing it at compile
time) lets us experiment with chunk sizes without breaking decrypt
compatibility, and is preparation for the parallel-pipeline work in
Roadmap 1.0 where worker count and chunk size interact.
The KDF section is a tagged variant (currently only `Raw`) so that
adding Argon2id later only adds a new variant + its salt/cost fields;
existing files keep decrypting because they carry `kdf_id = 0`.
Other changes bundled in
------------------------
* Switch RNG from `rand` (0.10) to `getrandom` (0.3). We only need
OS-provided random bytes for the nonce prefix; pulling in the full
`rand` crate for one `OsRng.fill_bytes` call was overkill, and
`rand` 0.10's `OsRng` API churn makes `getrandom` the cleaner fit.
* `FcryError` gains a `Format(String)` variant for header / framing
errors and a `From<getrandom::Error>` impl (replacing the
`rand::Error` impl).
* Drop the noisy `[reader]` / `[encrypt]` / `[decrypt]` stderr
tracing prints and the `dbg!(&cli.raw_key)` (which leaked the key
to stderr).
* Replace `unwrap()` on file open / create with `?` so I/O errors
surface as structured `FcryError::Io` instead of aborting.
* Remove the unused `AheadReader::read_exact` wrapper -- the
decryptor now reads the header through the underlying `BufRead`
directly before wrapping it in `AheadReader`.
Tests
-----
Add `tests/roundtrip.rs` (assert_cmd + tempfile) covering: empty
input, single byte, sub-chunk, exact chunk, chunk+1, multi-chunk,
custom small chunk size (4096), pathological 1-byte chunk size,
stdin/stdout pipe mode, wrong key rejection, tampered header,
tampered ciphertext, truncated ciphertext, bad magic, short raw key,
and the header-is-authoritative property (encrypt with a weird chunk
size, decrypt without specifying one). Also adds a unit test in
`header.rs` for header encode/decode roundtrip and bad-magic rejection.
TODO.md trimmed to the concrete follow-up sequence (manual STREAM
nonces, secrets/rlimit, atomic output, argon2id KDF + prompt,
multi-threaded pipeline, length-committed mode).
Test plan
---------
* `cargo clippy && cargo clippy --tests` -- clean.
* `cargo +nightly fmt` -- no diff.
* `cargo test` -- 16 integration + 2 header unit tests pass.
* Manual: `echo hi | fcry --raw-key 0123456789abcdef0123456789abcdef
| fcry -d --raw-key 0123456789abcdef0123456789abcdef` prints `hi`.
Trailers
--------
Refs: TODO.md (Roadmap 1.0 follow-up sequence)
Breaking-Change: file format; 0.9.x files cannot be decrypted by 0.10.x
This commit is contained in:
+48
-45
@@ -1,57 +1,60 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use chacha20poly1305::{KeyInit, XChaCha20Poly1305, aead::stream};
|
||||
use rand::{RngCore, rngs::OsRng};
|
||||
|
||||
use crate::error::*;
|
||||
use crate::reader::ReadInfoChunk;
|
||||
use crate::utils::BUFSIZE;
|
||||
use crate::header::{AlgId, Header, KdfParams, NONCE_PREFIX_LEN, TAG_LEN};
|
||||
use crate::reader::{AheadReader, ReadInfoChunk};
|
||||
use crate::utils::*;
|
||||
|
||||
pub fn encrypt<S: AsRef<str>>(
|
||||
input_file: Option<S>,
|
||||
output_file: Option<S>,
|
||||
key: [u8; 32],
|
||||
chunk_size: u32,
|
||||
) -> Result<(), FcryError> {
|
||||
let mut f_plain = read_from_file_or_stdin(input_file, BUFSIZE);
|
||||
let mut f_encrypted = write_to_file_or_stdout(output_file);
|
||||
let chunk_sz = chunk_size as usize;
|
||||
let mut f_plain = AheadReader::from(open_input(input_file)?, chunk_sz);
|
||||
let mut f_encrypted = open_output(output_file)?;
|
||||
|
||||
let mut nonce = [0u8; 19];
|
||||
OsRng.fill_bytes(&mut nonce);
|
||||
let mut nonce_prefix = [0u8; NONCE_PREFIX_LEN];
|
||||
getrandom::fill(&mut nonce_prefix)?;
|
||||
|
||||
// let key = XChaCha20Poly1305::generate_key(&mut OsRng);
|
||||
|
||||
f_encrypted.write_all(&nonce)?;
|
||||
let header = Header {
|
||||
alg: AlgId::XChaCha20Poly1305,
|
||||
flags: 0,
|
||||
chunk_size,
|
||||
kdf: KdfParams::Raw,
|
||||
nonce_prefix,
|
||||
};
|
||||
let aad = header.encode();
|
||||
f_encrypted.write_all(&aad)?;
|
||||
|
||||
let aead = XChaCha20Poly1305::new(&key.into());
|
||||
let mut stream_encryptor = stream::EncryptorBE32::from_aead(aead, &nonce.into());
|
||||
let mut stream_encryptor = stream::EncryptorBE32::from_aead(aead, &nonce_prefix.into());
|
||||
|
||||
let mut buf = vec![0; BUFSIZE];
|
||||
let mut buf = vec![0u8; chunk_sz];
|
||||
|
||||
loop {
|
||||
let read_result = f_plain.read_ahead(&mut buf)?;
|
||||
|
||||
match read_result {
|
||||
ReadInfoChunk::Normal(n) => {
|
||||
assert_eq!(n, BUFSIZE);
|
||||
assert_eq!(buf.len(), BUFSIZE);
|
||||
eprintln!("[encrypt]: read normal chunk");
|
||||
stream_encryptor.encrypt_next_in_place(&[], &mut buf)?;
|
||||
match f_plain.read_ahead(&mut buf)? {
|
||||
ReadInfoChunk::Normal(_) => {
|
||||
stream_encryptor.encrypt_next_in_place(&aad, &mut buf)?;
|
||||
f_encrypted.write_all(&buf)?;
|
||||
// buf grows after encrypt_next_in_place because of tag that is added
|
||||
// we shrink it to the BUFSIZE in order to read the correct size
|
||||
buf.truncate(BUFSIZE);
|
||||
buf.truncate(chunk_sz);
|
||||
}
|
||||
ReadInfoChunk::Last(n) => {
|
||||
eprintln!("[encrypt]: read last chunk");
|
||||
buf.truncate(n);
|
||||
stream_encryptor.encrypt_last_in_place(&[], &mut buf)?;
|
||||
stream_encryptor.encrypt_last_in_place(&aad, &mut buf)?;
|
||||
f_encrypted.write_all(&buf)?;
|
||||
break;
|
||||
}
|
||||
ReadInfoChunk::Empty => {
|
||||
eprintln!("[encrypt]: read empty chunk");
|
||||
panic!("[ERROR] Empty Chunk while reading");
|
||||
// Empty plaintext: still emit a final "last" tag so the decryptor
|
||||
// authenticates the (empty) stream rather than silently producing nothing.
|
||||
buf.clear();
|
||||
stream_encryptor.encrypt_last_in_place(&aad, &mut buf)?;
|
||||
f_encrypted.write_all(&buf)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,38 +67,38 @@ pub fn decrypt<S: AsRef<str>>(
|
||||
output_file: Option<S>,
|
||||
key: [u8; 32],
|
||||
) -> Result<(), FcryError> {
|
||||
let mut f_encrypted = read_from_file_or_stdin(input_file, BUFSIZE + 16);
|
||||
let mut f_plain = write_to_file_or_stdout(output_file);
|
||||
let mut reader = open_input(input_file)?;
|
||||
let header = Header::read(&mut reader)?;
|
||||
let aad = header.encode();
|
||||
|
||||
let mut nonce = [0u8; 19];
|
||||
f_encrypted.read_exact(&mut nonce)?;
|
||||
let chunk_sz = header.chunk_size as usize;
|
||||
let cipher_chunk = chunk_sz + TAG_LEN;
|
||||
|
||||
let mut f_encrypted = AheadReader::from(reader, cipher_chunk);
|
||||
let mut f_plain = open_output(output_file)?;
|
||||
|
||||
let aead = XChaCha20Poly1305::new(&key.into());
|
||||
let mut stream_decryptor = stream::DecryptorBE32::from_aead(aead, &nonce.into());
|
||||
let mut stream_decryptor = stream::DecryptorBE32::from_aead(aead, &header.nonce_prefix.into());
|
||||
|
||||
let mut buf = vec![0; BUFSIZE + 16];
|
||||
let mut buf = vec![0u8; cipher_chunk];
|
||||
|
||||
loop {
|
||||
let read_result = f_encrypted.read_ahead(&mut buf)?;
|
||||
|
||||
match read_result {
|
||||
ReadInfoChunk::Normal(n) => {
|
||||
assert_eq!(n, BUFSIZE + 16);
|
||||
eprintln!("[decrypt]: read normal chunk");
|
||||
stream_decryptor.decrypt_next_in_place(&[], &mut buf)?;
|
||||
match f_encrypted.read_ahead(&mut buf)? {
|
||||
ReadInfoChunk::Normal(_) => {
|
||||
stream_decryptor.decrypt_next_in_place(&aad, &mut buf)?;
|
||||
f_plain.write_all(&buf)?;
|
||||
buf.resize(BUFSIZE + 16, 0);
|
||||
buf.resize(cipher_chunk, 0);
|
||||
}
|
||||
ReadInfoChunk::Last(n) => {
|
||||
eprintln!("[decrypt]: read last chunk");
|
||||
buf.truncate(n);
|
||||
stream_decryptor.decrypt_last_in_place(&[], &mut buf)?;
|
||||
stream_decryptor.decrypt_last_in_place(&aad, &mut buf)?;
|
||||
f_plain.write_all(&buf)?;
|
||||
break;
|
||||
}
|
||||
ReadInfoChunk::Empty => {
|
||||
eprintln!("[decrypt]: read empty chunk");
|
||||
panic!("Empty Chunk while reading");
|
||||
return Err(FcryError::Format(
|
||||
"truncated ciphertext: missing final chunk".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -8,7 +8,8 @@ use std::io;
|
||||
pub enum FcryError {
|
||||
Io(io::Error),
|
||||
Crypto(aead::Error),
|
||||
Rng(rand::Error),
|
||||
Rng(getrandom::Error),
|
||||
Format(String),
|
||||
}
|
||||
|
||||
impl From<io::Error> for FcryError {
|
||||
@@ -23,8 +24,8 @@ impl From<aead::Error> for FcryError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rand::Error> for FcryError {
|
||||
fn from(e: rand::Error) -> Self {
|
||||
impl From<getrandom::Error> for FcryError {
|
||||
fn from(e: getrandom::Error) -> Self {
|
||||
FcryError::Rng(e)
|
||||
}
|
||||
}
|
||||
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! On-disk file format for fcry.
|
||||
//!
|
||||
//! Layout:
|
||||
//! ```text
|
||||
//! magic "fcry" 4 bytes
|
||||
//! version u8 1
|
||||
//! alg_id u8 1
|
||||
//! flags u8 1
|
||||
//! reserved u8 1 (must be 0)
|
||||
//! chunk_size u32 LE 4 (plaintext bytes per chunk)
|
||||
//! kdf_id u8 1
|
||||
//! kdf_params variable (depends on kdf_id)
|
||||
//! nonce_prefix [u8; 19] 19 (STREAM nonce prefix)
|
||||
//! --- end of header ---
|
||||
//! chunk[0..N] each chunk_size + 16 bytes,
|
||||
//! last may be shorter
|
||||
//! ```
|
||||
//!
|
||||
//! The full encoded header is fed as AAD to every chunk, so any tampering
|
||||
//! with chunk_size, nonce_prefix, kdf params, etc. causes authentication
|
||||
//! failure on every chunk.
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
use crate::error::FcryError;
|
||||
|
||||
const MAGIC: [u8; 4] = *b"fcry";
|
||||
const VERSION: u8 = 1;
|
||||
|
||||
pub const NONCE_PREFIX_LEN: usize = 19;
|
||||
pub const TAG_LEN: usize = 16;
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum AlgId {
|
||||
XChaCha20Poly1305 = 1,
|
||||
}
|
||||
|
||||
impl AlgId {
|
||||
fn from_u8(v: u8) -> Result<Self, FcryError> {
|
||||
match v {
|
||||
1 => Ok(Self::XChaCha20Poly1305),
|
||||
_ => Err(FcryError::Format(format!("unknown alg id: {v}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Key-derivation parameters stored in the header.
|
||||
///
|
||||
/// `Raw` means the key was supplied directly (no KDF). Future variants
|
||||
/// (e.g. Argon2id) will carry their salt + cost parameters here.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum KdfParams {
|
||||
Raw,
|
||||
}
|
||||
|
||||
impl KdfParams {
|
||||
fn id(&self) -> u8 {
|
||||
match self {
|
||||
Self::Raw => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_into(&self, _out: &mut Vec<u8>) {
|
||||
match self {
|
||||
Self::Raw => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_from(id: u8, _r: &mut impl Read) -> Result<Self, FcryError> {
|
||||
match id {
|
||||
0 => Ok(Self::Raw),
|
||||
_ => Err(FcryError::Format(format!("unknown kdf id: {id}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Header {
|
||||
pub alg: AlgId,
|
||||
pub flags: u8,
|
||||
pub chunk_size: u32,
|
||||
pub kdf: KdfParams,
|
||||
pub nonce_prefix: [u8; NONCE_PREFIX_LEN],
|
||||
}
|
||||
|
||||
impl Header {
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(64);
|
||||
out.extend_from_slice(&MAGIC);
|
||||
out.push(VERSION);
|
||||
out.push(self.alg as u8);
|
||||
out.push(self.flags);
|
||||
out.push(0); // reserved
|
||||
out.extend_from_slice(&self.chunk_size.to_le_bytes());
|
||||
out.push(self.kdf.id());
|
||||
self.kdf.write_into(&mut out);
|
||||
out.extend_from_slice(&self.nonce_prefix);
|
||||
out
|
||||
}
|
||||
|
||||
pub fn read(r: &mut impl Read) -> Result<Self, FcryError> {
|
||||
let mut magic = [0u8; 4];
|
||||
r.read_exact(&mut magic)?;
|
||||
if magic != MAGIC {
|
||||
return Err(FcryError::Format("not an fcry file (bad magic)".into()));
|
||||
}
|
||||
|
||||
let mut fixed = [0u8; 4];
|
||||
r.read_exact(&mut fixed)?;
|
||||
let [version, alg_id, flags, reserved] = fixed;
|
||||
if version != VERSION {
|
||||
return Err(FcryError::Format(format!("unsupported version: {version}")));
|
||||
}
|
||||
if reserved != 0 {
|
||||
return Err(FcryError::Format("reserved byte must be zero".into()));
|
||||
}
|
||||
let alg = AlgId::from_u8(alg_id)?;
|
||||
|
||||
let mut chunk_size_bytes = [0u8; 4];
|
||||
r.read_exact(&mut chunk_size_bytes)?;
|
||||
let chunk_size = u32::from_le_bytes(chunk_size_bytes);
|
||||
if chunk_size == 0 {
|
||||
return Err(FcryError::Format("chunk_size must be > 0".into()));
|
||||
}
|
||||
|
||||
let mut kdf_id = [0u8; 1];
|
||||
r.read_exact(&mut kdf_id)?;
|
||||
let kdf = KdfParams::read_from(kdf_id[0], r)?;
|
||||
|
||||
let mut nonce_prefix = [0u8; NONCE_PREFIX_LEN];
|
||||
r.read_exact(&mut nonce_prefix)?;
|
||||
|
||||
Ok(Self {
|
||||
alg,
|
||||
flags,
|
||||
chunk_size,
|
||||
kdf,
|
||||
nonce_prefix,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[test]
|
||||
fn roundtrip() {
|
||||
let h = Header {
|
||||
alg: AlgId::XChaCha20Poly1305,
|
||||
flags: 0,
|
||||
chunk_size: 1024 * 1024,
|
||||
kdf: KdfParams::Raw,
|
||||
nonce_prefix: [7u8; NONCE_PREFIX_LEN],
|
||||
};
|
||||
let bytes = h.encode();
|
||||
let mut cur = Cursor::new(&bytes);
|
||||
let parsed = Header::read(&mut cur).unwrap();
|
||||
assert_eq!(parsed.alg, h.alg);
|
||||
assert_eq!(parsed.flags, h.flags);
|
||||
assert_eq!(parsed.chunk_size, h.chunk_size);
|
||||
assert_eq!(parsed.nonce_prefix, h.nonce_prefix);
|
||||
assert_eq!(cur.position() as usize, bytes.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_bad_magic() {
|
||||
let mut bytes = Header {
|
||||
alg: AlgId::XChaCha20Poly1305,
|
||||
flags: 0,
|
||||
chunk_size: 4096,
|
||||
kdf: KdfParams::Raw,
|
||||
nonce_prefix: [0u8; NONCE_PREFIX_LEN],
|
||||
}
|
||||
.encode();
|
||||
bytes[0] ^= 1;
|
||||
assert!(matches!(
|
||||
Header::read(&mut Cursor::new(&bytes)),
|
||||
Err(FcryError::Format(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
+19
-10
@@ -2,11 +2,13 @@
|
||||
|
||||
mod crypto;
|
||||
mod error;
|
||||
mod header;
|
||||
mod reader;
|
||||
mod utils;
|
||||
|
||||
use crypto::*;
|
||||
use error::FcryError;
|
||||
use utils::DEFAULT_CHUNK_SIZE;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
@@ -27,25 +29,31 @@ struct Cli {
|
||||
#[clap(short, long)]
|
||||
output_file: Option<String>,
|
||||
|
||||
/// The raw bytes of the crypto key.
|
||||
/// Has to be exactly 32 bytes
|
||||
/// *** DANGEROUS, use for testing purposes only! ***
|
||||
/// The raw bytes of the crypto key. Has to be exactly 32 bytes.
|
||||
/// *** DANGEROUS: visible in process listings (ps/proc). Testing only. ***
|
||||
#[clap(short, long)]
|
||||
raw_key: String,
|
||||
|
||||
/// Plaintext chunk size in bytes (encryption only; decryption reads it from the header).
|
||||
#[clap(long, default_value_t = DEFAULT_CHUNK_SIZE)]
|
||||
chunk_size: u32,
|
||||
}
|
||||
|
||||
fn run(cli: Cli) -> Result<(), FcryError> {
|
||||
let input_file = cli.input_file;
|
||||
let output_file = cli.output_file;
|
||||
|
||||
let raw = cli.raw_key.as_bytes();
|
||||
if raw.len() != 32 {
|
||||
return Err(FcryError::Format(format!(
|
||||
"raw_key must be exactly 32 bytes, got {}",
|
||||
raw.len()
|
||||
)));
|
||||
}
|
||||
let mut key = [0u8; 32];
|
||||
dbg!(&cli.raw_key);
|
||||
key.clone_from_slice(cli.raw_key.as_bytes());
|
||||
key.copy_from_slice(raw);
|
||||
|
||||
if cli.decrypt {
|
||||
decrypt(input_file, output_file, key)?
|
||||
decrypt(cli.input_file, cli.output_file, key)?
|
||||
} else {
|
||||
encrypt(input_file, output_file, key)?
|
||||
encrypt(cli.input_file, cli.output_file, key, cli.chunk_size)?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -55,5 +63,6 @@ fn main() {
|
||||
let cli = Cli::parse();
|
||||
if let Err(e) = run(cli) {
|
||||
eprintln!("Error: {:?}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-10
@@ -4,7 +4,7 @@ use std::io;
|
||||
use std::io::{BufRead, Read};
|
||||
|
||||
pub enum ReadInfoChunk {
|
||||
Normal(usize),
|
||||
Normal(#[allow(dead_code)] usize),
|
||||
Last(usize),
|
||||
Empty,
|
||||
}
|
||||
@@ -47,21 +47,12 @@ impl AheadReader {
|
||||
}
|
||||
|
||||
pub fn read_ahead(&mut self, userbuf: &mut [u8]) -> io::Result<ReadInfoChunk> {
|
||||
// 1st read
|
||||
if self.bufsz == 0 {
|
||||
eprintln!("[reader] first read");
|
||||
return self.first_read(userbuf);
|
||||
}
|
||||
|
||||
eprintln!("[reader] normal read");
|
||||
// normal read (not the 1st one)
|
||||
self.normal_read(userbuf)
|
||||
}
|
||||
|
||||
pub fn read_exact(&mut self, userbuf: &mut [u8]) -> io::Result<()> {
|
||||
self.inner.read_exact(userbuf)
|
||||
}
|
||||
|
||||
fn first_read(&mut self, userbuf: &mut [u8]) -> io::Result<ReadInfoChunk> {
|
||||
// 1st read directly to userbuf (we have no cached data yet)
|
||||
let n = self.read_until_full(userbuf)?;
|
||||
|
||||
+16
-23
@@ -1,31 +1,24 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::reader::AheadReader;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
|
||||
use std::io::BufReader;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, Write},
|
||||
};
|
||||
/// Default plaintext chunk size: 1 MiB.
|
||||
///
|
||||
/// Stored in the header per file, so callers may override via CLI without
|
||||
/// breaking older files (the decryptor reads the size from the header).
|
||||
pub const DEFAULT_CHUNK_SIZE: u32 = 1024 * 1024;
|
||||
|
||||
pub const BUFSIZE: usize = 64 * 1024; // 64 KiB
|
||||
|
||||
pub(crate) fn read_from_file_or_stdin<S: AsRef<str>>(
|
||||
input_file: Option<S>,
|
||||
bufsz: usize,
|
||||
) -> AheadReader {
|
||||
match input_file {
|
||||
Some(f) => AheadReader::from(
|
||||
Box::new(BufReader::new(File::open(f.as_ref()).unwrap())),
|
||||
bufsz,
|
||||
),
|
||||
None => AheadReader::from(Box::new(io::stdin().lock()), bufsz),
|
||||
}
|
||||
pub(crate) fn open_input<S: AsRef<str>>(input_file: Option<S>) -> io::Result<Box<dyn BufRead>> {
|
||||
Ok(match input_file {
|
||||
Some(f) => Box::new(BufReader::new(File::open(f.as_ref())?)),
|
||||
None => Box::new(io::stdin().lock()),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn write_to_file_or_stdout<S: AsRef<str>>(output_file: Option<S>) -> Box<dyn Write> {
|
||||
match output_file {
|
||||
Some(f) => Box::new(File::create(f.as_ref()).unwrap()),
|
||||
pub(crate) fn open_output<S: AsRef<str>>(output_file: Option<S>) -> io::Result<Box<dyn Write>> {
|
||||
Ok(match output_file {
|
||||
Some(f) => Box::new(File::create(f.as_ref())?),
|
||||
None => Box::new(io::stdout()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user