140 lines
4.3 KiB
TypeScript
140 lines
4.3 KiB
TypeScript
// Copyright 2022 The Pigweed Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
// use this file except in compliance with the License. You may obtain a copy of
|
|
// the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
// License for the specific language governing permissions and limitations under
|
|
// the License.
|
|
|
|
/** Decodes and detokenizes strings from binary or Base64 input. */
|
|
import {Buffer} from 'buffer';
|
|
import {Frame} from 'pigweedjs/pw_hdlc';
|
|
import {TokenDatabase} from './token_database';
|
|
import {PrintfDecoder} from './printf_decoder';
|
|
|
|
const MAX_RECURSIONS = 9;
|
|
const BASE64CHARS = '[A-Za-z0-9+/-_]';
|
|
const PATTERN = new RegExp(
|
|
// Base64 tokenized strings start with the prefix character ($)
|
|
'\\$' +
|
|
// Tokenized strings contain 0 or more blocks of four Base64 chars.
|
|
`(?:${BASE64CHARS}{4})*` +
|
|
// The last block of 4 chars may have one or two padding chars (=).
|
|
`(?:${BASE64CHARS}{3}=|${BASE64CHARS}{2}==)?`,
|
|
'g'
|
|
);
|
|
|
|
interface TokenAndArgs {
|
|
token: number;
|
|
args: Uint8Array;
|
|
}
|
|
|
|
export class Detokenizer {
|
|
private database: TokenDatabase;
|
|
|
|
constructor(csvDatabase: string) {
|
|
this.database = new TokenDatabase(csvDatabase);
|
|
}
|
|
|
|
/**
|
|
* Detokenize frame data into actual string messages using the provided
|
|
* token database.
|
|
*
|
|
* If the frame doesn't match any token from database, the frame will be
|
|
* returned as string as-is.
|
|
*/
|
|
detokenize(tokenizedFrame: Frame): string {
|
|
return this.detokenizeUint8Array(tokenizedFrame.data);
|
|
}
|
|
|
|
/**
|
|
* Detokenize uint8 into actual string messages using the provided
|
|
* token database.
|
|
*
|
|
* If the data doesn't match any token from database, the data will be
|
|
* returned as string as-is.
|
|
*/
|
|
detokenizeUint8Array(data: Uint8Array): string {
|
|
const {token, args} = this.decodeUint8Array(data);
|
|
// Parse arguments if this is printf-style text.
|
|
const format = this.database.get(token);
|
|
if (format) {
|
|
return new PrintfDecoder().decode(String(format), args);
|
|
}
|
|
|
|
return new TextDecoder().decode(data);
|
|
}
|
|
|
|
/**
|
|
* Detokenize Base64-encoded frame data into actual string messages using the
|
|
* provided token database.
|
|
*
|
|
* If the frame doesn't match any token from database, the frame will be
|
|
* returned as string as-is.
|
|
*/
|
|
detokenizeBase64(
|
|
tokenizedFrame: Frame,
|
|
maxRecursion: number = MAX_RECURSIONS
|
|
): string {
|
|
const base64String = new TextDecoder().decode(tokenizedFrame.data);
|
|
return this.detokenizeBase64String(base64String, maxRecursion);
|
|
}
|
|
|
|
private detokenizeBase64String(
|
|
base64String: string,
|
|
recursions: number
|
|
): string {
|
|
return base64String.replace(PATTERN, base64Substring => {
|
|
const {token, args} = this.decodeBase64TokenFrame(base64Substring);
|
|
const format = this.database.get(token);
|
|
// Parse arguments if this is printf-style text.
|
|
if (format) {
|
|
const decodedOriginal = new PrintfDecoder().decode(
|
|
String(format),
|
|
args
|
|
);
|
|
// Detokenize nested Base64 tokens and their arguments.
|
|
if (recursions > 0) {
|
|
return this.detokenizeBase64String(decodedOriginal, recursions - 1);
|
|
}
|
|
return decodedOriginal;
|
|
}
|
|
return base64Substring;
|
|
});
|
|
}
|
|
|
|
private decodeUint8Array(data: Uint8Array): TokenAndArgs {
|
|
const token = new DataView(
|
|
data.buffer,
|
|
data.byteOffset,
|
|
4
|
|
).getUint32(0, true);
|
|
const args = new Uint8Array(data.buffer.slice(data.byteOffset + 4));
|
|
|
|
return {token, args};
|
|
}
|
|
|
|
private decodeBase64TokenFrame(base64Data: string): TokenAndArgs {
|
|
// Remove the prefix '$' and convert from Base64.
|
|
const prefixRemoved = base64Data.slice(1);
|
|
const noBase64 = Buffer.from(prefixRemoved, 'base64').toString('binary');
|
|
// Convert back to bytes and return token and arguments.
|
|
const bytes = noBase64.split('').map(ch => ch.charCodeAt(0));
|
|
const uIntArray = new Uint8Array(bytes);
|
|
const token = new DataView(
|
|
uIntArray.buffer,
|
|
uIntArray.byteOffset,
|
|
4
|
|
).getUint32(0, true);
|
|
const args = new Uint8Array(bytes.slice(4));
|
|
|
|
return {token, args};
|
|
}
|
|
}
|