///|
/// A projected record keeps an IP miss distinct from an absent field.
pub(all) struct Projection {
record_found : Bool
prefix_length : Int
fields : Array[(String, Value?)]
} derive(Eq, @debug.Debug)
///|
fn pointer_tokens(pointer : String) -> Array[String] raise MmdbError {
if pointer.length() > 2048 {
raise MmdbError("path-limit", -1, "Pointer exceeds 2048 UTF-16 code units")
}
if pointer.is_empty() {
return []
}
if pointer[0] != '/' {
raise MmdbError(
"invalid-path", -1, "JSON Pointer must be empty or start with slash",
)
}
let tokens : Array[String] = []
for part in pointer[1:].split("/") {
if tokens.length() >= 128 {
raise MmdbError("path-limit", -1, "Pointer exceeds 128 segments")
}
let out = StringBuilder()
let mut escaped = false
for ch in part {
if escaped {
match ch {
'0' => out.write_char('~')
'1' => out.write_char('/')
_ =>
raise MmdbError(
"invalid-path", -1, "Only ~0 and ~1 escapes are allowed",
)
}
escaped = false
} else if ch == '~' {
escaped = true
} else {
out.write_char(ch)
}
}
if escaped {
raise MmdbError("invalid-path", -1, "Incomplete tilde escape")
}
tokens.push(out.to_string())
}
tokens
}
///|
fn select_tokens(value : Value, tokens : Array[String]) -> Value? {
let mut current = value
for token in tokens {
match current {
Object(_) =>
match current.get(token) {
Some(next) => current = next
None => return None
}
List(items) => {
if token.is_empty() || (token.length() > 1 && token[0] == '0') {
return None
}
let mut index = 0
for ch in token {
if ch < '0' || ch > '9' {
return None
}
// Reject before multiplication can overflow, even on huge index tokens.
if index > items.length() / 10 {
return None
}
index = index * 10 + ch.to_int() - 48
if index >= items.length() {
return None
}
}
if index >= items.length() {
return None
}
current = items[index]
}
_ => return None
}
}
Some(current)
}
///|
/// RFC 6901 string form: empty selects root; ~0 is tilde and ~1 is slash.
/// Absent keys, out-of-range/noncanonical array indexes and scalar children are None.
pub fn Value::at_pointer(
self : Value,
pointer : String,
) -> Value? raise MmdbError {
select_tokens(self, pointer_tokens(pointer))
}
///|
priv struct SelectionBudget {
mut values : Int
mut payload : Int
}
///|
fn charge_selection(
value : Value,
budget : SelectionBudget,
) -> Unit raise MmdbError {
if budget.values <= 0 {
raise MmdbError("value-limit", -1, "Projected values budget exhausted")
}
budget.values = budget.values - 1
let bytes = match value {
Text(text) => @utf8.encode(text[:]).length()
Blob(bytes) => bytes.length()
List(items) => {
for item in items {
charge_selection(item, budget)
}
0
}
Object(items) => {
for item in items {
charge_selection(Text(item.0), budget)
charge_selection(item.1, budget)
}
0
}
_ => 0
}
if bytes > budget.payload {
raise MmdbError("payload-limit", -1, "Projected payload budget exhausted")
}
budget.payload = budget.payload - bytes
}
///|
fn projection_paths(
paths : Array[String],
) -> Array[Array[String]] raise MmdbError {
if paths.is_empty() || paths.length() > 64 {
raise MmdbError("path-limit", -1, "Expected 1 to 64 field paths")
}
let parsed : Array[Array[String]] = []
let seen : Map[String, Bool] = Map([])
for path in paths {
if seen.contains(path) {
raise MmdbError("invalid-path", -1, "Duplicate field path")
}
seen[path] = true
parsed.push(pointer_tokens(path))
}
parsed
}
///|
/// Validate a projection configuration even before any records arrive.
pub fn validate_paths(paths : Array[String]) -> Unit raise MmdbError {
ignore(projection_paths(paths))
}
///|
/// Opaque, reusable selection configuration, independent of a database or result.
pub struct FieldSelector {
priv paths : Array[String]
priv tokens : Array[Array[String]]
}
///|
/// Parse and validate once; snapshot the caller's array for stable reuse.
pub fn prepare_fields(paths : Array[String]) -> FieldSelector raise MmdbError {
let tokens = projection_paths(paths)
{ paths: paths.map(path => path), tokens, }
}
///|
/// Decode once under the normal per-record budget, then select 1..64 paths.
/// This is projection of a validated record, not lazy or partial decoding.
pub fn Reader::project(
self : Reader,
ip : String,
paths : Array[String],
) -> Projection raise MmdbError {
self.project_prepared(ip, prepare_fields(paths))
}
///|
/// Reuse validated paths; decoding and selection budgets reset for every call.
pub fn Reader::project_prepared(
self : Reader,
ip : String,
selector : FieldSelector,
) -> Projection raise MmdbError {
let paths = selector.paths
let parsed = selector.tokens
let record = self.lookup(ip)
// Charge repeated selections too; overlapping paths cannot multiply output freely.
let budget = {
values: self.limits.max_values,
payload: self.limits.max_payload_bytes,
}
let fields : Array[(String, Value?)] = []
for i in 0.. select_tokens(value, parsed[i])
None => None
}
if value is Some(selected) {
charge_selection(selected, budget)
}
fields.push((paths[i], value))
}
{
record_found: record.value is Some(_),
prefix_length: record.prefix_length,
fields,
}
}
///|
pub fn Projection::to_json(self : Projection) -> Json {
let fields : Map[String, Json] = Map([])
for field in self.fields {
let (path, value) = field
fields[path] = match value {
Some(value) => { "status": "present", "value": value.to_tagged_json() }
None => { "status": "missing" }
}
}
{
"status": (if self.record_found { "found" } else { "not_found" }).to_json(),
"prefix_length": self.prefix_length.to_json(),
"fields": fields.to_json(),
}
}