///|
pub fn Checker::feed_line(
self : Checker,
line : String,
) -> Unit raise CheckError {
if self.closed {
raise Rejected("state", self.line, "checker is failed or finished")
}
self.line += 1
try {
if line.length() > 8192 || self.bytes > 536870912 - line.length() - 1 {
self.reject("capacity", "line/input byte limit exceeded")
}
if line.iter().any(c => c < ' ' || c > '~') {
self.reject(
"ascii", "feed_line expects printable ASCII without line terminators",
)
}
self.bytes += line.length() + 1
if !self.body {
self.header_line(line)
} else if line.has_prefix(">") {
self.epoch_line(line)
} else {
self.observation_line(line)
}
} catch {
Rejected(code, at, detail) => {
self.closed = true
raise Rejected(code, if at == 0 { self.line } else { at }, detail)
}
}
}
///|
fn Checker::epoch_line(self : Checker, s : String) -> Unit raise CheckError {
if self.remaining != 0 {
self.reject(
"truncated-epoch", "new epoch before declared satellite records completed",
)
}
if s.length() < 35 || s.length() > 56 {
self.reject("epoch-width", "invalid epoch record width")
}
for index in [1, 6, 9, 12, 15, 29, 30] {
if s[index] != ' ' {
self.reject("epoch-layout", "reserved epoch separators must be spaces")
}
}
if !trimmed(s, 35, 41).is_empty() {
self.reject("epoch-layout", "reserved epoch columns must be blank")
}
let flag = digits(part(s, 31, 32))
if flag > 1 {
self.reject(
"unsupported-event", "event flags 2..6 require state semantics outside this version",
)
}
let t = timestamp(
part(s, 2, 6),
part(s, 7, 9),
part(s, 10, 12),
part(s, 13, 15),
part(s, 16, 18),
part(s, 18, 29),
)
let n = digits(part(s, 32, 35))
if n > 693 {
self.reject(
"satellite-count", "epoch count exceeds finite satellite namespace",
)
}
if n == 0 {
self.note(
"empty-epoch",
"epoch contains no satellite observations",
error=self.window is Some(_),
)
}
if self.epochs >= 1000000 {
self.reject("capacity", "more than one million epochs")
}
if self.last is Some(last) {
if t <= last {
self.note("epoch-order", "duplicate or decreasing epoch")
} else if self.interval is Some(step) {
let delta = t - last
if delta != step {
self.note(
"epoch-interval", "adjacent epoch spacing differs from header INTERVAL",
)
if delta > step && delta % step == 0L {
let slots = delta / step - 1L
// Calendar range caps result; total skipped slots may exceed Int.
if slots > 1000000000L ||
self.missing.to_int64() + slots > 1000000000L {
self.reject("capacity", "missing slot counter exceeds one billion")
}
self.missing += slots.to_int()
}
}
}
} else {
self.first = Some(t)
}
if self.window is Some(w) {
if t < w.start || t >= w.stop {
self.note(
"profile-window", "epoch outside selected half-open GPS delivery window",
)
}
if (t - w.start) % 300000000L != 0L {
self.note("profile-lattice", "epoch is not on the GPS :00/:30 lattice")
}
}
if flag == 1 {
self.power += 1
self.note("power-failure", "receiver power interruption flag", error=false)
}
let clock = trimmed(s, 41, 56)
if !clock.is_empty() {
ignore(decimal(clock))
self.clocks += 1
if self.clock_applied is None {
self.note(
"clock-header-missing", "clock offset present without RCV CLOCK OFFS APPL",
)
}
}
self.last = Some(t)
self.epochs += 1
self.remaining = n
self.epoch_satellites.clear()
}
///|
fn Checker::observation_line(
self : Checker,
s : String,
) -> Unit raise CheckError {
if self.remaining == 0 {
self.reject(
"unexpected-observation", "expected epoch header, not extra satellite record",
)
}
let id = part(s, 0, 3)
if !satellite_ok(id) {
self.reject("satellite-id", "invalid satellite identifier")
}
if self.epoch_satellites.contains(id) {
self.reject("duplicate-satellite", id + " repeated in current epoch")
}
let sys = part(id, 0, 1)
guard self.counts.get(sys) is Some(counters) else {
self.reject("undeclared-system", id)
return
}
if s.length() > 3 + counters.length() * 16 &&
!trimmed(s, 3 + counters.length() * 16, s.length()).is_empty() {
self.reject(
"extra-observation-fields", "payload exceeds declared observation types",
)
}
if self.records >= 2000000 {
self.reject("capacity", "more than two million satellite records")
}
for i, counter in counters {
let at = 3 + i * 16
let number = trimmed(s, at, at + 14)
let lli = trimmed(s, at + 14, at + 15)
let ssi = trimmed(s, at + 15, at + 16)
if !ssi.is_empty() && (ssi < "0" || ssi > "9") {
self.reject("ssi", "SSI must be blank or digit 0..9")
}
let lock = if lli.is_empty() { 0 } else { digits(lli) }
if lock > 7 {
self.reject("lli", "LLI must be blank or three-bit value 0..7")
}
if lock > 0 && !counter.code.has_prefix("L") {
self.note("lli-nonphase", "LLI set on non-phase observation", error=false)
}
if number.is_empty() {
counter.blank_fields += 1
} else {
let numeric = decimal(number)
if numeric == 0.0 {
counter.zero_values += 1
}
counter.nonempty_fields += 1
}
if lock != 0 {
counter.lli_nonzero += 1
}
if (lock & 1) != 0 {
counter.lli_bit0 += 1
}
if (lock & 2) != 0 {
counter.lli_bit1 += 1
}
if (lock & 4) != 0 {
counter.lli_bit2 += 1
}
}
self.system_records[sys] = self.system_records[sys] + 1
self.satellites[id] = true
self.epoch_satellites[id] = true
self.remaining -= 1
self.records += 1
}
///|
pub fn Checker::finish(self : Checker) -> Report raise CheckError {
if self.closed {
raise Rejected("state", self.line, "checker is failed or finished")
}
self.closed = true
if !self.body {
self.reject("truncated-header", "END OF HEADER not received")
}
if self.remaining != 0 {
self.reject(
"truncated-epoch", "EOF before declared satellite records completed",
)
}
if self.epochs == 0 {
self.reject("empty-body", "no observation epochs")
}
let first = self.first.unwrap()
let last = self.last.unwrap()
if first != self.header_first.unwrap() {
self.note(
"header-first",
"first observed epoch differs from header",
at=self.headers_seen["TIME OF FIRST OBS"],
)
}
if self.header_last is Some(value) && value != last {
self.note(
"header-last",
"last observed epoch differs from header",
at=self.headers_seen["TIME OF LAST OBS"],
)
}
if self.declared_satellites is Some(value) {
if value != self.satellites.length() {
self.note(
"header-satellite-total",
"declared " +
value.to_string() +
"; observed " +
self.satellites.length().to_string(),
at=self.headers_seen["# OF SATELLITES"],
)
}
if self.prn_headers.length() != value {
self.note(
"header-prn-details",
"PRN / # OF OBS satellite detail count differs from declared total",
error=false,
)
}
}
if self.window is Some(w) {
if first != w.start {
self.note(
"profile-first", "missing or extra leading portion of selected window",
)
}
if last != w.stop - 300000000L {
self.note(
"profile-last", "missing or extra trailing portion of selected window",
)
}
if self.epochs.to_int64() != (w.stop - w.start) / 300000000L {
self.note(
"profile-epoch-count", "epoch count differs from expected delivery lattice",
)
}
if self.header_first != Some(w.start) ||
self.header_last != Some(w.stop - 300000000L) {
self.note(
"profile-header-window", "declared first/last do not cover selected delivery window",
)
}
}
let systems = self.counts.keys().collect()
systems.sort()
let system_counts = systems.map(sys => {
let satellites = self.satellites
.keys()
.filter(id => id.has_prefix(sys))
.collect()
satellites.sort()
let signals : Array[SignalCounts] = self.counts[sys].map(c => {
code: c.code,
nonempty_fields: c.nonempty_fields,
blank_fields: c.blank_fields,
zero_values: c.zero_values,
lli_nonzero: c.lli_nonzero,
lli_bit0: c.lli_bit0,
lli_bit1: c.lli_bit1,
lli_bit2: c.lli_bit2,
})
{ system: sys, records: self.system_records[sys], satellites, signals, }
})
let labels = self.other_labels.keys().collect()
labels.sort()
let types : Map[String, Array[String]] = Map([])
for sys, codes in self.types {
types[sys] = codes.copy()
}
{
complete: true,
acceptable: self.errors == 0,
profile: match self.window {
Some(w) => w.kind
None => "bounded OBS structural/time checks"
},
time_unit: "100ns calendar ticks since 1980-01-01 00:00:00 GPS; not UTC",
header: {
version: self.version,
marker: self.marker,
time_system: self.time_system,
first: self.header_first.unwrap(),
last: self.header_last,
interval: self.interval,
declared_satellites: self.declared_satellites,
license: self.license,
doi: self.doi,
observation_types: types,
uninterpreted_labels: labels,
},
epochs: self.epochs,
records: self.records,
first,
last,
missing_epoch_slots: self.missing,
power_failure_epochs: self.power,
receiver_clock_epochs: self.clocks,
systems: system_counts,
findings: self.findings.copy(),
finding_counts: self.finding_counts.copy(),
omitted_findings: self.total_findings - self.findings.length(),
}
}