use crate::comm::dbus::validate_device_handle;
use crate::config;
use crate::error::FpgadError;
use crate::platforms::platform::{Fpga, OverlayHandler, Platform, list_fpga_managers};
use crate::platforms::xilinx_sys_components::xilinx_sys_fpga::XilinxSysFPGA;
use crate::platforms::xilinx_sys_components::xilinx_sys_overlay_handler::XilinxSysOverlayHandler;
use crate::system_io::{fs_read, fs_read_dir, fs_write, fs_write_bytes};
use fpgad_macros::platform;
use log::{error, info, trace, warn};
use std::path;
use std::path::{Component, Path, PathBuf};
use std::sync::OnceLock;
use zbus::fdo;
#[platform(compat_string = "xlnx,zynqmp-pcap-fpga,versal-fpga,zynq-devcfg-1.0,xlnx-sys,platform")]
#[derive(Debug)]
pub struct XilinxSysPlatform {
fpga: OnceLock<XilinxSysFPGA>,
overlay_handler: OnceLock<XilinxSysOverlayHandler>,
}
impl Default for XilinxSysPlatform {
fn default() -> Self {
Self::new()
}
}
impl XilinxSysPlatform {
pub fn new() -> Self {
trace!("creating new xilinx_sys_platform");
XilinxSysPlatform {
fpga: OnceLock::new(),
overlay_handler: OnceLock::new(),
}
}
}
impl Platform for XilinxSysPlatform {
fn fpga(&self, device_handle: &str) -> Result<&dyn Fpga, FpgadError> {
Ok(self.fpga.get_or_init(|| XilinxSysFPGA::new(device_handle)))
}
fn overlay_handler(&self, overlay_handle: &str) -> Result<&dyn OverlayHandler, FpgadError> {
if overlay_handle.is_empty() {
return Err(FpgadError::Argument(
"An overlay handle is required. Provided overlay handle is empty.".into(),
));
}
let handler = self
.overlay_handler
.get_or_init(|| XilinxSysOverlayHandler::new(overlay_handle));
let parent_path = handler.overlay_fs_path()?.parent().ok_or_else(|| {
FpgadError::Argument(format!(
"The path {:?} has no parent directory.",
handler.overlay_fs_path()
))
})?;
if !parent_path.exists() {
return Err(FpgadError::Argument(format!(
"The overlayfs path {parent_path:?} doesn't seem to exist."
)));
}
Ok(handler)
}
fn status_message(&self) -> Result<String, FpgadError> {
let mut ret_string = String::from(
"---- DEVICES ----\n\
| dev | platform | state |\n",
);
for device in list_fpga_managers()? {
let state = self.fpga(&device)?.state()?;
ret_string += format!(
"| {} | {} | {} |\n",
device,
self.platform_compat_string(),
state
)
.as_str();
}
ret_string += "\n---- OVERLAYS ----\n\
| overlay | status |\n";
for overlay in fs_read_dir(config::OVERLAY_CONTROL_DIR.as_ref())? {
let status = self.overlay_handler(&overlay)?.status()?;
ret_string.push_str(format!("| {overlay} | {status} |\n").as_ref());
}
Ok(ret_string)
}
fn platform_compat_string(&self) -> String {
Self::COMPAT_STRING.into()
}
fn is_available(&self) -> bool {
true
}
}
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))
}
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 = path::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 = path::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)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ReadSubCommand {
ReadProp,
ReadFlags,
}
impl ReadSubCommand {
pub fn as_str(self) -> &'static str {
match self {
ReadSubCommand::ReadFlags => "read_flags",
ReadSubCommand::ReadProp => "read_property",
}
}
}
impl std::str::FromStr for ReadSubCommand {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"read_flags" => Ok(ReadSubCommand::ReadFlags),
"read_property" => Ok(ReadSubCommand::ReadProp),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum WriteSubCommand {
WriteFlags,
WriteProp,
WriteByte,
}
impl WriteSubCommand {
pub fn as_str(self) -> &'static str {
match self {
WriteSubCommand::WriteFlags => "write_flags",
WriteSubCommand::WriteProp => "write_property",
WriteSubCommand::WriteByte => "write_property_bytes",
}
}
}
impl std::str::FromStr for WriteSubCommand {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"write_flags" => Ok(WriteSubCommand::WriteFlags),
"write_property" => Ok(WriteSubCommand::WriteProp),
"write_property_bytes" => Ok(WriteSubCommand::WriteByte),
_ => Err(()),
}
}
}
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 fn read_property(property_path_str: &str) -> Result<String, fdo::Error> {
info!("read_property called with property_path_str: {property_path_str}");
Ok(fs_read_property(property_path_str)?)
}
pub fn write_property(property_path_str: &str, data: &str) -> Result<String, fdo::Error> {
info!("write_property called with property_path_str: {property_path_str} and data: {data}");
let property_path = validate_property_path(Path::new(property_path_str))?;
fs_write(&property_path, false, data)?;
Ok(format!("{data} written to {property_path_str}"))
}
fn write_property_bytes(property_path_str: &str, data: &[u8]) -> Result<String, fdo::Error> {
info!(
"write_property_bytes called with property_path_str: {property_path_str} and data: {data:?}"
);
let property_path = validate_property_path(Path::new(property_path_str))?;
fs_write_bytes(&property_path, false, data)?;
Ok(format!(
"Byte string successfully written to {property_path_str}"
))
}
fn flags(fpga: &XilinxSysFPGA) -> Result<u32, FpgadError> {
let flag_path = Path::new(config::FPGA_MANAGERS_DIR)
.join(fpga.device_handle())
.join("flags");
let contents = fs_read(&flag_path)?;
let trimmed = contents.trim().trim_start_matches("0x");
u32::from_str_radix(trimmed, 16).map_err(|_| FpgadError::Flag("Parsing flags failed".into()))
}
fn set_flags(fpga: &XilinxSysFPGA, new_flags: u32) -> Result<String, FpgadError> {
let device_handle = fpga.device_handle();
let flag_path = Path::new(config::FPGA_MANAGERS_DIR)
.join(device_handle)
.join("flags");
trace!("Writing '0x{new_flags:X}' to '{flag_path:?}'");
if let Err(e) = fs_write(&flag_path, false, format!("0x{new_flags:X}")) {
error!("Failed to read state.");
return Err(e);
}
{
let state = fpga.state()?;
match state.as_str() {
"operating" => {
info!(
"{}'s state is 'operating' after writing flags.",
device_handle
)
}
_ => {
warn!(
"{}'s state is '{}' after writing flags.",
device_handle, state
);
}
}
};
let returned_flags = flags(fpga)?;
if returned_flags == new_flags {
Ok(format!(
"Flags set to '0x{:X}' for '{}'",
new_flags, device_handle
))
} else {
Err(FpgadError::Flag(format!(
"Setting flags of '{}' to '0x{:X}' failed. Resulting flag was '0x{:X}'",
device_handle, new_flags, returned_flags
)))
}
}
fn hex_from_string(value_str: &str) -> Result<Vec<u8>, FpgadError> {
let clean: String = value_str
.split_whitespace()
.collect::<String>()
.to_lowercase()
.replace("0x", "");
let mut chars = clean.chars().collect::<Vec<_>>();
if chars.len() % 2 != 0 {
chars.insert(0, '0');
}
chars
.chunks(2)
.map(|chunk| {
let s: String = chunk.iter().collect();
u8::from_str_radix(&s, 16)
.map_err(|e| FpgadError::Argument(format!("Invalid hex byte '{s}': {e}")))
})
.collect()
}
fn get_handle_from_path_or_handle(path: &str) -> Result<&str, FpgadError> {
if let Some(rest) = path.strip_prefix(config::FPGA_MANAGERS_DIR) {
if !path.ends_with("/flags") {
return Err(FpgadError::Argument(format!(
"Invalid flags path '{path}': when supplying a full sysfs path it must end \
with '/flags' (e.g. '/sys/class/fpga_manager/fpga0/flags')"
)));
}
let handle = rest
.split('/')
.next()
.filter(|s| !s.is_empty())
.ok_or_else(|| {
FpgadError::Argument(format!(
"Invalid FPGA manager path '{path}', could not extract device handle"
))
})?;
Ok(handle)
} else {
Ok(path)
}
}
pub fn xilinx_sys_write_handler(
sub_cmd_str: &str,
property_path: &str,
value_str: &str,
) -> Result<String, fdo::Error> {
match sub_cmd_str.parse::<WriteSubCommand>() {
Ok(WriteSubCommand::WriteFlags) => {
let device_handle = get_handle_from_path_or_handle(property_path)?;
validate_device_handle(device_handle)?;
let fpga = XilinxSysFPGA::new(device_handle);
let trimmed = value_str.trim();
let hex_str = trimmed
.strip_prefix("0x")
.or_else(|| trimmed.strip_prefix("0X"))
.unwrap_or(trimmed);
let parsed_flags = u32::from_str_radix(hex_str, 16).map_err(|e| {
FpgadError::Argument(format!(
"Invalid flags value '{value_str}': expected a hex u32 with or without \
'0x' prefix (e.g. '0x20' or '20' for decimal 32) ({e})"
))
})?;
set_flags(&fpga, parsed_flags).map_err(Into::into)
}
Ok(WriteSubCommand::WriteProp) => {
validate_property_path(Path::new(property_path))?;
write_property(property_path, value_str)
}
Ok(WriteSubCommand::WriteByte) => {
validate_property_path(Path::new(property_path))?;
let hex_data = hex_from_string(value_str)?;
write_property_bytes(property_path, &hex_data)
}
Err(()) => {
Err(FpgadError::Argument(format!("Unknown write subcommand '{sub_cmd_str}'")).into())
}
}
}
pub fn xilinx_sys_read_handler(
sub_cmd_str: &str,
property_path: &str,
) -> Result<String, fdo::Error> {
match sub_cmd_str.parse::<ReadSubCommand>() {
Ok(ReadSubCommand::ReadFlags) => {
let device_handle = get_handle_from_path_or_handle(property_path)?;
validate_device_handle(device_handle)?;
let fpga = XilinxSysFPGA::new(device_handle);
Ok(flags(&fpga)?.to_string())
}
Ok(ReadSubCommand::ReadProp) => {
validate_property_path(Path::new(property_path))?;
read_property(property_path)
}
Err(()) => {
Err(FpgadError::Argument(format!("Unknown read subcommand '{sub_cmd_str}'")).into())
}
}
}
#[cfg(test)]
mod test_validate_property_path {
use crate::platforms::xilinx_sys::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;
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 = path::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)));
}
}
#[cfg(test)]
mod test_get_handle_from_path_or_handle {
use super::*;
use googletest::prelude::*;
#[gtest]
fn accepts_plain_device_handle() {
let result = get_handle_from_path_or_handle("fpga0");
assert_that!(result, ok(eq(&"fpga0")));
}
#[gtest]
fn accepts_exact_flags_sysfs_path() {
let result = get_handle_from_path_or_handle("/sys/class/fpga_manager/fpga0/flags");
assert_that!(result, ok(eq(&"fpga0")));
}
#[gtest]
fn accepts_nonexistent_plain_device_handle() {
let result = get_handle_from_path_or_handle("fpga_nonexistent_test_device_12345");
assert_that!(result, ok(eq(&"fpga_nonexistent_test_device_12345")));
}
#[gtest]
fn accepts_nonexistent_device_in_sysfs_flags_path() {
let result = get_handle_from_path_or_handle(
"/sys/class/fpga_manager/fpga_nonexistent_test_device_12345/flags",
);
assert_that!(result, ok(eq(&"fpga_nonexistent_test_device_12345")));
}
#[gtest]
fn accepts_empty_device_handle() {
let result = get_handle_from_path_or_handle("");
assert_that!(result, ok(eq(&"")));
}
}
#[cfg(test)]
mod test_hex_from_string {
use super::*;
use googletest::prelude::*;
#[test]
fn parses_plain_hex_bytes() {
let result = hex_from_string("00 04 02 20 20");
let expected: Vec<u8> = vec![0, 4, 2, 32, 32];
assert_that!(result, ok(eq(&expected)));
}
#[test]
fn parses_continuous_single_byte() {
let result = hex_from_string("AA");
let expected: Vec<u8> = vec![0xAA];
assert_that!(result, ok(eq(&expected)));
}
#[test]
fn parses_lowercase_hex() {
let result = hex_from_string("aa bb cc");
let expected: Vec<u8> = vec![0xAA, 0xBB, 0xCC];
assert_that!(result, ok(eq(&expected)));
}
#[test]
fn parses_with_0x_prefix() {
let result = hex_from_string("0x00 0x04 0x02 0x20 0x20");
let expected: Vec<u8> = vec![0, 4, 2, 32, 32];
assert_that!(result, ok(eq(&expected)));
}
#[test]
fn parses_mixed_prefix_and_plain_tokens() {
let result = hex_from_string("0x00 04 0x02 20");
let expected: Vec<u8> = vec![0, 4, 2, 32];
assert_that!(result, ok(eq(&expected)));
}
#[test]
fn ignores_extra_whitespace() {
let result = hex_from_string(" 00 04 02 20 ");
let expected: Vec<u8> = vec![0, 4, 2, 32];
assert_that!(result, ok(eq(&expected)));
}
#[test]
fn rejects_invalid_hex_characters() {
let result = hex_from_string("00 GG 02");
assert_that!(
result,
err(pat!(FpgadError::Argument(contains_substring(
"Invalid hex"
))))
);
}
#[test]
fn parses_odd_length_as_stream() {
let result = hex_from_string("1FF");
let expected: Vec<u8> = vec![0x01, 0xFF];
assert_that!(result, ok(eq(&expected)));
}
#[test]
fn parses_no_spaces() {
let result = hex_from_string("DEADBEEF");
let expected: Vec<u8> = vec![0xDE, 0xAD, 0xBE, 0xEF];
assert_that!(result, ok(eq(&expected)));
}
#[test]
fn empty_input_returns_empty_vec() {
let result = hex_from_string("");
let expected: Vec<u8> = vec![];
assert_that!(result, ok(eq(&expected)));
}
}