From 369956f1f7777d1616d40ad73be77031b602baa5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Nov 2025 11:43:26 +0000 Subject: [PATCH 1/6] Implement CFGFormat with native parser and writer Implemented a complete CFGFormat class with high-performance native parser and writer. Features: - Native parser optimized for performance using char array processing - Support for native CFG types: strings, numbers, booleans, arrays - Section headers with [section.subsection] syntax - Comments with # and // syntax - Both quoted and bare keys - Escape sequences in strings (\n, \t, \r, \\, \", etc) - Array support with flexible formatting [item1, item2, ...] - Dual separator support (= and :) - Proper error handling with descriptive messages Implementation details: - FormatWriter: Buffers all output for performance, writes on close() - FormatReader: Single-pass parser with minimal allocations - Follows the same patterns as TOMLFormat and JSONFormat - LinkedHashMap for preserving key order - Stack-based group management for nested sections The implementation is focused on performance while maintaining code clarity and robustness, following the project's coding standards. --- .../omegaconfig/impl/formats/CFGFormat.java | 525 +++++++++++++++++- 1 file changed, 520 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/omegaconfig/impl/formats/CFGFormat.java b/src/main/java/org/omegaconfig/impl/formats/CFGFormat.java index 0170bcb..6158a52 100644 --- a/src/main/java/org/omegaconfig/impl/formats/CFGFormat.java +++ b/src/main/java/org/omegaconfig/impl/formats/CFGFormat.java @@ -1,11 +1,17 @@ package org.omegaconfig.impl.formats; import org.omegaconfig.OmegaConfig; +import org.omegaconfig.Tools; +import org.omegaconfig.api.formats.IFormatCodec; import org.omegaconfig.api.formats.IFormatReader; import org.omegaconfig.api.formats.IFormatWriter; -import org.omegaconfig.api.formats.IFormatCodec; +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.util.*; public class CFGFormat implements IFormatCodec { @Override public String id() { return OmegaConfig.FORMAT_CFG; } @@ -13,12 +19,521 @@ public class CFGFormat implements IFormatCodec { @Override public String mimeType() { return "text/x-cfg"; } @Override - public IFormatReader createReader(Path filePath) { - return null; + public IFormatReader createReader(Path filePath) throws IOException { + return new FormatReader(filePath); } @Override - public IFormatWriter createWritter(Path filePath) { - return null; + public IFormatWriter createWritter(Path filePath) throws IOException { + return new FormatWriter(filePath); + } + + public static class FormatWriter implements IFormatWriter { + private final Stack group = new Stack<>(); + private final BufferedWriter writer; + private final StringBuilder buffer = new StringBuilder(); + private final List comments = new ArrayList<>(); + private String currentSection = ""; + private boolean sectionHeaderWritten = false; + + public FormatWriter(Path path) throws IOException { + if (!path.toFile().getParentFile().exists() && !path.toFile().getParentFile().mkdirs()) { + throw new IOException("Failed to create parent directories for " + path); + } + this.writer = new BufferedWriter(new FileWriter(path.toFile(), StandardCharsets.UTF_8)); + } + + @Override + public void write(String comment) { + this.comments.add(comment); + } + + @Override + public void write(String fieldName, String value, Class type, Class subType) { + ensureSectionHeader(); + + // Write comments + for (String comment : this.comments) { + this.buffer.append("# ").append(comment).append("\n"); + } + this.comments.clear(); + + // Write key = value + this.buffer.append(escapeKey(fieldName)).append(" = "); + this.buffer.append(formatValue(value, type)); + this.buffer.append("\n"); + } + + @Override + public void write(String fieldName, String[] values, Class type, Class subType) { + ensureSectionHeader(); + + // Write comments + for (String comment : this.comments) { + this.buffer.append("# ").append(comment).append("\n"); + } + this.comments.clear(); + + // Write array + this.buffer.append(escapeKey(fieldName)).append(" = ["); + + if (values.length > 0) { + for (int i = 0; i < values.length; i++) { + if (i > 0) { + this.buffer.append(", "); + } + this.buffer.append(formatValue(values[i], subType)); + } + } + + this.buffer.append("]\n"); + } + + @Override + public void push(String groupName) { + this.group.push(groupName); + this.sectionHeaderWritten = false; + } + + @Override + public void pop() { + if (!this.group.isEmpty()) { + this.group.pop(); + this.sectionHeaderWritten = false; + } + } + + @Override + public void close() throws IOException { + this.writer.write(this.buffer.toString()); + this.writer.flush(); + this.writer.close(); + } + + private void ensureSectionHeader() { + String sectionName = buildSectionName(); + if (!sectionName.equals(currentSection) || !sectionHeaderWritten) { + if (!buffer.isEmpty()) { + buffer.append("\n"); + } + if (!sectionName.isEmpty()) { + buffer.append("[").append(sectionName).append("]\n"); + } + currentSection = sectionName; + sectionHeaderWritten = true; + } + } + + private String buildSectionName() { + if (group.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + Iterator it = group.iterator(); + while (it.hasNext()) { + sb.append(escapeKey(it.next())); + if (it.hasNext()) { + sb.append("."); + } + } + return sb.toString(); + } + + private String escapeKey(String key) { + // Simple keys don't need quotes + if (key.matches("[A-Za-z0-9_-]+")) { + return key; + } + // Quote and escape if needed + return "\"" + key.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; + } + + private String formatValue(String value, Class type) { + if (type == null) { + return "\"" + escapeString(value) + "\""; + } + + // Boolean + if (Boolean.class.isAssignableFrom(type) || boolean.class.isAssignableFrom(type)) { + return value.toLowerCase(); + } + + // Numbers + if (Number.class.isAssignableFrom(type) || + int.class.isAssignableFrom(type) || + long.class.isAssignableFrom(type) || + double.class.isAssignableFrom(type) || + float.class.isAssignableFrom(type) || + byte.class.isAssignableFrom(type) || + short.class.isAssignableFrom(type)) { + return value; + } + + // String (default) + return "\"" + escapeString(value) + "\""; + } + + private String escapeString(String str) { + return str.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + } + + public static class FormatReader implements IFormatReader { + private final LinkedHashMap values = new LinkedHashMap<>(); + private final Stack group = new Stack<>(); + private String currentSection = ""; + + public FormatReader(Path path) throws IOException { + char[] data = new String(Tools.readAllBytes(path), StandardCharsets.UTF_8).toCharArray(); + parseCfg(data); + } + + private void parseCfg(char[] data) throws IOException { + int i = 0; + int len = data.length; + + while (i < len) { + i = skipWhitespace(data, i, len); + if (i >= len) break; + + char c = data[i]; + + // Skip comments + if (c == '#' || (c == '/' && i + 1 < len && data[i + 1] == '/')) { + i = skipToEndOfLine(data, i, len); + continue; + } + + // Section header + if (c == '[') { + i = parseSectionHeader(data, i, len); + continue; + } + + // Key-value pair + if (isKeyStart(c)) { + i = parseKeyValue(data, i, len); + continue; + } + + i++; + } + } + + private int parseSectionHeader(char[] data, int start, int len) throws IOException { + int i = start + 1; + + // Skip whitespace + i = skipWhitespace(data, i, len); + + // Parse section name + StringBuilder sectionName = new StringBuilder(); + while (i < len && data[i] != ']') { + if (data[i] == '\n') { + throw new IOException("Unclosed section header at position " + i); + } + sectionName.append(data[i]); + i++; + } + + if (i >= len) { + throw new IOException("Unclosed section header"); + } + + // Skip closing bracket + i++; + + currentSection = sectionName.toString().trim(); + + // Skip to end of line + return skipToEndOfLine(data, i, len); + } + + private int parseKeyValue(char[] data, int start, int len) throws IOException { + int i = start; + + // Parse key + StringBuilder key = new StringBuilder(); + i = parseKey(data, i, len, key); + + // Skip whitespace + i = skipWhitespace(data, i, len); + + // Expect '=' or ':' + if (i >= len || (data[i] != '=' && data[i] != ':')) { + throw new IOException("Expected '=' or ':' after key at position " + i); + } + + // Skip separator + i++; + + // Skip whitespace + i = skipWhitespace(data, i, len); + + // Parse value + String fullKey = buildFullKey(key.toString()); + i = parseValue(data, i, len, fullKey); + + return i; + } + + private int parseKey(char[] data, int start, int len, StringBuilder key) throws IOException { + int i = start; + char c = data[i]; + + // Quoted key + if (c == '"' || c == '\'') { + char quote = c; + i++; + while (i < len && data[i] != quote) { + if (data[i] == '\\' && i + 1 < len) { + i++; + key.append(unescapeChar(data[i])); + } else { + key.append(data[i]); + } + i++; + } + if (i >= len) { + throw new IOException("Unclosed quoted key"); + } + i++; // Skip closing quote + } else { + // Bare key + while (i < len && (Character.isLetterOrDigit(data[i]) || data[i] == '_' || data[i] == '-' || data[i] == '.')) { + key.append(data[i]); + i++; + } + } + + return i; + } + + private int parseValue(char[] data, int start, int len, String key) throws IOException { + int i = start; + if (i >= len) { + throw new IOException("Expected value after '=' at position " + i); + } + + char c = data[i]; + + // String + if (c == '"' || c == '\'') { + return parseString(data, i, len, key); + } + + // Array + if (c == '[') { + return parseArray(data, i, len, key); + } + + // Literal (boolean, number) + return parseLiteral(data, i, len, key); + } + + private int parseString(char[] data, int start, int len, String key) throws IOException { + int i = start; + char quote = data[i]; + i++; + + StringBuilder value = new StringBuilder(); + while (i < len && data[i] != quote) { + if (data[i] == '\\' && i + 1 < len) { + i++; + value.append(unescapeChar(data[i])); + } else if (data[i] == '\n') { + throw new IOException("Newline not allowed in single-line string at position " + i); + } else { + value.append(data[i]); + } + i++; + } + + if (i >= len) { + throw new IOException("Unclosed string"); + } + + i++; // Skip closing quote + values.put(key, value.toString()); + return skipToEndOfLine(data, i, len); + } + + private int parseArray(char[] data, int start, int len, String key) throws IOException { + int i = start + 1; + List array = new ArrayList<>(); + + while (i < len) { + i = skipWhitespace(data, i, len); + if (i >= len) { + throw new IOException("Unclosed array"); + } + + // Check for end of array + if (data[i] == ']') { + i++; + values.put(key, array.toArray(new String[0])); + return skipToEndOfLine(data, i, len); + } + + // Skip comments + if (data[i] == '#' || (data[i] == '/' && i + 1 < len && data[i + 1] == '/')) { + i = skipToEndOfLine(data, i, len); + continue; + } + + // Parse array element + StringBuilder element = new StringBuilder(); + i = parseArrayElement(data, i, len, element); + array.add(element.toString()); + + // Skip whitespace + i = skipWhitespace(data, i, len); + + // Check for comma + if (i < len && data[i] == ',') { + i++; + } + } + + throw new IOException("Unclosed array"); + } + + private int parseArrayElement(char[] data, int start, int len, StringBuilder element) throws IOException { + int i = start; + char c = data[i]; + + // String + if (c == '"' || c == '\'') { + char quote = c; + i++; + while (i < len && data[i] != quote) { + if (data[i] == '\\' && i + 1 < len) { + i++; + element.append(unescapeChar(data[i])); + } else { + element.append(data[i]); + } + i++; + } + if (i >= len) { + throw new IOException("Unclosed string in array"); + } + i++; + return i; + } + + // Literal (number, boolean) + while (i < len && data[i] != ',' && data[i] != ']' && data[i] != '\n' && data[i] != '#' && !Character.isWhitespace(data[i])) { + element.append(data[i]); + i++; + } + + return i; + } + + private int parseLiteral(char[] data, int start, int len, String key) { + int i = start; + StringBuilder value = new StringBuilder(); + + while (i < len && data[i] != '\n' && data[i] != '#' && !(data[i] == '/' && i + 1 < len && data[i + 1] == '/')) { + if (!Character.isWhitespace(data[i])) { + value.append(data[i]); + } else if (value.length() > 0) { + // Stop at first whitespace after non-whitespace content + break; + } + i++; + } + + String literal = value.toString().trim(); + values.put(key, literal); + return skipToEndOfLine(data, i, len); + } + + private int skipWhitespace(char[] data, int start, int len) { + int i = start; + while (i < len && Character.isWhitespace(data[i]) && data[i] != '\n') { + i++; + } + return i; + } + + private int skipToEndOfLine(char[] data, int start, int len) { + int i = start; + while (i < len && data[i] != '\n') { + i++; + } + if (i < len && data[i] == '\n') { + i++; + } + return i; + } + + private boolean isKeyStart(char c) { + return Character.isLetterOrDigit(c) || c == '_' || c == '"' || c == '\''; + } + + private char unescapeChar(char c) { + return switch (c) { + case 'n' -> '\n'; + case 'r' -> '\r'; + case 't' -> '\t'; + case 'b' -> '\b'; + case 'f' -> '\f'; + case '\\' -> '\\'; + case '"' -> '"'; + case '\'' -> '\''; + default -> c; + }; + } + + private String buildFullKey(String key) { + if (currentSection.isEmpty()) { + return key; + } + return currentSection + "." + key; + } + + @Override + public String read(String fieldName) { + String key = Tools.concat("", (!group.isEmpty() ? "." : "") + fieldName, '.', group); + Object value = values.get(key); + if (value instanceof String s) { + return s; + } + return null; + } + + @Override + public String[] readArray(String fieldName) { + String key = Tools.concat("", (!group.isEmpty() ? "." : "") + fieldName, '.', group); + Object value = values.get(key); + if (value instanceof String[] s) { + return s; + } + return null; + } + + @Override + public void push(String group) { + this.group.push(group); + } + + @Override + public void pop() { + if (!this.group.isEmpty()) { + this.group.pop(); + } + } + + @Override + public void close() { + this.values.clear(); + this.group.clear(); + } } } From d5a73efe77f511188ca77edcf89420c1bdecf443 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Nov 2025 11:44:27 +0000 Subject: [PATCH 2/6] Add execute permission to gradlew --- gradlew | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 From 667318a768574cc2a8df2fc0530535f9d0eff6f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Nov 2025 11:51:43 +0000 Subject: [PATCH 3/6] Reimplement CFGFormat with correct CFG syntax (JSON superset) Fixed CFGFormat to follow the actual CFG specification from red-dove.com. CFG is a superset of JSON, not a TOML-like format. Key changes: - Top-level is a mapping enclosed in { } - Uses JSON-like syntax with : or = as separators - Keys can be identifiers or quoted strings - Newlines act as separators (trailing commas optional) - Nested mappings use { } blocks, not [section] headers - Comments with # - Support for cross-references ${...} (basic parsing) - Support for include directives @'file.cfg' (basic parsing) - Support for special values `...` (basic parsing) Native type support: - Strings (single or double quotes) - Numbers (integers, floats, hex like 0x123) - Booleans (true/false) - null - Arrays with flexible formatting - Nested mappings Performance optimizations: - Single-pass parser with char array processing - Minimal allocations - Buffered output for writer - LinkedHashMap for key order preservation Example CFG format: { string_value: 'hello' integer_value: 42 nested: { key: 'value' } list: [1, 2, 3] } --- .../omegaconfig/impl/formats/CFGFormat.java | 436 +++++++++++------- 1 file changed, 263 insertions(+), 173 deletions(-) diff --git a/src/main/java/org/omegaconfig/impl/formats/CFGFormat.java b/src/main/java/org/omegaconfig/impl/formats/CFGFormat.java index 6158a52..fda4021 100644 --- a/src/main/java/org/omegaconfig/impl/formats/CFGFormat.java +++ b/src/main/java/org/omegaconfig/impl/formats/CFGFormat.java @@ -13,6 +13,17 @@ import java.nio.file.Path; import java.util.*; +/** + * CFG Format implementation - a superset of JSON with additional features. + * CFG supports: + * - Comments with # + * - Keys as strings or identifiers + * - Separators: : or = + * - Newlines as separators (trailing commas optional) + * - Nested mappings and lists + * - Cross-references ${...} + * - Include directives @'file.cfg' + */ public class CFGFormat implements IFormatCodec { @Override public String id() { return OmegaConfig.FORMAT_CFG; } @Override public String extension() { return "." + this.id(); } @@ -33,14 +44,17 @@ public static class FormatWriter implements IFormatWriter { private final BufferedWriter writer; private final StringBuilder buffer = new StringBuilder(); private final List comments = new ArrayList<>(); - private String currentSection = ""; - private boolean sectionHeaderWritten = false; + private boolean firstInMapping = true; + private int indentLevel = 0; public FormatWriter(Path path) throws IOException { if (!path.toFile().getParentFile().exists() && !path.toFile().getParentFile().mkdirs()) { throw new IOException("Failed to create parent directories for " + path); } this.writer = new BufferedWriter(new FileWriter(path.toFile(), StandardCharsets.UTF_8)); + // CFG top-level is a mapping + this.buffer.append("{\n"); + this.indentLevel = 1; } @Override @@ -50,102 +64,105 @@ public void write(String comment) { @Override public void write(String fieldName, String value, Class type, Class subType) { - ensureSectionHeader(); + writeComments(); - // Write comments - for (String comment : this.comments) { - this.buffer.append("# ").append(comment).append("\n"); + if (!firstInMapping) { + this.buffer.append("\n"); } - this.comments.clear(); + firstInMapping = false; - // Write key = value - this.buffer.append(escapeKey(fieldName)).append(" = "); + // Write key + indent(); + this.buffer.append(formatKey(fieldName)).append(": "); this.buffer.append(formatValue(value, type)); - this.buffer.append("\n"); } @Override public void write(String fieldName, String[] values, Class type, Class subType) { - ensureSectionHeader(); + writeComments(); - // Write comments - for (String comment : this.comments) { - this.buffer.append("# ").append(comment).append("\n"); + if (!firstInMapping) { + this.buffer.append("\n"); } - this.comments.clear(); + firstInMapping = false; - // Write array - this.buffer.append(escapeKey(fieldName)).append(" = ["); + // Write key + indent(); + this.buffer.append(formatKey(fieldName)).append(": "); + // Write array + this.buffer.append("["); if (values.length > 0) { + this.buffer.append("\n"); for (int i = 0; i < values.length; i++) { - if (i > 0) { - this.buffer.append(", "); + indent(); + this.buffer.append(" ").append(formatValue(values[i], subType)); + if (i < values.length - 1) { + this.buffer.append(","); } - this.buffer.append(formatValue(values[i], subType)); + this.buffer.append("\n"); } + indent(); } - - this.buffer.append("]\n"); + this.buffer.append("]"); } @Override public void push(String groupName) { + writeComments(); + + if (!firstInMapping) { + this.buffer.append("\n"); + } + firstInMapping = false; + + indent(); + this.buffer.append(formatKey(groupName)).append(": {\n"); + this.group.push(groupName); - this.sectionHeaderWritten = false; + this.indentLevel++; + this.firstInMapping = true; } @Override public void pop() { if (!this.group.isEmpty()) { this.group.pop(); - this.sectionHeaderWritten = false; + this.indentLevel--; + this.buffer.append("\n"); + indent(); + this.buffer.append("}"); + this.firstInMapping = false; } } @Override public void close() throws IOException { + this.buffer.append("\n}\n"); this.writer.write(this.buffer.toString()); this.writer.flush(); this.writer.close(); } - private void ensureSectionHeader() { - String sectionName = buildSectionName(); - if (!sectionName.equals(currentSection) || !sectionHeaderWritten) { - if (!buffer.isEmpty()) { - buffer.append("\n"); - } - if (!sectionName.isEmpty()) { - buffer.append("[").append(sectionName).append("]\n"); - } - currentSection = sectionName; - sectionHeaderWritten = true; + private void writeComments() { + for (String comment : this.comments) { + indent(); + this.buffer.append("# ").append(comment).append("\n"); } + this.comments.clear(); } - private String buildSectionName() { - if (group.isEmpty()) { - return ""; - } - StringBuilder sb = new StringBuilder(); - Iterator it = group.iterator(); - while (it.hasNext()) { - sb.append(escapeKey(it.next())); - if (it.hasNext()) { - sb.append("."); - } - } - return sb.toString(); + private void indent() { + this.buffer.append(" ".repeat(this.indentLevel)); } - private String escapeKey(String key) { - // Simple keys don't need quotes - if (key.matches("[A-Za-z0-9_-]+")) { + private String formatKey(String key) { + // Use identifier if possible (alphanumeric + underscore) + if (key.matches("[A-Za-z_][A-Za-z0-9_]*")) { return key; } - // Quote and escape if needed - return "\"" + key.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; + // Otherwise use quoted string + return "\"" + escapeString(key) + "\""; } private String formatValue(String value, Class type) { @@ -158,6 +175,11 @@ private String formatValue(String value, Class type) { return value.toLowerCase(); } + // Null + if (value == null || value.equals("null")) { + return "null"; + } + // Numbers if (Number.class.isAssignableFrom(type) || int.class.isAssignableFrom(type) || @@ -178,14 +200,16 @@ private String escapeString(String str) { .replace("\"", "\\\"") .replace("\n", "\\n") .replace("\r", "\\r") - .replace("\t", "\\t"); + .replace("\t", "\\t") + .replace("\b", "\\b") + .replace("\f", "\\f"); } } public static class FormatReader implements IFormatReader { private final LinkedHashMap values = new LinkedHashMap<>(); private final Stack group = new Stack<>(); - private String currentSection = ""; + private final Map rawParsedData = new LinkedHashMap<>(); public FormatReader(Path path) throws IOException { char[] data = new String(Tools.readAllBytes(path), StandardCharsets.UTF_8).toCharArray(); @@ -193,91 +217,63 @@ public FormatReader(Path path) throws IOException { } private void parseCfg(char[] data) throws IOException { - int i = 0; - int len = data.length; - - while (i < len) { - i = skipWhitespace(data, i, len); - if (i >= len) break; - - char c = data[i]; - - // Skip comments - if (c == '#' || (c == '/' && i + 1 < len && data[i + 1] == '/')) { - i = skipToEndOfLine(data, i, len); - continue; - } + int i = skipWhitespaceAndComments(data, 0, data.length); - // Section header - if (c == '[') { - i = parseSectionHeader(data, i, len); - continue; - } - - // Key-value pair - if (isKeyStart(c)) { - i = parseKeyValue(data, i, len); - continue; - } - - i++; + // CFG top-level should be a mapping + if (i >= data.length || data[i] != '{') { + throw new IOException("CFG file must start with '{'"); } + + i++; // Skip opening brace + parseMapping(data, i, data.length, ""); } - private int parseSectionHeader(char[] data, int start, int len) throws IOException { - int i = start + 1; + private int parseMapping(char[] data, int start, int len, String prefix) throws IOException { + int i = start; + boolean firstEntry = true; - // Skip whitespace - i = skipWhitespace(data, i, len); + while (i < len) { + i = skipWhitespaceAndComments(data, i, len); + if (i >= len) break; - // Parse section name - StringBuilder sectionName = new StringBuilder(); - while (i < len && data[i] != ']') { - if (data[i] == '\n') { - throw new IOException("Unclosed section header at position " + i); + // Check for end of mapping + if (data[i] == '}') { + return i + 1; } - sectionName.append(data[i]); - i++; - } - if (i >= len) { - throw new IOException("Unclosed section header"); - } - - // Skip closing bracket - i++; + // Skip comma or newline separator + if (!firstEntry && (data[i] == ',' || data[i] == '\n')) { + i++; + i = skipWhitespaceAndComments(data, i, len); + if (i >= len) break; + if (data[i] == '}') { + return i + 1; + } + } - currentSection = sectionName.toString().trim(); + firstEntry = false; - // Skip to end of line - return skipToEndOfLine(data, i, len); - } + // Parse key + StringBuilder key = new StringBuilder(); + i = parseKey(data, i, len, key); - private int parseKeyValue(char[] data, int start, int len) throws IOException { - int i = start; + // Skip whitespace + i = skipWhitespaceAndComments(data, i, len); - // Parse key - StringBuilder key = new StringBuilder(); - i = parseKey(data, i, len, key); + // Expect ':' or '=' + if (i >= len || (data[i] != ':' && data[i] != '=')) { + throw new IOException("Expected ':' or '=' after key at position " + i); + } + i++; // Skip separator - // Skip whitespace - i = skipWhitespace(data, i, len); + // Skip whitespace + i = skipWhitespaceAndComments(data, i, len); - // Expect '=' or ':' - if (i >= len || (data[i] != '=' && data[i] != ':')) { - throw new IOException("Expected '=' or ':' after key at position " + i); + // Parse value + String fullKey = prefix.isEmpty() ? key.toString() : prefix + "." + key.toString(); + i = parseValue(data, i, len, fullKey); } - // Skip separator - i++; - - // Skip whitespace - i = skipWhitespace(data, i, len); - - // Parse value - String fullKey = buildFullKey(key.toString()); - i = parseValue(data, i, len, fullKey); - return i; } @@ -285,7 +281,7 @@ private int parseKey(char[] data, int start, int len, StringBuilder key) throws int i = start; char c = data[i]; - // Quoted key + // Quoted key (single or double quotes) if (c == '"' || c == '\'') { char quote = c; i++; @@ -303,8 +299,11 @@ private int parseKey(char[] data, int start, int len, StringBuilder key) throws } i++; // Skip closing quote } else { - // Bare key - while (i < len && (Character.isLetterOrDigit(data[i]) || data[i] == '_' || data[i] == '-' || data[i] == '.')) { + // Identifier key + if (!Character.isLetter(c) && c != '_') { + throw new IOException("Invalid key start character at position " + i); + } + while (i < len && (Character.isLetterOrDigit(data[i]) || data[i] == '_')) { key.append(data[i]); i++; } @@ -316,12 +315,12 @@ private int parseKey(char[] data, int start, int len, StringBuilder key) throws private int parseValue(char[] data, int start, int len, String key) throws IOException { int i = start; if (i >= len) { - throw new IOException("Expected value after '=' at position " + i); + throw new IOException("Expected value after separator at position " + i); } char c = data[i]; - // String + // String (single or double quotes) if (c == '"' || c == '\'') { return parseString(data, i, len, key); } @@ -331,7 +330,28 @@ private int parseValue(char[] data, int start, int len, String key) throws IOExc return parseArray(data, i, len, key); } - // Literal (boolean, number) + // Nested mapping + if (c == '{') { + i++; // Skip opening brace + return parseMapping(data, i, len, key); + } + + // Cross-reference ${...} + if (c == '$' && i + 1 < len && data[i + 1] == '{') { + return parseReference(data, i, len, key); + } + + // Include @'file' + if (c == '@') { + return parseInclude(data, i, len, key); + } + + // Special values `...` + if (c == '`') { + return parseSpecialValue(data, i, len, key); + } + + // Literal (boolean, number, null) return parseLiteral(data, i, len, key); } @@ -345,8 +365,6 @@ private int parseString(char[] data, int start, int len, String key) throws IOEx if (data[i] == '\\' && i + 1 < len) { i++; value.append(unescapeChar(data[i])); - } else if (data[i] == '\n') { - throw new IOException("Newline not allowed in single-line string at position " + i); } else { value.append(data[i]); } @@ -354,20 +372,21 @@ private int parseString(char[] data, int start, int len, String key) throws IOEx } if (i >= len) { - throw new IOException("Unclosed string"); + throw new IOException("Unclosed string at position " + i); } i++; // Skip closing quote values.put(key, value.toString()); - return skipToEndOfLine(data, i, len); + return i; } private int parseArray(char[] data, int start, int len, String key) throws IOException { int i = start + 1; List array = new ArrayList<>(); + boolean firstElement = true; while (i < len) { - i = skipWhitespace(data, i, len); + i = skipWhitespaceAndComments(data, i, len); if (i >= len) { throw new IOException("Unclosed array"); } @@ -376,27 +395,27 @@ private int parseArray(char[] data, int start, int len, String key) throws IOExc if (data[i] == ']') { i++; values.put(key, array.toArray(new String[0])); - return skipToEndOfLine(data, i, len); + return i; } - // Skip comments - if (data[i] == '#' || (data[i] == '/' && i + 1 < len && data[i + 1] == '/')) { - i = skipToEndOfLine(data, i, len); - continue; + // Skip comma or newline separator + if (!firstElement && (data[i] == ',' || data[i] == '\n')) { + i++; + i = skipWhitespaceAndComments(data, i, len); + if (i >= len) break; + if (data[i] == ']') { + i++; + values.put(key, array.toArray(new String[0])); + return i; + } } + firstElement = false; + // Parse array element StringBuilder element = new StringBuilder(); i = parseArrayElement(data, i, len, element); array.add(element.toString()); - - // Skip whitespace - i = skipWhitespace(data, i, len); - - // Check for comma - if (i < len && data[i] == ',') { - i++; - } } throw new IOException("Unclosed array"); @@ -426,8 +445,8 @@ private int parseArrayElement(char[] data, int start, int len, StringBuilder ele return i; } - // Literal (number, boolean) - while (i < len && data[i] != ',' && data[i] != ']' && data[i] != '\n' && data[i] != '#' && !Character.isWhitespace(data[i])) { + // Literal (number, boolean, null) + while (i < len && !Character.isWhitespace(data[i]) && data[i] != ',' && data[i] != ']' && data[i] != '#') { element.append(data[i]); i++; } @@ -435,30 +454,112 @@ private int parseArrayElement(char[] data, int start, int len, StringBuilder ele return i; } - private int parseLiteral(char[] data, int start, int len, String key) { + private int parseLiteral(char[] data, int start, int len, String key) throws IOException { int i = start; StringBuilder value = new StringBuilder(); - while (i < len && data[i] != '\n' && data[i] != '#' && !(data[i] == '/' && i + 1 < len && data[i + 1] == '/')) { - if (!Character.isWhitespace(data[i])) { - value.append(data[i]); - } else if (value.length() > 0) { - // Stop at first whitespace after non-whitespace content - break; - } + while (i < len && !Character.isWhitespace(data[i]) && data[i] != ',' && data[i] != '}' && data[i] != ']' && data[i] != '#') { + value.append(data[i]); i++; } String literal = value.toString().trim(); values.put(key, literal); - return skipToEndOfLine(data, i, len); + return i; } - private int skipWhitespace(char[] data, int start, int len) { - int i = start; - while (i < len && Character.isWhitespace(data[i]) && data[i] != '\n') { + private int parseReference(char[] data, int start, int len, String key) throws IOException { + // Parse ${...} cross-reference + // For now, store as string for basic support + int i = start + 2; // Skip ${ + StringBuilder ref = new StringBuilder("${"); + + while (i < len && data[i] != '}') { + ref.append(data[i]); i++; } + + if (i >= len) { + throw new IOException("Unclosed reference"); + } + + ref.append('}'); + i++; // Skip closing } + + values.put(key, ref.toString()); + return i; + } + + private int parseInclude(char[] data, int start, int len, String key) throws IOException { + // Parse @'file.cfg' include directive + // For now, store as string for basic support + int i = start + 1; // Skip @ + + i = skipWhitespaceAndComments(data, i, len); + if (i >= len || (data[i] != '"' && data[i] != '\'')) { + throw new IOException("Expected quoted filename after @ at position " + i); + } + + char quote = data[i]; + i++; + StringBuilder filename = new StringBuilder("@"); + filename.append(quote); + + while (i < len && data[i] != quote) { + filename.append(data[i]); + i++; + } + + if (i >= len) { + throw new IOException("Unclosed include filename"); + } + + filename.append(quote); + i++; // Skip closing quote + + values.put(key, filename.toString()); + return i; + } + + private int parseSpecialValue(char[] data, int start, int len, String key) throws IOException { + // Parse `...` special values + // For now, store as string for basic support + int i = start + 1; // Skip opening ` + StringBuilder special = new StringBuilder("`"); + + while (i < len && data[i] != '`') { + special.append(data[i]); + i++; + } + + if (i >= len) { + throw new IOException("Unclosed special value"); + } + + special.append('`'); + i++; // Skip closing ` + + values.put(key, special.toString()); + return i; + } + + private int skipWhitespaceAndComments(char[] data, int start, int len) { + int i = start; + while (i < len) { + // Skip whitespace + if (Character.isWhitespace(data[i])) { + i++; + continue; + } + + // Skip comments + if (data[i] == '#') { + i = skipToEndOfLine(data, i, len); + continue; + } + + break; + } return i; } @@ -473,10 +574,6 @@ private int skipToEndOfLine(char[] data, int start, int len) { return i; } - private boolean isKeyStart(char c) { - return Character.isLetterOrDigit(c) || c == '_' || c == '"' || c == '\''; - } - private char unescapeChar(char c) { return switch (c) { case 'n' -> '\n'; @@ -491,13 +588,6 @@ private char unescapeChar(char c) { }; } - private String buildFullKey(String key) { - if (currentSection.isEmpty()) { - return key; - } - return currentSection + "." + key; - } - @Override public String read(String fieldName) { String key = Tools.concat("", (!group.isEmpty() ? "." : "") + fieldName, '.', group); From ce9ff3b0bf96e499c2a9b83231e967f572882b5b Mon Sep 17 00:00:00 2001 From: J-RAP Date: Mon, 17 Nov 2025 06:02:39 -0600 Subject: [PATCH 4/6] use CFG for NumberSpec --- src/test/java/specs/NumberSpec.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/specs/NumberSpec.java b/src/test/java/specs/NumberSpec.java index 343b519..a6d7963 100644 --- a/src/test/java/specs/NumberSpec.java +++ b/src/test/java/specs/NumberSpec.java @@ -1,5 +1,6 @@ package specs; +import org.omegaconfig.OmegaConfig; import org.omegaconfig.api.annotations.NumberConditions; import org.omegaconfig.api.annotations.Spec; import org.omegaconfig.impl.fields.ListField; @@ -8,7 +9,7 @@ import java.util.ArrayList; import java.util.List; -@Spec("number") +@Spec(value = "number", format = OmegaConfig.FORMAT_CFG) public class NumberSpec { @Spec.Field public static byte aByte = 1; From cc295012a2fab97ce3969a24dfc85de89699924f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Nov 2025 12:26:24 +0000 Subject: [PATCH 5/6] Fix CFGFormat: Add newlines after values to prevent comment misplacement The comments were appearing on the same line as the previous field's value because newlines were missing after writing values and arrays. This caused comments for field N to appear at the end of field N-1's line. Fixed by adding newlines after: - Single value writes - Array writes Now the output properly shows: field1: value1 # Comment for field2 field2: value2 Instead of: field1: value1 # Comment for field2 field2: value2 --- src/main/java/org/omegaconfig/impl/formats/CFGFormat.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/omegaconfig/impl/formats/CFGFormat.java b/src/main/java/org/omegaconfig/impl/formats/CFGFormat.java index fda4021..186b534 100644 --- a/src/main/java/org/omegaconfig/impl/formats/CFGFormat.java +++ b/src/main/java/org/omegaconfig/impl/formats/CFGFormat.java @@ -75,6 +75,7 @@ public void write(String fieldName, String value, Class type, Class subTyp indent(); this.buffer.append(formatKey(fieldName)).append(": "); this.buffer.append(formatValue(value, type)); + this.buffer.append("\n"); } @Override @@ -105,6 +106,7 @@ public void write(String fieldName, String[] values, Class type, Class sub indent(); } this.buffer.append("]"); + this.buffer.append("\n"); } @Override From 26a640b0d6168fb9a3119eb6b297cc6d3268a728 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Nov 2025 12:33:14 +0000 Subject: [PATCH 6/6] Adjust CFGFormat comment spacing: blank line before comments, not after Changed comment placement logic to add a blank line BEFORE comments (when present), not after field values. This produces the desired format: field1: value1 # Comment for field2 field2: value2 Instead of: field1: value1 # Comment for field2 field2: value2 The comment is now tightly coupled with its field, with separation from the previous field via a blank line above the comment block. --- .../omegaconfig/impl/formats/CFGFormat.java | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/omegaconfig/impl/formats/CFGFormat.java b/src/main/java/org/omegaconfig/impl/formats/CFGFormat.java index 186b534..dd3a2de 100644 --- a/src/main/java/org/omegaconfig/impl/formats/CFGFormat.java +++ b/src/main/java/org/omegaconfig/impl/formats/CFGFormat.java @@ -64,28 +64,30 @@ public void write(String comment) { @Override public void write(String fieldName, String value, Class type, Class subType) { - writeComments(); - - if (!firstInMapping) { + // Add blank line before comments (if not first and has comments) + if (!firstInMapping && !comments.isEmpty()) { this.buffer.append("\n"); } - firstInMapping = false; + + writeComments(); // Write key indent(); this.buffer.append(formatKey(fieldName)).append(": "); this.buffer.append(formatValue(value, type)); this.buffer.append("\n"); + + firstInMapping = false; } @Override public void write(String fieldName, String[] values, Class type, Class subType) { - writeComments(); - - if (!firstInMapping) { + // Add blank line before comments (if not first and has comments) + if (!firstInMapping && !comments.isEmpty()) { this.buffer.append("\n"); } - firstInMapping = false; + + writeComments(); // Write key indent(); @@ -107,16 +109,18 @@ public void write(String fieldName, String[] values, Class type, Class sub } this.buffer.append("]"); this.buffer.append("\n"); + + firstInMapping = false; } @Override public void push(String groupName) { - writeComments(); - - if (!firstInMapping) { + // Add blank line before comments (if not first and has comments) + if (!firstInMapping && !comments.isEmpty()) { this.buffer.append("\n"); } - firstInMapping = false; + + writeComments(); indent(); this.buffer.append(formatKey(groupName)).append(": {\n");