mirror of
https://github.com/odin-lang/Odin.git
synced 2026-07-22 15:32:35 +00:00
620 lines
17 KiB
Odin
620 lines
17 KiB
Odin
package asn1
|
|
|
|
import "core:strings"
|
|
import dt "core:time"
|
|
|
|
// Cursor is a position into DER input, advanced by the read_* procs.
|
|
Cursor :: struct {
|
|
data: []byte,
|
|
pos: int,
|
|
}
|
|
|
|
// Points a Cursor at `data` and rewinds it to the start.
|
|
cursor_init :: proc "contextless" (r: ^Cursor, data: []byte) {
|
|
r.data = data
|
|
r.pos = 0
|
|
}
|
|
|
|
// Returns the number of unconsumed bytes.
|
|
remaining :: proc "contextless" (r: ^Cursor) -> int {
|
|
return len(r.data) - r.pos
|
|
}
|
|
|
|
// Reports whether the Cursor has been fully consumed.
|
|
is_empty :: proc "contextless" (r: ^Cursor) -> bool {
|
|
return r.pos >= len(r.data)
|
|
}
|
|
|
|
// Returns Leftover_Bytes if input remains. DER structures are exact: every SEQUENCE walk should end with done() on its sub-cursor.
|
|
done :: proc "contextless" (r: ^Cursor) -> Error {
|
|
if r.pos < len(r.data) {
|
|
return .Leftover_Bytes
|
|
}
|
|
return .None
|
|
}
|
|
|
|
// Reads one complete TLV element of any tag, returning the tag and a view of the content octets.
|
|
read_any :: proc "contextless" (r: ^Cursor) -> (tag: Tag, content: []byte, err: Error) {
|
|
tag, err = _read_tag(r)
|
|
if err != .None {
|
|
return
|
|
}
|
|
length: int
|
|
length, err = _read_length(r)
|
|
if err != .None {
|
|
return
|
|
}
|
|
if length > remaining(r) {
|
|
err = .Truncated
|
|
return
|
|
}
|
|
content = r.data[r.pos:r.pos + length]
|
|
r.pos += length
|
|
return
|
|
}
|
|
|
|
// Decodes the next element's tag without consuming anything.
|
|
peek_tag :: proc "contextless" (r: ^Cursor) -> (tag: Tag, err: Error) {
|
|
tmp := r^
|
|
return _read_tag(&tmp)
|
|
}
|
|
|
|
// Consumes one complete element of any tag.
|
|
skip :: proc "contextless" (r: ^Cursor) -> Error {
|
|
_, _, err := read_any(r)
|
|
return err
|
|
}
|
|
|
|
// Reads one element and requires its tag to match exactly.
|
|
expect :: proc "contextless" (r: ^Cursor, tag: Tag) -> (content: []byte, err: Error) {
|
|
got: Tag
|
|
got, content, err = read_any(r)
|
|
if err != .None {
|
|
return
|
|
}
|
|
if got != tag {
|
|
err = .Unexpected_Tag
|
|
}
|
|
return
|
|
}
|
|
|
|
// Enters a SEQUENCE, returning a sub-cursor over its content.
|
|
read_sequence :: proc "contextless" (r: ^Cursor) -> (seq: Cursor, err: Error) {
|
|
content, eerr := expect(r, universal(.Sequence, true))
|
|
if eerr != .None {
|
|
return {}, eerr
|
|
}
|
|
return Cursor{data = content}, .None
|
|
}
|
|
|
|
// Enters a SET, returning a sub-cursor over its content.
|
|
// NOTE: DER requires SET OF contents to be sorted; this cursor does
|
|
// not verify ordering, consumers that care (none in PKIX cert parsing) must check.
|
|
read_set :: proc "contextless" (r: ^Cursor) -> (set: Cursor, err: Error) {
|
|
content, eerr := expect(r, universal(.Set, true))
|
|
if eerr != .None {
|
|
return {}, eerr
|
|
}
|
|
return Cursor{data = content}, .None
|
|
}
|
|
|
|
// Handles `[number] EXPLICIT ... OPTIONAL`: if the next element is the given
|
|
// constructed context-specific tag, it is consumed and a sub-cursor over its
|
|
// content returned with present=true. Otherwise nothing is consumed.
|
|
read_explicit :: proc "contextless" (r: ^Cursor, number: u32) -> (inner: Cursor, present: bool, err: Error) {
|
|
if is_empty(r) {
|
|
return {}, false, .None
|
|
}
|
|
tag, perr := peek_tag(r)
|
|
if perr != .None {
|
|
return {}, false, perr
|
|
}
|
|
if tag != context_specific(number, true) {
|
|
return {}, false, .None
|
|
}
|
|
content, eerr := expect(r, tag)
|
|
if eerr != .None {
|
|
return {}, false, eerr
|
|
}
|
|
return Cursor{data = content}, true, .None
|
|
}
|
|
|
|
// Reads a BOOLEAN. DER: exactly one octet, 0x00 or 0xFF.
|
|
read_boolean :: proc "contextless" (r: ^Cursor) -> (value: bool, err: Error) {
|
|
content, eerr := expect(r, universal(.Boolean))
|
|
if eerr != .None {
|
|
return false, eerr
|
|
}
|
|
if len(content) != 1 {
|
|
return false, .Invalid_Boolean
|
|
}
|
|
switch content[0] {
|
|
case 0x00:
|
|
return false, .None
|
|
case 0xFF:
|
|
return true, .None
|
|
}
|
|
return false, .Invalid_Boolean
|
|
}
|
|
|
|
// Reads an INTEGER and returns the validated, minimally-encoded two's-complement content octets.
|
|
read_integer_bytes :: proc "contextless" (r: ^Cursor) -> (content: []byte, err: Error) {
|
|
content, err = expect(r, universal(.Integer))
|
|
if err != .None {
|
|
return
|
|
}
|
|
err = _check_integer(content)
|
|
return
|
|
}
|
|
|
|
// Reads an INTEGER that must fit in an i64.
|
|
read_i64 :: proc "contextless" (r: ^Cursor) -> (value: i64, err: Error) {
|
|
content, ierr := read_integer_bytes(r)
|
|
if ierr != .None {
|
|
return 0, ierr
|
|
}
|
|
if len(content) > 8 {
|
|
return 0, .Integer_Overflow
|
|
}
|
|
if content[0] & 0x80 != 0 {
|
|
value = -1 // sign-extend
|
|
}
|
|
for b in content {
|
|
value = value << 8 | i64(b)
|
|
}
|
|
return value, .None
|
|
}
|
|
|
|
// Reads a non-negative INTEGER and returns its magnitude octets with any leading 0x00 sign
|
|
// octet stripped.
|
|
read_unsigned_integer_bytes :: proc "contextless" (r: ^Cursor) -> (magnitude: []byte, err: Error) {
|
|
content, ierr := read_integer_bytes(r)
|
|
if ierr != .None {
|
|
return nil, ierr
|
|
}
|
|
if content[0] & 0x80 != 0 {
|
|
return nil, .Negative_Integer
|
|
}
|
|
if len(content) > 1 && content[0] == 0x00 {
|
|
content = content[1:]
|
|
}
|
|
return content, .None
|
|
}
|
|
|
|
// Reads a BIT STRING, returning the payload octets and the count of unused trailing bits
|
|
// in the final octet. DER: primitive form only, unused count 0..7 (0 if the payload is
|
|
// empty), and the unused bits themselves must be zero.
|
|
read_bit_string :: proc "contextless" (r: ^Cursor) -> (bits: []byte, unused: int, err: Error) {
|
|
content, eerr := expect(r, universal(.Bit_String))
|
|
if eerr != .None {
|
|
return nil, 0, eerr
|
|
}
|
|
if len(content) < 1 {
|
|
return nil, 0, .Invalid_Bit_String
|
|
}
|
|
unused = int(content[0])
|
|
bits = content[1:]
|
|
if unused > 7 {
|
|
return nil, 0, .Invalid_Bit_String
|
|
}
|
|
if len(bits) == 0 && unused != 0 {
|
|
return nil, 0, .Invalid_Bit_String
|
|
}
|
|
if unused > 0 {
|
|
mask := byte(1 << uint(unused)) - 1
|
|
if bits[len(bits) - 1] & mask != 0 {
|
|
return nil, 0, .Invalid_Bit_String
|
|
}
|
|
}
|
|
return bits, unused, .None
|
|
}
|
|
|
|
// Reads a BIT STRING that must be a whole number of octets (unused == 0).
|
|
read_bit_string_octets :: proc "contextless" (r: ^Cursor) -> (octets: []byte, err: Error) {
|
|
bits, unused, berr := read_bit_string(r)
|
|
if berr != .None {
|
|
return nil, berr
|
|
}
|
|
if unused != 0 {
|
|
return nil, .Invalid_Bit_String
|
|
}
|
|
return bits, .None
|
|
}
|
|
|
|
// Reads an OCTET STRING (primitive form only).
|
|
read_octet_string :: proc "contextless" (r: ^Cursor) -> (octets: []byte, err: Error) {
|
|
return expect(r, universal(.Octet_String))
|
|
}
|
|
|
|
// Reads a NULL (content must be empty).
|
|
read_null :: proc "contextless" (r: ^Cursor) -> Error {
|
|
content, err := expect(r, universal(.Null))
|
|
if err != .None {
|
|
return err
|
|
}
|
|
if len(content) != 0 {
|
|
return .Invalid_Null
|
|
}
|
|
return .None
|
|
}
|
|
|
|
// Reads an OBJECT IDENTIFIER and returns a view of its content octets,
|
|
// validated for minimal base-128 encoding. The validation is structural
|
|
// only: arc magnitude is unbounded per X.660, so PKIX consumers should
|
|
// compare these bytes directly against known-OID constants.
|
|
// oid_components/oid_to_string decode arcs when needed, reporting
|
|
// Arc_Overflow for arcs beyond u64.
|
|
read_oid :: proc "contextless" (r: ^Cursor) -> (raw: []byte, err: Error) {
|
|
raw, err = expect(r, universal(.Object_Identifier))
|
|
if err != .None {
|
|
return
|
|
}
|
|
if len(raw) == 0 {
|
|
return nil, .Invalid_Object_Identifier
|
|
}
|
|
// Validate: each subidentifier is base-128 with minimal encoding
|
|
// (no 0x80 lead octet) and terminates (last octet has bit 8 clear).
|
|
expect_start := true
|
|
for b in raw {
|
|
if expect_start && b == 0x80 {
|
|
return nil, .Invalid_Object_Identifier
|
|
}
|
|
expect_start = b & 0x80 == 0
|
|
}
|
|
if !expect_start {
|
|
return nil, .Invalid_Object_Identifier
|
|
}
|
|
return raw, .None
|
|
}
|
|
|
|
// Times are returned as core:time.Time. time.Time is i64 nanoseconds
|
|
// and so tops out near year 2262, while UTCTime/GeneralizedTime reach
|
|
// year 9999; dates beyond what time.Time can hold (notably RFC 5280's
|
|
// "99991231235959Z" no-well-defined-expiration sentinel) saturate to
|
|
// time.Time's bound rather than erroring, so a far-future cert still
|
|
// parses and reads as "effectively never expires". See _time_from_unix.
|
|
|
|
// read_utc_time reads a UTCTime in the RFC 5280 DER profile:
|
|
// "YYMMDDHHMMSSZ", with the sliding century window (00-49 → 20xx,
|
|
// 50-99 → 19xx).
|
|
read_utc_time :: proc "contextless" (r: ^Cursor) -> (value: dt.Time, err: Error) {
|
|
content, eerr := expect(r, universal(.UTC_Time))
|
|
if eerr != .None {
|
|
return {}, eerr
|
|
}
|
|
if len(content) != 13 || content[12] != 'Z' {
|
|
return {}, .Invalid_Time
|
|
}
|
|
yy, ok := _two_digits(content[0:2])
|
|
if !ok {
|
|
return {}, .Invalid_Time
|
|
}
|
|
year := 2000 + yy
|
|
if yy >= 50 {
|
|
year = 1900 + yy
|
|
}
|
|
secs := _unix_from_fields(year, content[2:12]) or_return
|
|
return _time_from_unix(secs), .None
|
|
}
|
|
|
|
// Reads a GeneralizedTime in the RFC 5280 DER profile: "YYYYMMDDHHMMSSZ", Zulu only, no fractional seconds.
|
|
read_generalized_time :: proc "contextless" (r: ^Cursor) -> (value: dt.Time, err: Error) {
|
|
content, eerr := expect(r, universal(.Generalized_Time))
|
|
if eerr != .None {
|
|
return {}, eerr
|
|
}
|
|
if len(content) != 15 || content[14] != 'Z' {
|
|
return {}, .Invalid_Time
|
|
}
|
|
hi, ok1 := _two_digits(content[0:2])
|
|
lo, ok2 := _two_digits(content[2:4])
|
|
if !ok1 || !ok2 {
|
|
return {}, .Invalid_Time
|
|
}
|
|
secs := _unix_from_fields(hi * 100 + lo, content[4:14]) or_return
|
|
return _time_from_unix(secs), .None
|
|
}
|
|
|
|
// Reads either time form, PKIX Validity uses UTCTime for dates through 2049 and GeneralizedTime from 2050 on.
|
|
read_time :: proc "contextless" (r: ^Cursor) -> (value: dt.Time, err: Error) {
|
|
tag, perr := peek_tag(r)
|
|
if perr != .None {
|
|
return {}, perr
|
|
}
|
|
if tag == universal(.Generalized_Time) {
|
|
return read_generalized_time(r)
|
|
}
|
|
return read_utc_time(r)
|
|
}
|
|
|
|
// OBJECT IDENTIFIER helpers (allocating).
|
|
|
|
// Decodes validated OID content octets (from read_oid) into their integer arcs,
|
|
// e.g. {1, 2, 840, 113549, 1, 1, 1}. Arcs beyond u64 (legal per X.660, see
|
|
// Arc_Overflow) are not representable; compare such OIDs by their raw bytes instead.
|
|
oid_components :: proc(raw: []byte, allocator := context.allocator) -> (arcs: []u64, err: Error) {
|
|
if len(raw) == 0 {
|
|
return nil, .Invalid_Object_Identifier
|
|
}
|
|
count := 1 // the first octet encodes two arcs
|
|
for b in raw {
|
|
if b & 0x80 == 0 {
|
|
count += 1
|
|
}
|
|
}
|
|
|
|
out, merr := make([]u64, count, allocator)
|
|
if merr != nil {
|
|
return nil, .Allocation_Failed
|
|
}
|
|
idx := 0
|
|
acc: u64 = 0
|
|
first := true
|
|
for b in raw {
|
|
if acc > max(u64) >> 7 {
|
|
delete(out, allocator)
|
|
return nil, .Arc_Overflow
|
|
}
|
|
acc = acc << 7 | u64(b & 0x7F)
|
|
if b & 0x80 != 0 {
|
|
continue
|
|
}
|
|
if first {
|
|
// X.690 section 8.19.4: the first subidentifier encodes the first
|
|
// two arcs as arc1*40 + arc2 (arc1 limited to 0..2; arc2
|
|
// unbounded only when arc1 == 2).
|
|
switch {
|
|
case acc < 40:
|
|
out[idx] = 0
|
|
out[idx + 1] = acc
|
|
case acc < 80:
|
|
out[idx] = 1
|
|
out[idx + 1] = acc - 40
|
|
case:
|
|
out[idx] = 2
|
|
out[idx + 1] = acc - 80
|
|
}
|
|
idx += 2
|
|
first = false
|
|
} else {
|
|
out[idx] = acc
|
|
idx += 1
|
|
}
|
|
acc = 0
|
|
}
|
|
return out, .None
|
|
}
|
|
|
|
// Renders OID content octets in dotted-decimal form ("1.2.840.113549.1.1.1") for diagnostics.
|
|
// The arcs are streamed directly into the result; the only allocation is the returned string.
|
|
oid_to_string :: proc(raw: []byte, allocator := context.allocator) -> (str: string, err: Error) {
|
|
if len(raw) == 0 {
|
|
return "", .Invalid_Object_Identifier
|
|
}
|
|
|
|
sb: strings.Builder
|
|
if _, berr := strings.builder_init(&sb, allocator); berr != nil {
|
|
return "", .Allocation_Failed
|
|
}
|
|
defer if err != .None {
|
|
strings.builder_destroy(&sb)
|
|
}
|
|
|
|
// Builder writes swallow allocator failures, so tally the written vs expected lengths
|
|
// and treat any shortfall as an allocation failure.
|
|
written, expected := 0, 0
|
|
acc: u64 = 0
|
|
first := true
|
|
for b in raw {
|
|
if acc > max(u64) >> 7 {
|
|
err = .Arc_Overflow
|
|
return "", err
|
|
}
|
|
acc = acc << 7 | u64(b & 0x7F)
|
|
if b & 0x80 != 0 {
|
|
continue
|
|
}
|
|
if first {
|
|
// See oid_components for the X.690 section 8.19.4 split of the first subidentifier.
|
|
arc1, arc2: u64
|
|
switch {
|
|
case acc < 40:
|
|
arc1, arc2 = 0, acc
|
|
case acc < 80:
|
|
arc1, arc2 = 1, acc - 40
|
|
case:
|
|
arc1, arc2 = 2, acc - 80
|
|
}
|
|
written += strings.write_u64(&sb, arc1)
|
|
written += strings.write_byte(&sb, '.')
|
|
written += strings.write_u64(&sb, arc2)
|
|
expected += _decimal_len(arc1) + 1 + _decimal_len(arc2)
|
|
first = false
|
|
} else {
|
|
written += strings.write_byte(&sb, '.')
|
|
written += strings.write_u64(&sb, acc)
|
|
expected += 1 + _decimal_len(acc)
|
|
}
|
|
acc = 0
|
|
}
|
|
if written != expected {
|
|
err = .Allocation_Failed
|
|
return "", err
|
|
}
|
|
return strings.to_string(sb), .None
|
|
}
|
|
|
|
@(private)
|
|
_decimal_len :: proc "contextless" (v: u64) -> (n: int) {
|
|
n = 1
|
|
x := v
|
|
for x >= 10 {
|
|
x /= 10
|
|
n += 1
|
|
}
|
|
return n
|
|
}
|
|
|
|
@(private)
|
|
_read_tag :: proc "contextless" (r: ^Cursor) -> (tag: Tag, err: Error) {
|
|
if is_empty(r) {
|
|
return {}, .Truncated
|
|
}
|
|
b := r.data[r.pos]
|
|
r.pos += 1
|
|
|
|
tag.class = Class(b >> 6)
|
|
tag.constructed = b & 0x20 != 0
|
|
number := u32(b & 0x1F)
|
|
|
|
if number != 0x1F {
|
|
tag.number = number
|
|
return tag, .None
|
|
}
|
|
|
|
// High-tag-number form (X.690 section 8.1.2.4): base-128, minimal (first
|
|
// octet may not be 0x80), and the resulting number must be >= 31.
|
|
number = 0
|
|
for i := 0; ; i += 1 {
|
|
if is_empty(r) {
|
|
return {}, .Truncated
|
|
}
|
|
nb := r.data[r.pos]
|
|
r.pos += 1
|
|
if i == 0 && nb == 0x80 {
|
|
return {}, .Invalid_Tag
|
|
}
|
|
if number > (max(u32) >> 7) {
|
|
return {}, .Invalid_Tag
|
|
}
|
|
number = number << 7 | u32(nb & 0x7F)
|
|
if nb & 0x80 == 0 {
|
|
break
|
|
}
|
|
}
|
|
if number < 0x1F {
|
|
return {}, .Invalid_Tag
|
|
}
|
|
tag.number = number
|
|
return tag, .None
|
|
}
|
|
|
|
@(private)
|
|
_read_length :: proc "contextless" (r: ^Cursor) -> (length: int, err: Error) {
|
|
if is_empty(r) {
|
|
return 0, .Truncated
|
|
}
|
|
b := r.data[r.pos]
|
|
r.pos += 1
|
|
|
|
if b & 0x80 == 0 {
|
|
return int(b), .None
|
|
}
|
|
|
|
n := int(b & 0x7F)
|
|
if n == 0 {
|
|
// 0x80: indefinite length, BER only.
|
|
return 0, .Invalid_Length
|
|
}
|
|
if n > 4 {
|
|
// Lengths beyond 2^31 are not plausible inputs here.
|
|
return 0, .Invalid_Length
|
|
}
|
|
if remaining(r) < n {
|
|
return 0, .Truncated
|
|
}
|
|
|
|
value := 0
|
|
for i in 0 ..< n {
|
|
value = value << 8 | int(r.data[r.pos + i])
|
|
}
|
|
r.pos += n
|
|
|
|
// DER minimality: no leading zero octet, and the long form may only be used for lengths >= 128.
|
|
if r.data[r.pos - n] == 0 || value < 0x80 {
|
|
return 0, .Invalid_Length
|
|
}
|
|
if value < 0 {
|
|
return 0, .Invalid_Length
|
|
}
|
|
return value, .None
|
|
}
|
|
|
|
// Enforces X.690 section 8.3: at least one octet, and minimal (the first nine bits may not be all-zero or all-one).
|
|
@(private)
|
|
_check_integer :: proc "contextless" (content: []byte) -> Error {
|
|
switch len(content) {
|
|
case 0:
|
|
return .Invalid_Integer
|
|
case 1:
|
|
return .None
|
|
}
|
|
if content[0] == 0x00 && content[1] & 0x80 == 0 {
|
|
return .Invalid_Integer
|
|
}
|
|
if content[0] == 0xFF && content[1] & 0x80 != 0 {
|
|
return .Invalid_Integer
|
|
}
|
|
return .None
|
|
}
|
|
|
|
@(private)
|
|
_two_digits :: proc "contextless" (b: []byte) -> (value: int, ok: bool) {
|
|
if b[0] < '0' || b[0] > '9' || b[1] < '0' || b[1] > '9' {
|
|
return 0, false
|
|
}
|
|
return int(b[0] - '0') * 10 + int(b[1] - '0'), true
|
|
}
|
|
|
|
// Converts Unix seconds to a time.Time, saturating at time.Time's i64-nanosecond
|
|
// bounds (near year 2262) rather than overflowing. See the note above
|
|
// read_utc_time for why far-future dates are saturated instead of rejected.
|
|
@(private)
|
|
_time_from_unix :: proc "contextless" (secs: i64) -> dt.Time {
|
|
NS_PER_SEC :: i64(1_000_000_000)
|
|
if secs > max(i64) / NS_PER_SEC {
|
|
return dt.Time{_nsec = max(i64)}
|
|
}
|
|
if secs < min(i64) / NS_PER_SEC {
|
|
return dt.Time{_nsec = min(i64)}
|
|
}
|
|
return dt.Time{_nsec = secs * NS_PER_SEC}
|
|
}
|
|
|
|
// Converts a year plus "MMDDHHMMSS" into seconds since the Unix epoch,
|
|
// validating field ranges. Computed directly (via the civil-date algorithm
|
|
// below) rather than through time.Time so the whole year 1..9999 range is
|
|
// computable before _time_from_unix decides how to represent it.
|
|
@(private)
|
|
_unix_from_fields :: proc "contextless" (year: int, fields: []byte) -> (unix_seconds: i64, err: Error) {
|
|
month, mo_ok := _two_digits(fields[0:2])
|
|
day, d_ok := _two_digits(fields[2:4])
|
|
hour, h_ok := _two_digits(fields[4:6])
|
|
minute, min_ok := _two_digits(fields[6:8])
|
|
second, s_ok := _two_digits(fields[8:10])
|
|
if !mo_ok || !d_ok || !h_ok || !min_ok || !s_ok {
|
|
return 0, .Invalid_Time
|
|
}
|
|
if month < 1 || month > 12 || day < 1 || day > 31 {
|
|
return 0, .Invalid_Time
|
|
}
|
|
if hour > 23 || minute > 59 || second > 59 {
|
|
return 0, .Invalid_Time
|
|
}
|
|
days := _days_from_civil(i64(year), month, day)
|
|
return days * 86400 + i64(hour) * 3600 + i64(minute) * 60 + i64(second), .None
|
|
}
|
|
|
|
// Returns the number of days since 1970-01-01 for a proleptic-Gregorian date
|
|
// (Ref: http://howardhinnant.github.io/date_algorithms.html#days_from_civil).
|
|
// Exact for any representable year; no epoch-range limit.
|
|
@(private)
|
|
_days_from_civil :: proc "contextless" (y: i64, m, d: int) -> i64 {
|
|
yy := y - (m <= 2 ? 1 : 0)
|
|
era := (yy >= 0 ? yy : yy - 399) / 400
|
|
yoe := yy - era * 400 // [0, 399]
|
|
doy := i64((153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1) // [0, 365]
|
|
doe := yoe * 365 + yoe / 4 - yoe / 100 + doy // [0, 146096]
|
|
return era * 146097 + doe - 719468
|
|
}
|