[colorer/Colorer-library] master: Refuse HRC overlays after a type body is loaded (c0a5428)

0 views
Skip to first unread message

farg...@farmanager.com

unread,
Sep 12, 2026, 2:31:04 PMSep 12
to farco...@googlegroups.com
Repository : https://github.com/colorer/Colorer-library
On branch : master
Link : https://github.com/colorer/Colorer-library/commit/c0a5428893c9f1bb1862f37b0e4f3e4feee1dda3

>---------------------------------------------------------------

commit c0a5428893c9f1bb1862f37b0e4f3e4feee1dda3
Author: Aleksey Dobrunov <cta...@ctapmex.com>
Date: Wed Sep 9 23:02:23 2026 +0500

Refuse HRC overlays after a type body is loaded


>---------------------------------------------------------------

c0a5428893c9f1bb1862f37b0e4f3e4feee1dda3
.agents/skills/colorer-hrc/hrc-ref.md | 4 +-
.agents/skills/colorer-hrc/overrides.md | 10 ++--
src/colorer/HrcLibrary.h | 11 +++-
src/colorer/ParserFactory.h | 6 +-
src/colorer/parsers/HrcLibraryImpl.cpp | 35 ++++++++----
src/colorer/parsers/HrcLibraryImpl.h | 2 +
tests/unit/data/ov_base.hrc | 17 ++++++
tests/unit/data/ov_replace.hrc | 17 ++++++
tests/unit/data/ov_settings.xml | 9 +++
tests/unit/test_baseeditor.cpp | 10 ++--
tests/unit/test_hrcparsing.cpp | 97 +++++++++++++++++++++++++++++++++
11 files changed, 195 insertions(+), 23 deletions(-)

diff --git a/.agents/skills/colorer-hrc/hrc-ref.md b/.agents/skills/colorer-hrc/hrc-ref.md
index eb2ad2a..4069534 100644
--- a/.agents/skills/colorer-hrc/hrc-ref.md
+++ b/.agents/skills/colorer-hrc/hrc-ref.md
@@ -63,7 +63,7 @@ Regions: CapitalCase (`StringQuote`). Types/packages: lowercase. Schemes: lowerc

Each matching filename/firstline adds its weight. Highest total wins; ties → first.

-Later HRC with the same `name` **replaces** the whole prototype (unload). `hrcsettings.xml` only adds/updates params and, if any chooser is present, **replaces** the chooser list — [overrides.md](overrides.md).
+Later HRC with the same `name` **replaces** the whole prototype (unload of the `FileType` shell only). Allowed only before any type body is loaded; otherwise `HrcLibraryException`. `hrcsettings.xml` only adds/updates params and, if any chooser is present, **replaces** the chooser list — same stage restriction — [overrides.md](overrides.md).

## Package

@@ -213,7 +213,7 @@ Loaded by `ParserFactory`. Current schemes: `xmlns="http://colorer.github.io/sch

`link` relative to the catalog file, or `jar:common.zip!hrc/proto.hrc`. Directory `link`: first-level `*.hrc` only, skip `*.ent.hrc`. XML entities in catalog often alias `hrd` paths (including `jar:`). DTD `SYSTEM` with env vars: `env:$VAR/…` (not `jar:`) or `jar:$VAR/archive.zip!…` — [overrides.md](overrides.md).

-Duplicate `prototype/@name` in a later HRC file **unloads** the first definition. `hrcsettings.xml` merges params / replaces choosers only; it cannot add types. Do not edit the base catalog to customize.
+Duplicate `prototype/@name` in a later HRC file **unloads** the first definition (prototype stage only; error if that type body is already loaded). `hrcsettings.xml` merges params / replaces choosers only; it cannot add types. Both overlays throw after any type is loaded. Do not edit the base catalog to customize.

Colorer-schemes (separate repo; example sibling path `../Colorer-schemes`): `build.sh base` → `_build/base/` loose files; `build.sh base.packed` → `_build/base-packed/` zip + `jar:` links. XML/zip loader changes must test packed.

diff --git a/.agents/skills/colorer-hrc/overrides.md b/.agents/skills/colorer-hrc/overrides.md
index bf29327..be283ec 100644
--- a/.agents/skills/colorer-hrc/overrides.md
+++ b/.agents/skills/colorer-hrc/overrides.md
@@ -19,6 +19,8 @@ Canonical sequence (`ParserFactory`; CLI `tools/colorer/ConsoleTools.cpp` does t

`loadCatalog` / `loadHrcPath` / `loadHrcSettings` / `loadFileType` are exclusive per library. After a type is loaded, parsers may run concurrently.

+HRC overlays (`loadHrcPath` / `loadProtoTypes`, `loadHrcSettings`, duplicate prototype replace) run **only while every type is still a prototype/package shell**. After any type body is loaded (`loadFileType` / `loadSource` FULL), those calls throw `HrcLibraryException`. `unloadFileType` removes only the `FileType` prototype; it does not touch schemes/regions, and it throws if that type’s content is already loaded. `FileType::setParamValue` and `loadHrdPath` are not HRC type overlays and stay allowed after load.
+
Empty `loadCatalog(nullptr)` uses **`COLORER_CATALOG`**. If that is unset → `ParserFactoryException`. There is no built-in filesystem search for `catalog.xml`.

---
@@ -27,8 +29,8 @@ Empty `loadCatalog(nullptr)` uses **`COLORER_CATALOG`**. If that is unset → `P

| Goal | Use | Effect |
|------|-----|--------|
-| New language / replace whole prototype (`name`, `group`, `description`, `location`, choosers, params) | Later HRC via `loadHrcPath` (or catalog `auto/`) with the **same** `prototype/@name` | First prototype is **unloaded**; the new one is loaded. Log: `Duplicate prototype`. |
-| Change **params** and/or **filename/firstline** of an already loaded type | `hrcsettings.xml` via `loadHrcSettings` | Params add/update. Choosers **replace the whole list** if any `<filename>`/`<firstline>` is present. |
+| New language / replace whole prototype (`name`, `group`, `description`, `location`, choosers, params) | Later HRC via `loadHrcPath` (or catalog `auto/`) with the **same** `prototype/@name`, **before** any type body is loaded | First prototype is **unloaded**; the new one is loaded. Log: `Duplicate prototype`. After a type is loaded → `HrcLibraryException`. |
+| Change **params** and/or **filename/firstline** of a registered prototype | `hrcsettings.xml` via `loadHrcSettings`, **before** any type body is loaded | Params add/update. Choosers **replace the whole list** if any `<filename>`/`<firstline>` is present. After a type is loaded → `HrcLibraryException`. |
| New or extra HRD style | `loadHrdPath` | New `HrdNode` appended. Lookup is **first** `class`+`name` match (`getHrdNode`). |
| Runtime param for this session only | `FileType::setParamValue` | Writes `user_value`; not persisted. `getParamValue` prefers `user_value` over default. |

@@ -77,7 +79,7 @@ Weights: same as HRC (`filename` default **2**, `firstline` default **1**).
| `user_defined == true` | Use **`COLORER_HRC_SETTINGS`**. If unset, skip (not an error). |
| `user_defined == false` | No-op (reserved for a fixed app-level path; none is compiled in). |

-Non-empty `location` always loads that file (normalized path).
+Non-empty `location` always loads that file (normalized path). Throws `HrcLibraryException` if any type body is already loaded.

CLI: `-cs<path>` (`user_defined=true`). Env `COLORER_HRC_SETTINGS` if `-cs` omitted.

@@ -89,7 +91,7 @@ File → `loadProtoTypes`. Directory → non-recursive `*.hrc`, skip `*.ent.hrc`

Prototype may live in its own HRC (no `<location>` → type body is this file) or in a `proto.hrc`-style index with `<location link="…"/>`.

-Same-name prototype after catalog: unload + replace (see above). New names: extra file types.
+Same-name prototype after catalog: unload + replace (see above). New names: extra file types. Both only before any type body is loaded.

Less preferred (still in base catalog): directory `hrc/auto` listed last in `catalog.xml`. Do not edit distro `empty.hrc`. Prefer an app user path over writing into the catalog tree.

diff --git a/src/colorer/HrcLibrary.h b/src/colorer/HrcLibrary.h
index 524e6ce..8681caa 100644
--- a/src/colorer/HrcLibrary.h
+++ b/src/colorer/HrcLibrary.h
@@ -19,7 +19,9 @@ class HrcLibraryException : public Exception
Defines basic operations of loading and accessing HRC information.

Loading (#loadSource, #loadProtoTypes, #loadFileType, #loadHrcSettings) is exclusive
- for one library. After a type is loaded, TextParser::parse may run concurrently
+ for one library. HRC overlays (#loadProtoTypes, #loadHrcSettings, duplicate
+ prototype replace) are allowed only while no type body has been loaded.
+ After a type is loaded, TextParser::parse may run concurrently
as a shared read. Do not destroy the library while parse is running.
#getRegion(const UnicodeString*) may load a type and is exclusive with parse.
RegionHandler callbacks must not call HrcLibrary.
@@ -41,12 +43,19 @@ class HrcLibrary
* Only the prototype and common/external packages are loaded.
* If the file does not contain a type definition, then before further use of this type,
* it must be loaded using the #loadFileType() method.
+ * A later file with the same prototype name replaces the previous prototype
+ * (unload of the FileType shell only). Allowed only before any type body is loaded.
* @param is XmlInputSource stream of HRC file
+ * @throw HrcLibraryException if any type content is already loaded
*/
void loadProtoTypes(XmlInputSource* is);

void loadFileType(FileType* filetype);

+ /** Merge params / choosers into existing prototypes from hrcsettings.xml.
+ * Allowed only before any type body is loaded.
+ * @throw HrcLibraryException if any type content is already loaded
+ */
void loadHrcSettings(const XmlInputSource& is);

/** Enumerates sequentially all prototypes
diff --git a/src/colorer/ParserFactory.h b/src/colorer/ParserFactory.h
index 12f59cc..22c8b42 100644
--- a/src/colorer/ParserFactory.h
+++ b/src/colorer/ParserFactory.h
@@ -18,8 +18,10 @@
*
* Several instances may exist in one process. Each owns an HrcLibrary.
* loadCatalog / loadHrcPath / loadHrcSettings / loadFileType are exclusive
- * per library. After a type is loaded, createTextParser() instances may parse
- * that library concurrently. Destroy the factory only after those parsers stop.
+ * per library. loadHrcPath / loadHrcSettings overlay prototypes only; they
+ * throw if any type body is already loaded. After a type is loaded,
+ * createTextParser() instances may parse that library concurrently.
+ * Destroy the factory only after those parsers stop.
*/
class ParserFactory
{
diff --git a/src/colorer/parsers/HrcLibraryImpl.cpp b/src/colorer/parsers/HrcLibraryImpl.cpp
index 5a9b69b..d65a226 100644
--- a/src/colorer/parsers/HrcLibraryImpl.cpp
+++ b/src/colorer/parsers/HrcLibraryImpl.cpp
@@ -45,6 +45,9 @@ void HrcLibrary::Impl::loadSource(XmlInputSource* input_source, const LoadType l
if (!input_source) {
throw HrcLibraryException("Can't open stream - 'null' is bad stream.");
}
+ if (load_type == LoadType::PROTOTYPE && typeContentLoaded) {
+ throw HrcLibraryException("HRC prototype overlay is only allowed before type content is loaded");
+ }

#ifdef COLORER_FEATURE_ZIPINPUTSOURCE
XmlJarCache::Current jar_scope(*zip_cache_ptr);
@@ -70,19 +73,24 @@ void HrcLibrary::Impl::loadSource(XmlInputSource* input_source, const LoadType l
current_load_type = temp_lt;
}

+bool HrcLibrary::Impl::isFileTypeContentLoaded(const FileType* filetype) const
+{
+ if (filetype == nullptr) {
+ return false;
+ }
+ const auto& ptype = filetype->pimpl;
+ return ptype->loadDone || ptype->type_loading || ptype->input_source_loading || ptype->load_broken;
+}
+
void HrcLibrary::Impl::unloadFileType(const FileType* filetype)
{
- bool loop = true;
- while (loop) {
- loop = false;
- for (auto scheme = schemeHash.begin(); scheme != schemeHash.end(); ++scheme) {
- if (scheme->second->fileType == filetype) {
- schemeHash.erase(scheme);
- loop = true;
- break;
- }
- }
+ if (filetype == nullptr) {
+ return;
+ }
+ if (isFileTypeContentLoaded(filetype)) {
+ throw HrcLibraryException("Cannot unload type '" + filetype->getName() + "': type content is already loaded");
}
+
for (auto ft = fileTypeVector.begin(); ft != fileTypeVector.end(); ++ft) {
if (*ft == filetype) {
fileTypeVector.erase(ft);
@@ -126,10 +134,16 @@ void HrcLibrary::Impl::loadFileType(FileType* filetype)
}

thisType->pimpl->input_source_loading = false;
+ if (thisType->pimpl->load_broken) {
+ typeContentLoaded = true;
+ }
}

void HrcLibrary::Impl::loadHrcSettings(const XmlInputSource& is)
{
+ if (typeContentLoaded) {
+ throw HrcLibraryException("hrcsettings overlay is only allowed before type content is loaded");
+ }
#ifdef COLORER_FEATURE_ZIPINPUTSOURCE
XmlJarCache::Current jar_scope(*zip_cache_ptr);
#endif
@@ -454,6 +468,7 @@ void HrcLibrary::Impl::addType(const XMLNode& elem)
}

type->pimpl->type_loading = true;
+ typeContentLoaded = true;

FileType* o_parseType = current_parse_type;
current_parse_type = type;
diff --git a/src/colorer/parsers/HrcLibraryImpl.h b/src/colorer/parsers/HrcLibraryImpl.h
index 5162927..f8c3d8f 100644
--- a/src/colorer/parsers/HrcLibraryImpl.h
+++ b/src/colorer/parsers/HrcLibraryImpl.h
@@ -63,8 +63,10 @@ class HrcLibrary::Impl
LoadType current_load_type = LoadType::FULL;
bool structureChanged = false;
bool updateStarted = false;
+ bool typeContentLoaded = false;

void unloadFileType(const FileType* filetype);
+ bool isFileTypeContentLoaded(const FileType* filetype) const;

void parseHRC(const XmlInputSource& is);
void parseHrcBlock(const XMLNode& elem);
diff --git a/tests/unit/data/ov_base.hrc b/tests/unit/data/ov_base.hrc
new file mode 100644
index 0000000..a6fd7d2
--- /dev/null
+++ b/tests/unit/data/ov_base.hrc
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<hrc>
+ <prototype name="ov" group="test" description="overlay base">
+ <filename>/\.base$/</filename>
+ <parameters>
+ <param name="flag" value="base"/>
+ </parameters>
+ </prototype>
+ <type name="ov">
+ <region name="Kw"/>
+ <scheme name="ov">
+ <keywords region="Kw" ignorecase="no">
+ <word name="foo"/>
+ </keywords>
+ </scheme>
+ </type>
+</hrc>
diff --git a/tests/unit/data/ov_replace.hrc b/tests/unit/data/ov_replace.hrc
new file mode 100644
index 0000000..6f316b3
--- /dev/null
+++ b/tests/unit/data/ov_replace.hrc
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<hrc>
+ <prototype name="ov" group="test" description="overlay replace">
+ <filename>/\.repl$/</filename>
+ <parameters>
+ <param name="flag" value="replace"/>
+ </parameters>
+ </prototype>
+ <type name="ov">
+ <region name="Kw"/>
+ <scheme name="ov">
+ <keywords region="Kw" ignorecase="no">
+ <word name="bar"/>
+ </keywords>
+ </scheme>
+ </type>
+</hrc>
diff --git a/tests/unit/data/ov_settings.xml b/tests/unit/data/ov_settings.xml
new file mode 100644
index 0000000..eeaddcd
--- /dev/null
+++ b/tests/unit/data/ov_settings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<hrc-settings>
+ <prototype name="ov">
+ <filename weight="100">/\.set$/</filename>
+ <parameters>
+ <param name="flag" value="settings"/>
+ </parameters>
+ </prototype>
+</hrc-settings>
diff --git a/tests/unit/test_baseeditor.cpp b/tests/unit/test_baseeditor.cpp
index 45239e6..973a177 100644
--- a/tests/unit/test_baseeditor.cpp
+++ b/tests/unit/test_baseeditor.cpp
@@ -324,17 +324,19 @@ TEST_CASE("loading another type does not disturb an existing editor", "[baseedit
{
ParserFactory factory;
loadTryLine(factory);
+ auto block_path = fs::path(__FILE__).parent_path() / "data" / "type_block.hrc";
+ UnicodeString block_location(block_path.c_str());
+ factory.loadHrcPath(&block_location);

MutableLines source({UnicodeString(u"int a")});
auto editor = makeEditor(factory, source);
REQUIRE(hasRegion(editor->getLineRegions(0), "try_line:Kw"));

- auto block_path = fs::path(__FILE__).parent_path() / "data" / "type_block.hrc";
- UnicodeString block_location(block_path.c_str());
- factory.loadHrcPath(&block_location);
+ auto* other = factory.getHrcLibrary().getFileType(UnicodeString("bl_quote"));
+ REQUIRE(other != nullptr);
+ factory.getHrcLibrary().loadFileType(other);

REQUIRE(hasRegion(editor->getLineRegions(0), "try_line:Kw"));
- REQUIRE(factory.getHrcLibrary().getFileType(UnicodeString("bl_quote")) != nullptr);
REQUIRE(factory.getHrcLibrary().getFileType(UnicodeString("try_line")) != nullptr);
}

diff --git a/tests/unit/test_hrcparsing.cpp b/tests/unit/test_hrcparsing.cpp
index f338fb9..5afc969 100644
--- a/tests/unit/test_hrcparsing.cpp
+++ b/tests/unit/test_hrcparsing.cpp
@@ -32,3 +32,100 @@ TEST_CASE("Load HRC expands a SYSTEM entity into the type tree", "[hrc]")
REQUIRE(type->getBaseScheme() != nullptr);
REQUIRE(*type->getBaseScheme()->getName() == UnicodeString("entinc:entinc"));
}
+
+TEST_CASE("Duplicate prototype replaces the FileType shell before type load", "[hrc]")
+{
+ XmlInputSource base = hrcInput("ov_base.hrc");
+ XmlInputSource repl = hrcInput("ov_replace.hrc");
+ HrcLibrary lib;
+ lib.loadProtoTypes(&base);
+ lib.loadProtoTypes(&repl);
+
+ auto* type = lib.getFileType(UnicodeString("ov"));
+ REQUIRE(type != nullptr);
+ REQUIRE(type->getDescription() == UnicodeString("overlay replace"));
+ REQUIRE(*type->getParamValue(UnicodeString("flag")) == UnicodeString("replace"));
+
+ UnicodeString repl_name(".repl");
+ REQUIRE(lib.chooseFileType(&repl_name, nullptr) == type);
+
+ lib.loadFileType(type);
+ REQUIRE(type->getBaseScheme() != nullptr);
+}
+
+TEST_CASE("hrcsettings updates a prototype before type load", "[hrc]")
+{
+ XmlInputSource base = hrcInput("ov_base.hrc");
+ XmlInputSource settings = hrcInput("ov_settings.xml");
+ HrcLibrary lib;
+ lib.loadProtoTypes(&base);
+ lib.loadHrcSettings(settings);
+
+ auto* type = lib.getFileType(UnicodeString("ov"));
+ REQUIRE(type != nullptr);
+ REQUIRE(*type->getParamValue(UnicodeString("flag")) == UnicodeString("settings"));
+
+ UnicodeString set_name(".set");
+ REQUIRE(lib.chooseFileType(&set_name, nullptr) == type);
+}
+
+TEST_CASE("Duplicate prototype after type load is an error", "[hrc]")
+{
+ XmlInputSource base = hrcInput("ov_base.hrc");
+ XmlInputSource repl = hrcInput("ov_replace.hrc");
+ HrcLibrary lib;
+ lib.loadProtoTypes(&base);
+ auto* type = lib.getFileType(UnicodeString("ov"));
+ lib.loadFileType(type);
+
+ REQUIRE_THROWS_AS(lib.loadProtoTypes(&repl), HrcLibraryException);
+ REQUIRE_THROWS_WITH(lib.loadProtoTypes(&repl),
+ Catch::Matchers::ContainsSubstring("before type content is loaded"));
+
+ auto* still = lib.getFileType(UnicodeString("ov"));
+ REQUIRE(still == type);
+ REQUIRE(still->getDescription() == UnicodeString("overlay base"));
+}
+
+TEST_CASE("hrcsettings after type load is an error", "[hrc]")
+{
+ XmlInputSource base = hrcInput("ov_base.hrc");
+ XmlInputSource settings = hrcInput("ov_settings.xml");
+ HrcLibrary lib;
+ lib.loadProtoTypes(&base);
+ lib.loadFileType(lib.getFileType(UnicodeString("ov")));
+
+ REQUIRE_THROWS_AS(lib.loadHrcSettings(settings), HrcLibraryException);
+ REQUIRE_THROWS_WITH(lib.loadHrcSettings(settings),
+ Catch::Matchers::ContainsSubstring("before type content is loaded"));
+ REQUIRE(*lib.getFileType(UnicodeString("ov"))->getParamValue(UnicodeString("flag")) == UnicodeString("base"));
+}
+
+TEST_CASE("Prototype overlay of a new name is refused after another type is loaded", "[hrc]")
+{
+ XmlInputSource base = hrcInput("ov_base.hrc");
+ XmlInputSource cue = hrcInput("type_cue.hrc");
+ HrcLibrary lib;
+ lib.loadProtoTypes(&base);
+ lib.loadFileType(lib.getFileType(UnicodeString("ov")));
+
+ REQUIRE_THROWS_AS(lib.loadProtoTypes(&cue), HrcLibraryException);
+ REQUIRE(lib.getFileType(UnicodeString("cue")) == nullptr);
+}
+
+TEST_CASE("Full load then duplicate prototype is an error", "[hrc]")
+{
+ XmlInputSource base = hrcInput("ov_base.hrc");
+ XmlInputSource repl = hrcInput("ov_replace.hrc");
+ HrcLibrary lib;
+ lib.loadSource(&base);
+
+ REQUIRE_THROWS_AS(lib.loadSource(&repl), HrcLibraryException);
+ REQUIRE_THROWS_WITH(lib.loadSource(&repl),
+ Catch::Matchers::ContainsSubstring("type content is already loaded"));
+
+ auto* type = lib.getFileType(UnicodeString("ov"));
+ REQUIRE(type != nullptr);
+ REQUIRE(type->getDescription() == UnicodeString("overlay base"));
+ REQUIRE(type->getBaseScheme() != nullptr);
+}


Reply all
Reply to author
Forward
0 new messages