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('div', '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 head.onclick = function () {
147 node.className = (node.className == 'node collapsed') ? 'node' : 'node collapsed';
148 };
149 return node;
150 }
151 fakeHover(current) {
152 this.node.className += ' hover';
153 if (current)
154 this.head.className += ' hover';
155 }
156 fakeOut(current) {
157 let re = / ?hover/;
158 this.node.className = this.node.className.replace(re, '');
159 if (current)
160 this.head.className = this.head.className.replace(re, '');
161 }
162 toHexDOM_sub(node, className, stream, start, end) {
163 if (start >= end)
164 return;
165 let sub = DOM.tag('span', className, stream.hexDump(start, end));
166 node.appendChild(sub);
167 }
168 toHexDOM(root, trim=true) {
169 let node = DOM.tag('span', 'hex');
170 if (root === undefined) root = node;
171 this.head.hexNode = node;
172 this.head.onmouseover = function () { this.hexNode.className = 'hexCurrent'; };
173 this.head.onmouseout = function () { this.hexNode.className = 'hex'; };
174 node.asn1 = this;
175 node.onmouseover = function () {
176 let current = !root.selected;
177 if (current) {
178 root.selected = this.asn1;
179 this.className = 'hexCurrent';
180 }
181 this.asn1.fakeHover(current);
182 };
183 node.onmouseout = function () {
184 let current = (root.selected == this.asn1);
185 this.asn1.fakeOut(current);
186 if (current) {
187 root.selected = null;
188 this.className = 'hex';
189 }
190 };
191 bindContextMenu(node);
192 if (root == node) {
193 let lineStart = this.posStart() & 0xF;
194 if (lineStart != 0) {
195 let skip = DOM.tag('span', 'skip');
196 let skipStr = '';
197 for (let j = lineStart; j > 0; --j)
198 skipStr += ' ';
199 if (lineStart >= 8)
200 skipStr += ' ';
201 skip.innerText = skipStr;
202 node.appendChild(skip);
203 }
204 }
205 // set the current start and end position as an attribute at the node to know the selected area
206 node.setAttribute('pos', this.posStart());
207 node.setAttribute('end', this.posEnd());
208 this.toHexDOM_sub(node, 'tag', this.stream, this.posStart(), this.posLen());
209 this.toHexDOM_sub(node, (this.length >= 0) ? 'dlen' : 'ulen', this.stream, this.posLen(), this.posContent());
210 if (this.sub === null) {
211 let start = this.posContent();
212 let end = this.posEnd();
213 if (!trim || end - start < 10 * 16)
214 node.appendChild(DOM.text(
215 this.stream.hexDump(start, end)));
216 else {
217 let end1 = start + 5 * 16 - (start & 0xF);
218 let start2 = end - 16 - (end & 0xF);
219 node.appendChild(DOM.text(this.stream.hexDump(start, end1)));
220 node.appendChild(DOM.tag('span', 'skip', '\u2026 skipping ' + (start2 - end1) + ' bytes \u2026\n'));
221 node.appendChild(DOM.text(this.stream.hexDump(start2, end)));
222 }
223 } else if (this.sub.length > 0) {
224 let first = this.sub[0];
225 let last = this.sub[this.sub.length - 1];
226 this.toHexDOM_sub(node, 'intro', this.stream, this.posContent(), first.posStart());
227 for (let i = 0, max = this.sub.length; i < max; ++i)
228 node.appendChild(this.sub[i].toHexDOM(root, trim));
229 this.toHexDOM_sub(node, 'outro', this.stream, last.posEnd(), this.posEnd());
230 } else
231 this.toHexDOM_sub(node, 'outro', this.stream, this.posContent(), this.posEnd());
232 return node;
233 }
234 static decode(stream, offset) {
235 return ASN1.decode(stream, offset, ASN1DOM);
236 }
237
238}