* Copyright (c) 2025 Huawei Technologies Co., Ltd.
* This program is free software, you can redistribute it and/or modify it under the terms and conditions of
* CANN Open Software License Agreement Version 2.0 (the "License").
* Please refer to the License for details. You may not use this file except in compliance with the License.
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
* See LICENSE in the root of the software repository for the full text of the License.
*/
#include "file_utils.h"
#include <limits>
#include <algorithm>
#include <atomic>
#include <cerrno>
#include <cstdio>
#include <fcntl.h>
#include <unistd.h>
#include <boost/filesystem.hpp>
#include <boost/system/error_code.hpp>
#include "ascendc_tool_log.h"
namespace ascendc {
namespace {
namespace fs = boost::filesystem;
class AtomicFileWriter final {
public:
AtomicFileWriter() = default;
~AtomicFileWriter() noexcept;
AtomicFileWriter(const AtomicFileWriter&) = delete;
AtomicFileWriter& operator=(const AtomicFileWriter&) = delete;
bool WriteTextToTemporaryFile(const std::string& destinationPath, const std::string& text);
bool CopyFileToTemporaryFile(const std::string& sourcePath, const std::string& destinationPath);
bool ReplaceDestinationAtomically() noexcept;
private:
int32_t CreateUniqueTemporaryFile(const std::string& destinationPath, mode_t permissions);
std::string canonicalDestinationPath_;
std::string ownedTemporaryFilePath_;
bool ownsTemporaryFile_{false};
bool temporaryFileWriteSucceeded_{false};
};
constexpr size_t FILE_COPY_BUFFER_BYTES = 64U * 1024U;
class OwnedFileDescriptor final {
public:
explicit OwnedFileDescriptor(int32_t descriptor) noexcept : descriptor_(descriptor) {}
~OwnedFileDescriptor() noexcept { (void)Close(); }
OwnedFileDescriptor(const OwnedFileDescriptor&) = delete;
OwnedFileDescriptor& operator=(const OwnedFileDescriptor&) = delete;
int32_t Get() const noexcept { return descriptor_; }
bool Close() noexcept
{
if (descriptor_ < 0) {
return true;
}
const int32_t descriptor = descriptor_;
descriptor_ = -1;
if (close(descriptor) != 0) {
ASCENDLOGE("Failed to close file descriptor: fd=%d errno=%d", descriptor, errno);
return false;
}
return true;
}
private:
int32_t descriptor_;
};
bool WriteAllBytes(int32_t borrowedFileDescriptor, const char* data, size_t byteCount) noexcept
{
size_t writtenByteCount = 0U;
while (writtenByteCount < byteCount) {
const size_t chunkSize = std::min(byteCount - writtenByteCount, FILE_COPY_BUFFER_BYTES);
const ssize_t writeResult = write(borrowedFileDescriptor, data + writtenByteCount, chunkSize);
if (writeResult < 0 && errno == EINTR) {
continue;
}
if (writeResult <= 0) {
ASCENDLOGE(
"Failed to write temporary file: fd=%d written=%zu requested=%zu errno=%d", borrowedFileDescriptor,
writtenByteCount, byteCount, writeResult < 0 ? errno : 0);
return false;
}
writtenByteCount += static_cast<size_t>(writeResult);
}
return true;
}
bool CopyAllBytes(int32_t borrowedSourceDescriptor, int32_t borrowedDestinationDescriptor) noexcept
{
char buffer[FILE_COPY_BUFFER_BYTES];
while (true) {
const ssize_t readByteCount = read(borrowedSourceDescriptor, buffer, sizeof(buffer));
if (readByteCount == 0) {
return true;
}
if (readByteCount < 0) {
if (errno == EINTR) {
continue;
}
ASCENDLOGE("Failed to read copy source: fd=%d errno=%d", borrowedSourceDescriptor, errno);
return false;
}
if (!WriteAllBytes(borrowedDestinationDescriptor, buffer, static_cast<size_t>(readByteCount))) {
return false;
}
}
}
AtomicFileWriter::~AtomicFileWriter() noexcept
{
if (!ownsTemporaryFile_) {
return;
}
const int32_t unlinkResult = unlink(ownedTemporaryFilePath_.c_str());
if (unlinkResult != 0 && errno != ENOENT) {
ASCENDLOGW(
"Failed to remove unpublished temporary file: path=%s errno=%d", ownedTemporaryFilePath_.c_str(), errno);
}
}
int32_t AtomicFileWriter::CreateUniqueTemporaryFile(const std::string& destinationPath, mode_t permissions)
{
if (!canonicalDestinationPath_.empty()) {
ASCENDLOGE("File replacement has already been prepared");
return -1;
}
if (!FileUtils::ResolveRegularFilePathForWrite(destinationPath, canonicalDestinationPath_)) {
return -1;
}
constexpr uint32_t maxCreationAttempts = 32U;
static std::atomic<uint64_t> temporaryFileSequence{0};
for (uint32_t attempt = 0U; attempt < maxCreationAttempts; ++attempt) {
ownedTemporaryFilePath_ = canonicalDestinationPath_ + ".asc_tmp_" + std::to_string(getpid()) + "_" +
std::to_string(temporaryFileSequence.fetch_add(1U, std::memory_order_relaxed));
const int32_t descriptor =
open(ownedTemporaryFilePath_.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, permissions);
if (descriptor >= 0) {
ownsTemporaryFile_ = true;
return descriptor;
}
if (errno != EEXIST) {
break;
}
}
ASCENDLOGE(
"Failed to create temporary output file: destination=%s temporary=%s errno=%d", destinationPath.c_str(),
ownedTemporaryFilePath_.c_str(), errno);
return -1;
}
bool AtomicFileWriter::WriteTextToTemporaryFile(const std::string& destinationPath, const std::string& text)
{
constexpr mode_t privateTextPermissions = 0600;
OwnedFileDescriptor output(CreateUniqueTemporaryFile(destinationPath, privateTextPermissions));
if (output.Get() < 0) {
return false;
}
const bool writeSucceeded = WriteAllBytes(output.Get(), text.data(), text.size());
const bool closeSucceeded = output.Close();
temporaryFileWriteSucceeded_ = writeSucceeded && closeSucceeded;
if (!temporaryFileWriteSucceeded_) {
ASCENDLOGE(
"Failed to prepare temporary output file: destination=%s temporary=%s", destinationPath.c_str(),
ownedTemporaryFilePath_.c_str());
}
return temporaryFileWriteSucceeded_;
}
bool AtomicFileWriter::CopyFileToTemporaryFile(const std::string& sourcePath, const std::string& destinationPath)
{
std::string canonicalSourcePath;
if (!FileUtils::ResolveRegularFilePathForRead(sourcePath, canonicalSourcePath)) {
return false;
}
OwnedFileDescriptor input(open(canonicalSourcePath.c_str(), O_RDONLY | O_CLOEXEC));
if (input.Get() < 0) {
ASCENDLOGE("Failed to open copy source: path=%s errno=%d", sourcePath.c_str(), errno);
return false;
}
constexpr mode_t copiedFilePermissions = 0644;
OwnedFileDescriptor output(CreateUniqueTemporaryFile(destinationPath, copiedFilePermissions));
if (output.Get() < 0) {
return false;
}
const bool copySucceeded = CopyAllBytes(input.Get(), output.Get());
const bool sourceCloseSucceeded = input.Close();
const bool outputCloseSucceeded = output.Close();
temporaryFileWriteSucceeded_ = copySucceeded && sourceCloseSucceeded && outputCloseSucceeded;
if (!temporaryFileWriteSucceeded_) {
ASCENDLOGE(
"Failed to prepare file copy: source=%s destination=%s temporary=%s", sourcePath.c_str(),
destinationPath.c_str(), ownedTemporaryFilePath_.c_str());
}
return temporaryFileWriteSucceeded_;
}
bool AtomicFileWriter::ReplaceDestinationAtomically() noexcept
{
if (!temporaryFileWriteSucceeded_) {
ASCENDLOGE(
"Cannot replace destination before temporary file write succeeds: destination=%s",
canonicalDestinationPath_.c_str());
return false;
}
if (rename(ownedTemporaryFilePath_.c_str(), canonicalDestinationPath_.c_str()) != 0) {
ASCENDLOGE("Failed to replace destination file: path=%s errno=%d", canonicalDestinationPath_.c_str(), errno);
return false;
}
ownsTemporaryFile_ = false;
temporaryFileWriteSucceeded_ = false;
return true;
}
}
std::string FileUtils::JoinPath(const std::string& left, const std::string& right)
{
if (left.empty()) {
return right;
}
if (right.empty()) {
return left;
}
return (fs::path(left) / fs::path(right)).string();
}
std::string FileUtils::ParentPath(const std::string& path) { return fs::path(path).parent_path().string(); }
std::string FileUtils::FileName(const std::string& path) { return fs::path(path).filename().string(); }
bool FileUtils::MakeAbsolutePath(const std::string& path, std::string& absolute)
{
ASCENDLOGD("Making path absolute: path=%s", path.c_str());
boost::system::error_code error;
const fs::path result = fs::absolute(fs::path(path), error);
if (error) {
ASCENDLOGE("Failed to make path absolute: path=%s error=%s", path.c_str(), error.message().c_str());
absolute.clear();
return false;
}
absolute = result.string();
ASCENDLOGD("Made path absolute: input=%s absolute=%s", path.c_str(), absolute.c_str());
return true;
}
bool FileUtils::IsSafeRelativePath(const std::string& path)
{
if (path.empty() || path.find('\\') != std::string::npos || path.find('\0') != std::string::npos) {
return false;
}
const fs::path filesystemPath(path);
if (!filesystemPath.is_relative()) {
return false;
}
if (filesystemPath.lexically_normal().generic_string() != path) {
return false;
}
for (const fs::path& component : filesystemPath) {
if (component == fs::path(".") || component == fs::path("..")) {
return false;
}
}
return true;
}
bool FileUtils::IsPathWithin(const std::string& path, const std::string& root)
{
if (path.empty() || root.empty()) {
return false;
}
const fs::path normalizedPath = fs::path(path).lexically_normal();
const fs::path normalizedRoot = fs::path(root).lexically_normal();
if (normalizedPath.is_absolute() != normalizedRoot.is_absolute()) {
return false;
}
fs::path::const_iterator pathPart = normalizedPath.begin();
for (fs::path::const_iterator rootPart = normalizedRoot.begin(); rootPart != normalizedRoot.end(); ++rootPart) {
if (pathPart == normalizedPath.end() || *pathPart != *rootPart) {
return false;
}
++pathPart;
}
return true;
}
bool FileUtils::PathExists(const std::string& path) noexcept
{
boost::system::error_code error;
const bool result = fs::exists(fs::path(path), error);
return !error && result;
}
bool FileUtils::IsDirectory(const std::string& path) noexcept
{
boost::system::error_code error;
const bool result = fs::is_directory(fs::path(path), error);
return !error && result;
}
bool FileUtils::IsRegularFile(const std::string& path) noexcept
{
boost::system::error_code error;
const bool result = fs::is_regular_file(fs::path(path), error);
return !error && result;
}
bool FileUtils::IsSymlink(const std::string& path) noexcept
{
boost::system::error_code error;
const fs::file_status status = fs::symlink_status(fs::path(path), error);
return !error && fs::is_symlink(status);
}
bool FileUtils::ResolveCanonicalPath(const std::string& path, std::string& resolved)
{
ASCENDLOGD("Resolving canonical path: path=%s", path.c_str());
boost::system::error_code error;
const fs::path canonical = fs::canonical(fs::path(path), error);
if (error) {
ASCENDLOGE("Failed to resolve canonical path: path=%s error=%s", path.c_str(), error.message().c_str());
resolved.clear();
return false;
}
resolved = canonical.string();
ASCENDLOGD("Resolved canonical path: path=%s resolved=%s", path.c_str(), resolved.c_str());
return true;
}
bool FileUtils::ResolveRegularFilePathForRead(const std::string& path, std::string& resolved)
{
resolved.clear();
if (path.empty() || path.find('\0') != std::string::npos) {
ASCENDLOGE("Input file path must be nonempty and contain no embedded NUL");
return false;
}
if (!ResolveCanonicalPath(path, resolved)) {
return false;
}
if (!IsRegularFile(resolved)) {
ASCENDLOGE("Rejected non-regular input file: path=%s", path.c_str());
resolved.clear();
return false;
}
return true;
}
bool FileUtils::OpenRegularFileForRead(const std::string& path, std::ifstream& borrowedInput)
{
std::string canonicalPath;
if (borrowedInput.is_open()) {
ASCENDLOGE("Cannot open file using an already open stream: path=%s", path.c_str());
return false;
}
if (!ResolveRegularFilePathForRead(path, canonicalPath)) {
return false;
}
borrowedInput.open(canonicalPath, std::ios::in | std::ios::binary);
if (!borrowedInput) {
ASCENDLOGE("Failed to open regular input file: path=%s", path.c_str());
return false;
}
return true;
}
bool FileUtils::OpenRegularFileForWrite(
const std::string& path, std::ofstream& borrowedOutput, std::ios::openmode writeMode)
{
std::string canonicalPath;
if (borrowedOutput.is_open()) {
ASCENDLOGE("Cannot open file using an already open stream: path=%s", path.c_str());
return false;
}
if (!ResolveRegularFilePathForWrite(path, canonicalPath)) {
return false;
}
borrowedOutput.open(canonicalPath, std::ios::out | std::ios::binary | writeMode);
if (!borrowedOutput) {
ASCENDLOGE("Failed to open regular output file: path=%s", path.c_str());
return false;
}
return true;
}
bool FileUtils::WriteTextFile(const std::string& path, const std::string& text, std::ios::openmode writeMode)
{
std::ofstream output;
if (!OpenRegularFileForWrite(path, output, writeMode)) {
return false;
}
output << text;
if (!FinalizeOutput(output)) {
ASCENDLOGE("Failed to finish writing text file: path=%s", path.c_str());
return false;
}
return true;
}
bool FileUtils::ResolveRegularFilePathForWrite(const std::string& path, std::string& resolved)
{
resolved.clear();
if (path.empty() || path.find('\0') != std::string::npos) {
ASCENDLOGE("Output file path must be nonempty and contain no embedded NUL");
return false;
}
const fs::path inputPath(path);
const fs::path filename = inputPath.filename();
if (filename.empty() || filename == "." || filename == "..") {
ASCENDLOGE("Output path must name a file: path=%s", path.c_str());
return false;
}
const std::string parentPath = inputPath.has_parent_path() ? inputPath.parent_path().string() : ".";
std::string canonicalParentPath;
if (!ResolveCanonicalPath(parentPath, canonicalParentPath)) {
return false;
}
boost::system::error_code error;
if (!fs::is_directory(fs::path(canonicalParentPath), error) || error) {
ASCENDLOGE(
"Failed to resolve output parent directory: path=%s error=%d message=%s", path.c_str(), error.value(),
error ? error.message().c_str() : "parent is not a directory");
return false;
}
const fs::path candidate = fs::path(canonicalParentPath) / filename;
const fs::file_status status = fs::symlink_status(candidate, error);
if (status.type() != fs::file_not_found && (error || !fs::is_regular_file(status))) {
ASCENDLOGE(
"Rejected output file: path=%s error=%d message=%s", path.c_str(), error.value(),
error ? error.message().c_str() : "output is not a regular file");
return false;
}
resolved = candidate.string();
return true;
}
bool FileUtils::ResolveDirectory(const std::string& path, std::string& resolved)
{
ASCENDLOGD("Resolving directory: path=%s", path.c_str());
boost::system::error_code error;
const fs::file_status inputStatus = fs::symlink_status(fs::path(path), error);
if (error) {
ASCENDLOGE("Failed to inspect directory: path=%s error=%s", path.c_str(), error.message().c_str());
resolved.clear();
return false;
}
if (fs::is_symlink(inputStatus)) {
ASCENDLOGE("Rejected symlink directory: path=%s", path.c_str());
resolved.clear();
return false;
}
std::string canonicalPath;
if (!ResolveCanonicalPath(path, canonicalPath)) {
resolved.clear();
return false;
}
if (!fs::is_directory(fs::path(canonicalPath), error) || error) {
if (error) {
ASCENDLOGE(
"Failed to inspect resolved directory: path=%s error=%s", canonicalPath.c_str(),
error.message().c_str());
} else {
ASCENDLOGE("Resolved path is not a directory: path=%s resolved=%s", path.c_str(), canonicalPath.c_str());
}
resolved.clear();
return false;
}
resolved = canonicalPath;
ASCENDLOGD("Resolved directory: path=%s resolved=%s", path.c_str(), resolved.c_str());
return true;
}
bool FileUtils::ResolveSubdirectory(const std::string& path, const std::string& root, std::string& resolved)
{
ASCENDLOGD("Resolving subdirectory: path=%s root=%s", path.c_str(), root.c_str());
std::string canonical;
if (!ResolveDirectory(path, canonical)) {
resolved.clear();
return false;
}
if (!IsPathWithin(canonical, root)) {
ASCENDLOGE(
"Resolved directory is outside root: path=%s resolved=%s root=%s", path.c_str(), canonical.c_str(),
root.c_str());
resolved.clear();
return false;
}
resolved.swap(canonical);
ASCENDLOGD("Resolved subdirectory: path=%s resolved=%s", path.c_str(), resolved.c_str());
return true;
}
bool FileUtils::CreateDirectories(const std::string& path)
{
ASCENDLOGD("Creating directories: path=%s", path.c_str());
if (path.empty()) {
return true;
}
boost::system::error_code error;
fs::create_directories(fs::path(path), error);
if (error) {
ASCENDLOGE("Failed to create directories: path=%s error=%s", path.c_str(), error.message().c_str());
return false;
}
if (!fs::is_directory(fs::path(path), error) || error) {
if (error) {
ASCENDLOGE("Failed to inspect created directory: path=%s error=%s", path.c_str(), error.message().c_str());
} else {
ASCENDLOGE("Created path is not a directory: path=%s", path.c_str());
}
return false;
}
ASCENDLOGD("Created or verified directories: path=%s", path.c_str());
return true;
}
bool FileUtils::RemoveAll(const std::string& path) noexcept
{
ASCENDLOGD("Removing path recursively: path=%s", path.c_str());
boost::system::error_code error;
(void)fs::remove_all(fs::path(path), error);
if (error) {
ASCENDLOGE("Failed to remove path recursively: path=%s error=%d", path.c_str(), error.value());
return false;
}
ASCENDLOGI("Removed path recursively: path=%s", path.c_str());
return true;
}
bool FileUtils::ReadRegularFile(const std::string& path, uintmax_t maximum, std::vector<uint8_t>& data)
{
ASCENDLOGD("Reading regular file: path=%s maximum=%ju", path.c_str(), maximum);
data.clear();
std::string absolutePath;
if (!MakeAbsolutePath(path, absolutePath)) {
return false;
}
const fs::path normalizedPath = fs::path(absolutePath).lexically_normal();
boost::system::error_code error;
const fs::file_status inputStatus = fs::symlink_status(normalizedPath, error);
if (error || fs::is_symlink(inputStatus) || !fs::is_regular_file(inputStatus)) {
if (error) {
ASCENDLOGE("Failed to inspect regular file: path=%s error=%s", path.c_str(), error.message().c_str());
} else {
ASCENDLOGE("Rejected non-regular file: path=%s", path.c_str());
}
return false;
}
std::string canonicalPath;
if (!ResolveRegularFilePathForRead(normalizedPath.string(), canonicalPath)) {
return false;
}
const uintmax_t fileSize = fs::file_size(fs::path(canonicalPath), error);
if (error) {
ASCENDLOGE("Failed to read regular file size: path=%s error=%s", path.c_str(), error.message().c_str());
return false;
}
if (fileSize > maximum || fileSize > static_cast<uintmax_t>(std::numeric_limits<size_t>::max()) ||
fileSize > static_cast<uintmax_t>(std::numeric_limits<std::streamsize>::max())) {
ASCENDLOGE("Regular file exceeds size limit: path=%s size=%ju maximum=%ju", path.c_str(), fileSize, maximum);
return false;
}
std::ifstream input(canonicalPath.c_str(), std::ios::binary);
if (!input.is_open()) {
ASCENDLOGE("Failed to open regular file: path=%s", path.c_str());
return false;
}
data.resize(static_cast<size_t>(fileSize));
if (!data.empty()) {
input.read(reinterpret_cast<char*>(data.data()), static_cast<std::streamsize>(data.size()));
}
input.close();
if (!input) {
ASCENDLOGE("Failed to read or close regular file: path=%s", path.c_str());
data.clear();
return false;
}
ASCENDLOGD("Read regular file: path=%s bytes=%zu", path.c_str(), data.size());
return true;
}
bool FileUtils::FinalizeOutput(std::ofstream& output)
{
ASCENDLOGD("Finalizing output stream");
output.flush();
const bool writeSucceeded = static_cast<bool>(output);
output.close();
const bool success = writeSucceeded && static_cast<bool>(output);
if (!success) {
ASCENDLOGE("Failed to flush or close output stream");
return false;
}
ASCENDLOGD("Finalized output stream");
return true;
}
bool FileUtils::WriteTextFileAtomically(const std::string& destinationPath, const std::string& text)
{
AtomicFileWriter writer;
return writer.WriteTextToTemporaryFile(destinationPath, text) && writer.ReplaceDestinationAtomically();
}
bool FileUtils::CopyFileAtomically(
const std::string& sourcePath, const std::string& destinationPath, const std::function<bool()>& beforeReplacement)
{
AtomicFileWriter writer;
if (!writer.CopyFileToTemporaryFile(sourcePath, destinationPath)) {
return false;
}
if (beforeReplacement && !beforeReplacement()) {
ASCENDLOGE(
"File replacement cancelled by prerequisite failure: source=%s destination=%s", sourcePath.c_str(),
destinationPath.c_str());
return false;
}
return writer.ReplaceDestinationAtomically();
}
bool FileUtils::CopyFile(const std::string& source, const std::string& destination) noexcept
{
ASCENDLOGD("Copying file: source=%s destination=%s", source.c_str(), destination.c_str());
boost::system::error_code error;
if (!fs::copy_file(fs::path(source), fs::path(destination), fs::copy_options::none, error)) {
if (error) {
ASCENDLOGE(
"Failed to copy file: source=%s destination=%s error=%s", source.c_str(), destination.c_str(),
error.message().c_str());
} else {
ASCENDLOGE("Failed to copy file: source=%s destination=%s", source.c_str(), destination.c_str());
}
return false;
}
ASCENDLOGI("Copied file: source=%s destination=%s", source.c_str(), destination.c_str());
return true;
}
}