pub mod control_interface;
pub mod status_interface;
use crate::config;
use crate::error::FpgadError;
use crate::system_io::{fs_read, fs_write};
use log::trace;
use std::path::{Component, Path, PathBuf, absolute};
pub fn fs_read_property(property_path_str: &str) -> Result<String, FpgadError> {
let property_path = validate_property_path(Path::new(property_path_str))?;
fs_read(&property_path)
}
pub(crate) fn validate_property_path(property_path: &Path) -> Result<PathBuf, FpgadError> {
validate_property_path_with_base(property_path, Path::new(config::FPGA_MANAGERS_DIR))
}
pub(crate) fn validate_property_path_with_base(
property_path: &Path,
base_path: &Path,
) -> Result<PathBuf, FpgadError> {
let property_path = PathBuf::from(property_path);
if property_path
.components()
.any(|component| matches!(component, Component::ParentDir))
{
return Err(FpgadError::Argument(format!(
"Cannot access property {}: path traversal ('..') is not allowed",
property_path.display()
)));
}
let canonical_base = absolute(base_path).map_err(|e| {
FpgadError::Argument(format!(
"Cannot access property {}: failed to resolve base path {}: {}",
property_path.display(),
base_path.display(),
e
))
})?;
let canonical_property = absolute(&property_path).map_err(|e| {
FpgadError::Argument(format!(
"Cannot access property {}: failed to resolve property path: {}",
property_path.display(),
e
))
})?;
if !canonical_property.starts_with(&canonical_base) {
return Err(FpgadError::Argument(format!(
"Cannot access property {}: resolved path {} is outside {}",
property_path.display(),
canonical_property.display(),
canonical_base.display()
)));
}
Ok(canonical_property)
}
#[allow(dead_code)]
pub fn read_firmware_source_dir() -> Result<String, FpgadError> {
trace!(
"Reading fw prefix from {}",
config::FIRMWARE_LOC_CONTROL_PATH
);
let fw_lookup_override = Path::new(config::FIRMWARE_LOC_CONTROL_PATH);
fs_read(fw_lookup_override)
}
pub fn write_firmware_source_dir(new_path: &str) -> Result<(), FpgadError> {
trace!(
"Writing fw prefix {} to {}",
new_path,
config::FIRMWARE_LOC_CONTROL_PATH
);
let fw_lookup_override = Path::new(config::FIRMWARE_LOC_CONTROL_PATH);
fs_write(fw_lookup_override, false, new_path)
}
pub fn extract_path_and_filename(path: &Path) -> Result<(PathBuf, PathBuf), FpgadError> {
let filename = path
.file_name()
.and_then(|f| f.to_str())
.ok_or(FpgadError::Argument(format!(
"Provided bitstream path {path:?} is not a file or a valid directory."
)))?;
let base_path = path
.parent()
.and_then(|p| p.to_str())
.ok_or(FpgadError::Argument(format!(
"Provided bitstream path {path:?} is missing a parent dir."
)))?;
Ok((base_path.into(), filename.into()))
}
pub(crate) fn validate_device_handle(device_handle: &str) -> Result<(), FpgadError> {
if device_handle.is_empty() || !device_handle.is_ascii() {
return Err(FpgadError::Argument(format!(
"{device_handle} is invalid name for fpga device.\
fpga name must be compliant with sysfs rules."
)));
}
let fpga_managers_dir = config::FPGA_MANAGERS_DIR;
if !PathBuf::from(fpga_managers_dir)
.join(device_handle)
.exists()
{
return Err(FpgadError::Argument(format!(
"Device {device_handle} not found."
)));
};
Ok(())
}
pub(crate) fn make_firmware_pair(
source_path: &Path,
firmware_path: &Path,
) -> Result<(PathBuf, PathBuf), FpgadError> {
if firmware_path.as_os_str().is_empty() {
return extract_path_and_filename(source_path);
}
if let Ok(suffix) = source_path.strip_prefix(firmware_path) {
let cleaned_suffix_path = suffix
.components()
.skip_while(|c| matches!(c, Component::RootDir))
.collect::<PathBuf>();
if cleaned_suffix_path.as_os_str().is_empty() {
return Err(FpgadError::Argument(format!(
"The resulting filename from stripping {firmware_path:?} from {source_path:?} \
was empty. Cannot write empty string to fpga."
)));
}
Ok((firmware_path.to_path_buf(), cleaned_suffix_path))
} else {
Err(FpgadError::Argument(format!(
"Could not find {source_path:?} inside {firmware_path:?}"
)))
}
}
#[cfg(test)]
mod test_make_firmware_pair {
use crate::comm::dbus::make_firmware_pair;
use crate::error::FpgadError;
use googletest::prelude::*;
use rstest::*;
use std::path::PathBuf;
#[gtest]
#[rstest]
#[case::all_good(
"/lib/firmware/file.bin",
"/lib/firmware/",
"/lib/firmware/",
"file.bin"
)]
#[case::no_fw_path("/lib/firmware/file.bin", "", "/lib/firmware/", "file.bin")]
#[case::no_fw_path_no_file("/lib/firmware/", "", "/lib/", "firmware")]
fn should_pass(
#[case] source: &str,
#[case] fw_path: &str,
#[case] exp_prefix: &str,
#[case] exp_suffix: &str,
) {
let result = make_firmware_pair(&PathBuf::from(source), &PathBuf::from(fw_path));
assert_that!(
result,
ok(eq(&(PathBuf::from(exp_prefix), PathBuf::from(exp_suffix))))
);
}
#[gtest]
#[rstest]
#[case::no_file(
"/lib/firmware/",
"/lib/firmware/",
err(displays_as(contains_substring("The resulting filename from stripping")))
)]
#[case::not_in_dir(
"/lib/firmware/file.bin",
"/snap/x1/data/file.bin",
err(displays_as(contains_substring("Could not find")))
)]
fn should_fail<M: for<'a> Matcher<&'a std::result::Result<(PathBuf, PathBuf), FpgadError>>>(
#[case] source: &str,
#[case] fw_path: &str,
#[case] condition: M,
) {
let result = make_firmware_pair(&PathBuf::from(source), &PathBuf::from(fw_path));
assert_that!(&result, condition);
}
}
#[cfg(test)]
mod test_validate_property_path {
use crate::comm::dbus::validate_property_path_with_base;
use googletest::prelude::*;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
fn unique_test_dir(test_name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before unix epoch")
.as_nanos();
std::env::temp_dir().join(format!("fpgad_validate_property_path_{test_name}_{nanos}"))
}
#[gtest]
fn should_pass_valid_path() {
let root = unique_test_dir("valid_path");
let base = root.join("fpga_manager");
let property = base.join("fpga0").join("name");
fs::create_dir_all(property.parent().expect("property should have parent"))
.expect("create parent dirs");
fs::write(&property, "name\n").expect("create property file");
let expected = fs::canonicalize(&property).expect("canonicalize property");
let result = validate_property_path_with_base(&property, &base);
fs::remove_dir_all(root).expect("cleanup temp dirs");
assert_that!(&result, ok(eq(&expected)));
}
#[gtest]
fn should_fail_for_path_outside_fpga_dir() {
let root = unique_test_dir("outside_base");
let base = root.join("fpga_manager");
let outside = root.join("outside").join("evil_file.sh");
fs::create_dir_all(&base).expect("create base dir");
fs::create_dir_all(outside.parent().expect("outside should have parent"))
.expect("create outside dir");
fs::write(&outside, "evil\n").expect("create outside file");
let result = validate_property_path_with_base(&outside, &base);
fs::remove_dir_all(root).expect("cleanup temp dirs");
assert_that!(&result, err(displays_as(contains_substring("is outside"))));
}
#[gtest]
fn should_fail_for_root_path_traversal() {
let root = unique_test_dir("root_traversal");
let base = root.join("fpga_manager");
fs::create_dir_all(&base).expect("create base dir");
let traversal = base.join("..").join("outside").join("evil_file.sh");
let result = validate_property_path_with_base(&traversal, &base);
fs::remove_dir_all(root).expect("cleanup temp dirs");
assert_that!(
&result,
err(displays_as(contains_substring("path traversal")))
);
}
#[gtest]
fn should_fail_for_device_path_traversal() {
let root = unique_test_dir("device_traversal");
let base = root.join("fpga_manager");
fs::create_dir_all(base.join("fpga0")).expect("create fpga0 dir");
let traversal = base.join("fpga0").join("..").join("name");
let result = validate_property_path_with_base(&traversal, &base);
fs::remove_dir_all(root).expect("cleanup temp dirs");
assert_that!(
&result,
err(displays_as(contains_substring("path traversal")))
);
}
#[cfg(unix)]
#[gtest]
fn should_allow_symlink_path_without_resolution() {
use std::os::unix::fs::symlink;
use std::path::absolute;
let root = unique_test_dir("symlink_escape");
let base = root.join("fpga_manager");
let outside = root.join("outside");
let link_target_file = outside.join("escaped_name");
let fpga0_dir = base.join("fpga0");
let link_in_base = fpga0_dir.join("link_outside");
fs::create_dir_all(&fpga0_dir).expect("create fpga0 dir");
fs::create_dir_all(&outside).expect("create outside dir");
fs::write(&link_target_file, "evil\n").expect("create outside target file");
symlink(&outside, &link_in_base).expect("create symlink escaping base");
let escaped_path = link_in_base.join("escaped_name");
let expected = absolute(&escaped_path).expect("resolve absolute escaped path");
let result = validate_property_path_with_base(&escaped_path, &base);
fs::remove_dir_all(root).expect("cleanup temp dirs");
assert_that!(&result, ok(eq(&expected)));
}
}