use std::path::Path;
use anyhow::{Context, Result};
use crate::cli::i18n;
pub fn decode_qrcode_to_otpauth_url(path: &Path) -> Result<String> {
#[cfg(target_os = "linux")]
{
linux::decode_in_sandboxed_child(path)
}
#[cfg(not(target_os = "linux"))]
{
decode_qrcode_from_file(path)
}
}
#[cfg(any(test, not(target_os = "linux")))]
fn decode_qrcode_from_file(path: &Path) -> Result<String> {
let qrcode_display = path.display().to_string();
let bytes =
std::fs::read(path).with_context(|| i18n::error_cannot_read_file(&qrcode_display))?;
decode_qrcode_bytes(&bytes, &qrcode_display)
}
pub fn decode_qrcode_bytes(bytes: &[u8], qrcode_display: &str) -> Result<String> {
let img = image::load_from_memory(bytes)
.with_context(|| i18n::error_failed_to_decode_qrcode(qrcode_display))?
.to_luma8();
let mut prepared = rqrr::PreparedImage::prepare(img);
let grids = prepared.detect_grids();
let grid = grids
.first()
.ok_or_else(|| anyhow::anyhow!(i18n::error_no_qrcode_found(qrcode_display)))?;
let (_, content) = grid
.decode()
.with_context(|| i18n::error_failed_to_decode_qrcode(qrcode_display))?;
Ok(content)
}
#[cfg(target_os = "linux")]
mod linux {
use std::fs::File;
use std::io::{Read, Write};
use std::os::fd::{FromRawFd, OwnedFd};
use std::path::Path;
use anyhow::{Context, Result, anyhow, bail};
use landlock::{
ABI, Access, AccessFs, CompatLevel, Compatible, Ruleset, RulesetAttr, RulesetStatus,
};
use super::decode_qrcode_bytes;
use crate::cli::i18n;
const TAG_OK: u8 = 0;
const TAG_ERR: u8 = 1;
pub(super) fn decode_in_sandboxed_child(path: &Path) -> Result<String> {
let (read_fd, write_fd) = create_pipe()?;
let pid = unsafe { libc::fork() };
if pid < 0 {
let err = std::io::Error::last_os_error();
drop(read_fd);
drop(write_fd);
return Err(err).context(i18n::error_qr_sandbox_failed());
}
if pid == 0 {
drop(read_fd);
child_main(path, write_fd);
}
drop(write_fd);
read_child_response(read_fd, pid)
}
fn create_pipe() -> Result<(OwnedFd, OwnedFd)> {
let mut fds = [0_i32; 2];
let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
if rc != 0 {
return Err(std::io::Error::last_os_error()).context(i18n::error_qr_sandbox_failed());
}
let read_fd = unsafe { OwnedFd::from_raw_fd(fds[0]) };
let write_fd = unsafe { OwnedFd::from_raw_fd(fds[1]) };
Ok((read_fd, write_fd))
}
fn read_child_response(read_fd: OwnedFd, pid: libc::pid_t) -> Result<String> {
let mut buf = Vec::new();
let read_result = File::from(read_fd).read_to_end(&mut buf);
let mut status = 0_i32;
let waited = unsafe { libc::waitpid(pid, &raw mut status, 0) };
if waited < 0 {
return Err(std::io::Error::last_os_error()).context(i18n::error_qr_sandbox_failed());
}
read_result.context(i18n::error_qr_sandbox_failed())?;
decode_response(&buf, status)
}
fn decode_response(buf: &[u8], status: i32) -> Result<String> {
let Some((&tag, rest)) = buf.split_first() else {
if libc_wifsignaled(status) {
bail!(i18n::error_qr_sandbox_child_signal(
&libc_wtermsig(status).to_string()
));
}
bail!(i18n::error_qr_sandbox_failed());
};
let payload = std::str::from_utf8(rest)
.map(str::to_owned)
.context(i18n::error_qr_sandbox_failed())?;
if tag == TAG_OK {
Ok(payload)
} else if tag == TAG_ERR {
Err(anyhow!(payload))
} else {
Err(anyhow!(i18n::error_qr_sandbox_failed()))
}
}
fn child_main(path: &Path, write_fd: OwnedFd) -> ! {
let qrcode_display = path.display().to_string();
let result = (|| -> Result<String> {
let bytes = std::fs::read(path)
.with_context(|| i18n::error_cannot_read_file(&qrcode_display))?;
apply_landlock()?;
decode_qrcode_bytes(&bytes, &qrcode_display)
})();
let mut file = File::from(write_fd);
let _ = match result {
Ok(url) => write_tagged(&mut file, TAG_OK, url.as_bytes()),
Err(err) => write_tagged(&mut file, TAG_ERR, format!("{err:#}").as_bytes()),
};
drop(file);
unsafe { libc::_exit(0) }
}
fn write_tagged(file: &mut File, tag: u8, body: &[u8]) -> std::io::Result<()> {
file.write_all(&[tag])?;
file.write_all(body)
}
fn apply_landlock() -> Result<()> {
let abi = ABI::V1;
let access_all = AccessFs::from_all(abi);
let status = Ruleset::default()
.set_compatibility(CompatLevel::BestEffort)
.handle_access(access_all)
.context(i18n::error_qr_sandbox_failed())?
.create()
.context(i18n::error_qr_sandbox_failed())?
.restrict_self()
.context(i18n::error_qr_sandbox_failed())?;
if status.ruleset == RulesetStatus::NotEnforced {
log::warn!("{}", i18n::error_qr_sandbox_not_enforced());
}
Ok(())
}
const fn libc_wifsignaled(status: i32) -> bool {
let term_sig = status & 0x7F;
term_sig != 0 && term_sig != 0x7F
}
const fn libc_wtermsig(status: i32) -> i32 {
status & 0x7F
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_response_returns_url_on_ok_tag() {
let mut payload = vec![TAG_OK];
payload.extend_from_slice(b"otpauth://totp/x?secret=AA");
let url = decode_response(&payload, 0).unwrap();
assert_eq!(url, "otpauth://totp/x?secret=AA");
}
#[test]
fn decode_response_propagates_child_error_message() {
let mut payload = vec![TAG_ERR];
payload.extend_from_slice(b"boom");
let err = decode_response(&payload, 0).unwrap_err();
assert!(err.to_string().contains("boom"));
}
#[test]
fn decode_response_fails_on_unknown_tag() {
crate::cli::i18n::init_for_tests();
let payload = [99u8, b'h', b'i'];
assert!(decode_response(&payload, 0).is_err());
}
#[test]
fn decode_response_fails_with_signal_status_on_empty_payload() {
crate::cli::i18n::init_for_tests();
let err = decode_response(&[], 11).unwrap_err();
assert!(
err.to_string().contains("11"),
"signal number should appear in message, got: {err}",
);
}
#[test]
fn decode_response_fails_generically_on_empty_payload_with_clean_exit() {
crate::cli::i18n::init_for_tests();
assert!(decode_response(&[], 0).is_err());
}
#[test]
fn decode_response_handles_invalid_utf8() {
crate::cli::i18n::init_for_tests();
let payload = [TAG_OK, 0xFFu8, 0xFEu8];
assert!(decode_response(&payload, 0).is_err());
}
#[test]
fn wifsignaled_detects_segv_status() {
assert!(libc_wifsignaled(11));
assert_eq!(libc_wtermsig(11), 11);
}
#[test]
fn wifsignaled_is_false_for_normal_exit() {
assert!(!libc_wifsignaled(0));
assert!(!libc_wifsignaled(1 << 8));
}
#[test]
fn wifsignaled_is_false_for_stopped_status() {
assert!(!libc_wifsignaled(0x7F));
}
}
}
#[cfg(test)]
mod tests {
use assert_fs::TempDir;
use assert_fs::prelude::*;
use super::*;
fn known_good_qrcode_png() -> Vec<u8> {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/cmd/otp/add/add-totp-from-qrcode.in/qrcode.png");
std::fs::read(path).expect("test QR PNG fixture must exist")
}
fn empty_png_bytes() -> Vec<u8> {
let img = image::RgbaImage::from_pixel(4, 4, image::Rgba([255, 255, 255, 255]));
let mut bytes: Vec<u8> = Vec::new();
img.write_to(
&mut std::io::Cursor::new(&mut bytes),
image::ImageFormat::Png,
)
.expect("encoding a tiny PNG must succeed");
bytes
}
#[test]
fn decode_bytes_returns_otpauth_url_from_valid_png() {
crate::cli::i18n::init_for_tests();
let bytes = known_good_qrcode_png();
let url = decode_qrcode_bytes(&bytes, "fixture.png").unwrap();
assert!(
url.starts_with("otpauth://"),
"expected otpauth URL, got: {url}",
);
}
#[test]
fn decode_bytes_errors_on_non_image_input() {
crate::cli::i18n::init_for_tests();
let result = decode_qrcode_bytes(b"not an image at all", "junk.bin");
assert!(result.is_err());
let message = result.unwrap_err().to_string();
assert!(
message.contains("Failed to decode QR code"),
"expected decode error message, got: {message}",
);
}
#[test]
fn decode_bytes_errors_when_image_has_no_qrcode() {
crate::cli::i18n::init_for_tests();
let result = decode_qrcode_bytes(&empty_png_bytes(), "empty.png");
assert!(result.is_err());
let message = result.unwrap_err().to_string();
assert!(
message.contains("No QR code found"),
"expected no-QR message, got: {message}",
);
}
#[test]
fn from_file_errors_on_missing_path() {
crate::cli::i18n::init_for_tests();
let result = decode_qrcode_from_file(std::path::Path::new(
"/definitely/does/not/exist/qrcode.png",
));
assert!(result.is_err());
}
#[test]
fn from_file_decodes_known_good_png() {
crate::cli::i18n::init_for_tests();
let tmp = TempDir::new().expect("create tempdir");
let png = tmp.child("qrcode.png");
png.write_binary(&known_good_qrcode_png())
.expect("write png");
let url = decode_qrcode_from_file(png.path()).unwrap();
assert!(url.starts_with("otpauth://"));
}
#[test]
fn public_decoder_returns_otpauth_url_from_known_good_png() {
crate::cli::i18n::init_for_tests();
let tmp = TempDir::new().expect("create tempdir");
let png = tmp.child("qrcode.png");
png.write_binary(&known_good_qrcode_png())
.expect("write png");
let url = decode_qrcode_to_otpauth_url(png.path()).unwrap();
assert!(url.starts_with("otpauth://"));
}
#[test]
fn public_decoder_propagates_no_qrcode_error() {
crate::cli::i18n::init_for_tests();
let tmp = TempDir::new().expect("create tempdir");
let png = tmp.child("empty.png");
png.write_binary(&empty_png_bytes()).expect("write png");
let err = decode_qrcode_to_otpauth_url(png.path()).unwrap_err();
assert!(err.to_string().contains("No QR code found"));
}
#[test]
fn public_decoder_errors_on_missing_file() {
crate::cli::i18n::init_for_tests();
let tmp = TempDir::new().expect("create tempdir");
let missing = tmp.child("absent.png");
let err = decode_qrcode_to_otpauth_url(missing.path()).unwrap_err();
let message = err.to_string();
assert!(
message.contains("Cannot read file") || message.contains("absent.png"),
"expected file-read failure, got: {message}",
);
}
}