///|
priv struct MetaStats { depth : Int; weight : Int }
///|
priv struct MetaFrame { anchor : Int; mut depth : Int; mut weight : Int }
///|
priv struct MetadataGuard {
max_depth : Int
budget : Int
mut used : Int
frames : Array[MetaFrame]
anchors : Map[Int,MetaStats]
mut failure : String?
empty_tags : Array[(Int,String)]
scalars : Array[(Int,Int,String)]
input : String
offsets : Array[Int]
}
///|
fn MetadataGuard::charge(self : MetadataGuard, units : Int) -> Bool {
if self.failure != None { return false }
if units < 0 || units > self.budget-self.used { self.failure=Some("work-limit"); false }
else { self.used+=units; true }
}
///|
fn MetadataGuard::accept(self : MetadataGuard, stats : MetaStats, anchor : Int) -> Unit {
if self.failure != None { return }
if stats.depth > self.max_depth { self.failure=Some("depth-limit"); return }
if anchor != 0 { self.anchors[anchor]=stats }
if self.frames.last() is Some(parent) {
if stats.depth >= self.max_depth { self.failure=Some("depth-limit"); return }
parent.depth=parent.depth.max(stats.depth+1)
if stats.weight > self.budget-parent.weight { self.failure=Some("work-limit"); return }
parent.weight+=stats.weight
}
}
///|
impl @yaml_events.MarkedEventReceiver for MetadataGuard with on_event(self,event,marker) {
if self.failure != None { return }
match event {
Scalar(value~,style~,tag~,id~) => {
if !self.charge(value.length()) { return }
let explicit_numeric=tag.map(tag => tag.handle+tag.suffix).map(name => name == "tag:yaml.org,2002:int" || name == "tag:yaml.org,2002:float").unwrap_or(false)
if (style is @yaml_events.TScalarStyle::Plain || explicit_numeric) && value.length() > 0 && [43,45,46,48,49,50,51,52,53,54,55,56,57].contains(ch(value,0)) {
let resolved=native_number(value)
if explicit_numeric && resolved == None { self.failure=Some("syntax"); return }
if tag == None || explicit_numeric {
let replacement=match resolved { Some(number) if number.is_nan() || (number.is_pos_inf() || number.is_neg_inf()) => value; Some(number) => number.to_string(); None => Json(value).stringify() }
if replacement != value || !(style is @yaml_events.TScalarStyle::Plain) {
let start=self.offsets[marker.index]
let mut length=value.length()
if style is @yaml_events.TScalarStyle::SingleQuoted || style is @yaml_events.TScalarStyle::DoubleQuoted {
let quote=ch(self.input,start); let mut end=start+1
while end < self.input.length() {
if ch(self.input,end) == quote && !(quote == 34 && escaped(self.input,end)) {
if quote == 39 && ch(self.input,end+1) == 39 { end+=2; continue }
break
}
end+=width(self.input,end)
}
length=end+1-start
}
self.scalars.push((marker.index,length,replacement))
}
}
}
if tag is Some(tag) {
let name=tag.handle+tag.suffix
if name == "tag:yaml.org,2002:bool" && !["true","True","TRUE","false","False","FALSE"].contains(value) { self.failure=Some("syntax"); return }
if name == "tag:yaml.org,2002:int" && (value.contains(".") || value.contains("e") || value.contains("E")) && !value.contains("0x") { self.failure=Some("syntax"); return }
if name == "tag:yaml.org,2002:float" && (value.contains("0x") || value.contains("0o") || value.contains("0b")) { self.failure=Some("syntax"); return }
if name == "tag:yaml.org,2002:map" || name == "tag:yaml.org,2002:seq" {
if value != "" { self.failure=Some("syntax"); return }
self.empty_tags.push((marker.index,if name.has_suffix("map") { "map" } else { "seq" }))
} else if !["!","tag:yaml.org,2002:str","tag:yaml.org,2002:null","tag:yaml.org,2002:bool","tag:yaml.org,2002:int","tag:yaml.org,2002:float"].contains(name) { self.failure=Some("syntax"); return }
}
if self.charge(1) { self.accept({depth:0,weight:1},id) }
}
SequenceStart(id~,tag~) | MappingStart(id~,tag~) => {
if tag is Some(tag) && !["!","tag:yaml.org,2002:map","tag:yaml.org,2002:seq"].contains(tag.handle+tag.suffix) { self.failure=Some("syntax"); return }
if !self.charge(1) { return }
if self.frames.length() > self.max_depth { self.failure=Some("depth-limit"); return }
self.frames.push({anchor:id,depth:0,weight:1})
}
SequenceEnd | MappingEnd => {
guard self.frames.pop() is Some(frame) else { self.failure=Some("syntax"); return }
self.accept({depth:frame.depth,weight:frame.weight},frame.anchor)
}
Alias(id~) => {
guard self.anchors.get(id) is Some(stats) else { self.failure=Some("depth-limit"); return }
if self.charge(1) && self.charge(stats.weight) { self.accept(stats,0) }
}
_ => ()
}
}
///|
/// The alias-free pass validates physical structure without recursive alias
/// weighing. Replacing '*' inside quoted strings/comments changes only values,
/// which are unused by this structural pass. Last-key wins avoids false key
/// collisions introduced by that replacement; the real pass rejects duplicates.
fn metadata_preflight(input : String, context : ParseContext) -> String? {
let budget=context.limits.metadata_work.min((context.limits.work-context.work).max(0))
try {
let mut physical=input.replace_all(old="*",new="X")
for tag in ["map","seq","str","null","bool","int","float"] { physical=physical.replace_all(old="!!"+tag,new="").replace_all(old="!",new="") }
physical=physical.replace_all(old="e",new="q").replace_all(old="E",new="Q")
let _=@yaml.loads(physical,flavor=@yaml.Flavor::new(depth=context.limits.metadata_depth,duplicates=Last,aliases=false,budget~))
} catch {
@yaml.Refused::TooDeep(_) => { context.failure=Some("depth-limit"); return None }
@yaml.Refused::Exceeded(..) => { context.failure=Some("work-limit"); return None }
_ => { context.failure=Some("syntax"); return None }
}
let offsets=[0]; let mut units=0
for c in input { units+=if c.to_int() > 65535 { 2 } else { 1 }; offsets.push(units) }
let preflight : MetadataGuard={max_depth:context.limits.metadata_depth,budget,used:0,frames:[],anchors:Map([]),failure:None,empty_tags:[],scalars:[],input,offsets}
try {
@yaml_events.Parser::Parser(input).load(preflight,false)
} catch {
_ => { context.failure=Some("syntax"); return None }
}
if preflight.failure is Some(code) { context.failure=Some(code); return None }
context.work+=preflight.used
normalize_scalars(input,preflight.empty_tags,preflight.scalars)
}
///|
fn normalize_scalars(input : String, tags : Array[(Int,String)], scalars : Array[(Int,Int,String)]) -> String? {
let edits : Array[(Int,Int,String)]=[]; let mut lower=0
let offsets=[0]; let mut units=0
for c in input { units+=if c.to_int() > 65535 { 2 } else { 1 }; offsets.push(units) }
for (character_index,kind) in tags {
if character_index < 0 || character_index >= offsets.length() { return None }
let offset=offsets[character_index]
let mut found : (Int,Int)?=None; let mut i=lower; let mut quote=0; let mut comment=false
while i < offset {
let c=ch(input,i)
if c == 10 || c == 13 { comment=false }
if !comment {
if quote == 0 && c == 35 && (i == 0 || is_space(ch(input,i-1))) { comment=true }
if c == 34 && !(quote == 34 && escaped(input,i)) { if quote == 34 { quote=0 } else if quote == 0 { quote=34 } }
if c == 39 { if quote == 39 { if ch(input,i+1) == 39 { i+=2; continue }; quote=0 } else if quote == 0 { quote=39 } }
if quote == 0 && !comment {
for token in ["!!"+kind,"!"] {
if input[i:].has_prefix(token) && (i == 0 || is_space(ch(input,i-1)) || [58,45,91,44].contains(ch(input,i-1))) {
let end=i+token.length()
if end <= offset && (end == input.length() || is_space(ch(input,end)) || [10,13,44,93,125].contains(ch(input,end))) { found=Some((i,end)) }
}
}
}
}
i+=width(input,i)
}
guard found is Some((start,end)) else { return None }
edits.push((start,end,if kind == "map" { "{}" } else { "[]" })); lower=offset
}
for (character_index,length,replacement) in scalars {
if character_index < 0 || character_index >= offsets.length() { return None }
let start=offsets[character_index]; let end=start+length
if end > input.length() { return None }; edits.push((start,end,replacement))
}
edits.sort_by((a,b) => a.0.compare(b.0))
let out=StringBuilder(); let mut consumed=0
for (start,end,replacement) in edits {
if start < consumed { return None }
out.write_string(part(input,consumed,start)); out.write_string(replacement); consumed=end
}
out.write_string(part(input,consumed,input.length()))
Some(out.to_string())
}
///|
/// js-yaml 4.3 JSON_SCHEMA accepts finite numeric literals and explicit
/// infinities/NaN. Unlike MoonYAML's core resolver, overflowing literals remain
/// strings and binary integers are numbers. This scanner also avoids delegating
/// attacker-sized exponent loops to the dependency.
fn native_number(value : String) -> Double? {
let mut i=0; let mut sign=1.0
if ch(value,0) == 43 || ch(value,0) == 45 { if ch(value,0) == 45 { sign = -1.0 }; i+=1 }
let rest=part(value,i,value.length())
if [".inf",".Inf",".INF"].contains(rest) { return Some(sign*(1.0/0.0)) }
if i == 0 && [".nan",".NaN",".NAN"].contains(rest) { return Some(0.0/0.0) }
if rest.length() > 2 && ch(rest,0) == 48 {
let radix=match ch(rest,1) { 98 => 2; 111 => 8; 120 => 16; _ => 0 }
if radix != 0 {
let mut number=0.0
for j=2; j=48 && c<=57 { c-48 } else if c>=65 && c<=70 { c-55 } else if c>=97 && c<=102 { c-87 } else { return None }
if digit>=radix { return None }; number=number*radix.to_double()+digit.to_double()
}
return if (number.is_pos_inf() || number.is_neg_inf()) { None } else { Some(sign*number) }
}
}
let start=i
while ch(value,i)>=48 && ch(value,i)<=57 { i+=1 }
let whole=i-start
if ch(value,i) == 46 {
i+=1; let fraction=i
while ch(value,i)>=48 && ch(value,i)<=57 { i+=1 }
if whole == 0 && (start != 0 || i == fraction) { return None }
} else if whole == 0 { return None }
if ch(value,i) == 101 || ch(value,i) == 69 {
i+=1; if ch(value,i) == 43 || ch(value,i) == 45 { i+=1 }
let exponent=i; while ch(value,i)>=48 && ch(value,i)<=57 { i+=1 }
if i == exponent { return None }
}
if i != value.length() { return None }
try { let number=@string.parse_double(value[:]); if (number.is_pos_inf() || number.is_neg_inf()) || number.is_nan() { None } else { Some(number) } } catch { _ => None }
}