use std::fmt;
use std::path::Path;
use crate::cli_spec::{SourceError, ValueSource};
use crate::document::{DocumentFile, Format, Value};
const MAX_FILE_BYTES: u64 = 16 * 1024 * 1024;
const MAX_STREAM_BYTES: usize = 1024 * 1024;
type Result<T> = std::result::Result<T, SourceError>;
#[derive(Clone, PartialEq, Eq)]
pub struct SecretString(String);
impl SecretString {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn expose_secret(&self) -> &str {
&self.0
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl fmt::Debug for SecretString {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("***")
}
}
impl fmt::Display for SecretString {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("***")
}
}
impl From<String> for SecretString {
fn from(value: String) -> Self {
Self(value)
}
}
impl ValueSource {
pub fn read(&self) -> Result<String> {
read_with(self, Policy::Plain)
}
pub fn read_secret(&self) -> Result<SecretString> {
read_with(self, Policy::Secret).map(SecretString)
}
}
fn read_with(source: &ValueSource, policy: Policy) -> Result<String> {
match source {
ValueSource::Literal(value) => Ok(value.clone()),
ValueSource::Env(name) => std::env::var(name).map_err(|error| {
let reason = match error {
std::env::VarError::NotPresent => "is unset",
std::env::VarError::NotUnicode(_) => "is not valid UTF-8",
};
SourceError::unreadable(format!("environment variable `{name}` {reason}"))
}),
ValueSource::File {
path,
dot_path,
format,
} => read_file(path, dot_path, format.as_deref(), policy),
ValueSource::Stdin => read_stream(std::io::stdin().lock(), "stdin"),
ValueSource::Fd(number) => read_fd(*number),
ValueSource::Prompt => read_prompt(),
ValueSource::Host { scheme, .. } => Err(SourceError::unreadable(format!(
"`{scheme}` is a host-defined source; this crate cannot read it"
))),
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Policy {
Plain,
Secret,
}
fn read_file(
path: &Path,
dot_path: &str,
named_format: Option<&str>,
policy: Policy,
) -> Result<String> {
let format = match named_format {
Some(name) => Format::from_cli_name(name).ok_or_else(|| {
SourceError::invalid(format!("`file+{name}:` is not a format this build reads"))
})?,
None => Format::detect(path).ok_or_else(|| match Format::unavailable(path) {
Some(feature) => SourceError::unreadable(format!(
"cannot read {}: this build has no {feature} support",
path.display()
)),
None => SourceError::invalid(format!(
"cannot tell the config format of {} from its name; name it with \
file+FORMAT:{}#{dot_path}, or use a .json/.toml/.yaml/.env/.ini file",
path.display(),
path.display()
)),
})?,
};
let document = DocumentFile::open_capped(path, Some(format), MAX_FILE_BYTES).map_err(
|error| match policy {
Policy::Secret => SourceError::unreadable(format!(
"cannot read {} config {}: {}",
format.name(),
path.display(),
error.redacted_message()
)),
Policy::Plain => SourceError::unreadable(format!(
"cannot read {} config {}: {error}",
format.name(),
path.display()
)),
},
)?;
let value = document.value_at(dot_path).map_err(|error| {
if error.code() == "document_path_not_found" {
SourceError::unreadable(format!("{dot_path} was not found in {}", path.display()))
} else {
SourceError::unreadable(format!("cannot resolve {dot_path} in {}", path.display()))
}
})?;
scalar(value, path, dot_path, policy)
}
fn scalar(value: Value, path: &Path, dot_path: &str, policy: Policy) -> Result<String> {
let refused = |kind: &str| {
SourceError::unreadable(format!(
"{dot_path} in {} is {kind}, which is not a value",
path.display()
))
};
match value {
Value::String(value) => Ok(value),
other if policy == Policy::Secret => Err(SourceError::unreadable(format!(
"{dot_path} in {} is {}; a secret must be a string",
path.display(),
other.kind_name()
))),
Value::Integer(value) => Ok(value.to_string()),
Value::Unsigned(value) => Ok(value.to_string()),
Value::Float(value) => Ok(value.to_string()),
Value::Number(value) => Ok(value),
Value::Bool(value) => Ok(value.to_string()),
Value::Null => Err(refused("null")),
Value::Array(_) => Err(refused("an array")),
Value::Object(_) => Err(refused("an object")),
}
}
fn read_stream<R: std::io::Read>(reader: R, source: &str) -> Result<String> {
use std::io::Read;
let mut bytes = Vec::new();
reader
.take((MAX_STREAM_BYTES + 1) as u64)
.read_to_end(&mut bytes)
.map_err(|error| SourceError::unreadable(format!("read from {source}: {error}")))?;
if bytes.len() > MAX_STREAM_BYTES {
return Err(SourceError::unreadable(format!(
"{source} exceeds {MAX_STREAM_BYTES} bytes"
)));
}
String::from_utf8(bytes)
.map_err(|_| SourceError::unreadable(format!("{source} must carry valid UTF-8")))
}
#[cfg(unix)]
fn read_fd(number: i32) -> Result<String> {
#[cfg(feature = "libc")]
let file = {
use std::os::fd::FromRawFd;
let duplicated = unsafe { libc::dup(number) };
if duplicated < 0 {
return Err(SourceError::unreadable(format!(
"open file descriptor {number}: {}",
std::io::Error::last_os_error()
)));
}
unsafe { std::fs::File::from_raw_fd(duplicated) }
};
#[cfg(not(feature = "libc"))]
let file = std::fs::File::open(format!("/dev/fd/{number}")).map_err(|error| {
SourceError::unreadable(format!("open file descriptor {number}: {error}"))
})?;
read_stream(file, "file descriptor")
}
#[cfg(not(unix))]
fn read_fd(_number: i32) -> Result<String> {
Err(SourceError::unreadable(
"the `fd` source is unsupported on this platform",
))
}
#[cfg(all(unix, feature = "libc"))]
fn read_prompt() -> Result<String> {
use std::io::Write;
let mut tty = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/tty")
.map_err(|error| {
SourceError::unreadable(format!("open the controlling terminal: {error}"))
})?;
let restore_tty = tty.try_clone().map_err(|error| {
SourceError::unreadable(format!("prepare terminal echo restoration: {error}"))
})?;
let original = disable_terminal_echo(&tty)
.map_err(|error| SourceError::unreadable(format!("disable terminal echo: {error}")))?;
let _echo = EchoGuard {
tty: restore_tty,
original,
};
write!(tty, "Value: ")
.map_err(|error| SourceError::unreadable(format!("write the prompt: {error}")))?;
let reader = std::io::BufReader::new(&mut tty);
let value = read_prompt_line(reader);
let _ = writeln!(tty);
value
}
#[cfg(all(unix, not(feature = "libc")))]
fn read_prompt() -> Result<String> {
Err(SourceError::unreadable(
"the `prompt` source needs Cargo feature `libc` to turn terminal echo off",
))
}
#[cfg(windows)]
fn read_prompt() -> Result<String> {
use std::io::Write as _;
let input = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("CONIN$")
.map_err(|error| SourceError::unreadable(format!("open the console input: {error}")))?;
let mut output = std::fs::OpenOptions::new()
.write(true)
.open("CONOUT$")
.map_err(|error| SourceError::unreadable(format!("open the console output: {error}")))?;
let restore_console = input.try_clone().map_err(|error| {
SourceError::unreadable(format!("prepare console echo restoration: {error}"))
})?;
let restore_output = output.try_clone().map_err(|error| {
SourceError::unreadable(format!("prepare console echo restoration: {error}"))
})?;
let original = disable_console_echo(&input)
.map_err(|error| SourceError::unreadable(format!("disable console echo: {error}")))?;
let _echo = EchoGuard {
console: restore_console,
output: restore_output,
original,
};
write!(output, "Value: ")
.map_err(|error| SourceError::unreadable(format!("write the prompt: {error}")))?;
let value = read_console_line(&input);
let _ = writeln!(output);
value
}
#[cfg(all(not(unix), not(windows)))]
fn read_prompt() -> Result<String> {
Err(SourceError::unreadable(
"the `prompt` source is unsupported on this platform",
))
}
#[cfg(windows)]
mod windows_console {
use std::ffi::c_void;
pub(super) const ENABLE_ECHO_INPUT: u32 = 0x0004;
#[link(name = "kernel32")]
unsafe extern "system" {
pub(super) fn GetConsoleMode(console: *mut c_void, mode: *mut u32) -> i32;
pub(super) fn SetConsoleMode(console: *mut c_void, mode: u32) -> i32;
pub(super) fn ReadConsoleW(
console: *mut c_void,
buffer: *mut u16,
units_to_read: u32,
units_read: *mut u32,
input_control: *mut c_void,
) -> i32;
}
}
#[cfg(windows)]
fn disable_console_echo(console: &std::fs::File) -> std::io::Result<u32> {
let original = console_mode(console)?;
set_console_mode(console, original & !windows_console::ENABLE_ECHO_INPUT)?;
if console_mode(console)? & windows_console::ENABLE_ECHO_INPUT != 0 {
let _ = set_console_mode(console, original);
return Err(std::io::Error::other("console echo is still enabled"));
}
Ok(original)
}
#[cfg(windows)]
fn console_mode(console: &std::fs::File) -> std::io::Result<u32> {
use std::os::windows::io::AsRawHandle as _;
let mut mode = 0u32;
let status = unsafe { windows_console::GetConsoleMode(console.as_raw_handle(), &mut mode) };
if status == 0 {
return Err(std::io::Error::last_os_error());
}
Ok(mode)
}
#[cfg(windows)]
fn set_console_mode(console: &std::fs::File, mode: u32) -> std::io::Result<()> {
use std::os::windows::io::AsRawHandle as _;
let status = unsafe { windows_console::SetConsoleMode(console.as_raw_handle(), mode) };
if status == 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
#[cfg(windows)]
fn read_console_line(console: &std::fs::File) -> Result<String> {
use std::os::windows::io::AsRawHandle as _;
let mut buffer = vec![0u16; MAX_STREAM_BYTES + 4];
let units_to_read = u32::try_from(buffer.len())
.map_err(|_| SourceError::unreadable("the console read buffer does not fit a request"))?;
let mut units_read = 0u32;
let status = unsafe {
windows_console::ReadConsoleW(
console.as_raw_handle(),
buffer.as_mut_ptr(),
units_to_read,
&mut units_read,
std::ptr::null_mut(),
)
};
if status == 0 {
return Err(SourceError::unreadable(format!(
"read from the console: {}",
std::io::Error::last_os_error()
)));
}
let units = buffer
.get(..units_read as usize)
.ok_or_else(|| SourceError::unreadable("the console reported reading past its buffer"))?;
let text = String::from_utf16(units)
.map_err(|_| SourceError::unreadable("the console answered malformed UTF-16"))?;
read_prompt_line(std::io::Cursor::new(text.as_bytes()))
}
#[cfg(all(unix, feature = "libc"))]
fn disable_terminal_echo(tty: &std::fs::File) -> std::io::Result<libc::termios> {
let original = terminal_attributes(tty)?;
let mut quiet = original;
quiet.c_lflag &= !libc::ECHO;
set_terminal_attributes(tty, &quiet)?;
if terminal_attributes(tty)?.c_lflag & libc::ECHO != 0 {
let _ = set_terminal_attributes(tty, &original);
return Err(std::io::Error::other("terminal echo is still enabled"));
}
Ok(original)
}
#[cfg(all(unix, feature = "libc"))]
fn terminal_attributes(tty: &std::fs::File) -> std::io::Result<libc::termios> {
use std::os::fd::AsRawFd as _;
let mut attributes = std::mem::MaybeUninit::<libc::termios>::uninit();
let status = unsafe { libc::tcgetattr(tty.as_raw_fd(), attributes.as_mut_ptr()) };
if status != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(unsafe { attributes.assume_init() })
}
#[cfg(all(unix, feature = "libc"))]
fn set_terminal_attributes(tty: &std::fs::File, attributes: &libc::termios) -> std::io::Result<()> {
use std::os::fd::AsRawFd as _;
let status = unsafe { libc::tcsetattr(tty.as_raw_fd(), libc::TCSAFLUSH, attributes) };
if status != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
#[cfg(any(all(unix, feature = "libc"), windows, test))]
fn read_prompt_line<R: std::io::BufRead>(reader: R) -> Result<String> {
use std::io::BufRead;
let mut limited = reader.take((MAX_STREAM_BYTES + 2) as u64);
let mut value = String::new();
limited
.read_line(&mut value)
.map_err(|error| SourceError::unreadable(format!("read from the terminal: {error}")))?;
let value = value.trim_end_matches(['\r', '\n']);
if value.len() > MAX_STREAM_BYTES {
return Err(SourceError::unreadable(format!(
"prompt exceeds {MAX_STREAM_BYTES} bytes"
)));
}
Ok(value.to_string())
}
#[cfg(all(unix, feature = "libc"))]
struct EchoGuard {
tty: std::fs::File,
original: libc::termios,
}
#[cfg(all(unix, feature = "libc"))]
impl Drop for EchoGuard {
fn drop(&mut self) {
use std::io::Write as _;
if set_terminal_attributes(&self.tty, &self.original).is_ok() {
return;
}
let _ = writeln!(
&mut self.tty,
"\nwarning: could not restore terminal echo; run `stty echo` to fix this terminal"
);
}
}
#[cfg(windows)]
struct EchoGuard {
console: std::fs::File,
output: std::fs::File,
original: u32,
}
#[cfg(windows)]
impl Drop for EchoGuard {
fn drop(&mut self) {
use std::io::Write as _;
if set_console_mode(&self.console, self.original).is_ok() {
return;
}
let _ = writeln!(
&mut self.output,
"\nwarning: could not restore console echo; close this console window to get it back"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(all(unix, feature = "libc"))]
#[test]
fn terminal_echo_control_goes_to_the_terminal_not_to_a_program() {
use std::io::Write as _;
let path = std::env::temp_dir().join(format!(
"afdata_not_a_tty_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
let mut file = match std::fs::File::create(&path) {
Ok(file) => file,
Err(_) => return,
};
let _ = file.write_all(b"not a terminal");
let error = disable_terminal_echo(&file)
.err()
.map(|error| error.raw_os_error());
let _ = std::fs::remove_file(&path);
assert_eq!(
error,
Some(Some(libc::ENOTTY)),
"echo control must fail as a terminal call, not as a missing program"
);
}
#[cfg(all(unix, feature = "libc"))]
#[test]
fn a_failed_restore_is_reported_rather_than_swallowed() {
let path = std::env::temp_dir().join(format!(
"afdata_echo_guard_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
let Ok(file) = std::fs::File::options()
.create(true)
.truncate(true)
.read(true)
.write(true)
.open(&path)
else {
return;
};
let original = unsafe { std::mem::zeroed::<libc::termios>() };
drop(EchoGuard {
tty: file,
original,
});
let announced = std::fs::read_to_string(&path).unwrap_or_default();
let _ = std::fs::remove_file(&path);
assert!(
announced.contains("could not restore terminal echo"),
"a terminal left without echo must say so: {announced:?}"
);
}
#[cfg(windows)]
mod windows_echo {
use super::super::{EchoGuard, disable_console_echo};
const ERROR_INVALID_HANDLE: i32 = 6;
fn scratch_file(label: &str) -> Option<(std::path::PathBuf, std::fs::File)> {
let path = std::env::temp_dir().join(format!(
"afdata_{label}_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
let file = std::fs::File::options()
.create(true)
.truncate(true)
.read(true)
.write(true)
.open(&path)
.ok()?;
Some((path, file))
}
#[test]
fn echo_control_goes_to_the_console_not_to_a_program() {
let Some((path, file)) = scratch_file("not_a_console") else {
return;
};
let error = disable_console_echo(&file)
.err()
.and_then(|error| error.raw_os_error());
drop(file);
let _ = std::fs::remove_file(&path);
assert_eq!(
error,
Some(ERROR_INVALID_HANDLE),
"echo control must fail as a console call, not as a missing program"
);
}
#[test]
fn a_failed_restore_is_reported_rather_than_swallowed() {
let Some((path, file)) = scratch_file("console_echo_guard") else {
return;
};
let Ok(output) = file.try_clone() else {
let _ = std::fs::remove_file(&path);
return;
};
drop(EchoGuard {
console: file,
output,
original: 0,
});
let announced = std::fs::read_to_string(&path).unwrap_or_default();
let _ = std::fs::remove_file(&path);
assert!(
announced.contains("could not restore console echo"),
"a console left without echo must say so: {announced:?}"
);
}
}
use crate::cli_spec::SourceSet;
use std::path::PathBuf;
fn temp_config(name: &str, extension: &str, content: &str) -> PathBuf {
let path = std::env::temp_dir().join(format!(
"afdata-value-source-{name}-{}.{extension}",
std::process::id()
));
std::fs::write(&path, content).expect("write test config");
path
}
fn readable_formats() -> Vec<(&'static str, &'static str, &'static str, &'static str)> {
#[allow(unused_mut)]
let mut cases: Vec<(&str, &str, &str, &str)> =
vec![("json", "json", r#"{"a":{"b":" v "}}"#, "a.b")];
#[cfg(feature = "toml")]
cases.push(("toml", "toml", "[a]\nb = ' v '\n", "a.b"));
#[cfg(feature = "yaml")]
cases.push(("yaml", "yaml", "a:\n b: ' v '\n", "a.b"));
#[cfg(feature = "dotenv")]
cases.push(("dotenv", "env", "A_B=' v '\n", "A_B"));
cases
}
#[test]
fn a_file_source_reads_one_address_out_of_every_format() {
for (name, extension, content, dot_path) in readable_formats() {
let path = temp_config(name, extension, content);
let source = ValueSource::File {
path: path.clone(),
dot_path: dot_path.to_string(),
format: None,
};
let read = source.read();
let secret = source.read_secret();
std::fs::remove_file(&path).expect("remove test config");
assert_eq!(read.as_deref(), Ok(" v "), "{name}");
assert_eq!(
secret.expect("secret read").expose_secret(),
" v ",
"{name}"
);
}
}
#[test]
fn an_empty_string_is_still_a_value() {
let path = temp_config("empty", "json", r#"{"empty":""}"#);
let source = ValueSource::File {
path: path.clone(),
dot_path: "empty".to_string(),
format: None,
};
assert_eq!(source.read().as_deref(), Ok(""));
let secret = source.read_secret().expect("empty secret remains explicit");
assert!(secret.is_empty());
std::fs::remove_file(&path).expect("remove test config");
}
#[cfg(feature = "ini")]
#[test]
fn a_named_format_reads_a_file_whose_name_cannot_say_what_it_is() {
let path = temp_config("named", "conf", "http-password=abc123\nauto-liquidity=2m\n");
let named = ValueSource::File {
path: path.clone(),
dot_path: "http-password".to_string(),
format: Some("ini".to_string()),
};
let unnamed = ValueSource::File {
path: path.clone(),
dot_path: "http-password".to_string(),
format: None,
};
let bad_name = ValueSource::File {
path: path.clone(),
dot_path: "http-password".to_string(),
format: Some("nonsense".to_string()),
};
let read = named.read_secret();
let without = unnamed.read();
let bad = bad_name.read();
std::fs::remove_file(&path).expect("remove test config");
assert_eq!(read.expect("named format").expose_secret(), "abc123");
let without = without.expect_err("no extension to detect");
assert!(without.message().contains("file+FORMAT:"), "{without}");
let bad = bad.expect_err("unknown format");
assert!(
bad.message().contains("not a format this build reads"),
"{bad}"
);
}
#[test]
fn a_non_string_scalar_is_a_value_but_never_a_secret() {
let path = temp_config("scalar", "json", r#"{"port":5432,"on":true}"#);
let port = ValueSource::File {
path: path.clone(),
dot_path: "port".to_string(),
format: None,
};
assert_eq!(port.read().as_deref(), Ok("5432"));
let error = port.read_secret().expect_err("a secret must be a string");
assert!(error.message().contains("must be a string"), "{error}");
let on = ValueSource::File {
path: path.clone(),
dot_path: "on".to_string(),
format: None,
};
assert_eq!(on.read().as_deref(), Ok("true"));
std::fs::remove_file(&path).expect("remove test config");
}
#[test]
fn a_secret_read_never_echoes_what_it_read() {
let canary = "AFDATA_SOURCE_CANARY";
let path = temp_config("malformed", "json", &format!(r#"{{"a": [ {canary}"#));
let source = ValueSource::File {
path: path.clone(),
dot_path: "a".to_string(),
format: None,
};
let plain = source.read().expect_err("malformed");
let secret = source.read_secret().expect_err("malformed");
std::fs::remove_file(&path).expect("remove test config");
assert!(
!secret.message().contains(canary),
"secret read leaked: {secret}"
);
assert!(plain.message().contains("cannot read"), "{plain}");
}
#[test]
fn a_collection_is_not_a_value() {
let path = temp_config("collection", "json", r#"{"a":{"b":1},"c":[1],"d":null}"#);
for (dot_path, expected) in [("a", "an object"), ("c", "an array"), ("d", "null")] {
let source = ValueSource::File {
path: path.clone(),
dot_path: dot_path.to_string(),
format: None,
};
let error = source.read().expect_err(dot_path);
assert!(error.message().contains(expected), "{dot_path}: {error}");
}
std::fs::remove_file(&path).expect("remove test config");
}
#[test]
fn a_host_scheme_is_not_this_crates_to_read() {
let error = SourceSet::config()
.host_scheme("container", "container:NAME")
.parse("container:x")
.expect("parses")
.read()
.expect_err("this crate cannot read it");
assert_eq!(error.code(), "value_source_unreadable");
}
#[test]
fn an_unset_environment_source_names_what_it_tried() {
const ABSENT: &str = "AFDATA_TEST_ABSENT_VALUE_SOURCE";
let error = ValueSource::Env(ABSENT.to_string())
.read()
.expect_err("unset");
assert_eq!(error.code(), "value_source_unreadable");
assert!(error.message().contains(ABSENT), "{error}");
}
#[test]
fn a_secret_string_cannot_be_printed_by_accident() {
let secret = SecretString::new("s3cret");
assert_eq!(format!("{secret}"), "***");
assert_eq!(format!("{secret:?}"), "***");
assert!(!format!("{secret:?} {secret}").contains("s3cret"));
assert_eq!(secret.expose_secret(), "s3cret");
#[derive(Debug)]
struct Config {
#[allow(dead_code)]
token_secret: SecretString,
}
let printed = format!(
"{:?}",
Config {
token_secret: secret
}
);
assert!(!printed.contains("s3cret"), "{printed}");
}
#[test]
fn a_stream_is_read_verbatim_and_capped() {
assert_eq!(
read_stream(" v \n".as_bytes(), "test").as_deref(),
Ok(" v \n")
);
let oversized = vec![b'x'; MAX_STREAM_BYTES + 1];
let error = read_stream(oversized.as_slice(), "test").expect_err("over the cap");
assert!(error.message().contains("exceeds"), "{error}");
}
#[test]
fn a_prompt_line_is_bounded_before_allocation_can_grow_without_limit() {
let exact = format!("{}\r\n", "x".repeat(MAX_STREAM_BYTES));
assert_eq!(
read_prompt_line(std::io::Cursor::new(exact))
.expect("cap-sized line")
.len(),
MAX_STREAM_BYTES
);
let oversized = format!("{}\n", "x".repeat(MAX_STREAM_BYTES + 1));
let error = read_prompt_line(std::io::Cursor::new(oversized)).expect_err("over the cap");
assert!(error.message().contains("exceeds"), "{error}");
}
#[cfg(unix)]
#[test]
fn an_fd_source_never_closes_the_callers_descriptor() {
use std::io::{Read, Seek};
use std::os::fd::AsRawFd;
let path = temp_config("fd", "txt", "descriptor value");
let mut file = std::fs::File::open(&path).expect("open test descriptor");
let source = ValueSource::Fd(file.as_raw_fd());
assert_eq!(source.read().as_deref(), Ok("descriptor value"));
file.rewind()
.expect("the caller still owns an open descriptor");
let mut reread = String::new();
file.read_to_string(&mut reread)
.expect("read through caller-owned descriptor");
assert_eq!(reread, "descriptor value");
std::fs::remove_file(&path).expect("remove test config");
}
}