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:
2026-06-12 22:49:23 +02:00
parent f44cfc6190
commit 2f16e735c3
13 changed files with 533 additions and 356 deletions
+16 -19
View File
@@ -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 { .. } => {}