fpgad 0.2.0

An FPGA manager daemon that handles the dirty work for you.
Documentation
// This file is part of fpgad, an application to manage FPGA subsystem together with device-tree and kernel modules.
//
// Copyright 2025 Canonical Ltd.
//
// SPDX-License-Identifier: GPL-3.0-only
//
// fpgad is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3, as published by the Free Software Foundation.
//
// fpgad is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with this program.  If not, see http://www.gnu.org/licenses/.

//!
//! # FPGAd dbus interfaces
//!
//! There are two interfaces available: `status`, which contains methods to access read-only properties of the FPGA and overlay subsystems, and `control` which contains methods to access writable properties of the FPGA and overlay subsystems.
//! Directly following this paragraph is a summary of all available methods, with the rest of this document explaining each method in detail.
//! Please note that these interfaces are used in the `cli` part of FPGAd (e.g. in [load.rs](../../../../cli/src/load.rs)), and so those implementations can be used as examples with greater context than those provided here.
//! There is further usage documentation, including the use of these interfaces from cmdline by way of `dbusctl`, in [daemon/README.md](../../../README.md).
//!
//!
//! For both interfaces, all methods are asynchronous and should be awaited immediately to avoid async pollution.
//!
//! ## Custom Errors
//!
//! All functions are shown to return the `fdo::Error` type, which is a wrapper for "FreeDesktopOrg", i.e. these are DBus error types. In the case that FPGAd returns an error, and it is not a DBus communication error, this error will be of type `fdo::Error:Failure` and the error string with begin with the `FpgadError` identifier such as `"FpgadError::Argument:"` i.e. the full error will be reported (from `busctl`) as
//! ```text
//! Call failed: FpgadError::Argument: <error text>
//! ```
//! or in the case of an IOError, the rust error will appear like:
//! ```text
//! [2026-01-08T16:29:05Z ERROR cli] org.freedesktop.DBus.Error.IOError: FpgadError::IOWrite: <fpgad's error text>: No such file or directory (os error 2)
//! Error: MethodError(OwnedErrorName("org.freedesktop.DBus.Error.IOError"), Some("FpgadError::IOWrite: <fpgad's error text>: No such file or directory (os error 2)"), Msg { type: Error, serial: 11, sender: UniqueName(":1.77"), reply-serial: 4, body: Str, fds: [] })
//! ```
//! whereby the `FpgadError` type could  be mapped to an equivalent `fdo::Error` type.
//!
//! ## FPGA API Summary
//!
//! | Interface | Method                   | Parameters                                                                                                     | Returns / Notes                                                                                                                             |
//! |-----------|--------------------------|----------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
//! | [status](status_interface)   | `get_fpga_state`           | `platform_string: &str`, `device_handle: &str`                                             | `String` – Current FPGA state (`unknown`, `operating`, etc.)                                                                                |
//! | [status](status_interface)   | `get_fpga_flags`           | `platform_string: &str`, `device_handle: &str`                                             | `String` – Current FPGA flags from hexadecimal integer to string                                                                            |
//! | [status](status_interface)   | `get_overlay_status`   | `platform_string: &str`, `overlay_handle: &str`                                            | `String` – Overlay status; errors if handle empty or invalid                                                                                |
//! | [status](status_interface)   | `get_overlays`               | None                                                                                       | `String` – List of available overlay handles (`\n` separated)                                                                               |
//! | [status](status_interface)   | `get_platform_type`     | `device_handle: &str`                                                                      | `String` – Compatibility string for device                                                                                                  |
//! | [status](status_interface)   | `get_platform_types`   | None                                                                                       | `String` – List of all fpga devices and their compatibility strings (`<device>:<compat>\n`)                                                 |
//! | [status](status_interface)   | `read_property`             | `property_path_str: &str`                                                                  | `String` – Contents of arbitrary FPGA attribute value                                                                                       |
//! | [control](control_interface) | `set_fpga_flags`         | `platform_string: &str`, `device_handle: &str`, `flags: u32`                               | `String` – Confirmation with new flags in hex                                                                                               |
//! | [control](control_interface) | `write_bitstream_direct` | `platform_string: &str`, `device_handle: &str`, `bitstream_path_str: &str`, `firmware_lookup_path: &str` | `String` – Confirmation of bitstream load; acquires write lock                                                                              |
//! | [control](control_interface) | `apply_overlay`           | `platform_string: &str`, `overlay_handle: &str`, `overlay_source_path: &str`, `firmware_lookup_path: &str` | `String` – Overlay applied; confirmation including firmware prefix; write lock used to protect against firmware search path race conditions |
//! | [control](control_interface) | `remove_overlay`         | `platform_string: &str`, `overlay_handle: &str`                                            | `String` – Overlay removed; confirmation including overlay filesystem path                                                                  |
//! | [control](control_interface) | `write_property`         | `property_path_str: &str`, `data: &str`                                                    | `String` – Confirmation of data written; path must be under `/sys/class/fpga_manager/`                                                      |
//! | [control](control_interface) | `write_property_bytes` | `property_path_str: &str`, `data: &[u8]`                                                   | `String` – Confirmation of bytes written; path must be under `/sys/class/fpga_manager/`                                                     |

pub mod control_interface;
pub mod status_interface;

use crate::config;
use crate::error::FpgadError;
use crate::system_io::fs_read;
use std::path;
use std::path::{Component, Path, PathBuf};

/// Validates that a property path is constrained under the fpga manager directory and does not contain explicit parent traversal segments.
/// This is used to validate paths for all read/write property access methods in the control and status interfaces.
///
/// # Arguments
/// * `property_path` - The property path to validate as a Path.
///
/// # Returns: `Result<PathBuf, FpgadError>`
/// A `PathBuf` representing the validated property path if it is valid, or a `FpgadError` if the path is invalid.
///
/// # Examples
/// ```rust,no_run
/// let valid_path = validate_property_path("/sys/class/fpga_manager/fpga0/name")?;
/// assert_eq!(valid_path.to_string_lossy(), "/sys/class/fpga_manager/fpga0/name");
/// let invalid_path = validate_property_path("/sys/class/fpga_manager/../etc/passwd");
/// assert!(invalid_path.is_err());
/// ```
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))
}

/// Validates that a property path is constrained under a specified base path and does not contain
/// explicit parent traversal segments. This is a more general version of `validate_property_path` which
/// can be used to validate paths under different base directories, such as the firmware lookup control path.
///
/// # Arguments
/// * `property_path` - The property path to validate as a Path.
/// * `base_path` - The base path under which the property path must be constrained.
///
/// # Returns: `Result<PathBuf, FpgadError>`
/// A `PathBuf` representing the validated property path if it is valid, or a `FpgadError` if the path is invalid.
///
/// # Examples
/// ```rust,no_run
/// let valid_path = validate_property_path_with_base("/sys/class/fpga_manager/fpga0/name", Path::new("/sys/class/fpga_manager/"))?;
/// assert_eq!(valid_path.to_string_lossy(), "/sys/class/fpga_manager/fpga0/name");
/// let invalid_path = validate_property_path_with_base("/sys/class/fpga_manager/../etc/passwd", Path::new("/sys/class/fpga_manager/"));
/// assert!(invalid_path.is_err());
/// ```
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 = 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)
}

/// Read the current contents of an FPGA device property, e.g. "name". The property path must be a subdirectory of the fpga manager directory (typically, /sys/class/fpga_manager/)
///
/// # Arguments
///
/// * `property_path_str`: path to the variable to read e.g. /sys/class/fpga_manager/fpga0/name
///
/// # Returns: `Result<String, FpgadError>`
/// * `String` - the contents of the property path
///
/// * `FpgadError::Argument` if the path is not found within the compile time [config::FPGA_MANAGERS_DIR]
///
/// # Examples
///
/// ```rust,no_run
/// let device_name = fs_read_property("/sys/class/fpga_manager/fpga0/name")?;
/// assert_eq!(device_name, "Xilinx ZynqMP FPGA Manager\n")
/// ```
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)
}

/// Helper function to check that a device with given handle does exist.
///
/// # Arguments
///
/// * `device_handle`: name of the device in sysfs, e.g. `fpga0`
///
/// # Returns: `Result<(), FpgadError>`
/// * `()` on success
/// * `FpgadError::Argument` in the case that `device_handle` is not found on the system or
///   device_handle contains invalid characters/is empty.
///
/// # Examples
///
/// ```rust,no_run
/// assert!(validate_device_handle("fpga0").is_ok())
/// assert!(validate_device_handle("").is_err())
/// ```
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(())
}

#[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)));
    }
}