// Copyright (C) 2025 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 path validation and permission management for downloads.
//!
//! This module provides functions for validating download paths, converting
//! between different path formats, and setting appropriate file permissions for
//! downloaded content across different API versions.
// Standard library imports
use std::error::Error;
use std::fmt::Display;
use std::fs::{self, OpenOptions};
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
// External dependencies
use request_core::config::Version;
use request_utils::context::Context;
use request_utils::storage;
// Constants for path validation
const MAX_FILE_PATH_LENGTH: usize = 4096; // Maximum allowed path length in bytes
// Path prefixes for validation and conversion
const ABSOLUTE_PREFIX: &str = "/";
const RELATIVE_PREFIX: &str = "./";
const FILE_PREFIX: &str = "file://";
const INTERNAL_PREFIX: &str = "internal://";
// Valid storage areas for API 10
const AREA1: &str = "/data/storage/el1/base";
const AREA2: &str = "/data/storage/el2/base";
const AREA5: &str = "/data/storage/el5/base";
// ACL permission strings for service account access
const SA_PERMISSION_RWX: &str = "g:3815:rwx"; // Read, write, execute permissions
const SA_PERMISSION_X: &str = "g:3815:x"; // Execute-only permissions
const SA_PERMISSION_CLEAN: &str = "g:3815:---"; // No permissions
/// Gets a validated download path with appropriate permissions.
///
/// Validates and converts the provided path string based on the API version,
/// checking for existing files if overwrite is disabled, and sets appropriate
/// permissions on the file and parent directories.
///
/// # Arguments
/// * `version` - API version to determine path handling logic
/// * `context` - Application context for accessing storage directories
/// * `save_as` - Path string to validate and convert
/// * `overwrite` - Whether to allow overwriting existing files
///
/// # Returns
/// A valid `PathBuf` for the download if successful, or a `DownloadPathError`
/// if validation fails
///
/// # Errors
/// * `DownloadPathError::EmptyPath` - If the path is empty
/// * `DownloadPathError::TooLongPath` - If the path exceeds the maximum length
/// * `DownloadPathError::InvalidPath` - If the path is not in an allowed storage
/// area
/// * `DownloadPathError::AlreadyExists` - If the file already exists and
/// overwrite is false
/// * `DownloadPathError::CreateFile` - If file creation fails
/// * `DownloadPathError::SetPermission` - If setting file permissions fails
/// * `DownloadPathError::AclAccess` - If setting ACL permissions fails
pub fn get_download_path(
version: Version,
context: &Context,
saveas: &str,
overwrite: bool,
) -> Result<PathBuf, DownloadPathError> {
// Convert path according to API version rules
let path = convert_path(version, context, saveas)?;
// Check for existing file if overwrite is disabled
if !overwrite && path.exists() {
return Err(DownloadPathError::AlreadyExists);
}
// Set appropriate file permissions
set_file_permission(&path, &context)?;
Ok(path)
}
/// Converts a path string to a `PathBuf` based on API version rules.
///
/// Handles different path conversion logic for API 9 and API 10, validating
/// paths against appropriate constraints for each version.
///
/// # Arguments
/// * `version` - API version determining conversion rules
/// * `context` - Application context for accessing directories
/// * `save_as` - Path string to convert
///
/// # Returns
/// A `PathBuf` representing the converted path, or a `DownloadPathError` on
/// validation failure
pub fn convert_path(
version: Version,
context: &Context,
saveas: &str,
) -> Result<PathBuf, DownloadPathError> {
match version {
Version::API9 => {
// Handle absolute paths directly for API 9
if let Some(0) = saveas.find(ABSOLUTE_PREFIX) {
if saveas.len() == ABSOLUTE_PREFIX.len() {
return Err(DownloadPathError::EmptyPath);
}
return Ok(PathBuf::from(saveas));
} else {
// Handle internal cache paths for API 9
const INTERNAL_PATTERN: &str = "internal://cache/";
let file_name = match saveas.find(INTERNAL_PATTERN) {
Some(0) => saveas.split_at(INTERNAL_PATTERN.len()).1,
_ => saveas,
};
if file_name.is_empty() {
return Err(DownloadPathError::EmptyPath);
}
let cache_dir = context.get_cache_dir();
// Validate path length to prevent buffer overflows
if cache_dir.len() + file_name.len() + 1 > MAX_FILE_PATH_LENGTH {
return Err(DownloadPathError::TooLongPath);
}
Ok(PathBuf::from(cache_dir).join(file_name))
}
}
Version::API10 => {
// Convert to absolute path using API 10 rules
let absolute_path = convert_to_absolute_path(&context, saveas)?;
// Validate path is within allowed storage areas for API 10
if !absolute_path.starts_with(AREA1)
&& !absolute_path.starts_with(AREA2)
&& !absolute_path.starts_with(AREA5)
{
return Err(DownloadPathError::InvalidPath);
}
Ok(absolute_path)
}
}
}
/// Converts various path formats to absolute paths for API 10.
///
/// Handles absolute, file://, internal://, and relative paths, converting them
/// to appropriate absolute paths with validation.
///
/// # Arguments
/// * `context` - Application context for resolving relative paths
/// * `path` - Path string in various formats
///
/// # Returns
/// An absolute `PathBuf`, or a `DownloadPathError` on validation failure
fn convert_to_absolute_path(context: &Context, path: &str) -> Result<PathBuf, DownloadPathError> {
// Handle absolute paths
if let Some(0) = path.find(ABSOLUTE_PREFIX) {
if path.len() == ABSOLUTE_PREFIX.len() {
return Err(DownloadPathError::EmptyPath);
}
return Ok(PathBuf::from(path));
}
// Handle file:// scheme with bundle name validation
if let Some(0) = path.find(FILE_PREFIX) {
let path = path.split_at(FILE_PREFIX.len()).1;
if path.is_empty() {
return Err(DownloadPathError::EmptyPath);
}
// Validate path has bundle name and path parts
let Some(index) = path.find('/') else {
return Err(DownloadPathError::InvalidPath);
};
let (bundle_name, path) = path.split_at(index);
// Ensure bundle name matches the application
if bundle_name != context.get_bundle_name() {
return Err(DownloadPathError::BundleNameNotMap);
}
return Ok(PathBuf::from(path));
}
// Handle internal:// paths
if let Some(0) = path.find(INTERNAL_PREFIX) {
let path = path.split_at(INTERNAL_PREFIX.len()).1;
if path.is_empty() {
return Err(DownloadPathError::EmptyPath);
}
let cache_dir = context.get_cache_dir();
return Ok(PathBuf::from(cache_dir).join(path));
}
// Handle relative paths
let path = if let Some(0) = path.find(RELATIVE_PREFIX) {
path.split_at(RELATIVE_PREFIX.len()).1
} else {
path
};
if path.is_empty() {
return Err(DownloadPathError::EmptyPath);
}
let cache_dir = context.get_cache_dir();
Ok(PathBuf::from(cache_dir).join(path))
}
/// Sets appropriate permissions on a file and its parent directories.
///
/// Creates the file if it doesn't exist, sets standard permissions, and
/// configures ACL permissions for the service account to access the file and
/// its parent directories.
///
/// # Arguments
/// * `path` - Path to the file to configure permissions for
/// * `context` - Application context
///
/// # Returns
/// `Ok(())` if permissions are successfully set, or a `DownloadPathError` on
/// failure
///
/// # Errors
/// * `DownloadPathError::CreateFile` - If file creation fails
/// * `DownloadPathError::SetPermission` - If setting standard file permissions
/// fails
/// * `DownloadPathError::AclAccess` - If setting ACL permissions fails
pub fn set_file_permission(path: &PathBuf, context: &Context) -> Result<(), DownloadPathError> {
// Create the file if it doesn't exist
let _ = OpenOptions::new()
.write(true)
.create(true)
.open(&path)
.map_err(|e| DownloadPathError::CreateFile(e))?;
// Restrict to owner+group read/write only (no access for others).
// The service account (g:3815) accesses the file via the ACL entry set
// below, not via the other permission bits. POSIX ACL mask is bound to the
// group class of the mode: 0o660 keeps the mask at rw so the g:3815:rwx
// entry stays effective for SA read/write, while closing world access.
let perm = fs::Permissions::from_mode(0o660);
if let Err(e) = fs::set_permissions(&path, perm) {
return Err(DownloadPathError::SetPermission(e));
}
// Log the base directory for debugging
let base_dir = context.get_base_dir();
info!("Base directory: {:?}", base_dir);
// Set execute permissions on parent directories for traversal
let mut path_clone = path.clone();
// Process parent directories up to a reasonable limit
while path_clone.to_string_lossy().to_string().len() >= 10 {
info!("Current path: {:?}", path_clone);
if let Err(e) =
storage::acl_set_access(&path_clone.to_string_lossy().to_string(), SA_PERMISSION_X)
{
error!("set acl x failed: {:?}", e);
}
path_clone.pop();
}
// Set read/write/execute permissions on the file for the service account
info!("Setting ACL access for path: {:?}", path);
if let Err(e) = storage::acl_set_access(&path.to_string_lossy().to_string(), SA_PERMISSION_RWX)
{
return Err(DownloadPathError::AclAccess(e));
}
Ok(())
}
/// Error types for download path validation and permission operations.
#[derive(Debug)]
pub enum DownloadPathError {
/// The provided path is empty
EmptyPath,
/// The path exceeds the maximum allowed length
TooLongPath,
/// The path is not in an allowed storage area
InvalidPath,
/// The bundle name in the path doesn't match the application
BundleNameNotMap,
/// The file already exists and overwrite is disabled
AlreadyExists,
/// File creation failed with an IO error
CreateFile(std::io::Error),
/// Setting file permissions failed with an IO error
SetPermission(std::io::Error),
/// Setting ACL access permissions failed
AclAccess(i32),
}
impl Error for DownloadPathError {}
impl Display for DownloadPathError {
/// Formats the error as a human-readable string via the `Display` trait.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&format!("{}", self))
}
}