#include "secsgem/secs2/sml.hpp" #include #include #include #include namespace secsgem::secs2 { namespace { struct Cursor { const std::string& s; std::size_t i = 0; bool done() const { return i >= s.size(); } char peek() const { return s[i]; } char get() { return s[i++]; } void skip_ws() { while (i < s.size() && std::isspace(static_cast(s[i]))) ++i; } void expect(char c) { skip_ws(); if (i >= s.size() || s[i] != c) throw SmlError(std::string("expected `") + c + "` at " + std::to_string(i)); ++i; } bool consume(char c) { skip_ws(); if (i < s.size() && s[i] == c) { ++i; return true; } return false; } std::string read_word() { skip_ws(); const std::size_t start = i; while (i < s.size() && !std::isspace(static_cast(s[i])) && s[i] != '<' && s[i] != '>' && s[i] != '[' && s[i] != ']' && s[i] != '"') { ++i; } if (i == start) throw SmlError("expected word at " + std::to_string(i)); return s.substr(start, i - start); } std::string read_quoted() { skip_ws(); if (i >= s.size() || s[i] != '"') throw SmlError("expected `\"`"); ++i; const std::size_t start = i; while (i < s.size() && s[i] != '"') { // No escape sequences in the SML emitter; mirror that here. ++i; } if (i >= s.size()) throw SmlError("unterminated string"); std::string out = s.substr(start, i - start); ++i; // consume closing quote return out; } }; Format parse_tag(const std::string& tag) { if (tag == "L") return Format::List; if (tag == "A") return Format::ASCII; if (tag == "J") return Format::JIS8; if (tag == "C") return Format::C2; if (tag == "B") return Format::Binary; if (tag == "BOOLEAN") return Format::Boolean; if (tag == "U1") return Format::U1; if (tag == "U2") return Format::U2; if (tag == "U4") return Format::U4; if (tag == "U8") return Format::U8; if (tag == "I1") return Format::I1; if (tag == "I2") return Format::I2; if (tag == "I4") return Format::I4; if (tag == "I8") return Format::I8; if (tag == "F4") return Format::F4; if (tag == "F8") return Format::F8; throw SmlError("unknown SML tag `" + tag + "`"); } // Parse one byte literal — accepts 0xHH and decimal forms. uint8_t parse_byte(const std::string& w) { return static_cast(std::strtoul(w.c_str(), nullptr, 0)); } Item parse_item(Cursor& c); Item parse_scalar_body(Cursor& c, Format fmt) { switch (fmt) { case Format::ASCII: case Format::JIS8: { auto s = c.read_quoted(); return fmt == Format::ASCII ? Item::ascii(s) : Item::jis8(s); } case Format::C2: { // C2 is a vector of 16-bit code points; SML round-trip for C2 is // not yet specified by the emitter — accept whitespace-separated // integers for now. std::vector v; while (true) { c.skip_ws(); if (c.done() || c.peek() == '>') break; v.push_back(static_cast(std::strtoul(c.read_word().c_str(), nullptr, 0))); } return Item::c2(v); } case Format::Binary: { std::vector bytes; while (true) { c.skip_ws(); if (c.done() || c.peek() == '>') break; bytes.push_back(parse_byte(c.read_word())); } return Item::binary(bytes); } case Format::Boolean: { std::vector bytes; while (true) { c.skip_ws(); if (c.done() || c.peek() == '>') break; auto w = c.read_word(); bool b = (w == "T" || w == "TRUE" || w == "true" || w == "1"); bytes.push_back(b ? 1 : 0); } return Item::boolean(bytes); } #define SML_NUM(fmtT, ctype, factory, parser) \ case Format::fmtT: { \ std::vector v; \ while (true) { \ c.skip_ws(); \ if (c.done() || c.peek() == '>') break; \ auto w = c.read_word(); \ v.push_back(static_cast(parser(w.c_str(), nullptr, 0))); \ } \ return factory(v); \ } SML_NUM(U1, uint8_t, Item::u1, std::strtoul) SML_NUM(U2, uint16_t, Item::u2, std::strtoul) SML_NUM(U4, uint32_t, Item::u4, std::strtoul) SML_NUM(U8, uint64_t, Item::u8, std::strtoull) SML_NUM(I1, int8_t, Item::i1, std::strtol) SML_NUM(I2, int16_t, Item::i2, std::strtol) SML_NUM(I4, int32_t, Item::i4, std::strtol) SML_NUM(I8, int64_t, Item::i8, std::strtoll) #undef SML_NUM case Format::F4: { std::vector v; while (true) { c.skip_ws(); if (c.done() || c.peek() == '>') break; v.push_back(std::strtof(c.read_word().c_str(), nullptr)); } return Item::f4(v); } case Format::F8: { std::vector v; while (true) { c.skip_ws(); if (c.done() || c.peek() == '>') break; v.push_back(std::strtod(c.read_word().c_str(), nullptr)); } return Item::f8(v); } case Format::List: // Unreachable; handled in parse_item. throw SmlError("internal: List shape in scalar dispatch"); } throw SmlError("unhandled format in scalar dispatch"); } Item parse_item(Cursor& c) { c.expect('<'); auto tag = c.read_word(); Format fmt = parse_tag(tag); // Optional `[n]` count — accepted, ignored. if (c.consume('[')) { (void)c.read_word(); c.expect(']'); } if (fmt == Format::List) { Item::List children; while (true) { c.skip_ws(); if (c.done()) throw SmlError("unterminated list"); if (c.peek() == '>') { c.get(); break; } children.push_back(parse_item(c)); } return Item::list(std::move(children)); } Item result = parse_scalar_body(c, fmt); c.expect('>'); return result; } } // namespace Item parse_sml(const std::string& text) { Cursor c{text}; c.skip_ws(); return parse_item(c); } std::optional try_parse_sml(const std::string& text) { try { return parse_sml(text); } catch (const SmlError&) { return std::nullopt; } } } // namespace secsgem::secs2