///|
/// A fully parsed miniSEED 3 record.
pub(all) struct Record {
header : @miniseed3.FixedHeader
sid : String
extra_headers : String
samples : @encoding.Samples
start : @time.Time
raw : Bytes
} derive(Eq, Debug)
///|
pub fn Record::header(self : Record) -> @miniseed3.FixedHeader {
self.header
}
///|
pub fn Record::sid(self : Record) -> String {
self.sid
}
///|
pub fn Record::extra_headers(self : Record) -> String {
self.extra_headers
}
///|
pub fn Record::raw(self : Record) -> Bytes {
self.raw
}
///|
pub fn Record::start_time(self : Record) -> @time.Time {
self.start
}
///|
pub fn Record::sample_data(self : Record) -> @encoding.Samples {
self.samples
}
///|
/// Numeric samples; text and opaque records return an empty array.
pub fn Record::samples(self : Record) -> Array[Double] {
match self.samples {
FloatArray(a) => a
Raw(_) => []
}
}
///|
/// Exclusive end time, i.e. the expected start of the next contiguous record.
pub fn Record::end_time(self : Record) -> @time.Time? {
let rate = self.header.rate_hz()
if rate <= 0.0 || self.header.sample_count() == 0 {
return None
}
let duration_ns = (self.header.sample_count().to_double() * 1.0e9 / rate)
.round()
.to_int64()
Some(self.start.add_ns(duration_ns))
}
///|
/// Parse one complete record. Trailing bytes are ignored.
pub fn parse_record(raw : Bytes) -> Result[Record, @miniseed3.ParseError] {
let h = match @miniseed3.parse_header(raw, 0) {
Ok(h) => h
Err(e) => return Err(e)
}
let total = match checked_total_length(h) {
Ok(n) => n
Err(e) => return Err(e)
}
if total > raw.length() {
return Err(Truncated(total - raw.length()))
}
let sid_start = @miniseed3.fixed_header_size()
let sid_end = sid_start + h.identifier_length()
let sid_str = match
decode_utf8(
@binary.slice(raw, sid_start, h.identifier_length()),
"source identifier",
) {
Ok(s) => s
Err(e) => return Err(e)
}
match @sid.parse(sid_str) {
Ok(_) => ()
Err(e) => return Err(BadSid(e.to_string()))
}
let extra_start = sid_end
let extra_str = match
decode_utf8(
@binary.slice(raw, extra_start, h.extra_headers_length()),
"extra headers",
) {
Ok(s) => s
Err(e) => return Err(e)
}
if extra_str.trim().length() > 0 && !@json.valid(extra_str) {
return Err(MalformedExtraHeaders("invalid JSON"))
}
let payload_start = extra_start + h.extra_headers_length()
let payload_len = h.payload_length().reinterpret_as_int()
let payload = @binary.slice(raw, payload_start, payload_len)
let computed = @miniseed3.crc32c_region(raw, 0, total, 28)
if computed != h.crc() {
return Err(BadCrc(computed, h.crc()))
}
let start = match
@time.Time::from_header(
h.time_ns(),
h.time_year(),
h.time_doy(),
h.time_hour(),
h.time_min(),
h.time_sec(),
) {
Some(t) => t
None => return Err(BadTime("year/day-of-year/time is out of range"))
}
let samples = match
@encoding.decode_samples(h.encoding(), h.sample_count(), payload) {
Ok(s) => s
Err(e) => return Err(e)
}
Ok({
header: h,
sid: sid_str,
extra_headers: extra_str,
samples,
start,
raw: @binary.slice(raw, 0, total),
})
}
///|
/// Return a record's byte length, rejecting UInt-to-Int overflow.
fn checked_total_length(
h : @miniseed3.FixedHeader,
) -> Result[Int, @miniseed3.ParseError] {
let base = @miniseed3.fixed_header_size() +
h.identifier_length() +
h.extra_headers_length()
if h.payload_length() > (0x7FFFFFFF - base).reinterpret_as_uint() {
return Err(LengthMismatch("record length overflows Int"))
}
Ok(base + h.payload_length().reinterpret_as_int())
}
///|
pub fn total_length(h : @miniseed3.FixedHeader) -> Int {
match checked_total_length(h) {
Ok(n) => n
Err(_) => -1
}
}
///|
/// Parse all recoverable records. A bad record body does not stop later records.
pub fn iterate_records(
raw : Bytes,
) -> Array[Result[Record, @miniseed3.ParseError]] {
let out : Array[Result[Record, @miniseed3.ParseError]] = []
let mut off = 0
while off < raw.length() {
if raw.length() - off < @miniseed3.fixed_header_size() {
out.push(
Err(Truncated(@miniseed3.fixed_header_size() - (raw.length() - off))),
)
break
}
match @miniseed3.parse_header(raw, off) {
Err(e) => {
out.push(Err(e))
break
}
Ok(h) => {
let total = match checked_total_length(h) {
Err(e) => {
out.push(Err(e))
break
}
Ok(n) => n
}
if total <= 0 || total > raw.length() - off {
out.push(Err(Truncated(total - (raw.length() - off))))
break
}
let record_bytes = @binary.slice(raw, off, total)
out.push(parse_record(record_bytes))
off = off + total
}
}
}
out
}
///|
fn decode_utf8(
raw : Bytes,
field : String,
) -> Result[String, @miniseed3.ParseError] {
let chars : Array[Char] = []
let mut i = 0
while i < raw.length() {
let b0 = raw[i].to_int()
let mut code = 0
let mut width = 0
if b0 < 0x80 {
code = b0
width = 1
} else if b0 >= 0xC2 && b0 <= 0xDF && has_cont(raw, i, 1) {
code = ((b0 & 0x1F) << 6) | (raw[i + 1].to_int() & 0x3F)
width = 2
} else if b0 >= 0xE0 && b0 <= 0xEF && has_cont(raw, i, 2) {
let b1 = raw[i + 1].to_int()
if (b0 == 0xE0 && b1 < 0xA0) || (b0 == 0xED && b1 >= 0xA0) {
return utf8_error(field)
}
code = ((b0 & 0x0F) << 12) |
((b1 & 0x3F) << 6) |
(raw[i + 2].to_int() & 0x3F)
width = 3
} else if b0 >= 0xF0 && b0 <= 0xF4 && has_cont(raw, i, 3) {
let b1 = raw[i + 1].to_int()
if (b0 == 0xF0 && b1 < 0x90) || (b0 == 0xF4 && b1 >= 0x90) {
return utf8_error(field)
}
code = ((b0 & 0x07) << 18) |
((b1 & 0x3F) << 12) |
((raw[i + 2].to_int() & 0x3F) << 6) |
(raw[i + 3].to_int() & 0x3F)
width = 4
} else {
return utf8_error(field)
}
chars.push(code.unsafe_to_char())
i = i + width
}
Ok(String::from_array(chars))
}
///|
fn has_cont(raw : Bytes, off : Int, count : Int) -> Bool {
if off + count >= raw.length() {
return false
}
let mut i = 1
while i <= count {
let b = raw[off + i].to_int()
if b < 0x80 || b > 0xBF {
return false
}
i = i + 1
}
true
}
///|
fn utf8_error(field : String) -> Result[String, @miniseed3.ParseError] {
if field == "source identifier" {
Err(BadSid("invalid UTF-8"))
} else {
Err(MalformedExtraHeaders("invalid UTF-8"))
}
}