Extract PPBR package path validation
This commit is contained in:
129
src/assets/brush_package.cpp
Normal file
129
src/assets/brush_package.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
#include "assets/brush_package.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <utility>
|
||||
|
||||
namespace pp::assets {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] std::uint16_t read_u16_le(std::span<const std::byte> bytes, std::size_t offset) noexcept
|
||||
{
|
||||
const auto lo = static_cast<std::uint16_t>(std::to_integer<unsigned char>(bytes[offset]));
|
||||
const auto hi = static_cast<std::uint16_t>(std::to_integer<unsigned char>(bytes[offset + 1U]));
|
||||
return static_cast<std::uint16_t>(lo | static_cast<std::uint16_t>(hi << 8U));
|
||||
}
|
||||
|
||||
[[nodiscard]] bool is_word_extension(std::string_view value) noexcept
|
||||
{
|
||||
if (value.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const unsigned char ch : value) {
|
||||
if (std::isalnum(ch) == 0 && ch != '_') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
pp::foundation::Status validate_ppbr_header(
|
||||
std::string_view magic,
|
||||
std::uint16_t major,
|
||||
std::uint16_t minor) noexcept
|
||||
{
|
||||
if (magic != "PPBR") {
|
||||
return pp::foundation::Status::invalid_argument("PPBR header magic is invalid");
|
||||
}
|
||||
|
||||
// DEBT-0049: preserve legacy version acceptance until PPBR compatibility fixtures exist.
|
||||
if (major != ppbr_legacy_major_version && minor != ppbr_legacy_minor_version) {
|
||||
return pp::foundation::Status::invalid_argument("PPBR version is unsupported");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpbrHeader> parse_ppbr_header(std::span<const std::byte> bytes) noexcept
|
||||
{
|
||||
if (bytes.size() < ppbr_header_size) {
|
||||
return pp::foundation::Result<PpbrHeader>::failure(
|
||||
pp::foundation::Status::out_of_range("PPBR header is truncated"));
|
||||
}
|
||||
|
||||
const std::string_view magic(reinterpret_cast<const char*>(bytes.data()), 4U);
|
||||
const auto major = read_u16_le(bytes, 4U);
|
||||
const auto minor = read_u16_le(bytes, 6U);
|
||||
const auto status = validate_ppbr_header(magic, major, minor);
|
||||
if (!status.ok()) {
|
||||
return pp::foundation::Result<PpbrHeader>::failure(status);
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpbrHeader>::success(PpbrHeader {
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
});
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::string> normalize_ppbr_export_path(std::string_view requested_path)
|
||||
{
|
||||
if (requested_path.empty()) {
|
||||
return pp::foundation::Result<std::string>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPBR export path must not be empty"));
|
||||
}
|
||||
|
||||
std::string path(requested_path);
|
||||
if (requested_path.find(".ppbr") == std::string_view::npos) {
|
||||
path += ".ppbr";
|
||||
}
|
||||
|
||||
return pp::foundation::Result<std::string>::success(std::move(path));
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpbrExportPaths> plan_ppbr_export_paths(
|
||||
std::string_view requested_path,
|
||||
std::string_view override_data_directory,
|
||||
bool export_data,
|
||||
PpbrDataDirectoryPolicy data_directory_policy)
|
||||
{
|
||||
const auto normalized = normalize_ppbr_export_path(requested_path);
|
||||
if (!normalized) {
|
||||
return pp::foundation::Result<PpbrExportPaths>::failure(normalized.status());
|
||||
}
|
||||
|
||||
const auto slash = normalized.value().find_last_of("/\\");
|
||||
if (slash == std::string::npos || slash + 1U >= normalized.value().size()) {
|
||||
return pp::foundation::Result<PpbrExportPaths>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPBR export path must include a directory and file name"));
|
||||
}
|
||||
|
||||
const auto dot = normalized.value().find_last_of('.');
|
||||
if (dot == std::string::npos || dot <= slash + 1U || dot + 1U >= normalized.value().size()) {
|
||||
return pp::foundation::Result<PpbrExportPaths>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPBR export path must include a file extension"));
|
||||
}
|
||||
|
||||
PpbrExportPaths paths;
|
||||
paths.package_path = normalized.value();
|
||||
paths.directory = normalized.value().substr(0, slash);
|
||||
paths.stem = normalized.value().substr(slash + 1U, dot - slash - 1U);
|
||||
paths.extension = normalized.value().substr(dot + 1U);
|
||||
if (!is_word_extension(paths.extension)) {
|
||||
return pp::foundation::Result<PpbrExportPaths>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPBR export path extension contains unsupported characters"));
|
||||
}
|
||||
|
||||
if (data_directory_policy == PpbrDataDirectoryPolicy::override_directory) {
|
||||
paths.data_directory = std::string(override_data_directory) + "/" + paths.stem + "_data";
|
||||
} else {
|
||||
paths.data_directory = paths.directory + "/" + paths.stem + "_data";
|
||||
}
|
||||
paths.data_directory_enabled = export_data && !paths.data_directory.empty();
|
||||
|
||||
return pp::foundation::Result<PpbrExportPaths>::success(std::move(paths));
|
||||
}
|
||||
|
||||
} // namespace pp::assets
|
||||
53
src/assets/brush_package.h
Normal file
53
src/assets/brush_package.h
Normal file
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
constexpr std::size_t ppbr_header_size = 8;
|
||||
constexpr std::uint16_t ppbr_legacy_major_version = 0;
|
||||
constexpr std::uint16_t ppbr_legacy_minor_version = 1;
|
||||
|
||||
enum class PpbrDataDirectoryPolicy {
|
||||
next_to_package,
|
||||
override_directory,
|
||||
};
|
||||
|
||||
struct PpbrHeader {
|
||||
std::uint16_t major = 0;
|
||||
std::uint16_t minor = 0;
|
||||
};
|
||||
|
||||
struct PpbrExportPaths {
|
||||
std::string package_path;
|
||||
std::string directory;
|
||||
std::string stem;
|
||||
std::string extension;
|
||||
std::string data_directory;
|
||||
bool data_directory_enabled = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Status validate_ppbr_header(
|
||||
std::string_view magic,
|
||||
std::uint16_t major,
|
||||
std::uint16_t minor) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpbrHeader> parse_ppbr_header(
|
||||
std::span<const std::byte> bytes) noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::string> normalize_ppbr_export_path(
|
||||
std::string_view requested_path);
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpbrExportPaths> plan_ppbr_export_paths(
|
||||
std::string_view requested_path,
|
||||
std::string_view override_data_directory,
|
||||
bool export_data,
|
||||
PpbrDataDirectoryPolicy data_directory_policy);
|
||||
|
||||
} // namespace pp::assets
|
||||
Reference in New Issue
Block a user