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
1 change: 1 addition & 0 deletions include/naab/lockfile.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ struct Lockfile {
std::string naab_version; // NAAb version string (e.g., "0.7.0")
std::string platform; // "linux/arm64", "darwin/arm64", etc.
std::vector<LockfileEntry> runtimes;
bool parse_failed = false; // True if lockfile JSON was malformed

// Load from file. Returns empty Lockfile if file doesn't exist.
static Lockfile load(const std::string& path);
Expand Down
36 changes: 35 additions & 1 deletion src/cli/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2851,7 +2851,41 @@ int main(int argc, char** argv) {
} else {
fmt::print("✗ {} needs formatting\n", filename);
if (show_diff) {
fmt::print("\nFormatted output:\n{}\n", formatted);
// Generate unified diff between source and formatted
std::istringstream src_stream(source);
std::istringstream fmt_stream(formatted);
std::vector<std::string> src_lines, fmt_lines;
std::string line;
while (std::getline(src_stream, line)) src_lines.push_back(line);
while (std::getline(fmt_stream, line)) fmt_lines.push_back(line);
fmt::print("\n--- {}\n+++ {} (formatted)\n", filename, filename);
size_t si = 0, fi = 0;
while (si < src_lines.size() || fi < fmt_lines.size()) {
if (si < src_lines.size() && fi < fmt_lines.size() &&
src_lines[si] == fmt_lines[fi]) {
fmt::print(" {}\n", src_lines[si]);
si++; fi++;
} else {
// Find next matching line
size_t match_s = si, match_f = fi;
bool found = false;
for (size_t d = 1; d < 10 && !found; d++) {
for (size_t ds = 0; ds <= d && !found; ds++) {
size_t df = d - ds;
if (si + ds < src_lines.size() && fi + df < fmt_lines.size() &&
src_lines[si + ds] == fmt_lines[fi + df]) {
match_s = si + ds; match_f = fi + df; found = true;
}
}
}
while (si < match_s) { fmt::print("-{}\n", src_lines[si++]); }
while (fi < match_f) { fmt::print("+{}\n", fmt_lines[fi++]); }
if (!found) {
if (si < src_lines.size()) fmt::print("-{}\n", src_lines[si++]);
if (fi < fmt_lines.size()) fmt::print("+{}\n", fmt_lines[fi++]);
}
}
}
}
return 1;
}
Expand Down
2 changes: 2 additions & 0 deletions src/debugger/debugger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ std::map<std::string, interpreter::NaabVal> Debugger::listGlobalVariables() {
int Debugger::addWatch(const std::string& expression) {
int id = next_watch_id_++;
watches_[id] = expression;
// Note: watch expressions are stored but evaluation is not yet implemented.
// evaluateWatches() will return an error for each watch until this is completed.
return id;
}

Expand Down
48 changes: 48 additions & 0 deletions src/runtime/governance_checks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ static std::string normalizeUnicode(const std::string& code) {
// U+00AD SOFT HYPHEN
if (cp == 0x00AD) { i += 1; continue; }

// Superscript digits in Latin-1 Supplement
if (cp == 0x00B2) { result += '2'; i += 1; continue; } // ²
if (cp == 0x00B3) { result += '3'; i += 1; continue; } // ³
if (cp == 0x00B9) { result += '1'; i += 1; continue; } // ¹

// Cyrillic confusables (U+0400-U+04FF)
char replacement = 0;
switch (cp) {
Expand Down Expand Up @@ -270,6 +275,46 @@ static std::string normalizeUnicode(const std::string& code) {
continue;
}

// Superscript letters/digits (U+2070-U+207F)
// U+2070=⁰, U+00B9=¹ (2-byte), U+00B2=² (2-byte), U+00B3=³ (2-byte)
// U+2074=⁴, U+2075=⁵, U+2076=⁶, U+2077=⁷, U+2078=⁸, U+2079=⁹
// U+207F=ⁿ
if (cp >= 0x2074 && cp <= 0x2079) {
result += static_cast<char>('0' + (cp - 0x2070));
i += 2; continue;
}
if (cp == 0x2070) { result += '0'; i += 2; continue; }
if (cp == 0x207F) { result += 'n'; i += 2; continue; }

// Subscript letters/digits (U+2080-U+209C)
// U+2080=₀..U+2089=₉, U+2090=ₐ, U+2091=ₑ, U+2092=ₒ,
// U+2093=ₓ, U+2094=ₔ, U+2095=ₕ, U+2096=ₖ, U+2097=ₗ,
// U+2098=ₘ, U+2099=ₙ, U+209A=ₚ, U+209B=ₛ, U+209C=ₜ
if (cp >= 0x2080 && cp <= 0x2089) {
result += static_cast<char>('0' + (cp - 0x2080));
i += 2; continue;
}
{
char sub_rep = 0;
switch (cp) {
case 0x2090: sub_rep = 'a'; break; // ₐ
case 0x2091: sub_rep = 'e'; break; // ₑ
case 0x2092: sub_rep = 'o'; break; // ₒ
case 0x2093: sub_rep = 'x'; break; // ₓ
case 0x2094: sub_rep = 'e'; break; // ₔ (schwa → e)
case 0x2095: sub_rep = 'h'; break; // ₕ
case 0x2096: sub_rep = 'k'; break; // ₖ
case 0x2097: sub_rep = 'l'; break; // ₗ
case 0x2098: sub_rep = 'm'; break; // ₘ
case 0x2099: sub_rep = 'n'; break; // ₙ
case 0x209A: sub_rep = 'p'; break; // ₚ
case 0x209B: sub_rep = 's'; break; // ₛ
case 0x209C: sub_rep = 't'; break; // ₜ
default: break;
}
if (sub_rep) { result += sub_rep; i += 2; continue; }
}

// Fullwidth Latin letters (U+FF21-FF3A = A-Z, U+FF41-FF5A = a-z)
if (cp >= 0xFF21 && cp <= 0xFF3A) {
result += static_cast<char>('A' + (cp - 0xFF21));
Expand Down Expand Up @@ -5276,6 +5321,9 @@ std::string GovernanceEngine::checkCodeInjection(const std::string& language,
pats.push_back("os\\.system\\s*\\(");
pats.push_back("subprocess\\.(?:call|Popen|run|check_output|check_call)\\s*\\(");
pats.push_back("ctypes\\.(?:CDLL|cdll)\\s*\\(");
// Block dangerous imports (bare import grants access to system calls)
pats.push_back("\\bimport\\s+(?:os|subprocess|shutil|ctypes|pty|commands)\\b");
pats.push_back("\\bfrom\\s+(?:os|subprocess|shutil|ctypes|pty|commands)\\b");
} else if (language == "go" || language == "golang") {
pats.push_back("exec\\.Command\\s*\\(");
} else if (language == "rust") {
Expand Down
24 changes: 15 additions & 9 deletions src/runtime/governance_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -499,17 +499,17 @@ static void loadFromJson(const nlohmann::json& j, GovernanceRules& rules_) {
}

// --- V3.0 Expanded Sections ---
if (j.contains("version"))
if (j.contains("version") && j["version"].is_string())
rules_.version = j["version"].get<std::string>();
if (j.contains("extends"))
if (j.contains("extends") && j["extends"].is_string())
rules_.extends_path = j["extends"].get<std::string>();
if (j.contains("description"))
if (j.contains("description") && j["description"].is_string())
rules_.description = j["description"].get<std::string>();

// V3 Languages: per_language configs
if (j.contains("languages") && j["languages"].is_object()) {
auto& lang = j["languages"];
if (lang.contains("require_explicit")) {
if (lang.contains("require_explicit") && lang["require_explicit"].is_boolean()) {
rules_.languages.require_explicit = lang["require_explicit"].get<bool>(); rules_.explicitly_set.insert("languages.require_explicit"); }

// Sync to new struct too
Expand All @@ -519,10 +519,10 @@ static void loadFromJson(const nlohmann::json& j, GovernanceRules& rules_) {
if (lang.contains("per_language") && lang["per_language"].is_object()) {
for (auto& [lang_name, cfg] : lang["per_language"].items()) {
LanguageConfig lc;
if (cfg.contains("timeout")) lc.timeout = cfg["timeout"].get<int>();
if (cfg.contains("max_lines")) lc.max_lines = cfg["max_lines"].get<int>();
if (cfg.contains("max_output_size")) lc.max_output_size = cfg["max_output_size"].get<int>();
if (cfg.contains("version_hint")) lc.version_hint = cfg["version_hint"].get<std::string>();
if (cfg.contains("timeout") && cfg["timeout"].is_number_integer()) lc.timeout = cfg["timeout"].get<int>();
if (cfg.contains("max_lines") && cfg["max_lines"].is_number_integer()) lc.max_lines = cfg["max_lines"].get<int>();
if (cfg.contains("max_output_size") && cfg["max_output_size"].is_number_integer()) lc.max_output_size = cfg["max_output_size"].get<int>();
if (cfg.contains("version_hint") && cfg["version_hint"].is_string()) lc.version_hint = cfg["version_hint"].get<std::string>();

if (cfg.contains("dangerous_calls")) {
auto [en, lv] = parseEnforcementLevel(cfg["dangerous_calls"]);
Expand Down Expand Up @@ -555,7 +555,7 @@ static void loadFromJson(const nlohmann::json& j, GovernanceRules& rules_) {
}
if (cfg.contains("imports") && cfg["imports"].is_object()) {
auto& imp = cfg["imports"];
if (imp.contains("mode")) lc.imports.mode = imp["mode"].get<std::string>();
if (imp.contains("mode") && imp["mode"].is_string()) lc.imports.mode = imp["mode"].get<std::string>();
if (imp.contains("blocked"))
for (auto& b : imp["blocked"]) lc.imports.blocked.push_back(b.get<std::string>());
if (imp.contains("allowed"))
Expand Down Expand Up @@ -3268,6 +3268,12 @@ bool GovernanceEngine::reloadIfChanged() {
sb->setAllowExec(false);
sb->removeCapability(security::Capability::SYS_EXEC);
}
if (new_rp->capabilities.filesystem.mode == "none") {
sb->removeCapability(security::Capability::FS_READ);
sb->removeCapability(security::Capability::FS_WRITE);
} else if (new_rp->capabilities.filesystem.mode == "read") {
sb->removeCapability(security::Capability::FS_WRITE);
}
}

// Re-configure BSD/CDD detectors with new rules
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/js_executor_adapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ interpreter::NaabVal JsExecutorAdapter::executeWithReturn(
" - NAAb strings are passed as JS strings\n\n");
}

return interpreter::NaabVal::makeNull(); // Return null on error
throw std::runtime_error(std::string("JavaScript execution error: ") + error_msg);
}
}

Expand Down
13 changes: 12 additions & 1 deletion src/runtime/lockfile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,13 @@ Lockfile Lockfile::load(const std::string& path) {
lf.runtimes.push_back(e);
}
}
} catch (const std::exception& e) {
fprintf(stderr, "[lockfile] Warning: failed to parse %s: %s\n",
path.c_str(), e.what());
lf.parse_failed = true;
} catch (...) {
// Malformed lockfile — return with only the data we could parse
fprintf(stderr, "[lockfile] Warning: failed to parse %s\n", path.c_str());
lf.parse_failed = true;
}
return lf;
}
Expand All @@ -97,6 +102,12 @@ Lockfile Lockfile::load(const std::string& path) {
// ============================================================================

void Lockfile::save(const std::string& path) const {
// Don't overwrite a valid lockfile with data from a failed parse
if (parse_failed) {
fprintf(stderr, "[lockfile] Skipping save — parse failed, preserving existing %s\n",
path.c_str());
return;
}
// Create parent directory if needed
std::filesystem::path p(path);
if (p.has_parent_path()) {
Expand Down
8 changes: 7 additions & 1 deletion src/runtime/python_c_executor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,13 @@ interpreter::NaabVal PythonCExecutor::callFunction(
const std::string& function_name,
const std::vector<interpreter::NaabVal>& args
) {
throw std::runtime_error("PythonCExecutor::callFunction() not yet implemented for C API");
(void)function_name;
(void)args;
throw std::runtime_error(
"Python C API error: callFunction() is not implemented\n\n"
" Help:\n"
" - Use executeWithReturn() to run Python code instead\n"
" - Or use the subprocess-based Python executor (default)\n");
}

/**
Expand Down
Loading