// Copyright (C) 2023 Huawei Device Co., Ltd.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! File handle management for network tasks.
//!
//! Opens and manages the upload/download files and body files attached to a
//! task, resolves sandbox paths from UID and bundle name, and provides
//! thread-safe access to file handles via `Arc<Mutex<File>>`.

use std::fs::{File, OpenOptions};
use std::io;
use std::os::fd::{AsRawFd, FromRawFd};
use std::os::unix::fs::OpenOptionsExt;
use std::sync::{Arc, Mutex};

// Linux open(2) flag bits (see <fcntl.h>) used to harden sandbox file opens.
// Defined as raw constants so we avoid pulling in the libc crate just for
// flags.
// O_NOFOLLOW: reject the final path component when it is a symlink.
// O_CLOEXEC:  close the descriptor on exec to avoid leaking it to children.
// Intermediate directory symlinks are still followed by open(2), so every
// opened file is additionally verified against the app base directory via
// `/proc/self/fd` (see verify_within_base). Checking after the open is
// race-free: the descriptor pins the file, so swapping in a symlink
// afterwards cannot redirect it.
const O_NOFOLLOW: i32 = 0o400_000;
const O_CLOEXEC: i32 = 0o2_000_000;

use crate::error::{ErrorCode, ServiceError};
use crate::manage::account;
use crate::task::bundle::get_name_and_index;
use crate::task::config::{Action, TaskConfig};
use crate::task::ATOMIC_SERVICE;

/// Container for all files associated with a network task.
///
/// Manages the main task files (upload/download targets) and their sizes,
/// as well as any additional body files used for complex requests.
pub(crate) struct AttachedFiles {
    /// Main files for the task (upload sources or download destinations).
    pub(crate) files: Files,
    /// Sizes of the main files in bytes (negative values indicate unknown
    /// size).
    pub(crate) sizes: Vec<i64>,
    /// Additional body files for complex request scenarios.
    pub(crate) body_files: Files,
}

impl AttachedFiles {
    /// Opens all files specified in the task configuration.
    ///
    /// Creates a new `AttachedFiles` instance by opening both the main task
    /// files and any additional body files according to the provided
    /// configuration.
    ///
    /// # Errors
    /// Returns a `ServiceError` if any file fails to open.
    pub(crate) fn open(config: &TaskConfig) -> Result<AttachedFiles, ServiceError> {
        let (files, sizes) = open_task_files(config)?;
        let body_files = open_body_files(config)?;
        Ok(Self {
            files,
            sizes,
            body_files,
        })
    }
}

/// Opens the main task files based on the provided configuration.
///
/// Handles both upload and download scenarios, opening files in appropriate
/// modes and collecting their sizes where applicable.
///
/// # Errors
/// Returns a `ServiceError` if file opening or metadata retrieval fails.
fn open_task_files(config: &TaskConfig) -> Result<(Files, Vec<i64>), ServiceError> {
    let tid = config.common_data.task_id;
    let uid = config.common_data.uid;

    let mut files = Vec::new();
    let mut sizes = Vec::new();
    // Cache bundle name to avoid redundant calculations for multiple files
    let mut bundle_cache = BundleCache::new(config);

    for (idx, fs) in config.file_specs.iter().enumerate() {
        match config.common_data.action {
            Action::Upload => {
                let file = if fs.is_user_file {
                    // For user-provided files, use the file descriptor directly
                    match fs.fd {
                        Some(fd) => unsafe { File::from_raw_fd(fd) },
                        None => {
                            error!("None user file failed - task_id: {}, idx: {}", tid, idx);
                            sys_event!(
                                ExecFault,
                                DfxCode::SA_ERROR_01,
                                &format!("None user file failed - task_id: {}, idx: {}", tid, idx)
                            );
                            return Err(ServiceError::IoError(io::Error::new(
                                io::ErrorKind::Other,
                                "none user file",
                            )));
                        }
                    }
                } else {
                    // For non-user files, open from the app's storage
                    let bundle_name = bundle_cache.get_value()?;
                    open_file_readonly(uid, &bundle_name, &fs.path)
                        .map_err(ServiceError::IoError)?
                };
                // Get file size for upload progress tracking
                let size = cvt_res_error!(
                    file.metadata()
                        .map(|data| data.len())
                        .map_err(ServiceError::IoError),
                    "Cannot get upload file's size - task_id: {}, idx: {}",
                    tid,
                    idx
                );
                // Use Arc<Mutex<File>> to ensure thread-safe access
                files.push(Arc::new(Mutex::new(file)));
                debug!(
                    "Get file size succeed - task_id: {}, idx: {}, size: {}",
                    tid, idx, size
                );
                sizes.push(size as i64);
            }
            Action::Download => {
                let file = if fs.is_user_file {
                    // For user-provided files, use the file descriptor directly
                    match fs.fd {
                        Some(fd) => unsafe { File::from_raw_fd(fd) },
                        None => {
                            error!("None user file failed - task_id: {}, idx: {}", tid, idx);
                            sys_event!(
                                ExecFault,
                                DfxCode::SA_ERROR_01,
                                &format!("None user file failed - task_id: {}, idx: {}", tid, idx)
                            );
                            return Err(ServiceError::IoError(io::Error::new(
                                io::ErrorKind::Other,
                                "none user file",
                            )));
                        }
                    }
                } else {
                    // For non-user files, open from the app's storage in read-write mode
                    let bundle_name = bundle_cache.get_value()?;
                    open_file_readwrite(uid, &bundle_name, &fs.path)
                        .map_err(ServiceError::IoError)?
                };
                // Use Arc<Mutex<File>> to ensure thread-safe access
                files.push(Arc::new(Mutex::new(file)));
                // Set size to -1 for downloads (unknown size initially)
                sizes.push(-1)
            }
            _ => unreachable!("Action::Any in open_task_files should never reach"),
        }
    }
    Ok((Files::new(files), sizes))
}

/// Opens additional body files specified in the task configuration.
///
/// These files are typically used for complex request scenarios requiring
/// additional data beyond the main task files.
///
/// # Errors
/// Returns a `ServiceError` if any body file fails to open.
fn open_body_files(config: &TaskConfig) -> Result<Files, ServiceError> {
    let tid = config.common_data.task_id;
    let uid = config.common_data.uid;
    let mut bundle_cache = BundleCache::new(config);
    let mut body_files = Vec::new();

    for (idx, path) in config.body_file_paths.iter().enumerate() {
        let bundle_name = bundle_cache.get_value()?;
        let file = open_file_readwrite(uid, &bundle_name, path).map_err(|e| {
            error!("Open body_file failed - task_id: {}, idx: {}", tid, idx);
            sys_event!(
                ExecFault,
                DfxCode::SA_ERROR_02,
                &format!("Open body_file failed - task_id: {}, idx: {}", tid, idx)
            );
            ServiceError::IoError(e)
        })?;
        body_files.push(Arc::new(Mutex::new(file)))
    }

    Ok(Files::new(body_files))
}

/// Opens a file in read-write mode at the specified path.
///
/// Converts the provided path using the UID and bundle name, then opens the
/// file with read and append permissions. `O_NOFOLLOW` rejects a trailing
/// symlink, and the opened file is verified to still lie under the app base
/// directory (see [`verify_within_base`]), so neither a trailing symlink nor
/// a symlinked intermediate directory inside the attacker-controlled sandbox
/// subtree can redirect the open to a file outside the caller's sandbox.
///
/// # Errors
/// Returns an `io::Error` if the file cannot be opened or fails the sandbox
/// check.
fn open_file_readwrite(uid: u64, bundle_name: &str, path: &str) -> io::Result<File> {
    let (base, full) = app_base_and_path(uid, bundle_name, path)?;
    let file = OpenOptions::new()
        .read(true)
        .append(true)
        .custom_flags(O_NOFOLLOW | O_CLOEXEC)
        .open(full)?;
    Ok(cvt_res_error!(
        verify_within_base(&base, file),
        "open_file_readwrite failed"
    ))
}

/// Opens a file in read-only mode at the specified path.
///
/// Converts the provided path using the UID and bundle name, then opens the
/// file with read-only permissions. `O_NOFOLLOW` rejects a trailing symlink,
/// and the opened file is verified to still lie under the app base directory
/// (see [`verify_within_base`]), so neither a trailing symlink nor a
/// symlinked intermediate directory inside the attacker-controlled sandbox
/// subtree can redirect the open to a file outside the caller's sandbox.
///
/// # Errors
/// Returns an `io::Error` if the file cannot be opened or fails the sandbox
/// check.
fn open_file_readonly(uid: u64, bundle_name: &str, path: &str) -> io::Result<File> {
    let (base, full) = app_base_and_path(uid, bundle_name, path)?;
    let file = OpenOptions::new()
        .read(true)
        .custom_flags(O_NOFOLLOW | O_CLOEXEC)
        .open(full)?;
    Ok(cvt_res_error!(
        verify_within_base(&base, file),
        "open_file_readonly failed"
    ))
}

/// Splits a caller-supplied path into the app base directory and the fully
/// converted absolute path beneath it.
///
/// The base is everything up to and including the `<uuid>/base/<bundle>`
/// component produced by [`convert_path`]. Components above the base are
/// system-controlled and trusted; components below it live in the app sandbox
/// and are attacker-controlled, so the opened file must be verified against
/// the base (see [`verify_within_base`]).
///
/// # Errors
/// Returns an `io::Error` if the converted path is not anchored in the app
/// base directory.
fn app_base_and_path(uid: u64, bundle_name: &str, path: &str) -> io::Result<(String, String)> {
    let converted = convert_path(uid, bundle_name, path);
    let marker = format!("{}/base/{}", get_uuid_from_uid(uid), bundle_name);
    match converted.find(&marker) {
        Some(pos) => {
            let end = pos + marker.len();
            Ok((converted[..end].to_string(), converted))
        }
        None => Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "path is not anchored in the app base directory",
        )),
    }
}

/// Verifies that an already-opened file lies strictly under the app base
/// directory, returning it unchanged on success.
///
/// The check compares the canonicalized base against the real path of the
/// opened descriptor obtained from `/proc/self/fd`. Verifying after the open
/// (instead of canonicalizing the path before it) is what makes this safe:
/// the descriptor pins the file, so an attacker swapping a sandbox directory
/// for a symlink after the check cannot redirect an already opened file.
///
/// # Errors
/// Returns an `io::Error` if the base directory cannot be canonicalized, the
/// descriptor's real path cannot be read, or the file escaped the base.
fn verify_within_base(base: &str, file: File) -> io::Result<File> {
    let real_base = std::fs::canonicalize(base)?;
    let real_file = std::fs::read_link(format!("/proc/self/fd/{}", file.as_raw_fd()))?;
    // `Path::starts_with` compares whole components, so a sibling directory
    // whose name merely shares a prefix (e.g. "base2") does not pass.
    if real_file != real_base && real_file.starts_with(&real_base) {
        Ok(file)
    } else {
        Err(io::Error::new(
            io::ErrorKind::PermissionDenied,
            "file escaped the app base directory",
        ))
    }
}

/// Converts a relative path to an absolute path based on the user and bundle.
///
/// Transforms paths by replacing "storage" with "app" and "base" with a path
/// containing the user's UUID and bundle name.
///
/// # Examples
/// ```
/// # use crate::task::files::convert_path;
/// let path = convert_path(200000, "example.bundle", "storage/base/file.txt");
/// assert!(path.contains("app/1/base/example.bundle/file.txt"));
/// ```
pub(crate) fn convert_path(uid: u64, bundle_name: &str, path: &str) -> String {
    let uuid = get_uuid_from_uid(uid);
    let base_replace = format!("{}/base/{}", uuid, bundle_name);
    path.replacen("storage", "app", 1)
        .replacen("base", &base_replace, 1)
}

/// Extracts the UUID from a UID by dividing by 200000.
///
/// This is a standard way to map user identifiers to unique account
/// identifiers.
fn get_uuid_from_uid(uid: u64) -> u64 {
    uid / 200000
}

/// Checks if a task's account matches the current foreground account.
///
/// Returns `true` if the task's account matches the foreground account or if
/// the task's account is 0 (indicating it can run under any account).
///
/// # Examples
/// ```
/// # use crate::task::files::check_current_account;
/// // Assuming foreground account is 1
/// assert!(check_current_account(200000)); // task_account is 1
/// assert!(check_current_account(0)); // account 0 can run under any account
/// assert!(!check_current_account(400000)); // task_account is 2
/// ```
pub(crate) fn check_current_account(task_uid: u64) -> bool {
    let task_account = get_uuid_from_uid(task_uid);
    let (foreground_accounts, _active_accounts) = account::query_active_accounts();
    // 0 account_id tasks can be run under other account_ids
    let b = (task_account == 0) || foreground_accounts.contains(&task_account);
    if !b {
        info!(
            "check_current_account: {:?}, {}",
            foreground_accounts, task_account
        );
    }
    b
}

/// Caches bundle name resolution for a task configuration.
///
/// This struct avoids redundant bundle name conversions by caching the result
/// of the first conversion for subsequent use.
pub(crate) struct BundleCache<'a> {
    /// Reference to the task configuration used for bundle name resolution.
    config: &'a TaskConfig,
    /// Cached result of the bundle name conversion.
    value: Option<Result<String, ServiceError>>,
}

impl<'a> BundleCache<'a> {
    /// Creates a new bundle cache for the given task configuration.
    ///
    /// Initializes the cache with no precomputed value.
    pub(crate) fn new(config: &'a TaskConfig) -> Self {
        Self {
            config,
            value: None,
        }
    }

    /// Gets the resolved bundle name, using the cached value if available.
    ///
    /// If the bundle name has already been resolved, returns the cached value.
    /// Otherwise, resolves the bundle name and caches the result.
    ///
    /// # Errors
    /// Returns a `ServiceError` if the bundle name cannot be resolved.
    pub(crate) fn get_value(&mut self) -> Result<String, ServiceError> {
        let ret = match &self.value {
            // Return cached success result
            Some(ret) => match ret {
                Ok(name) => Ok(name.to_owned()),
                // For cached errors, retry the conversion
                Err(_e) => convert_bundle_name(self.config),
            },
            // If no cache, perform the conversion
            None => convert_bundle_name(self.config),
        };

        // Update cache with the new result
        self.value = Some(ret.clone());
        ret
    }
}

/// Converts a bundle name based on the task configuration.
///
/// Handles both atomic service bundles and regular application bundles,
/// applying appropriate formatting for each type.
///
/// # Errors
/// Returns a `ServiceError` if the bundle name cannot be converted.
fn convert_bundle_name(config: &TaskConfig) -> Result<String, ServiceError> {
    let is_account = config.bundle_type == ATOMIC_SERVICE;
    let bundle_name = config.bundle.as_str();

    if is_account {
        // Format for atomic service bundles
        let atomic_account = config.atomic_account.as_str();
        Ok(format!("+auid-{}+{}", atomic_account, bundle_name))
    } else {
        // Handle regular application bundles with possible clone indices
        let uid = config.common_data.uid;
        check_app_clone_bundle_name(uid, bundle_name)
    }
}

/// Checks for app clone bundle names and applies appropriate formatting.
///
/// If the app is a clone (indicated by an index > 0), formats the bundle name
/// to include the clone index.
///
/// # Errors
/// Returns a `ServiceError` if the bundle name and index cannot be retrieved.
fn check_app_clone_bundle_name(uid: u64, bundle_name: &str) -> Result<String, ServiceError> {
    let mut ret_name = bundle_name.to_string();

    if let Some((index, name)) = get_name_and_index(uid as i32) {
        // Log mismatch between provided and retrieved bundle names
        if bundle_name != name {
            info!("bundle name not matching. {:?}, {:?}", bundle_name, name);
        }

        // For clone apps, append the clone index to the bundle name
        if index > 0 {
            ret_name = format!("+clone-{}+{}", index, bundle_name);
        }

        return Ok(ret_name);
    }

    info!("can not get bundle name and index.");
    Err(ServiceError::ErrorCode(ErrorCode::Other))
}

/// Thread-safe collection of file handles.
///
/// Provides a safe interface to access multiple files concurrently,
/// using `Arc<Mutex<File>>` to ensure thread-safe file operations.
pub(crate) struct Files(Vec<Arc<Mutex<File>>>);

impl Files {
    /// Creates a new file collection from a vector of file handles.
    fn new(files: Vec<Arc<Mutex<File>>>) -> Self {
        Self(files)
    }

    /// Returns the number of files in the collection.
    pub(crate) fn len(&self) -> usize {
        self.0.len()
    }

    /// Gets a file handle at the specified index, if it exists.
    ///
    /// Returns a clone of the `Arc<Mutex<File>>` if the index is valid,
    /// allowing thread-safe access to the file.
    pub(crate) fn get(&self, index: usize) -> Option<Arc<Mutex<File>>> {
        self.0.get(index).cloned()
    }
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::io::{Read, Write};
    use std::os::unix::fs::symlink;

    use super::*;

    const ELOOP: i32 = 40;

    /// Temporary directory that is removed when dropped.
    struct TestDir(String);

    impl TestDir {
        fn new(name: &str) -> Self {
            let dir =
                std::env::temp_dir().join(format!("files_ut_{}_{}", std::process::id(), name));
            let _ = fs::remove_dir_all(&dir);
            fs::create_dir_all(&dir).unwrap();
            TestDir(dir.to_string_lossy().into_owned())
        }

        fn base(&self) -> String {
            format!("{}/base", self.0)
        }
    }

    impl Drop for TestDir {
        fn drop(&mut self) {
            let _ = fs::remove_dir_all(&self.0);
        }
    }

    /// Opens `path` exactly like `open_file_readwrite`, then verifies it.
    fn open_verified(base: &str, path: &str) -> io::Result<File> {
        let file = OpenOptions::new()
            .read(true)
            .append(true)
            .custom_flags(O_NOFOLLOW | O_CLOEXEC)
            .open(path)?;
        verify_within_base(base, file)
    }

    // @tc.name: app_base_and_path_anchors_at_bundle_dir
    // @tc.desc: Test that app_base_and_path anchors the base at the bundle dir
    // @tc.precon: NA
    // @tc.step: 1. Call app_base_and_path with a standard sandbox path
    // @tc.expect: Base is the <uuid>/base/<bundle> dir, full path is under it
    // @tc.type: FUNC
    #[test]
    fn app_base_and_path_anchors_at_bundle_dir() {
        let (base, full) = app_base_and_path(
            200001,
            "com.example.app",
            "/data/storage/el2/base/files/a.txt",
        )
        .unwrap();
        assert_eq!(base, "/data/app/el2/1/base/com.example.app");
        assert_eq!(full, "/data/app/el2/1/base/com.example.app/files/a.txt");
    }

    // @tc.name: app_base_and_path_rejects_unanchored_path
    // @tc.desc: Test that app_base_and_path rejects paths outside the sandbox
    // @tc.precon: NA
    // @tc.step: 1. Call app_base_and_path with a path lacking the base anchor
    // @tc.expect: Returns an InvalidInput error
    // @tc.type: FUNC
    #[test]
    fn app_base_and_path_rejects_unanchored_path() {
        assert!(app_base_and_path(200001, "com.example.app", "/etc/passwd").is_err());
    }

    // @tc.name: regular_file_under_base_passes
    // @tc.desc: Test that a regular file under the base dir opens and verifies
    // @tc.precon: NA
    // @tc.step: 1. Create a file under base 2. Open and verify it
    // @tc.expect: Open succeeds and file content is readable
    // @tc.type: FUNC
    #[test]
    fn regular_file_under_base_passes() {
        let dir = TestDir::new("regular");
        let base = dir.base();
        fs::create_dir_all(format!("{}/files", base)).unwrap();
        fs::write(format!("{}/files/a.txt", base), b"hello").unwrap();

        let mut file = open_verified(&base, &format!("{}/files/a.txt", base)).unwrap();
        let mut content = String::new();
        file.read_to_string(&mut content).unwrap();
        assert_eq!(content, "hello");
    }

    // @tc.name: append_mode_writes_at_end
    // @tc.desc: Test that append mode keeps existing content and appends
    // @tc.precon: NA
    // @tc.step: 1. Create a file with content 2. Open in append mode and write
    // @tc.expect: New data is appended after the existing content
    // @tc.type: FUNC
    #[test]
    fn append_mode_writes_at_end() {
        let dir = TestDir::new("append");
        let base = dir.base();
        fs::create_dir_all(&base).unwrap();
        fs::write(format!("{}/a.txt", base), b"ab").unwrap();

        let mut file = open_verified(&base, &format!("{}/a.txt", base)).unwrap();
        file.write_all(b"cd").unwrap();
        drop(file);
        assert_eq!(fs::read(format!("{}/a.txt", base)).unwrap(), b"abcd");
    }

    // @tc.name: intermediate_symlink_is_rejected
    // @tc.desc: Test that an intermediate symlink cannot redirect the open
    // @tc.precon: NA
    // @tc.step: 1. Plant a symlink dir under base 2. Open a file through it
    // @tc.expect: Fails with PermissionDenied after post-open verification
    // @tc.type: FUNC
    #[test]
    fn intermediate_symlink_is_rejected() {
        let dir = TestDir::new("intermediate_symlink");
        let base = dir.base();
        let outside = format!("{}/outside", dir.0);
        fs::create_dir_all(&base).unwrap();
        fs::create_dir_all(&outside).unwrap();
        fs::write(format!("{}/secret.txt", outside), b"secret").unwrap();
        symlink(&outside, format!("{}/link", base)).unwrap();

        let err = open_verified(&base, &format!("{}/link/secret.txt", base)).unwrap_err();
        assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
    }

    // @tc.name: trailing_symlink_is_rejected
    // @tc.desc: Test that a trailing symlink is rejected by O_NOFOLLOW
    // @tc.precon: NA
    // @tc.step: 1. Plant a symlink file under base 2. Open it
    // @tc.expect: Open fails with ELOOP
    // @tc.type: FUNC
    #[test]
    fn trailing_symlink_is_rejected() {
        let dir = TestDir::new("trailing_symlink");
        let base = dir.base();
        fs::create_dir_all(&base).unwrap();
        fs::write(format!("{}/real.txt", dir.0), b"real").unwrap();
        symlink(format!("{}/real.txt", dir.0), format!("{}/link.txt", base)).unwrap();

        let err = open_verified(&base, &format!("{}/link.txt", base)).unwrap_err();
        assert_eq!(err.raw_os_error(), Some(ELOOP));
    }

    // @tc.name: dotdot_escape_is_rejected
    // @tc.desc: Test that a '..' component escaping the base dir is rejected
    // @tc.precon: NA
    // @tc.step: 1. Open a file above base via a '..' component
    // @tc.expect: Fails with PermissionDenied after post-open verification
    // @tc.type: FUNC
    #[test]
    fn dotdot_escape_is_rejected() {
        let dir = TestDir::new("dotdot");
        let base = dir.base();
        fs::create_dir_all(&base).unwrap();
        fs::write(format!("{}/outside.txt", dir.0), b"x").unwrap();

        let err = open_verified(&base, &format!("{}/../outside.txt", base)).unwrap_err();
        assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
    }

    // @tc.name: sibling_directory_with_shared_prefix_is_rejected
    // @tc.desc: Test that a sibling dir sharing a name prefix is rejected
    // @tc.precon: NA
    // @tc.step: 1. Open a file under a sibling dir named '<base>2'
    // @tc.expect: Fails with PermissionDenied (component-wise comparison)
    // @tc.type: FUNC
    #[test]
    fn sibling_directory_with_shared_prefix_is_rejected() {
        let dir = TestDir::new("prefix_sibling");
        let base = dir.base();
        fs::create_dir_all(&base).unwrap();
        fs::create_dir_all(format!("{}/base2", dir.0)).unwrap();
        fs::write(format!("{}/base2/evil.txt", dir.0), b"x").unwrap();

        let err = open_verified(&base, &format!("{}/base2/evil.txt", dir.0)).unwrap_err();
        assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
    }

    // @tc.name: base_directory_itself_is_rejected
    // @tc.desc: Test that the base directory itself does not pass verification
    // @tc.precon: NA
    // @tc.step: 1. Open the base directory 2. Verify it
    // @tc.expect: Fails with PermissionDenied (must lie strictly under base)
    // @tc.type: FUNC
    #[test]
    fn base_directory_itself_is_rejected() {
        let dir = TestDir::new("base_itself");
        let base = dir.base();
        fs::create_dir_all(&base).unwrap();

        let file = File::open(&base).unwrap();
        let err = verify_within_base(&base, file).unwrap_err();
        assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
    }
}