///|
/// Limits are local resource policies, not restrictions imposed by GEDCOM 7.
pub struct Limits {
max_nodes : Int32
max_depth : Int32
max_line_units : Int32
max_total_units : Int32
}
///|
pub fn Limits::new(
max_nodes~ : Int32 = 100000,
max_depth~ : Int32 = 128,
max_line_units~ : Int32 = 1048576,
max_total_units~ : Int32 = 16777216,
) -> Limits raise GedError {
if max_nodes < 1 ||
max_nodes > 1000000 ||
max_depth < 1 ||
max_depth > 1024 ||
max_line_units < 1 ||
max_total_units < 1 {
raise Limit("limits must be positive; nodes <= 1000000 and depth <= 1024")
}
{ max_nodes, max_depth, max_line_units, max_total_units, }
}
///|
/// Incremental Unicode string chunks. Chunk boundaries must not split a character.
pub struct Parser {
priv limits : Limits
priv nodes : Array[Node]
priv stack : Array[Int32]
priv has_children : Array[Bool]
priv continuations : Map[Int32, Array[String]]
priv mut pending : String
priv mut total : Int32
priv mut line : Int32
priv mut after_cr : Bool
priv mut closed : Bool
priv mut failed : Bool
}
///|
pub fn Parser::new(limits? : Limits) -> Parser raise GedError {
let limits = match limits {
Some(l) => l
None => Limits::new()
}
{
limits,
nodes: [],
stack: [],
has_children: [],
continuations: Map([]),
pending: "",
total: 0,
line: 0,
after_cr: false,
closed: false,
failed: false,
}
}
///|
fn valid_tag_char(c : UInt16) -> Bool {
(c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c == 95
}
///|
fn valid_id(s : String) -> Bool {
s.length() > 0 && s != "VOID" && s.code_units().all(valid_tag_char)
}
///|
fn valid_tag(s : String) -> Bool {
if s.length() == 0 {
return false
}
let first = s[0]
((first >= 65 && first <= 90) || (first == 95 && s.length() > 1)) &&
s.code_units().all(valid_tag_char)
}
///|
fn check_chars(s : String, line : Int32) -> Unit raise GedError {
let units = s.code_units()
let mut i = 0
while i < units.length() {
let c = units[i]
if (c < 32 && c != 9) || (c >= 127 && c <= 159) || c == 65534 || c == 65535 {
raise Syntax(line, "forbidden control or noncharacter")
}
if c >= 55296 && c <= 56319 {
if i + 1 >= units.length() || units[i + 1] < 56320 || units[i + 1] > 57343 {
raise Syntax(line, "unpaired high surrogate")
}
i = i + 2
} else if c >= 56320 && c <= 57343 {
raise Syntax(line, "unpaired low surrogate")
} else {
i = i + 1
}
}
}
///|
fn read_token(
s : String,
start : Int32,
line : Int32,
) -> (String, Int32) raise GedError {
let mut end = start
while end < s.length() && s[end] != 32 {
end = end + 1
}
if end == start {
raise Syntax(line, "expected token separated by one space")
}
(s[start:end].to_owned(), if end < s.length() { end + 1 } else { end })
}
///|
fn parse_line(s : String, line : Int32, depth : Int32) -> Node raise GedError {
check_chars(s, line)
let (level_text, after_level) = read_token(s, 0, line)
if after_level == s.length() ||
!level_text.code_units().all(fn(c) { c >= 48 && c <= 57 }) ||
(level_text.length() > 1 && level_text[0] == 48) {
raise Syntax(line, "invalid level or missing tag")
}
let level = @string.parse_int(level_text) catch {
_ => raise Syntax(line, "level is outside integer range")
}
if level > depth {
raise Limit("nesting depth exceeded")
}
let (token, after_token) = read_token(s, after_level, line)
let (xref, tag, value_start) = if token.has_prefix("@") {
if token.length() < 3 || !token.has_suffix("@") {
raise Syntax(line, "invalid cross-reference identifier")
}
let id = token[1:token.length() - 1].to_owned()
if !valid_id(id) {
raise Syntax(line, "invalid or reserved identifier")
}
let (tag, after_tag) = read_token(s, after_token, line)
(Some(id), tag, after_tag)
} else {
(None, token, after_token)
}
if !valid_tag(tag) {
raise Syntax(line, "invalid tag")
}
// A delimiter with no line value is not a GEDCOM 7 line.
if s.has_suffix(" ") && value_start == s.length() {
raise Syntax(line, "empty line value must omit its delimiter")
}
let value = s[value_start:].to_owned()
let payload = if value == "" {
Empty
} else if value == "@VOID@" {
Void
} else if value.has_prefix("@@") {
Text(value[1:].to_owned())
} else if value.has_prefix("@") {
if !value.has_suffix("@") || value.length() < 3 {
raise Syntax(line, "leading @ text must be escaped as @@")
}
let id = value[1:value.length() - 1].to_owned()
if !valid_id(id) {
raise Syntax(line, "invalid pointer")
}
Pointer(id)
} else {
Text(value)
}
{ line, level, xref, tag, payload, parent: None, }
}
///|
fn Parser::take_line(self : Parser, value : String) -> Unit raise GedError {
self.line = self.line + 1
let value = if self.line == 1 && value.has_prefix("\u{FEFF}") {
value[1:].to_owned()
} else {
value
}
if value.length() == 0 {
raise Syntax(self.line, "blank lines are not GEDCOM 7")
}
let node = parse_line(value, self.line, self.limits.max_depth)
if node.level > self.stack.length() {
raise Syntax(self.line, "level jump or child of CONT")
}
while self.stack.length() > node.level {
self.stack.remove(self.stack.length() - 1) |> ignore
}
let parent = if node.level > 0 {
Some(self.stack[node.level - 1])
} else {
None
}
if node.tag == "CONT" {
let parent = match parent {
Some(p) => p
None => raise Syntax(self.line, "CONT cannot be a record")
}
if node.xref != None || self.has_children[parent] {
raise Syntax(
self.line,
"CONT must precede ordinary substructures and cannot have an id",
)
}
let base = self.nodes[parent]
let first = match base.payload {
Empty => ""
Text(t) => t
_ => raise Syntax(self.line, "pointer cannot have CONT")
}
let next = match node.payload {
Empty => ""
Text(t) => t
_ => raise Syntax(self.line, "CONT cannot be a pointer")
}
self.continuations.get_or_init(parent, fn() { [first] }).push(next)
} else {
if self.nodes.length() >= self.limits.max_nodes {
raise Limit("node count exceeded")
}
if parent is Some(p) {
self.has_children[p] = true
}
let index = self.nodes.length()
self.nodes.push({
line: node.line,
level: node.level,
xref: node.xref,
tag: node.tag,
payload: node.payload,
parent,
})
self.has_children.push(false)
self.stack.push(index)
}
}
///|
fn Parser::feed_impl(self : Parser, chunk : String) -> Unit raise GedError {
if self.closed || self.failed {
raise InvalidState("parser is closed or failed")
}
// StringView slices clamp split surrogate pairs. Reject invalid strings before
// slicing rather than silently losing a character supplied by a JS host.
let mut u = 0
while u < chunk.length() {
let c = chunk[u]
if c >= 55296 && c <= 56319 {
if u + 1 >= chunk.length() || chunk[u + 1] < 56320 || chunk[u + 1] > 57343 {
raise Syntax(self.line + 1, "string chunk splits a Unicode character")
}
u = u + 2
} else if c >= 56320 && c <= 57343 {
raise Syntax(self.line + 1, "unpaired low surrogate in string chunk")
} else {
u = u + 1
}
}
if chunk.length() > self.limits.max_total_units - self.total {
raise Limit("input size exceeded")
}
self.total = self.total + chunk.length()
let mut start = 0
for i = 0; i < chunk.length(); i = i + 1 {
let c = chunk[i]
if self.after_cr {
self.after_cr = false
if c == 10 {
start = i + 1
continue
}
}
if c == 10 || c == 13 {
if i - start > self.limits.max_line_units - self.pending.length() {
raise Limit("line size exceeded")
}
let line = self.pending + chunk[start:i].to_owned()
self.pending = ""
self.take_line(line)
self.after_cr = c == 13
start = i + 1
}
}
if chunk.length() - start > self.limits.max_line_units - self.pending.length() {
raise Limit("line size exceeded")
}
self.pending = self.pending + chunk[start:].to_owned()
}
///|
pub fn Parser::feed(self : Parser, chunk : String) -> Unit raise GedError {
errdefer {
self.failed = true
}
self.feed_impl(chunk)
}
///|
pub fn Parser::finish(self : Parser) -> Document raise GedError {
if self.closed || self.failed {
raise InvalidState("parser is closed or failed")
}
self.closed = true
if self.pending != "" {
self.take_line(self.pending)
}
if self.nodes.length() == 0 {
raise Syntax(1, "empty document")
}
let records = []
let ids : Map[String, Int32] = Map([])
for i, node in self.nodes {
if node.level == 0 {
records.push(i)
}
// First definition is indexed; duplicates remain available to the auditor.
if node.xref is Some(id) && ids.get(id) == None {
ids.set(id, i)
}
}
let nodes = self.nodes.copy()
for (index, parts) in self.continuations {
let base = nodes[index]
nodes[index] = {
line: base.line,
level: base.level,
xref: base.xref,
tag: base.tag,
payload: Text(parts.join("\n")),
parent: base.parent,
}
}
{ nodes, records, ids, }
}
///|
pub fn parse(text : String, limits? : Limits) -> Document raise GedError {
let parser = Parser::new(limits?)
parser.feed(text)
parser.finish()
}
///|
/// Decode strictly; malformed UTF-8 is rejected instead of silently replaced.
pub fn parse_bytes(bytes : Bytes, limits? : Limits) -> Document raise GedError {
let text = @utf8.decode(bytes) catch { _ => raise Syntax(1, "invalid UTF-8") }
parse(text, limits?)
}