Add PPI header recognition tests
This commit is contained in:
@@ -75,7 +75,8 @@ target_link_libraries(pp_foundation
|
||||
pp_project_warnings)
|
||||
|
||||
add_library(pp_assets STATIC
|
||||
src/assets/image_format.cpp)
|
||||
src/assets/image_format.cpp
|
||||
src/assets/ppi_header.cpp)
|
||||
target_include_directories(pp_assets
|
||||
PUBLIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
|
||||
@@ -78,7 +78,7 @@ Known local toolchain state:
|
||||
`platform-build` automation wrapper for `pp_foundation`, `pp_assets`,
|
||||
`pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`,
|
||||
`pp_ui_core`, `pano_cli`, and their current headless test binaries,
|
||||
including layout XML parse coverage.
|
||||
including PPI header and layout XML parse coverage.
|
||||
- `vcpkg` is not on PATH yet; see DEBT-0007.
|
||||
|
||||
Known warnings after the current CMake app build:
|
||||
|
||||
@@ -31,6 +31,7 @@ agent or engineer to remove them without reconstructing context from chat.
|
||||
| DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document model but is not yet wired to legacy `Canvas`, PPI load/save, selection masks, or undo/redo | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2` | Legacy document behavior is represented by `pp_document` tests and the app consumes it through a boundary/facade |
|
||||
| DEBT-0011 | Open | Modernization | `package-smoke` validates the Windows CMake app artifact only, not AppX/APK/Apple/WebGL package outputs | Platform package targets are not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Package-smoke covers Windows AppX, Android APK variants, Apple bundles, and WebGL output where local toolchains are present |
|
||||
| DEBT-0012 | Open | Modernization | `pp_vendor_tinyxml2` compiles the retained vendored tinyxml2 copy for `pp_ui_core` layout parsing | vcpkg is not validated yet, but layout parsing needs a structured XML parser now | `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | Replace with vcpkg tinyxml2 target once desktop and mobile triplets are validated |
|
||||
| DEBT-0013 | Open | Modernization | `pp_assets` and `pano_cli inspect-project` recognize only the fixed PPI header, not thumbnail bytes or the project body | Full PPI parsing requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_ppi_header_tests` | Full PPI load/save fixtures cover thumbnail, layers, frames, metadata, corrupt payloads, and round-trip compatibility |
|
||||
|
||||
## Closed Debt
|
||||
|
||||
|
||||
@@ -245,8 +245,9 @@ Status: in progress. `tests/` exists, `desktop-fast` runs headlessly, and
|
||||
PowerShell/bash wrappers exist for
|
||||
configure/build/test/analyze/platform-build/package-smoke. `pano_cli` exists
|
||||
with JSON automation commands for creating a `pp_document` model and
|
||||
inspecting image signatures; full document/app integration is debt-tracked as
|
||||
DEBT-0010.
|
||||
inspecting image signatures, PPI headers, and layout XML; full document/app
|
||||
integration is debt-tracked as DEBT-0010 and full PPI body parsing is
|
||||
debt-tracked as DEBT-0013.
|
||||
|
||||
Implementation tasks:
|
||||
|
||||
@@ -304,7 +305,8 @@ boundary/overread tests. It also owns strict decimal `uint32` parsing used by
|
||||
`pano_cli`, with rejection tests for empty, signed, mixed, and overflowing
|
||||
input. A deterministic `TraceRecorder` now records component/name/thread/frame
|
||||
and stroke timing spans with invalid-end tests. `pp_assets` has started with
|
||||
PNG/JPEG signature detection and corrupt/truncated/unsupported tests.
|
||||
PNG/JPEG signature detection plus PPI header recognition, with
|
||||
corrupt/truncated/unsupported tests.
|
||||
`pp_paint` has started with CPU reference math for the five current shader
|
||||
blend modes. `pp_document` has started with a pure canvas/layer/frame model
|
||||
and layer/frame invariant tests. `pp_renderer_api` has started with renderer-neutral
|
||||
@@ -518,7 +520,7 @@ Last verified on 2026-06-01:
|
||||
|
||||
```powershell
|
||||
cmake --preset windows-msvc-default
|
||||
cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_paint_blend_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pano_cli PanoPainter
|
||||
cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_paint_blend_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pano_cli PanoPainter
|
||||
ctest --preset desktop-fast --build-config Debug
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug
|
||||
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli
|
||||
@@ -533,6 +535,7 @@ Results:
|
||||
- `pp_foundation_parse_tests` passed.
|
||||
- `pp_foundation_trace_tests` passed.
|
||||
- `pp_assets_image_format_tests` passed.
|
||||
- `pp_assets_ppi_header_tests` passed.
|
||||
- `pp_paint_blend_tests` passed.
|
||||
- `pp_document_tests` passed.
|
||||
- `pp_renderer_api_tests` passed.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string[]]$Presets = @("android-arm64"),
|
||||
[string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_parse_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_paint_blend_tests", "pp_document_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests")
|
||||
[string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_parse_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_assets_ppi_header_tests", "pp_paint_blend_tests", "pp_document_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests")
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
@@ -3,7 +3,7 @@ set -u
|
||||
|
||||
preset="${1:-android-arm64}"
|
||||
shift || true
|
||||
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_paint_blend_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}"
|
||||
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_paint_blend_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}"
|
||||
start="$(date +%s)"
|
||||
|
||||
cmake --preset "$preset"
|
||||
|
||||
73
src/assets/ppi_header.cpp
Normal file
73
src/assets/ppi_header.cpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#include "assets/ppi_header.h"
|
||||
|
||||
#include "foundation/binary_stream.h"
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<std::uint32_t> read_u32(pp::foundation::ByteReader& reader) noexcept
|
||||
{
|
||||
return reader.read_u32_le();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(std::span<const std::byte> bytes) noexcept
|
||||
{
|
||||
if (bytes.size() < ppi_header_size) {
|
||||
return pp::foundation::Result<PpiHeaderInfo>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI header is truncated"));
|
||||
}
|
||||
|
||||
pp::foundation::ByteReader reader(bytes.subspan(0, ppi_header_size));
|
||||
const auto magic = reader.read_bytes(4);
|
||||
if (!magic || magic.value()[0] != std::byte { 'P' } || magic.value()[1] != std::byte { 'P' }
|
||||
|| magic.value()[2] != std::byte { 'I' } || magic.value()[3] != std::byte { 0 }) {
|
||||
return pp::foundation::Result<PpiHeaderInfo>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI header magic is invalid"));
|
||||
}
|
||||
|
||||
PpiHeaderInfo info;
|
||||
const auto doc_major = read_u32(reader);
|
||||
const auto doc_minor = read_u32(reader);
|
||||
const auto soft_major = read_u32(reader);
|
||||
const auto soft_minor = read_u32(reader);
|
||||
const auto soft_fix = read_u32(reader);
|
||||
const auto soft_build = read_u32(reader);
|
||||
const auto thumb_width = read_u32(reader);
|
||||
const auto thumb_height = read_u32(reader);
|
||||
const auto thumb_components = read_u32(reader);
|
||||
if (!doc_major || !doc_minor || !soft_major || !soft_minor || !soft_fix || !soft_build
|
||||
|| !thumb_width || !thumb_height || !thumb_components) {
|
||||
return pp::foundation::Result<PpiHeaderInfo>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI header is truncated"));
|
||||
}
|
||||
|
||||
info.document_version = { doc_major.value(), doc_minor.value() };
|
||||
info.software_version = {
|
||||
soft_major.value(),
|
||||
soft_minor.value(),
|
||||
soft_fix.value(),
|
||||
soft_build.value(),
|
||||
};
|
||||
info.thumbnail = {
|
||||
thumb_width.value(),
|
||||
thumb_height.value(),
|
||||
thumb_components.value(),
|
||||
};
|
||||
|
||||
if (info.document_version.major != 0 || info.document_version.minor < 1) {
|
||||
return pp::foundation::Result<PpiHeaderInfo>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI document version is unsupported"));
|
||||
}
|
||||
|
||||
if (info.thumbnail.width != 128 || info.thumbnail.height != 128 || info.thumbnail.components != 4) {
|
||||
return pp::foundation::Result<PpiHeaderInfo>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI thumbnail descriptor is invalid"));
|
||||
}
|
||||
|
||||
return pp::foundation::Result<PpiHeaderInfo>::success(info);
|
||||
}
|
||||
|
||||
}
|
||||
40
src/assets/ppi_header.h
Normal file
40
src/assets/ppi_header.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
|
||||
namespace pp::assets {
|
||||
|
||||
constexpr std::size_t ppi_header_size = 40;
|
||||
|
||||
struct PpiVersion {
|
||||
std::uint32_t major = 0;
|
||||
std::uint32_t minor = 0;
|
||||
};
|
||||
|
||||
struct PpiSoftwareVersion {
|
||||
std::uint32_t major = 0;
|
||||
std::uint32_t minor = 0;
|
||||
std::uint32_t fix = 0;
|
||||
std::uint32_t build = 0;
|
||||
};
|
||||
|
||||
struct PpiThumbnailInfo {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::uint32_t components = 0;
|
||||
};
|
||||
|
||||
struct PpiHeaderInfo {
|
||||
PpiVersion document_version;
|
||||
PpiSoftwareVersion software_version;
|
||||
PpiThumbnailInfo thumbnail;
|
||||
};
|
||||
|
||||
[[nodiscard]] pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(
|
||||
std::span<const std::byte> bytes) noexcept;
|
||||
|
||||
}
|
||||
@@ -46,6 +46,16 @@ add_test(NAME pp_assets_image_format_tests COMMAND pp_assets_image_format_tests)
|
||||
set_tests_properties(pp_assets_image_format_tests PROPERTIES
|
||||
LABELS "assets;desktop-fast")
|
||||
|
||||
add_executable(pp_assets_ppi_header_tests
|
||||
assets/ppi_header_tests.cpp)
|
||||
target_link_libraries(pp_assets_ppi_header_tests PRIVATE
|
||||
pp_assets
|
||||
pp_test_harness)
|
||||
|
||||
add_test(NAME pp_assets_ppi_header_tests COMMAND pp_assets_ppi_header_tests)
|
||||
set_tests_properties(pp_assets_ppi_header_tests PROPERTIES
|
||||
LABELS "assets;desktop-fast")
|
||||
|
||||
add_executable(pp_paint_blend_tests
|
||||
paint/blend_tests.cpp)
|
||||
target_link_libraries(pp_paint_blend_tests PRIVATE
|
||||
|
||||
108
tests/assets/ppi_header_tests.cpp
Normal file
108
tests/assets/ppi_header_tests.cpp
Normal file
@@ -0,0 +1,108 @@
|
||||
#include "assets/ppi_header.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
using pp::assets::parse_ppi_header;
|
||||
using pp::assets::ppi_header_size;
|
||||
using pp::foundation::StatusCode;
|
||||
|
||||
namespace {
|
||||
|
||||
void append_u32(std::vector<std::byte>& bytes, std::uint32_t value)
|
||||
{
|
||||
bytes.push_back(static_cast<std::byte>(value & 0xffU));
|
||||
bytes.push_back(static_cast<std::byte>((value >> 8U) & 0xffU));
|
||||
bytes.push_back(static_cast<std::byte>((value >> 16U) & 0xffU));
|
||||
bytes.push_back(static_cast<std::byte>((value >> 24U) & 0xffU));
|
||||
}
|
||||
|
||||
std::vector<std::byte> valid_header()
|
||||
{
|
||||
std::vector<std::byte> bytes {
|
||||
std::byte { 'P' },
|
||||
std::byte { 'P' },
|
||||
std::byte { 'I' },
|
||||
std::byte { 0 },
|
||||
};
|
||||
append_u32(bytes, 0);
|
||||
append_u32(bytes, 4);
|
||||
append_u32(bytes, 0);
|
||||
append_u32(bytes, 2);
|
||||
append_u32(bytes, 3);
|
||||
append_u32(bytes, 1024);
|
||||
append_u32(bytes, 128);
|
||||
append_u32(bytes, 128);
|
||||
append_u32(bytes, 4);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void parses_legacy_ppi_header(pp::tests::Harness& h)
|
||||
{
|
||||
const auto bytes = valid_header();
|
||||
const auto header = parse_ppi_header(bytes);
|
||||
|
||||
PP_EXPECT(h, bytes.size() == ppi_header_size);
|
||||
PP_EXPECT(h, header.ok());
|
||||
PP_EXPECT(h, header.value().document_version.major == 0U);
|
||||
PP_EXPECT(h, header.value().document_version.minor == 4U);
|
||||
PP_EXPECT(h, header.value().software_version.fix == 3U);
|
||||
PP_EXPECT(h, header.value().software_version.build == 1024U);
|
||||
PP_EXPECT(h, header.value().thumbnail.width == 128U);
|
||||
PP_EXPECT(h, header.value().thumbnail.height == 128U);
|
||||
PP_EXPECT(h, header.value().thumbnail.components == 4U);
|
||||
}
|
||||
|
||||
void rejects_truncated_invalid_magic_and_bad_thumbnail(pp::tests::Harness& h)
|
||||
{
|
||||
auto truncated = valid_header();
|
||||
truncated.pop_back();
|
||||
|
||||
auto bad_magic = valid_header();
|
||||
bad_magic[0] = std::byte { 'X' };
|
||||
|
||||
auto bad_thumb = valid_header();
|
||||
bad_thumb[32] = std::byte { 64 };
|
||||
|
||||
const auto truncated_result = parse_ppi_header(truncated);
|
||||
const auto magic_result = parse_ppi_header(bad_magic);
|
||||
const auto thumb_result = parse_ppi_header(bad_thumb);
|
||||
|
||||
PP_EXPECT(h, !truncated_result.ok());
|
||||
PP_EXPECT(h, truncated_result.status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !magic_result.ok());
|
||||
PP_EXPECT(h, magic_result.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !thumb_result.ok());
|
||||
PP_EXPECT(h, thumb_result.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
void rejects_unsupported_document_versions(pp::tests::Harness& h)
|
||||
{
|
||||
auto bad_major = valid_header();
|
||||
bad_major[4] = std::byte { 1 };
|
||||
|
||||
auto bad_minor = valid_header();
|
||||
bad_minor[8] = std::byte { 0 };
|
||||
|
||||
const auto major_result = parse_ppi_header(bad_major);
|
||||
const auto minor_result = parse_ppi_header(bad_minor);
|
||||
|
||||
PP_EXPECT(h, !major_result.ok());
|
||||
PP_EXPECT(h, major_result.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !minor_result.ok());
|
||||
PP_EXPECT(h, minor_result.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
pp::tests::Harness harness;
|
||||
harness.run("parses_legacy_ppi_header", parses_legacy_ppi_header);
|
||||
harness.run("rejects_truncated_invalid_magic_and_bad_thumbnail", rejects_truncated_invalid_magic_and_bad_thumbnail);
|
||||
harness.run("rejects_unsupported_document_versions", rejects_unsupported_document_versions);
|
||||
return harness.finish();
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "assets/image_format.h"
|
||||
#include "assets/ppi_header.h"
|
||||
#include "document/document.h"
|
||||
#include "foundation/parse.h"
|
||||
#include "foundation/result.h"
|
||||
@@ -28,6 +29,10 @@ struct ParseLayoutArgs {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct InspectProjectArgs {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
void print_error(std::string_view command, std::string_view message)
|
||||
{
|
||||
std::cout << "{\"ok\":false,\"command\":\"" << command
|
||||
@@ -40,6 +45,7 @@ void print_help()
|
||||
<< "pano_cli commands:\n"
|
||||
<< " create-document --width N --height N [--layers N]\n"
|
||||
<< " inspect-image --path FILE\n"
|
||||
<< " inspect-project --path FILE\n"
|
||||
<< " parse-layout --path FILE\n"
|
||||
<< " --help\n";
|
||||
}
|
||||
@@ -166,6 +172,68 @@ int inspect_image(int argc, char** argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
pp::foundation::Status parse_inspect_project_args(int argc, char** argv, InspectProjectArgs& args)
|
||||
{
|
||||
for (int i = 2; i < argc; ++i) {
|
||||
const std::string_view key(argv[i]);
|
||||
if (key == "--path") {
|
||||
if (i + 1 >= argc) {
|
||||
return pp::foundation::Status::invalid_argument("missing value for option");
|
||||
}
|
||||
|
||||
args.path = argv[++i];
|
||||
} else {
|
||||
return pp::foundation::Status::invalid_argument("unknown option");
|
||||
}
|
||||
}
|
||||
|
||||
if (args.path.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("path must not be empty");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
int inspect_project(int argc, char** argv)
|
||||
{
|
||||
InspectProjectArgs args;
|
||||
const auto status = parse_inspect_project_args(argc, argv, args);
|
||||
if (!status.ok()) {
|
||||
print_error("inspect-project", status.message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
std::ifstream stream(args.path, std::ios::binary);
|
||||
if (!stream) {
|
||||
print_error("inspect-project", "project file could not be opened");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const std::vector<char> chars {
|
||||
std::istreambuf_iterator<char>(stream),
|
||||
std::istreambuf_iterator<char>()
|
||||
};
|
||||
const auto* data = reinterpret_cast<const std::byte*>(chars.data());
|
||||
const auto header = pp::assets::parse_ppi_header(std::span<const std::byte>(data, chars.size()));
|
||||
if (!header) {
|
||||
print_error("inspect-project", header.status().message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
std::cout << "{\"ok\":true,\"command\":\"inspect-project\""
|
||||
<< ",\"documentVersion\":\"" << header.value().document_version.major
|
||||
<< "." << header.value().document_version.minor << "\""
|
||||
<< ",\"softwareVersion\":\"" << header.value().software_version.major
|
||||
<< "." << header.value().software_version.minor
|
||||
<< "." << header.value().software_version.fix
|
||||
<< "." << header.value().software_version.build << "\""
|
||||
<< ",\"thumbnail\":{\"width\":" << header.value().thumbnail.width
|
||||
<< ",\"height\":" << header.value().thumbnail.height
|
||||
<< ",\"components\":" << header.value().thumbnail.components
|
||||
<< "}}\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
pp::foundation::Status parse_layout_args(int argc, char** argv, ParseLayoutArgs& args)
|
||||
{
|
||||
for (int i = 2; i < argc; ++i) {
|
||||
@@ -243,6 +311,10 @@ int main(int argc, char** argv)
|
||||
return inspect_image(argc, argv);
|
||||
}
|
||||
|
||||
if (command == "inspect-project") {
|
||||
return inspect_project(argc, argv);
|
||||
}
|
||||
|
||||
if (command == "parse-layout") {
|
||||
return parse_layout(argc, argv);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user