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
1 change: 0 additions & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
npm run lint
npm run lint:package
2 changes: 0 additions & 2 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
npm run lint
npm run lint:package
npm run lint:secrets
npm run license:check
npm run docs
npm test
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { Buffer } from "buffer";
* Local response loaded from a file.
* Note: Has to be initialized through init() before use.
*/
export abstract class LocalResponseBase {
private file: Buffer;
export abstract class BaseLocalResponse {
private fileBytes: Buffer;
private readonly inputHandle: Buffer | string;
/** Whether the local response payload has been loaded. */
protected initialized = false;
Expand All @@ -18,7 +18,13 @@ export abstract class LocalResponseBase {
* Creates an instance of LocalResponse.
*/
constructor(inputFile: Buffer | string) {
this.file = Buffer.alloc(0);
if (inputFile === undefined || inputFile === null) {
throw new TypeError("input cannot be null or undefined");
}
if (typeof inputFile === "string" ? !inputFile.trim() : !inputFile.length) {
throw new TypeError("input cannot be empty");
}
this.fileBytes = Buffer.alloc(0);
this.inputHandle = inputFile;
}

Expand All @@ -31,7 +37,7 @@ export abstract class LocalResponseBase {
return;
}
if (Buffer.isBuffer(this.inputHandle)) {
this.file = this.inputHandle;
this.fileBytes = this.inputHandle;
} else if (typeof this.inputHandle === "string") {
let fileContents;
try {
Expand All @@ -40,7 +46,10 @@ export abstract class LocalResponseBase {
} catch {
fileContents = this.inputHandle;
}
this.file = Buffer.from(fileContents.replace(/\r/g, "").replace(/\n/g, ""), "utf-8");
this.fileBytes = Buffer.from(
fileContents.replace(/\r/g, "").replace(/\n/g, ""),
"utf-8"
);
} else {
throw new MindeeError("Incompatible type for input.");
}
Expand All @@ -56,7 +65,7 @@ export abstract class LocalResponseBase {
await this.init();
}
try {
const content = this.file.toString("utf-8");
const content = this.fileBytes.toString("utf-8");
return JSON.parse(content);
} catch {
throw new MindeeError("File is not a valid dictionary.");
Expand All @@ -77,7 +86,7 @@ export abstract class LocalResponseBase {
const algorithm = "sha256";
try {
const hmac = crypto.createHmac(algorithm, secretKey);
hmac.update(this.file);
hmac.update(this.fileBytes);
return hmac.digest("hex");
} catch {
throw new MindeeError("Could not get HMAC signature from payload.");
Expand All @@ -96,6 +105,31 @@ export abstract class LocalResponseBase {
"The `init()` method must be called before calling `isValidHmacSignature()`."
);
}
return signature === this.getHmacSignature(secretKey);
if (
(!signature || !signature?.trim())
|| (!secretKey || (typeof secretKey === "string" ? !secretKey?.trim() : !secretKey?.length))
) {
return false;
}

const expectedSignature = this.getHmacSignature(secretKey);
if (!expectedSignature?.trim()) {
return false;
}

const expectedBytes = Buffer.from(expectedSignature, "utf-8");
const actualBytes = Buffer.from(signature.toLowerCase(), "utf-8");

if (expectedBytes.length !== actualBytes.length) {
return false;
}
return crypto.timingSafeEqual(expectedBytes, actualBytes);
}

/**
* Print the file as a UTF-8 string.
*/
public toString(): string {
return this.fileBytes.toString("utf-8");
}
}
4 changes: 2 additions & 2 deletions src/v1/parsing/localResponse.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LocalResponseBase } from "@/parsing/localResponseBase.js";
import { BaseLocalResponse } from "@/parsing/baseLocalResponse.js";
import { AsyncPredictResponse, Inference, PredictResponse } from "@/v1/index.js";
import { StringDict } from "@/parsing/index.js";
import { MindeeError } from "@/errors/index.js";
Expand All @@ -7,7 +7,7 @@ import { MindeeError } from "@/errors/index.js";
* Local response loaded from a file.
* Note: Has to be initialized through init() before use.
*/
export class LocalResponse extends LocalResponseBase {
export class LocalResponse extends BaseLocalResponse {
/** Loads a local JSON payload into a typed prediction response wrapper. */
async loadPrediction<T extends Inference>(
productClass: new (httpResponse: StringDict) => T
Expand Down
4 changes: 2 additions & 2 deletions src/v2/parsing/localResponse.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { StringDict } from "@/parsing/stringDict.js";
import { MindeeError } from "@/errors/index.js";
import { LocalResponseBase } from "@/parsing/localResponseBase.js";
import { BaseLocalResponse } from "@/parsing/baseLocalResponse.js";
import { BaseResponse } from "./baseResponse.js";

/**
* Local response loaded from a file.
* Note: Has to be initialized through init() before use.
*/
export class LocalResponse extends LocalResponseBase {
export class LocalResponse extends BaseLocalResponse {

/**
* Deserialize the loaded local response into a product response class.
Expand Down
6 changes: 2 additions & 4 deletions tests/input/compression.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,10 @@ describe("Input Sources - compression and resize #OptionalDepsRequired", { skip:
const resizes = [
await compressPdf(pdfResizeInput.fileObject, 85),
await compressPdf(pdfResizeInput.fileObject, 75),
await compressPdf(pdfResizeInput.fileObject, 50),
await compressPdf(pdfResizeInput.fileObject, 10)
await compressPdf(pdfResizeInput.fileObject, 50)
];

const fileNames = ["compress85.pdf", "compress75.pdf", "compress50.pdf", "compress10.pdf"];
const fileNames = ["compress85.pdf", "compress75.pdf", "compress50.pdf"];
for (let i = 0; i < resizes.length; i++) {
await fs.promises.writeFile(path.join(outputPath, fileNames[i]), resizes[i]);
}
Expand All @@ -163,7 +162,6 @@ describe("Input Sources - compression and resize #OptionalDepsRequired", { skip:
assert.ok(initialFileStats.size > renderedFileStats[0].size);
assert.ok(renderedFileStats[0].size > renderedFileStats[1].size);
assert.ok(renderedFileStats[1].size > renderedFileStats[2].size);
assert.ok(renderedFileStats[2].size > renderedFileStats[3].size);
});

it("PDF Compress With Text Keeps Text", async () => {
Expand Down
81 changes: 49 additions & 32 deletions tests/v2/parsing/localResponse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,53 +14,70 @@ const filePath: string = path.join(V2_PRODUCT_PATH, "extraction/standard_field_t

/**
* Asserts that a local response is valid.
* @param localResponse The local response to validate.
*/
async function assertLocalResponse(localResponse: LocalResponse) {
async function assertLocalResponse(localResponse: LocalResponse, fileContent: string) {
await localResponse.init();
assert.notStrictEqual(localResponse.asDict(), null);
assert.strictEqual(localResponse.isValidHmacSignature(dummySecretKey, "invalid signature"), false);
assert.notStrictEqual(await localResponse.asDict(), null);

assert.strictEqual(localResponse.getHmacSignature(dummySecretKey), signature);
Comment thread
ianardee marked this conversation as resolved.

assert.strictEqual(localResponse.isValidHmacSignature(dummySecretKey, "invalid signature"), false);
assert.strictEqual(localResponse.isValidHmacSignature(dummySecretKey, null as any), false);
assert.strictEqual(localResponse.isValidHmacSignature(null as any, signature), false);
assert.strictEqual(localResponse.isValidHmacSignature(null as any, null as any), false);
assert.strictEqual(localResponse.isValidHmacSignature(dummySecretKey, ""), false);
assert.ok(localResponse.isValidHmacSignature(dummySecretKey, signature));
const inferenceResponse = await localResponse.deserializeResponse(ExtractionResponse);
assert.ok(inferenceResponse instanceof ExtractionResponse);
assert.notStrictEqual(inferenceResponse.inference, null);
assert.ok(localResponse.isValidHmacSignature(dummySecretKey, signature.toUpperCase()));

const response = await localResponse.deserializeResponse(ExtractionResponse);
assert.ok(response instanceof ExtractionResponse);
assert.notStrictEqual(response.inference, null);
assert.strictEqual(response.inference.model.id, "test-model-id");
assert.strictEqual(
response.inference.result.fields.getSimpleField("field_simple_string").stringValue,
"field_simple_string-value"
);

assert.strictEqual(
JSON.stringify(response.getRawHttp()), JSON.stringify(JSON.parse(fileContent))
);

assert.strictEqual(
localResponse.toString(),
fileContent.replace(/[\r\n]/g, "")
);
}

describe("MindeeV2 - Load Local Response", () => {
it("should load a string properly.", async () => {
const fileObj = await fs.readFile(filePath, { encoding: "utf-8" });
await assertLocalResponse(new LocalResponse(fileObj));
it("should load a response from a JSON string.", async () => {
const fileContent = await fs.readFile(filePath, { encoding: "utf-8" });
await assertLocalResponse(new LocalResponse(fileContent), fileContent);
});

it("should load a file properly.", async () => {
await assertLocalResponse(new LocalResponse(filePath));
it("should load a response from a buffer", async () => {
const fileContent = (await fs.readFile(filePath, { encoding: "utf-8" })).replace(/\r/g, "").replace(/\n/g, "");
const fileBuffer = Buffer.from(fileContent, "utf-8");
await assertLocalResponse(new LocalResponse(fileBuffer), fileContent);
});

it("should load a buffer properly.", async () => {
const fileStr = (await fs.readFile(filePath, { encoding: "utf-8" })).replace(/\r/g, "").replace(/\n/g, "");
const fileBuffer = Buffer.from(fileStr, "utf-8");
await assertLocalResponse(new LocalResponse(fileBuffer));
it("should load a response from a JSON file", async () => {
await assertLocalResponse(new LocalResponse(filePath), await fs.readFile(filePath, { encoding: "utf-8" }));
});

it("should deserialize a prediction.", async () => {
const fileObj = await fs.readFile(filePath, { encoding: "utf-8" });
const localResponse = new LocalResponse(fileObj);
const response = await localResponse.deserializeResponse(ExtractionResponse);
assert.ok(response instanceof ExtractionResponse);
it("should raise an exception when given an invalid JSON string", async () => {
const localResponse = new LocalResponse("{invalid json");
await assert.rejects(async () => {
await localResponse.deserializeResponse(ExtractionResponse);
});
});

assert.strictEqual(JSON.stringify(response.getRawHttp()), JSON.stringify(JSON.parse(fileObj)));
it("should raise an exception when given an empty value", () => {
assert.throws(() => new LocalResponse(""));
assert.throws(() => new LocalResponse(Buffer.alloc(0)));
});

it("should load an inference of a catalog model", async () => {
const jsonPath = path.join(
V2_PRODUCT_PATH,
"extraction",
"financial_document",
"complete.json"
);
const localResponse = new LocalResponse(jsonPath);
const response: ExtractionResponse = await localResponse.deserializeResponse(ExtractionResponse);
assert.strictEqual(response.inference.model.id, "12345678-1234-1234-1234-123456789abc");
it("should raise an exception when given a null value", () => {
assert.throws(() => new LocalResponse(null as any));
assert.throws(() => new LocalResponse(undefined as any));
});
});