Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 53 additions & 5 deletions src/core/lib/Protobuf.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,17 @@ class Protobuf {

try {
message = this.parsedProto.root.nested[this.mainMessageName];
const packageDecode = message.toObject(message.decode(input), {
const reader = protobuf.Reader.create(input);
let decoded;
try {
decoded = message.decode(reader);
} catch (error) {
if (error && !Number.isInteger(error.byteOffset)) {
error.byteOffset = reader.pos;
}
throw error;
}
const packageDecode = message.toObject(decoded, {
bytes: String,
longs: Number,
enums: String,
Expand All @@ -198,7 +208,11 @@ class Protobuf {

} catch (error) {
if (message) {
throw new Error("Input " + error);
const wrapped = new Error("Input " + error);
if (Number.isInteger(error?.byteOffset)) {
wrapped.byteOffset = error.byteOffset;
}
throw wrapped;
} else {
return rawDecode;
}
Expand Down Expand Up @@ -372,7 +386,7 @@ class Protobuf {
}
// Throw an error if we have gone beyond the end of the data
if (this.offset > this.LENGTH) {
throw new Error("Exhausted Buffer");
throw this._parseError("Exhausted Buffer", this.LENGTH);
}
return object;
}
Expand Down Expand Up @@ -400,6 +414,20 @@ class Protobuf {
return object;
}

/**
* Create a parse error annotated with the byte offset.
*
* @private
* @param {string} message
* @param {number} byteOffset
* @returns {Error}
*/
_parseError(message, byteOffset) {
const error = new Error(message);
error.byteOffset = byteOffset;
return error;
}

/**
* Parse a field and return the Object read from the record
*
Expand All @@ -408,6 +436,7 @@ class Protobuf {
*/
_parseField() {
// Get the field headers
const fieldOffset = this.offset;
const header = this._fieldHeader();
const type = header.type;
const key = header.key;
Expand All @@ -430,8 +459,9 @@ class Protobuf {
case 5:
return { "key": key, "value": this._uint32() };
// unknown type
default:
throw new Error("Unknown type 0x" + type.toString(16));
default: {
throw this._parseError("Unknown type 0x" + type.toString(16), fieldOffset);
}
}
}

Expand All @@ -455,6 +485,9 @@ class Protobuf {
* @returns {number}
*/
_fieldType() {
if (this.offset >= this.LENGTH) {
throw this._parseError("Exhausted Buffer", this.LENGTH);
}
// Field type stored in lower 3 bits of tag byte
return this.data[this.offset] & this.TYPE;
}
Expand All @@ -472,6 +505,9 @@ class Protobuf {
let shift = -3;
let fieldNumber = 0;
do {
if (this.offset >= this.LENGTH) {
throw this._parseError("Exhausted Buffer", this.LENGTH);
}
fieldNumber += shift < 28 ?
shift === -3 ?
(this.data[this.offset] & this.NUMBER) >> -shift :
Expand All @@ -495,6 +531,9 @@ class Protobuf {
let shift = 0;
// Keep reading while upper bit set
do {
if (this.offset >= this.LENGTH) {
throw this._parseError("Exhausted Buffer", this.LENGTH);
}
value += shift < 28 ?
(this.data[this.offset] & this.VALUE) << shift :
(this.data[this.offset] & this.VALUE) * Math.pow(2, shift);
Expand All @@ -510,6 +549,9 @@ class Protobuf {
* @returns {number}
*/
_uint64() {
if (this.offset + 8 > this.LENGTH) {
throw this._parseError("Exhausted Buffer", this.LENGTH);
}
// Read off a Uint64 with little-endian
const lowerHalf = this.data[this.offset++] + (this.data[this.offset++] * 0x100) + (this.data[this.offset++] * 0x10000) + this.data[this.offset++] * 0x1000000;
const upperHalf = this.data[this.offset++] + (this.data[this.offset++] * 0x100) + (this.data[this.offset++] * 0x10000) + this.data[this.offset++] * 0x1000000;
Expand All @@ -525,6 +567,9 @@ class Protobuf {
_lenDelim(fieldNum) {
// Read off the field length
const length = this._varInt();
if (this.offset + length > this.LENGTH) {
throw this._parseError("Exhausted Buffer", this.LENGTH);
}
const fieldBytes = this.data.slice(this.offset, this.offset + length);
let field;
try {
Expand All @@ -551,6 +596,9 @@ class Protobuf {
* @returns {number}
*/
_uint32() {
if (this.offset + 4 > this.LENGTH) {
throw this._parseError("Exhausted Buffer", this.LENGTH);
}
// Use a dataview to read off the integer
const dataview = new DataView(new Uint8Array(this.data.slice(this.offset, this.offset + 4)).buffer);
const value = dataview.getUint32(0, true);
Expand Down
4 changes: 3 additions & 1 deletion src/core/operations/ProtobufDecode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ class ProtobufDecode extends Operation {
try {
return Protobuf.decode(input, args);
} catch (err) {
throw new OperationError(err);
const message = err instanceof Error ? err.message : String(err);
const offset = Number.isInteger(err?.byteOffset) ? ` at byte offset ${err.byteOffset}` : "";
throw new OperationError(message + offset);
}
}

Expand Down
82 changes: 82 additions & 0 deletions tests/operations/tests/Protobuf.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,88 @@
import TestRegister from "../../lib/TestRegister.mjs";

TestRegister.addTests([
{
name: "Protobuf Decode: malformed input reports byte offset",
input: "08",
expectedOutput: "Exhausted Buffer at byte offset 1",
recipeConfig: [
{
"op": "From Hex",
"args": ["Auto"]
},
{
"op": "Protobuf Decode",
"args": ["", false, false]
}
]
},
{
name: "Protobuf Decode: invalid wire type reports byte offset",
input: "0f",
expectedOutput: "Unknown type 0x7 at byte offset 0",
recipeConfig: [
{
"op": "From Hex",
"args": ["Auto"]
},
{
"op": "Protobuf Decode",
"args": ["", false, false]
}
]
},
{
name: "Protobuf Decode: continued field header reports byte offset",
input: "80",
expectedOutput: "Exhausted Buffer at byte offset 1",
recipeConfig: [
{ "op": "From Hex", "args": ["Auto"] },
{ "op": "Protobuf Decode", "args": ["", false, false] }
]
},
{
name: "Protobuf Decode: truncated fixed32 reports byte offset",
input: "05",
expectedOutput: "Exhausted Buffer at byte offset 1",
recipeConfig: [
{ "op": "From Hex", "args": ["Auto"] },
{ "op": "Protobuf Decode", "args": ["", false, false] }
]
},
{
name: "Protobuf Decode: truncated fixed64 reports byte offset",
input: "0901020304050607",
expectedOutput: "Exhausted Buffer at byte offset 8",
recipeConfig: [
{ "op": "From Hex", "args": ["Auto"] },
{ "op": "Protobuf Decode", "args": ["", false, false] }
]
},
{
name: "Protobuf Decode: truncated length-delimited payload reports byte offset",
input: "0a030102",
expectedOutput: "Exhausted Buffer at byte offset 4",
recipeConfig: [
{ "op": "From Hex", "args": ["Auto"] },
{ "op": "Protobuf Decode", "args": ["", false, false] }
]
},
{
name: "Protobuf Decode: schema-backed nested failure reports absolute byte offset",
input: "0a0108",
expectedOutput: "Input RangeError: index out of range: 3 + 1 > 3 at byte offset 3",
recipeConfig: [
{ "op": "From Hex", "args": ["Auto"] },
{
"op": "Protobuf Decode",
"args": [
"message Outer { optional Inner inner = 1; } message Inner { optional int32 value = 1; }",
false,
false
]
}
]
},
{
name: "Protobuf Decode: no schema",
input: "0d1c0000001203596f751a024d65202b2a0a0a066162633132331200",
Expand Down
Loading