asn1tools-js
Version:
ASN.1 encoding and decoding library for TypeScript/JavaScript, compatible with Python asn1tools
257 lines • 11 kB
JavaScript
;
/**
* Complex ASN.1 type implementations
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChoiceType = exports.SequenceOfType = exports.SequenceType = void 0;
const types_1 = require("../types");
const encoding_1 = require("./encoding");
const types_2 = require("./types");
/**
* ASN.1 SEQUENCE type
*/
class SequenceType extends types_2.BaseType {
constructor(name, members) {
super(name, types_1.BER.TAG.SEQUENCE);
this.constructed = true;
this.members = members;
}
encode(value) {
if (!value || typeof value !== 'object') {
throw new types_1.EncodeError(`SEQUENCE ${this.name}: expected object, got ${typeof value}`);
}
const encodedMembers = [];
for (const member of this.members) {
const memberValue = value[member.name];
if (memberValue === undefined || memberValue === null) {
if (member.optional) {
continue; // Skip optional members
}
if (member.defaultValue !== undefined) {
// Use default value
const encoded = member.type.encode(member.defaultValue);
encodedMembers.push(encoded);
}
else {
throw new types_1.EncodeError(`SEQUENCE ${this.name}: missing required member '${member.name}'`);
}
}
else {
const encoded = member.type.encode(memberValue);
encodedMembers.push(encoded);
}
}
// Concatenate all encoded members
const totalLength = encodedMembers.reduce((sum, arr) => sum + arr.length, 0);
const content = new Uint8Array(totalLength);
let offset = 0;
for (const encoded of encodedMembers) {
content.set(encoded, offset);
offset += encoded.length;
}
return this.encodeWithTag(content);
}
decode(data, offset = 0) {
const { content, totalLength } = this.decodeWithTag(data, offset);
const result = {};
let contentOffset = 0;
let memberIndex = 0;
while (contentOffset < content.length && memberIndex < this.members.length) {
const member = this.members[memberIndex];
if (!member) {
throw new types_1.DecodeError(`SEQUENCE ${this.name}: internal error - member at index ${memberIndex} not found`, offset);
}
try {
const decoded = member.type.decode(content, contentOffset);
result[member.name] = decoded.value;
contentOffset += decoded.length;
memberIndex++;
}
catch (error) {
if (member.optional) {
// Skip optional member that's not present
memberIndex++;
continue;
}
if (member.defaultValue !== undefined) {
// Use default value
result[member.name] = member.defaultValue;
memberIndex++;
continue;
}
throw new types_1.DecodeError(`SEQUENCE ${this.name}: failed to decode member '${member.name}': ${error instanceof Error ? error.message : String(error)}`, offset + contentOffset);
}
}
// Check for missing required members
while (memberIndex < this.members.length) {
const member = this.members[memberIndex];
if (!member) {
throw new types_1.DecodeError(`SEQUENCE ${this.name}: internal error - member at index ${memberIndex} not found`, offset);
}
if (!member.optional && member.defaultValue === undefined) {
throw new types_1.DecodeError(`SEQUENCE ${this.name}: missing required member '${member.name}'`, offset);
}
if (member.defaultValue !== undefined) {
result[member.name] = member.defaultValue;
}
memberIndex++;
}
return { value: result, length: totalLength };
}
}
exports.SequenceType = SequenceType;
/**
* ASN.1 SEQUENCE OF type
*/
class SequenceOfType extends types_2.BaseType {
constructor(name, elementType) {
super(name, types_1.BER.TAG.SEQUENCE);
this.constructed = true;
this.elementType = elementType;
}
encode(value) {
if (!Array.isArray(value)) {
throw new types_1.EncodeError(`SEQUENCE OF ${this.name}: expected array, got ${typeof value}`);
}
const encodedElements = [];
for (let i = 0; i < value.length; i++) {
try {
const encoded = this.elementType.encode(value[i]);
encodedElements.push(encoded);
}
catch (error) {
throw new types_1.EncodeError(`SEQUENCE OF ${this.name}: failed to encode element ${i}: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Concatenate all encoded elements
const totalLength = encodedElements.reduce((sum, arr) => sum + arr.length, 0);
const content = new Uint8Array(totalLength);
let offset = 0;
for (const encoded of encodedElements) {
content.set(encoded, offset);
offset += encoded.length;
}
return this.encodeWithTag(content);
}
decode(data, offset = 0) {
const { content, totalLength } = this.decodeWithTag(data, offset);
const result = [];
let contentOffset = 0;
while (contentOffset < content.length) {
try {
const decoded = this.elementType.decode(content, contentOffset);
result.push(decoded.value);
contentOffset += decoded.length;
}
catch (error) {
throw new types_1.DecodeError(`SEQUENCE OF ${this.name}: failed to decode element ${result.length}: ${error instanceof Error ? error.message : String(error)}`, offset + contentOffset);
}
}
return { value: result, length: totalLength };
}
}
exports.SequenceOfType = SequenceOfType;
/**
* ASN.1 CHOICE type
*/
class ChoiceType extends types_2.BaseType {
constructor(name, choices) {
super(name, types_1.BER.TAG.CHOICE); // Special marker tag
this.choices = new Map();
this.tagToChoice = new Map();
for (const choice of choices) {
const choiceObj = { type: choice.type };
if (choice.tag !== undefined) {
choiceObj.tag = choice.tag;
}
this.choices.set(choice.name, choiceObj);
// Map tag to choice name for decoding
const choiceTag = choice.tag ?? choice.type.tag;
this.tagToChoice.set(choiceTag, choice.name);
}
}
encode(value) {
if (!value || typeof value !== 'object') {
throw new types_1.EncodeError(`CHOICE ${this.name}: expected object with single property, got ${typeof value}`);
}
const keys = Object.keys(value);
if (keys.length !== 1) {
throw new types_1.EncodeError(`CHOICE ${this.name}: expected object with exactly one property, got ${keys.length}`);
}
const choiceName = keys[0];
if (!choiceName) {
throw new types_1.EncodeError(`CHOICE ${this.name}: invalid choice name`);
}
const choiceValue = value[choiceName];
const choice = this.choices.get(choiceName);
if (!choice) {
throw new types_1.EncodeError(`CHOICE ${this.name}: unknown choice '${choiceName}'`);
}
// For CHOICE, we encode the selected alternative directly
// If there's a context-specific tag, apply it
if (choice.tag !== undefined) {
// Apply context-specific tag
const innerEncoded = choice.type.encode(choiceValue);
const tagBytes = (0, encoding_1.encodeTag)(choice.tag, types_1.BER.CLASS.CONTEXT_SPECIFIC | types_1.BER.ENCODING.CONSTRUCTED);
const lengthBytes = (0, encoding_1.encodeLength)(innerEncoded.length);
const result = new Uint8Array(tagBytes.length + lengthBytes.length + innerEncoded.length);
let offset = 0;
result.set(tagBytes, offset);
offset += tagBytes.length;
result.set(lengthBytes, offset);
offset += lengthBytes.length;
result.set(innerEncoded, offset);
return result;
}
else {
return choice.type.encode(choiceValue);
}
}
decode(data, offset = 0) {
if (offset >= data.length) {
throw new types_1.DecodeError(`CHOICE ${this.name}: unexpected end of data`, offset);
}
// Peek at the tag to determine which choice to decode
const tagInfo = (0, encoding_1.decodeTag)(data, offset);
const choiceName = this.tagToChoice.get(tagInfo.tag);
if (!choiceName) {
throw new types_1.DecodeError(`CHOICE ${this.name}: no choice found for tag ${tagInfo.tag}`, offset);
}
const choice = this.choices.get(choiceName);
if (!choice) {
throw new types_1.DecodeError(`CHOICE ${this.name}: internal error - choice '${choiceName}' not found`, offset);
}
// Decode using the appropriate choice type
if (choice.tag !== undefined) {
// Handle context-specific tagged choice
const lengthInfo = (0, encoding_1.decodeLength)(data, offset + tagInfo.length);
const contentStart = offset + tagInfo.length + lengthInfo.octets;
const contentEnd = contentStart + lengthInfo.length;
if (contentEnd > data.length) {
throw new types_1.DecodeError(`CHOICE ${this.name}: not enough data for choice content`, offset);
}
const contentData = data.slice(contentStart, contentEnd);
const decoded = choice.type.decode(contentData, 0);
return {
value: { [choiceName]: decoded.value },
length: tagInfo.length + lengthInfo.octets + lengthInfo.length
};
}
else {
const decoded = choice.type.decode(data, offset);
return {
value: { [choiceName]: decoded.value },
length: decoded.length
};
}
}
// Override the base class method since CHOICE doesn't have a fixed tag
decodeWithTag(_data, _offset = 0) {
throw new Error('CHOICE type should not use decodeWithTag - use decode directly');
}
encodeWithTag(_content) {
throw new Error('CHOICE type should not use encodeWithTag - use encode directly');
}
}
exports.ChoiceType = ChoiceType;
//# sourceMappingURL=complex-types.js.map