ref: babf901b4a508c3ec5d1f89655f10377bbdf9637
dir: /appl/lib/asn1.b/
implement ASN1;
include "sys.m";
sys: Sys;
include "asn1.m";
# Masks
TAG_MASK : con 16r1F;
CONSTR_MASK : con 16r20;
CLASS_MASK : con 16rC0;
# Decoding errors
OK, ESHORT, ETOOBIG, EVALLEN, ECONSTR, EPRIM, EINVAL, EUNIMPL: con iota;
debug : con 0;
init()
{
sys = load Sys Sys->PATH;
}
# Decode the whole array as a BER encoding of an ASN1 type.
# If there's an error, the return string will contain the error.
# Depending on the error, the returned elem may or may not
# be nil.
decode(a: array of byte) : (string, ref Elem)
{
(ecode, i, elem) := ber_decode(a, 0, len a);
return (errstr(ecode, i, len a), elem);
}
# Like decode, but continue decoding after first element
# of array ends.
decode_seq(a: array of byte) : (string, list of ref Elem)
{
(ecode, i, elist) := seq_decode(a, 0, len a, -1, 1);
return (errstr(ecode, i, len a), elist);
}
# Decode the whole array as a BER encoding of an ASN1 value,
# (i.e., the part after the tag and length).
# Assume the value is encoded as universal tag "kind".
# The constr arg is 1 if the value is constructed, 0 if primitive.
# If there's an error, the return string will contain the error.
# Depending on the error, the returned value may or may not
# be nil.
decode_value(a: array of byte, kind, constr: int) : (string, ref Value)
{
n := len a;
(ecode, i, val) := value_decode(a, 0, n, n, kind, constr);
return (errstr(ecode, i, len a), val);
}
# The rest of the decoding routines take the array (a), the
# starting position (i), and the ending position +1 (n).
# They return (err code, new i, [... varies]).
# for debugging
ber_ind := "";
ber_ind_save := "";
# Decode an ASN1 (tag, length, value).
ber_decode(a: array of byte, i, n: int) : (int, int, ref Elem)
{
if(debug) {
ber_ind_save = ber_ind;
ber_ind = ber_ind + " ";
sys->print("%sber_decode, byte %d\n", ber_ind, i);
}
err, length: int;
tag : Tag;
val : ref Value;
elem : ref Elem = nil;
(err, i, tag) = tag_decode(a, i, n);
if(err == OK) {
(err, i, length) = length_decode(a, i, n);
if(err == OK) {
if(debug)
sys->print("%sgot tag %s, length %d, now at byte %d\n",
ber_ind, tag.tostring(), length, i);
if(tag.class == Universal)
(err, i, val) = value_decode(a, i, n, length, tag.num, tag.constr);
else
(err, i, val) = value_decode(a, i, n, length, OCTET_STRING, 0);
if(val != nil)
elem = ref Elem(tag, val);
}
}
if(debug) {
sys->print("%send ber_decode, byte %d\n", ber_ind, i);
if(val != nil) {
sys->print("%sdecode result:\n", ber_ind);
print_elem(elem);
}
if(err != OK)
sys->print("%serror: %s\n", ber_ind, errstr(err, i, i));
ber_ind = ber_ind_save;
}
return (err, i, elem);
}
# Decode a tag field. As well as Tag, return an int that
# is 1 if the type is constructed, 0 if not.
tag_decode(a: array of byte, i, n: int) : (int, int, Tag)
{
err := OK;
class, num, constr: int;
if(n-i >= 2) {
v := int a[i++];
class = v & CLASS_MASK;
if(v & CONSTR_MASK)
constr = 1;
else
constr = 0;
num = v & TAG_MASK;
if(num == TAG_MASK)
# long tag number
(err, i, num) = uint7_decode(a, i, n);
}
else
err = ESHORT;
return (err, i, Tag(class, num, constr));
}
# Decode a length field. Assume it fits in a Limbo int.
# If "indefinite length", return -1.
length_decode(a: array of byte, i, n: int) : (int, int, int)
{
err := OK;
num := 0;
if(i < n) {
v := int a[i++];
if(v & 16r80)
return int_decode(a, i, n, v&16r7F, 1);
else if(v == 16r80)
num = -1;
else
num = v;
}
else
err = ESHORT;
return (err, i, num);
}
# Decode a value according to the encoding of the Universal
# type with number "kind" and constructed/primitive according
# to "constr", with given length (may be -1, for "indefinite").
value_decode(a: array of byte, i, n, length, kind, constr: int) : (int, int, ref Value)
{
err := OK;
val : ref Value;
va : array of byte;
if(length == -1) {
if(!constr)
err = EINVAL;
}
else if(i+length > n)
err = EVALLEN;
if(err != OK)
return (err, i, nil);
case kind {
0 =>
# marker for end of indefinite constructions
if(length == 0)
val = ref Value.EOC;
else
err = EINVAL;
BOOLEAN =>
if(constr)
err = ECONSTR;
else if(length != 1)
err = EVALLEN;
else {
val = ref Value.Bool(int a[0]);
i++;
}
INTEGER or ENUMERATED =>
if(constr)
err = ECONSTR;
else if(length <= 4) {
num : int;
(err, i, num) = int_decode(a, i, i+length, length, 0);
if(err == OK)
val = ref Value.Int(num);
}
else {
va = array[length] of byte;
va[0:] = a[i:i+length];
val = ref Value.BigInt(va);
i += length;
}
BIT_STRING =>
if(constr) {
if(length == -1 && i+2 <= n && a[i] == byte 0 && a[i+1] == byte 0) {
val = ref Value.BitString(0, nil);
i += 2;
}
else
# TODO: recurse and concat results
err = EUNIMPL;
}
else {
if(length < 2) {
if(length == 1 && a[0] == byte 0) {
val = ref Value.BitString(0, nil);
i ++;
}
else
err = EINVAL;
}
else {
bitsunused := int a[i];
if(bitsunused > 7)
err = EINVAL;
else if(length > 16r0FFFFFFF)
err = ETOOBIG;
else {
va = array[length-1] of byte;
va[0:] = a[i+1:i+length];
val = ref Value.BitString(bitsunused, va);
i += length;
}
}
}
OCTET_STRING or ObjectDescriptor =>
(err, i, va) = octet_decode(a, i, n, length, constr);
if(err == OK)
val = ref Value.Octets(va);
NULL =>
if(constr)
err = ECONSTR;
else if(length != 0)
err = EVALLEN;
else
val = ref Value.Null;
OBJECT_ID =>
if(constr)
err = ECONSTR;
else if (length == 0)
err = EVALLEN;
else {
subids : list of int = nil;
iend := i+length;
while(i < iend) {
x : int;
(err, i, x) = uint7_decode(a, i, n);
if(err != OK)
break;
subids = x :: subids;
}
if(err == OK) {
if(i != iend)
err = EVALLEN;
else {
m := len subids;
ia := array[m+1] of int;
while(subids != nil) {
y := hd subids;
subids = tl subids;
if(m == 1) {
ia[1] = y % 40;
ia[0] = y / 40;
}
else
ia[m--] = y;
}
val = ref Value.ObjId(ref Oid(ia));
}
}
}
EXTERNAL or EMBEDDED_PDV =>
# TODO: parse this internally
va = array[length] of byte;
va[0:] = a[i:i+length];
val = ref Value.Other(va);
i += length;
REAL =>
# let the appl decode, with math module
if(constr)
err = ECONSTR;
else {
va = array[length] of byte;
va[0:] = a[i:i+length];
val = ref Value.Real(va);
i += length;
}
SEQUENCE or SET=>
vl : list of ref Elem;
(err, i, vl) = seq_decode(a, i, n, length, constr);
if(err == OK) {
if(kind == SEQUENCE)
val = ref Value.Seq(vl);
else
val = ref Value.Set(vl);
}
NumericString or PrintableString or TeletexString
or VideotexString or IA5String or UTCTime
or GeneralizedTime or GraphicString or VisibleString
or GeneralString or UniversalString or BMPString =>
(err, i, va) = octet_decode(a, i, n, length, constr);
if(err == OK)
# sometimes wrong: need to do char set conversion
val = ref Value.String(string va);
* =>
va = array[length] of byte;
va[0:] = a[i:i+length];
val = ref Value.Other(va);
i += length;
}
return (err, i, val);
}
# Decode an int in format where count bytes are
# concatenated to form value.
# Although ASN1 allows any size integer, we return
# an error if the result doesn't fit in a Limbo int.
# If unsigned is not set, make sure to propagate sign bit.
int_decode(a: array of byte, i, n, count, unsigned: int) : (int, int, int)
{
err := OK;
num := 0;
if(n-i >= count) {
if((count > 4) || (unsigned && count == 4 && (int a[i] & 16r80)))
err = ETOOBIG;
else {
if(!unsigned && count > 0 && count < 4 && (int a[i] & 16r80))
num = -1; # all bits set
for(j := 0; j < count; j++) {
v := int a[i++];
num = (num << 8) | v;
}
}
}
else
err = ESHORT;
return (err, i, num);
}
# Decode an unsigned int in format where each
# byte except last has high bit set, and remaining
# seven bits of each byte are concatenated to form value.
# Although ASN1 allows any size integer, we return
# an error if the result doesn't fit in a Limbo int.
uint7_decode(a: array of byte, i, n: int) : (int, int, int)
{
err := OK;
num := 0;
more := 1;
while(more && i < n) {
v := int a[i++];
if(num & 16r7F000000) {
err = ETOOBIG;
break;
}
num <<= 7;
more = v & 16r80;
num |= (v & 16r7F);
}
if(n == i)
err = ESHORT;
return (err, i, num);
}
# Decode an octet string, recursively if constr.
# We've already checked that length==-1 implies constr==1,
# and otherwise that specified length fits within a[i..n].
octet_decode(a: array of byte, i, n, length, constr: int) : (int, int, array of byte)
{
err := OK;
va : array of byte;
if(length >= 0 && !constr) {
va = array[length] of byte;
va[0:] = a[i:i+length];
i += length;
}
else {
# constructed, either definite or indefinite length
lva : list of array of byte = nil;
elem : ref Elem;
istart := i;
totbytes := 0;
cloop:
for(;;) {
if(length >= 0 && i >= istart+length) {
if(i != istart+length)
err = EVALLEN;
break cloop;
}
oldi := i;
(err, i, elem) = ber_decode(a, i, n);
if(err != OK)
break;
pick v := elem.val {
Octets =>
lva = v.bytes :: lva;
totbytes += len v.bytes;
EOC =>
if(length != -1) {
i = oldi;
err = EINVAL;
}
break cloop;
* =>
i = oldi;
err = EINVAL;
break cloop;
}
}
if(err == OK) {
va = array[totbytes] of byte;
j := totbytes;
while(lva != nil) {
x := hd lva;
lva = tl lva;
m := len x;
va[j-m:] = x[0:];
j -= m;
}
}
}
return (err, i, va);
}
# Decode a sequence or set.
# We've already checked that length==-1 implies constr==1,
# and otherwise that specified length fits within a[i..n].
seq_decode(a : array of byte, i, n, length, constr: int) : (int, int, list of ref Elem)
{
err := OK;
ans : list of ref Elem = nil;
if(!constr)
err = EPRIM;
else {
# constructed, either definite or indefinite length
lve : list of ref Elem = nil;
elem : ref Elem;
istart := i;
cloop:
for(;;) {
if(length >= 0 && i >= istart+length) {
if(i != istart+length)
err = EVALLEN;
break cloop;
}
oldi := i;
(err, i, elem) = ber_decode(a, i, n);
if(err != OK)
break;
pick v := elem.val {
EOC =>
if(length != -1) {
i = oldi;
err = EINVAL;
}
break cloop;
* =>
lve = elem :: lve;
}
}
if(err == OK) {
# reverse back to original order
while(lve != nil) {
e := hd lve;
lve = tl lve;
ans = e :: ans;
}
}
}
return (err, i, ans);
}
# Encode e by BER rules
encode(e: ref Elem) : (string, array of byte)
{
(err, n) := enc(nil, e, 0, 1);
if(err != "")
return (err, nil);
b := array[n] of byte;
enc(b, e, 0, 0);
return ("", b);
}
# Encode e into array b, only putting in bytes if !lenonly.
# Start at loc i, return index after.
enc(b: array of byte, e: ref Elem, i, lenonly: int) : (string, int)
{
(err, vlen, constr) := val_enc(b, e, 0, 1);
if(err != "")
return (err, i);
tag := e.tag;
v := tag.class | constr;
if(tag.num < 31) {
if(!lenonly)
b[i] = byte (v | tag.num);
i++;
}
else {
if(!lenonly)
b[i] = byte (v | 31);
if(tag.num < 0)
return ("negative tag number", i);
i = uint7_enc(b, tag.num, i+1, lenonly);
}
if(vlen < 16r80) {
if(!lenonly)
b[i] = byte vlen;
i++;
}
else {
ilen := int_enc(b, vlen, 1, 0, 1);
if(!lenonly) {
b[i] = byte (16r80 | ilen);
i = int_enc(b, vlen, 1, i+1, 0);
}
else
i += 1+ilen;
}
if(!lenonly)
val_enc(b, e, i, 0);
i += vlen;
return ("", i);
}
# Encode e.val into array b, only putting in bytes if !lenonly.
# Start at loc i, return (err, index after, constructed or primitive)
val_enc(b: array of byte, e: ref Elem, i, lenonly: int) : (string, int, int)
{
kind := e.tag.num;
cl := e.tag.class;
ok := 1;
v : int;
bb : array of byte;
constr := 0;
if(cl != Universal) {
pick vv := e.val {
Bool =>
kind = BOOLEAN;
Int =>
kind = INTEGER;
BigInt =>
kind = INTEGER;
Octets =>
kind = OCTET_STRING;
Real =>
kind = REAL;
Other =>
kind = OCTET_STRING;
BitString =>
kind = BIT_STRING;
Null =>
kind = NULL;
ObjId =>
kind = OBJECT_ID;
String =>
kind = UniversalString;
Seq =>
kind = SEQUENCE;
Set =>
kind = SET;
}
}
case kind {
BOOLEAN =>
(ok, v) = e.is_int();
if(ok) {
if(v != 0)
v = 255;
i = int_enc(b, v, 1, i, lenonly);
}
INTEGER or ENUMERATED =>
(ok, v) = e.is_int();
if(ok)
i = int_enc(b, v, 0, i, lenonly);
else {
(ok, bb) = e.is_bigint();
if(ok) {
if(!lenonly)
b[i:] = bb;
i += len bb;
}
}
BIT_STRING =>
(ok, v, bb) = e.is_bitstring();
if(ok) {
if(bb == nil) {
if(!lenonly)
b[i] = byte 0;
i++;
}
else {
if(v < 0 || v > 7)
ok = 0;
else {
if(!lenonly) {
b[i] = byte v;
b[i+1:] = bb;
}
i += 1 + len bb;
}
}
}
OCTET_STRING or ObjectDescriptor or EXTERNAL or REAL
or EMBEDDED_PDV =>
pick vv := e.val {
Octets or Real or Other =>
if(!lenonly && vv.bytes != nil)
b[i:] = vv.bytes;
i += len vv.bytes;
* =>
ok = 0;
}
NULL =>
;
OBJECT_ID =>
oid : ref Oid;
(ok, oid) = e.is_oid();
if(ok) {
n := len oid.nums;
for(k := 0; k < n; k++) {
v = oid.nums[k];
if(k == 0) {
v *= 40;
if(n > 1)
v += oid.nums[++k];
}
i = uint7_enc(b, v, i, lenonly);
}
}
SEQUENCE or SET =>
pick vv := e.val {
Seq or Set =>
constr = CONSTR_MASK;
for(l := vv.l; l != nil; l = tl l) {
err : string;
(err, i) = enc(b, hd l, i, lenonly);
if(err != "")
return (err, i, 0);
}
}
NumericString or PrintableString or TeletexString
or VideotexString or IA5String or UTCTime
or GeneralizedTime or GraphicString or VisibleString
or GeneralString or UniversalString or BMPString =>
pick vv := e.val {
String =>
bb = array of byte vv.s;
if(!lenonly && bb != nil)
b[i:] = bb;
i += len bb;
* =>
ok = 0;
}
* =>
ok = 0;
}
if(!ok)
return ("bad value for encoding kind", i, constr);
return ("", i, constr);
}
# Encode num as unsigned 7 bit values with top bit 1 on all bytes
# except last, into array b, only putting in bytes if !lenonly.
# Start at loc i, return index after.
uint7_enc(b: array of byte, num, i, lenonly: int) : int
{
n := 1;
v := num>>7;
while(v > 0) {
v >>= 7;
n++;
}
if(lenonly)
i += n;
else {
for(k := (n-1)*7; k > 0; k -= 7)
b[i++] = byte ((num>>k) | 16r80);
b[i++] = byte (num & 16r7F);
}
return i;
}
# Encode num as unsigned or signed integer into array b,
# only putting in bytes if !lenonly.
# Encoding is length followed by bytes to concatenate.
# Start at loc i, return index after.
int_enc(b: array of byte, num, unsigned, i, lenonly: int) : int
{
v := num;
if(v < 0)
v = -(v+1);
n := 1;
prevv := v;
v >>= 8;
while(v > 0) {
prevv = v;
v >>= 8;
n++;
}
if(!unsigned && (prevv & 16r80))
n++;
if(lenonly)
i += n;
else {
for(k := (n-1)*8; k >= 0; k -= 8)
b[i++] = byte (num>>k);
}
return i;
}
# Compare two arrays of integers; return true if they match
intarr_eq(a: array of int, b: array of int) : int
{
alen := len a;
if(alen != len b)
return 0;
for(i := 0; i < alen; i++)
if(a[i] != b[i])
return 0;
return 1;
}
# Look for o in tab; if found, return index, else return -1.
oid_lookup(o: ref Oid, tab: array of Oid) : int
{
for(i := 0; i < len tab; i++)
if(intarr_eq(o.nums, tab[i].nums))
return i;
return -1;
}
# If e is a SEQUENCE, return (1, e's element list)
# else return (error, nil).
Elem.is_seq(e: self ref Elem) : (int, list of ref Elem)
{
if(e.tag.class == Universal && e.tag.num == SEQUENCE) {
pick v := e.val {
Seq =>
return (1, v.l);
}
}
return (0, nil);
}
# If e is a SET, return (1, e's element list)
# else return (error, nil).
Elem.is_set(e: self ref Elem) : (int, list of ref Elem)
{
if(e.tag.class == Universal && e.tag.num == SET) {
pick v := e.val {
Set =>
return (1, v.l);
}
}
return (0, nil);
}
# If e is an INTEGER that fits in a limbo int, return (1, val)
# else return (0, 0l).
Elem.is_int(e: self ref Elem) : (int, int)
{
if(e.tag.class == Universal && (e.tag.num == INTEGER || e.tag.num == BOOLEAN)) {
pick v := e.val {
Bool or
Int =>
return (1, v.v);
}
}
return (0, 0);
}
# If e is an INTEGER that doesn't fit in a limbo int, return (1, bytes),
# or even if it does fit, return it as an array of bytes.
# else return (0, nil).
Elem.is_bigint(e: self ref Elem) : (int, array of byte)
{
if(e.tag.class == Universal && e.tag.num == INTEGER) {
pick v := e.val {
BigInt =>
return (1, v.bytes);
Int =>
x := v.v;
a := array[4] of byte;
for(i := 0; i < 4; i++)
a[i] = byte ((x >> (8*(3-i))) & 16rFF);
for(j := 0; j < 3; j++)
if(a[j] != byte 0)
break;
return (1, a[j:]);
}
}
return (0, nil);
}
# If e is a bitstring, return (1, unused bits, bytes containing bit string),
# else return (0, nil)
Elem.is_bitstring(e: self ref Elem) : (int, int, array of byte)
{
if(e.tag.class == Universal && e.tag.num == BIT_STRING) {
pick v := e.val {
BitString =>
return (1, v.unusedbits, v.bits);
}
}
return (0, 0, nil);
}
# If e is an octetstring, return (1, bytes),
# else return (0, nil)
Elem.is_octetstring(e: self ref Elem) : (int, array of byte)
{
if(e.tag.class == Universal && e.tag.num == OCTET_STRING) {
pick v := e.val {
Octets =>
return (1, v.bytes);
}
}
return (0, nil);
}
# If e is an object id, return (1, ref Oid),
# else return (0, nil)
Elem.is_oid(e: self ref Elem) : (int, ref Oid)
{
if(e.tag.class == Universal && e.tag.num == OBJECT_ID) {
pick v := e.val {
ObjId =>
return (1, v.id);
}
}
return (0, nil);
}
# If e is some kind of string (excluding times), return (1, string),
# else return (0, "")
Elem.is_string(e: self ref Elem) : (int, string)
{
if(e.tag.class == Universal) {
case e.tag.num {
NumericString or PrintableString or TeletexString
or VideotexString or IA5String or GraphicString
or VisibleString or GeneralString or UniversalString
or BMPString =>
pick v := e.val {
String =>
return (1, v.s);
}
}
}
return (0, nil);
}
# If e is some kind of time, return (1, string),
# else return (0, "")
Elem.is_time(e: self ref Elem) : (int, string)
{
if(e.tag.class == Universal
&& (e.tag.num == UTCTime || e.tag.num == GeneralizedTime)) {
pick v := e.val {
String =>
return (1, v.s);
}
}
return (0, nil);
}
# Return printable error string for code ecode.
# i is position where error is first noted.
# n is the end of the passed data: if i!=n then
# we didn't use all the data and an error should
# be returned about that.
errstr(ecode, i, n: int) : string
{
if(ecode == OK && i == n)
return "";
err := "BER decode: ";
case ecode {
OK =>
err += "OK";
ESHORT =>
err += "need more data";
ETOOBIG =>
err += "value exceeds implementation limit";
EVALLEN =>
err += "value has wrong length";
ECONSTR =>
err += "value is constructed, should be primitive";
EPRIM =>
err += "value is primitive";
EINVAL =>
err += "value encoding invalid";
* =>
err += "unknown error " + string ecode;
}
if(err == "" && i != n)
err += "extra data";
err += " at byte " + string i;
return err;
}
# Printing functions, for debugging
Tag.tostring(t: self Tag) : string
{
ans := "";
snum := string t.num;
if(t.class == Universal) {
case t.num {
BOOLEAN => ans = "BOOLEAN";
INTEGER => ans = "INTEGER";
BIT_STRING => ans = "BIT STRING";
OCTET_STRING => ans = "OCTET STRING";
NULL => ans = "NULL";
OBJECT_ID => ans = "OBJECT IDENTIFER";
ObjectDescriptor => ans = "OBJECT_DES";
EXTERNAL => ans = "EXTERNAL";
REAL => ans = "REAL";
ENUMERATED => ans = "ENUMERATED";
EMBEDDED_PDV => ans = "EMBEDDED PDV";
SEQUENCE => ans = "SEQUENCE";
SET => ans = "SET";
NumericString => ans = "NumericString";
PrintableString => ans = "PrintableString";
TeletexString => ans = "TeletexString";
VideotexString => ans = "VideotexString";
IA5String => ans = "IA5String";
UTCTime => ans = "UTCTime";
GeneralizedTime => ans = "GeneralizedTime";
GraphicString => ans = "GraphicString";
VisibleString => ans = "VisibleString";
GeneralString => ans = "GeneralString";
UniversalString => ans = "UniversalString";
BMPString => ans = "BMPString";
* => ans = "UNIVERSAL " + snum;
}
}
else {
case t.class {
Application =>
ans = "APPLICATION " + snum;
Context =>
ans = "CONTEXT "+ snum;
Private =>
ans = "PRIVATE " + snum;
}
}
return ans;
}
Elem.tostring(e: self ref Elem) : string
{
return estring(e, "");
}
Value.tostring(v: self ref Value) : string
{
return vstring(v, "");
}
estring(e: ref Elem, indent: string) : string
{
return indent + e.tag.tostring() + " " + vstring(e.val, indent);
}
vstring(val: ref Value, indent: string) : string
{
ans := "";
pick v := val {
Bool or Int =>
ans += string v.v;
Octets or BigInt or Real or Other =>
ans += bastring(v.bytes, indent + "\t");
BitString =>
ans += " bits (unused " +string v.unusedbits + ")" + bastring(v.bits, indent + "\t");
Null or EOC =>
;
ObjId =>
ans += v.id.tostring();
String =>
ans += "\"" + v.s + "\"";
Seq or Set =>
ans += "{\n";
newindent := indent + "\t";
l := v.l;
while(l != nil) {
if(ans[len ans-1] != '\n')
ans[len ans] = '\n';
ans += estring(hd l, newindent);
l = tl l;
}
if(ans[len ans-1] != '\n')
ans[len ans] = '\n';
ans += indent + "}";
}
return ans;
}
bastring(a: array of byte, indent: string) : string
{
if(sys == nil)
sys = load Sys Sys->PATH;
ans := indent;
nlindent := "\n" + indent;
for(i := 0; i < len a; i++) {
if(i < len a - 1 && i%10 == 0)
ans += nlindent ;
ans += sys->sprint("%2x ", int a[i]);
}
return ans;
}
Oid.tostring(o: self ref Oid) : string
{
ans := "";
for(i := 0; i < len o.nums; i++) {
ans += string o.nums[i];
if(i < len o.nums - 1)
ans[len ans] = '.';
}
return ans;
}
print_elem(e: ref Elem)
{
s := e.tostring();
a := array of byte s;
sys->write(sys->fildes(1), a, len a);
sys->print("\n");
}