1// ASN.1 JavaScript decoder
2// Copyright (c) 2008-2024 Lapo Luchini <lapo@lapo.it>
3
4// Permission to use, copy, modify, and/or distribute this software for any
5// purpose with or without fee is hereby granted, provided that the above
6// copyright notice and this permission notice appear in all copies.
7//
8// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
16import { ASN1 } from './asn1.js';
17import { oids } from './oids.js';
18import { bindContextMenu } from './context.js';
19
20const
21 lineLength = 80,
22 contentLength = 8 * lineLength,
23 DOM = {
24 ellipsis: '\u2026',
25 tag: function (tagName, className, text) {
26 let t = document.createElement(tagName);
27 t.className = className;
28 if (text) t.innerText = text;
29 return t;
30 },
31 text: function (str) {
32 return document.createTextNode(str);
33 },
34 space: function () {
35 return DOM.tag('span', 'spaces', ' ');
36 },
37 breakLines: function (str, length) {
38 let lines = str.split(/\r?\n/),
39 o = '';
40 for (let i = 0; i < lines.length; ++i) {
41 let line = lines[i];
42 if (i > 0) o += '\n';
43 while (line.length > length) {
44 o += line.substring(0, length);
45 o += '\n';
46 line = line.substring(length);
47 }
48 o += line;
49 }
50 return o;
51 },
52 };
53
54export class ASN1DOM extends ASN1 {
55
56 toDOM(spaces) {
57 spaces = spaces || '';
58 let isOID = (typeof oids === 'object') && (this.tag.isUniversal() && (this.tag.tagNumber == 0x06) || (this.tag.tagNumber == 0x0D));
59 let node = DOM.tag('div', 'node');
60 node.asn1 = this;
61 let head = DOM.tag('span', 'head');
62 head.appendChild(DOM.tag('span', 'spaces', spaces));
63 const typeName = this.typeName().replace(/_/g, ' ');
64 if (this.def) {
65 if (this.def.id) {
66 head.appendChild(DOM.tag('span', 'name id', this.def.id));
67 head.appendChild(DOM.space());
68 }
69 if (this.def.name && this.def.name != typeName) {
70 head.appendChild(DOM.tag('span', 'name type', this.def.name));
71 head.appendChild(DOM.space());
72 }
73 if (this.def.mismatch) {
74 head.appendChild(DOM.tag('span', 'name type', '[?]'));
75 head.appendChild(DOM.space());
76 }
77 }
78 head.appendChild(DOM.text(typeName));
79 let content;
80 try {
81 content = this.content(contentLength);
82 } catch (e) {
83 content = 'Cannot decode: ' + e;
84 }
85 let oid;
86 if (content !== null) {
87 let preview = DOM.tag('span', 'preview'),
88 shortContent;
89 if (isOID)
90 content = content.split('\n', 1)[0];
91 shortContent = (content.length > lineLength) ? content.substring(0, lineLength) + DOM.ellipsis : content;
92 preview.appendChild(DOM.space());
93 preview.appendChild(DOM.text(shortContent));
94 if (isOID) {
95 oid = oids[content];
96 if (oid) {
97 if (oid.d) {
98 preview.appendChild(DOM.space());
99 let oidd = DOM.tag('span', 'oid description', oid.d);
100 preview.appendChild(oidd);
101 }
102 if (oid.c) {
103 preview.appendChild(DOM.space());
104 let oidc = DOM.tag('span', 'oid comment', '(' + oid.c + ')');
105 preview.appendChild(oidc);
106 }
107 }
108 }
109 head.appendChild(preview);
110 content = DOM.breakLines(content, lineLength);
111 content = content.replace(/</g, '<');
112 content = content.replace(/\n/g, '<br>');
113 }
114 node.appendChild(head);
115 this.node = node;
116 this.head = head;
117 let value = DOM.tag('div', 'value');
118 let s = 'Offset: ' + this.stream.pos + '<br>';
119 s += 'Length: ' + this.header + '+';
120 if (this.length >= 0)
121 s += this.length;
122 else
123 s += (-this.length) + ' (undefined)';
124 if (this.tag.tagConstructed)
125 s += '<br>(constructed)';
126 else if ((this.tag.isUniversal() && ((this.tag.tagNumber == 0x03) || (this.tag.tagNumber == 0x04))) && (this.sub !== null))
127 s += '<br>(encapsulates)';
128 //TODO if (this.tag.isUniversal() && this.tag.tagNumber == 0x03) s += "Unused bits: "
129 if (content !== null) {
130 s += '<br>Value:<br><b>' + content + '</b>';
131 if (isOID && oid) {
132 if (oid.d) s += '<br>' + oid.d;
133 if (oid.c) s += '<br>' + oid.c;
134 if (oid.w) s += '<br>(warning!)';
135 }
136 }
137 value.innerHTML = s;
138 node.appendChild(value);
139 let sub = DOM.tag('div', 'sub');
140 if (this.sub !== null) {
141 spaces += '\xA0 ';
142 for (let i = 0, max = this.sub.length; i < max; ++i)
143 sub.appendChild(this.sub[i].toDOM(spaces));
144 }
145 node.appendChild(sub);
146 bindContextMenu(node);
147 return node;
148 }
149 fakeHover(current) {
150 this.node.classList.add('hover');
151 if (current)
152 this.head.classList.add('hover');
153 }
154 fakeOut(current) {
155 this.node.classList.remove('hover');
156 if (current)
157 this.head.classList.remove('hover');
158 }
159 toHexDOM_sub(node, className, stream, start, end) {
160 if (start >= end)
161 return;
162 let sub = DOM.tag('span', className, stream.hexDump(start, end));
163 node.appendChild(sub);
164 }
165 toHexDOM(root, trim=true) {
166 let node = DOM.tag('span', 'hex');
167 if (root === undefined) root = node;
168 this.head.hexNode = node;
169 this.head.onmouseover = function () { this.hexNode.className = 'hexCurrent'; };
170 this.head.onmouseout = function () { this.hexNode.className = 'hex'; };
171 node.asn1 = this;
172 node.onmouseover = function () {
173 let current = !root.selected;
174 if (current) {
175 root.selected = this.asn1;
176 this.className = 'hexCurrent';
177 }
178 this.asn1.fakeHover(current);
179 };
180 node.onmouseout = function () {
181 let current = (root.selected == this.asn1);
182 this.asn1.fakeOut(current);
183 if (current) {
184 root.selected = null;
185 this.className = 'hex';
186 }
187 };
188 bindContextMenu(node);
189 if (root == node) {
190 let lineStart = this.posStart() & 0xF;
191 if (lineStart != 0) {
192 let skip = DOM.tag('span', 'skip');
193 let skipStr = '';
194 for (let j = lineStart; j > 0; --j)
195 skipStr += ' ';
196 if (lineStart >= 8)
197 skipStr += ' ';
198 skip.innerText = skipStr;
199 node.appendChild(skip);
200 }
201 }
202 this.toHexDOM_sub(node, 'tag', this.stream, this.posStart(), this.posLen());
203 this.toHexDOM_sub(node, (this.length >= 0) ? 'dlen' : 'ulen', this.stream, this.posLen(), this.posContent());
204 if (this.sub === null) {
205 let start = this.posContent();
206 let end = this.posEnd();
207 if (!trim || end - start < 10 * 16)
208 node.appendChild(DOM.text(
209 this.stream.hexDump(start, end)));
210 else {
211 let end1 = start + 5 * 16 - (start & 0xF);
212 let start2 = end - 16 - (end & 0xF);
213 node.appendChild(DOM.text(this.stream.hexDump(start, end1)));
214 node.appendChild(DOM.tag('span', 'skip', '\u2026 skipping ' + (start2 - end1) + ' bytes \u2026\n'));
215 node.appendChild(DOM.text(this.stream.hexDump(start2, end)));
216 }
217 } else if (this.sub.length > 0) {
218 let first = this.sub[0];
219 let last = this.sub[this.sub.length - 1];
220 this.toHexDOM_sub(node, 'intro', this.stream, this.posContent(), first.posStart());
221 for (let i = 0, max = this.sub.length; i < max; ++i)
222 node.appendChild(this.sub[i].toHexDOM(root, trim));
223 this.toHexDOM_sub(node, 'outro', this.stream, last.posEnd(), this.posEnd());
224 } else
225 this.toHexDOM_sub(node, 'outro', this.stream, this.posContent(), this.posEnd());
226 return node;
227 }
228 static decode(stream, offset) {
229 return ASN1.decode(stream, offset, ASN1DOM);
230 }
231
232}