blob: c2518da1b53856a5211145cc1fcdd5c7c25ea9ab [file] [log] [blame]
// 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 arguments and formats them with the provided format string. */
const SPECIFIER_REGEX = /%[a-zA-Z%]/g;
// Conversion specifiers by type; n is not supported.
const SIGNED_INT = 'di'.split('');
const UNSIGNED_INT = 'oxXup'.split('');
const FLOATING_POINT = 'fFeEaAgG'.split('');
enum DecodedStatusFlags {
// Status flags for a decoded argument. These values should match the
// DecodingStatus enum in pw_tokenizer/internal/decode.h.
OK = 0, // decoding was successful
MISSING = 1, // the argument was not present in the data
TRUNCATED = 2, // the argument was truncated during encoding
DECODE_ERROR = 4, // an error occurred while decoding the argument
SKIPPED = 8, // argument was skipped due to a previous error
}
interface DecodedArg {
size: number;
value: string | number | null;
}
// ZigZag decode function from protobuf's wire_format module.
function zigzagDecode(value: number): number {
if (!(value & 0x1)) return value >> 1;
return (value >> 1) ^ ~0;
}
export class PrintfDecoder {
// Reads a unicode string from the encoded data.
private decodeString(args: Uint8Array): DecodedArg {
if (args.length === 0) return {size: 0, value: null};
let sizeAndStatus = args[0];
let status = DecodedStatusFlags.OK;
if (sizeAndStatus & 0x80) {
status |= DecodedStatusFlags.TRUNCATED;
sizeAndStatus &= 0x7f;
}
const rawData = args.slice(0, sizeAndStatus + 1);
const data = rawData.slice(1);
if (data.length < sizeAndStatus) {
status |= DecodedStatusFlags.DECODE_ERROR;
}
const decoded = new TextDecoder().decode(data);
return {size: rawData.length, value: decoded};
}
private decodeSignedInt(args: Uint8Array): DecodedArg {
if (args.length === 0) return {size: 0, value: null};
let count = 0;
let result = 0;
let shift = 0;
for (count = 0; count < args.length; count++) {
const byte = args[count];
result |= (byte & 0x7f) << shift;
if (!(byte & 0x80)) {
return {value: zigzagDecode(result), size: count + 1};
}
shift += 7;
if (shift >= 64) break;
}
return {size: 0, value: null};
}
private decodeUnsignedInt(args: Uint8Array): DecodedArg {
const arg = this.decodeSignedInt(args);
// Since ZigZag encoding is used, unsigned integers must be masked off to
// their original bit length.
if (arg.value !== null) {
let num = arg.value as number;
num = num >>> 0;
arg.value = num;
}
return arg;
}
private decodeChar(args: Uint8Array): DecodedArg {
const arg = this.decodeSignedInt(args);
if (arg.value !== null) {
const num = arg.value as number;
arg.value = String.fromCharCode(num);
}
return arg;
}
private decodeFloat(args: Uint8Array): DecodedArg {
if (args.length < 4) return {size: 0, value: ''};
const floatValue = new DataView(args.buffer, args.byteOffset, 4).getFloat32(
0,
true
);
return {size: 4, value: floatValue};
}
private format(specifier: string, args: Uint8Array): DecodedArg {
if (specifier == '%') return {size: 0, value: '%'}; // literal %
specifier = specifier.slice(1);
if (specifier === 's') {
return this.decodeString(args);
}
if (specifier === 'c') {
return this.decodeChar(args);
}
if (SIGNED_INT.indexOf(specifier) !== -1) {
return this.decodeSignedInt(args);
}
if (UNSIGNED_INT.indexOf(specifier) !== -1) {
return this.decodeUnsignedInt(args);
}
if (FLOATING_POINT.indexOf(specifier) !== -1) {
return this.decodeFloat(args);
}
// Unsupported specifier, return as-is
return {size: 0, value: '%' + specifier};
}
decode(formatString: string, args: Uint8Array): string {
return formatString.replace(SPECIFIER_REGEX, specifier => {
const decodedArg = this.format(specifier, args);
args = args.slice(decodedArg.size);
if (decodedArg === null) return '';
return String(decodedArg.value);
});
}
}