From 1562ee006896966d87f7f103de3e2cfd5bcd3fc7 Mon Sep 17 00:00:00 2001 From: Axel Meyer Date: Thu, 21 May 2026 15:26:48 +0200 Subject: [PATCH] feat: validate schema-v2 map tables with layer-name whitelist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds validate_map_table_v2 — enforces schema_version=2, required fields (id, size, atlases), non-empty atlases[], per-layer tile-count check against w*h, and a VALID_LAYER_NAMES whitelist that logs a warning for unknown layers instead of rejecting them. Moves require_field before the v2 validator so it is in scope at call time. engine.warn replaced with engine.print (no warn binding). Co-Authored-By: Claude Opus 4.7 (1M context) --- init.lua | 92 +++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 78 insertions(+), 14 deletions(-) diff --git a/init.lua b/init.lua index f354013..2096b89 100644 --- a/init.lua +++ b/init.lua @@ -77,6 +77,83 @@ local function upgrade_v1_to_v2(v1) } end +-- ===================================================================== +-- Schema-validation helper (used by both v1 and v2 validators) +-- ===================================================================== + +-- Schema-validation helper: reads required field with type-check. +local function require_field(t, key, expected_type, source) + local v = t[key] + if v == nil then + error(string.format("maps.load: schema violation in %s: missing required field '%s'", + source, key)) + end + if type(v) ~= expected_type then + error(string.format("maps.load: schema violation in %s: field '%s' must be %s, got %s", + source, key, expected_type, type(v))) + end + return v +end + +-- ===================================================================== +-- Schema-v2 Layer-Whitelist + Validation +-- ===================================================================== + +local VALID_LAYER_NAMES = { + foundation = true, + subsurface = true, + surface = true, + topsurface = true, + lower_wall = true, + wall = true, + upper_wall = true, + canopy = true, +} + +local function validate_map_table_v2(t, source) + require_field(t, "schema_version", "number", source) + if t.schema_version ~= 2 then + error(string.format("maps.load: schema_version %d not supported (expected 2)", + t.schema_version)) + end + require_field(t, "id", "string", source) + local size = require_field(t, "size", "table", source) + require_field(size, "w", "number", source .. ".size") + require_field(size, "h", "number", source .. ".size") + local expected = size.w * size.h + + local atlases = require_field(t, "atlases", "table", source) + if #atlases == 0 then + error(string.format("maps.load: schema violation in %s: atlases[] must be non-empty", source)) + end + if #atlases > 256 then + error(string.format("maps.load: schema violation in %s: atlases[] has %d entries, max 256", source, #atlases)) + end + + local layers = t.layers or {} + for layer_name, layer_data in pairs(layers) do + if not VALID_LAYER_NAMES[layer_name] then + engine.print(string.format("maps.load: %s: ignoring unknown layer '%s'", source, layer_name)) + else + local tiles = require_field(layer_data, "tiles", "table", + source .. ".layers." .. layer_name) + if #tiles ~= expected then + error(string.format("maps.load: schema violation in %s.layers.%s: tiles length %d != %d", + source, layer_name, #tiles, expected)) + end + end + end + + if t.roof ~= nil then + if type(t.roof) ~= "table" then + error(string.format("maps.load: %s: roof must be array", source)) + end + if #t.roof ~= expected then + error(string.format("maps.load: %s: roof length %d != %d", source, #t.roof, expected)) + end + end +end + -- ===================================================================== -- Internal helpers -- ===================================================================== @@ -103,20 +180,6 @@ local function resolve_tilemap_id(ref) return engine.module.id() .. "." .. ref end --- Schema-validation helper: reads required field with type-check. -local function require_field(t, key, expected_type, source) - local v = t[key] - if v == nil then - error(string.format("maps.load: schema violation in %s: missing required field '%s'", - source, key)) - end - if type(v) ~= expected_type then - error(string.format("maps.load: schema violation in %s: field '%s' must be %s, got %s", - source, key, expected_type, type(v))) - end - return v -end - local function validate_map_table(t, source) require_field(t, "id", "string", source) require_field(t, "tilemap", "string", source) @@ -389,5 +452,6 @@ end M.encode_gid = encode_gid M.decode_gid = decode_gid M.upgrade_v1_to_v2 = upgrade_v1_to_v2 +M.validate_map_table_v2 = validate_map_table_v2 return M