feat: split crate into library and thin CLI binary
The crypto engine was only reachable through the fcry binary; embedding
it in another Rust project meant shelling out to the CLI. Restructure
the crate so the binary sits on top of a proper library API.
- Add src/lib.rs exposing encrypt/decrypt/decrypt_range/derive_key, the
header and policy types, and the secret-handling primitives.
- Replace the positional-argument wrapper ladder
(encrypt_with_output_options, decrypt_with_argon_cap, ...) with
options structs: EncryptOptions, DecryptOptions, DecryptRangeOptions
and HeaderReadOptions. OutSinkOptions becomes the public
OutputOptions and no longer carries the input path; the input is now
an explicit parameter to OutSink::open_with_options so the
same-file-aliasing guard's inputs are visible at each call site.
- File parameters take Option<PathBuf>/&Path instead of AsRef<str>, so
non-UTF-8 paths work.
- FcryError implements Display and std::error::Error so it composes
with anyhow/thiserror-style error handling in downstream crates.
- Move read_key_file and normalize_passphrase from main.rs into
secrets.rs so library users get the same strict 32-byte key-file
parsing and NFC passphrase normalization. The world-readable
key-file warning stays in the CLI wrapper (read_key_file_cli).
- Drop now-unneeded #[allow(dead_code)] markers; ReadInfoChunk::Normal
loses its unused byte-count payload.
- Add rustfmt.toml (StdExternalCrate grouping, crate-granularity
imports) and reformat imports accordingly.
- Add tests/library_api.rs covering a file round-trip and a range
decrypt through the public API with a raw key.
User-visible change: CLI behavior is unchanged except error output,
which is now human-readable Display text ("Error: wrong key or
passphrase") instead of the Rust Debug representation.
Test plan: cargo clippy (default, --tests, --benches) is clean;
cargo +nightly fmt produces no diff; cargo test passes 43 tests
including the new library_api integration tests.
This commit is contained in:
+16
-19
@@ -1,8 +1,10 @@
|
||||
// SPDX-License-Identifier: MIT-0
|
||||
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{self, BufRead, BufReader, Seek, SeekFrom, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, BufRead, BufReader, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::policy;
|
||||
|
||||
@@ -22,10 +24,10 @@ pub(crate) struct Input {
|
||||
pub length: Option<u64>,
|
||||
}
|
||||
|
||||
pub(crate) fn open_input<S: AsRef<str>>(input_file: Option<S>) -> io::Result<Input> {
|
||||
pub(crate) fn open_input(input_file: Option<&Path>) -> io::Result<Input> {
|
||||
match input_file {
|
||||
Some(f) => {
|
||||
let file = File::open(f.as_ref())?;
|
||||
let file = File::open(f)?;
|
||||
// Stat the open FD (not the path) so we can't be raced between
|
||||
// stat and open.
|
||||
let length = file
|
||||
@@ -48,9 +50,8 @@ pub(crate) fn open_input<S: AsRef<str>>(input_file: Option<S>) -> io::Result<Inp
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct OutSinkOptions {
|
||||
pub struct OutputOptions {
|
||||
pub force: bool,
|
||||
pub input_file: Option<PathBuf>,
|
||||
pub temp_dir: Option<PathBuf>,
|
||||
pub buffer_verify_stdout: bool,
|
||||
}
|
||||
@@ -205,7 +206,7 @@ fn output_aliases_input(output: &Path, input: Option<&Path>) -> io::Result<bool>
|
||||
/// partial/garbage file does not replace any existing target.
|
||||
///
|
||||
/// For stdout: behaves as a passthrough; `commit()` is a no-op.
|
||||
pub enum OutSink {
|
||||
pub(crate) enum OutSink {
|
||||
Stdout(io::Stdout),
|
||||
BufferVerify {
|
||||
temp: SecureTempFile,
|
||||
@@ -217,14 +218,10 @@ pub enum OutSink {
|
||||
}
|
||||
|
||||
impl OutSink {
|
||||
#[allow(dead_code)]
|
||||
pub fn open<S: AsRef<str>>(output_file: Option<S>) -> io::Result<Self> {
|
||||
Self::open_with_options(output_file, &OutSinkOptions::default())
|
||||
}
|
||||
|
||||
pub fn open_with_options<S: AsRef<str>>(
|
||||
output_file: Option<S>,
|
||||
options: &OutSinkOptions,
|
||||
pub(crate) fn open_with_options(
|
||||
output_file: Option<&Path>,
|
||||
input_file: Option<&Path>,
|
||||
options: &OutputOptions,
|
||||
) -> io::Result<Self> {
|
||||
match output_file {
|
||||
None if options.buffer_verify_stdout => {
|
||||
@@ -235,10 +232,10 @@ impl OutSink {
|
||||
}
|
||||
None => Ok(Self::Stdout(io::stdout())),
|
||||
Some(f) => {
|
||||
let final_path = PathBuf::from(f.as_ref());
|
||||
let final_path = f.to_path_buf();
|
||||
if final_path.exists()
|
||||
&& !options.force
|
||||
&& !output_aliases_input(&final_path, options.input_file.as_deref())?
|
||||
&& !output_aliases_input(&final_path, input_file)?
|
||||
{
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::AlreadyExists,
|
||||
@@ -256,7 +253,7 @@ impl OutSink {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn commit(mut self) -> io::Result<()> {
|
||||
pub(crate) fn commit(mut self) -> io::Result<()> {
|
||||
match &mut self {
|
||||
Self::Stdout(s) => s.flush()?,
|
||||
Self::BufferVerify { .. } => {}
|
||||
|
||||
Reference in New Issue
Block a user