Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 119 additions & 42 deletions app/src/component_library.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,75 @@
#include "component_type_registry.h"
#include "logging_core.h"
#include <filesystem>
#include <optional>

#include "amplifier_engine.h"
#include "component_interface.h"
#include "component_registry.h"
#include "node_graph_engine.h"
#include <fstream>

namespace {

namespace fs = std::filesystem;

// --- Data-file path containment (S1) ----------------------------------------
// Mirrors extension_manifest.cpp's resolveWithinRoot discipline: a library
// data-file path is only honored if its canonical form stays inside the
// library JSON file's directory. Absolute paths outside it, '..' traversal,
// and unresolvable paths are skipped instead of reading arbitrary files.

bool containsParentTraversal(const fs::path &path) {
for (const auto &part : path) {
if (part == "..")
return true;
}
return false;
}

bool pathWithinRoot(const fs::path &root, const fs::path &candidate) {
std::error_code ec;
const fs::path canonical_root = fs::weakly_canonical(root, ec);
if (ec)
return false;

ec.clear();
const fs::path canonical_candidate = fs::weakly_canonical(candidate, ec);
if (ec)
return false;

auto root_it = canonical_root.begin();
auto candidate_it = canonical_candidate.begin();
for (; root_it != canonical_root.end(); ++root_it, ++candidate_it) {
if (candidate_it == canonical_candidate.end() || *root_it != *candidate_it)
return false;
}
return true;
}

// Resolve a data-file path from a library definition against the library JSON
// file's directory. Returns the canonical path on success, or nullopt when the
// entry must be skipped.
std::optional<fs::path> resolveDataFilePath(const fs::path &json_dir, const std::string &input) {
const fs::path p(input);
if (p.empty())
return std::nullopt;
if (containsParentTraversal(p))
return std::nullopt;

const fs::path candidate = p.is_absolute() ? p : (json_dir / p);
if (!pathWithinRoot(json_dir, candidate))
return std::nullopt;

std::error_code ec;
const fs::path resolved = fs::weakly_canonical(candidate, ec);
if (ec)
return std::nullopt;
return resolved;
}

} // namespace

std::vector<ValidationIssue> ComponentLibrary::validate(const std::string &type,
const nlohmann::json &parameters) const {
std::vector<ValidationIssue> issues;
Expand Down Expand Up @@ -88,39 +150,41 @@ void ComponentLibrary::loadFile(const std::string &filepath) {
nlohmann::json j;
try {
ifs >> j;
} catch (const nlohmann::json::parse_error &e) {
LOG_WARN("ComponentLibrary: JSON parse error in %s: %s", filepath.c_str(), e.what());
return;
}

if (!j.contains("type") || !j.contains("part_number") || !j.contains("parameters")) {
LOG_WARN("ComponentLibrary: missing required fields in %s", filepath.c_str());
return;
}
if (!j.contains("type") || !j.contains("part_number") || !j.contains("parameters")) {
LOG_WARN("ComponentLibrary: missing required fields in %s", filepath.c_str());
return;
}

ComponentDefinition def;
def.schema_version = j.value("schema_version", 1);
def.type = j["type"].get<std::string>();
def.part_number = j["part_number"].get<std::string>();
def.manufacturer = j.value("manufacturer", "");
def.description = j.value("description", "");
def.parameters = j["parameters"];
def.test_conditions = j.value("test_conditions", nlohmann::json::object());
def.notes = j.value("notes", "");
def.source_path = filepath;
def.issues = validate(def.type, def.parameters);

ComponentDefinition def;
def.schema_version = j.value("schema_version", 1);
def.type = j["type"].get<std::string>();
def.part_number = j["part_number"].get<std::string>();
def.manufacturer = j.value("manufacturer", "");
def.description = j.value("description", "");
def.parameters = j["parameters"];
def.test_conditions = j.value("test_conditions", nlohmann::json::object());
def.notes = j.value("notes", "");
def.source_path = filepath;
def.issues = validate(def.type, def.parameters);

// Parse data_files array if present
if (j.contains("data_files") && j["data_files"].is_array()) {
for (const auto &df : j["data_files"]) {
if (df.contains("type") && df.contains("path")) {
def.data_files.push_back(
{df["type"].get<std::string>(), df["path"].get<std::string>()});
// Parse data_files array if present
if (j.contains("data_files") && j["data_files"].is_array()) {
for (const auto &df : j["data_files"]) {
if (df.contains("type") && df.contains("path")) {
def.data_files.push_back(
{df["type"].get<std::string>(), df["path"].get<std::string>()});
}
}
}
}

m_definitions.push_back(std::move(def));
m_definitions.push_back(std::move(def));
} catch (const nlohmann::json::exception &e) {
// Covers parse_error AND type_error (e.g. a required field present but
// wrong-typed). A malformed library entry is skipped, not fatal.
LOG_WARN("ComponentLibrary: invalid JSON in %s: %s", filepath.c_str(), e.what());
return;
}
}

std::vector<const ComponentDefinition *> ComponentLibrary::all() const {
Expand All @@ -136,10 +200,16 @@ void ComponentLibrary::scan(const std::string &directory) {
namespace fs = std::filesystem;
if (!fs::exists(directory))
return;
for (const auto &entry : fs::recursive_directory_iterator(directory)) {
if (entry.is_regular_file() && entry.path().extension() == ".json") {
loadFile(entry.path().string());
try {
for (const auto &entry : fs::recursive_directory_iterator(directory)) {
if (entry.is_regular_file() && entry.path().extension() == ".json") {
loadFile(entry.path().string());
}
}
} catch (const fs::filesystem_error &e) {
// An unreadable subtree (e.g. permission denied) must not abort the
// scan of the remaining roots.
LOG_WARN("ComponentLibrary: skipping unreadable directory in scan: %s", e.what());
}
}

Expand Down Expand Up @@ -172,18 +242,25 @@ IComponentEngine *ComponentLibrary::instantiate(const ComponentDefinition &def,
if (df.type == "s_parameters" && amp) {
std::filesystem::path json_dir =
std::filesystem::path(def.source_path).parent_path();
std::filesystem::path sparam_path = json_dir / df.path;
amp->setSParamFilepath(sparam_path.string());

if (amp->sparamLoaded()) {
LOG_INFO("Loaded S-param file for %s: %s", def.part_number.c_str(),
sparam_path.string().c_str());
} else {
LOG_WARN("Failed to load S-param file for %s: %s (falling back to "
"single-point params)",
def.part_number.c_str(), sparam_path.string().c_str());
// S1: the data-file path must stay within the library JSON
// file's directory; absolute paths and '..' escapes are
// rejected (skipped) rather than reading arbitrary files.
if (auto sparam_path = resolveDataFilePath(json_dir, df.path)) {
amp->setSParamFilepath(sparam_path->string());

if (amp->sparamLoaded()) {
LOG_INFO("Loaded S-param file for %s: %s", def.part_number.c_str(),
sparam_path->string().c_str());
} else {
LOG_WARN("Failed to load S-param file for %s: %s (falling back to "
"single-point params)",
def.part_number.c_str(), sparam_path->string().c_str());
}
break; // Only load first S-param file
}
break; // Only load first S-param file
LOG_WARN("ComponentLibrary: rejecting S-param data file path '%s' for %s "
"(must stay within the library directory)",
df.path.c_str(), def.part_number.c_str());
}
}
}
Expand Down
Loading