Skip to content
Open
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
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { _decompress } from "./_decompress";
import { compressToBase64, decompressFromBase64 } from "./base64";
import { compressToCustom, decompressFromCustom } from "./custom";
import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from "./encodedURIComponent";
import { isCompressed } from "./isCompressed";
import { loadBinaryFile, saveBinaryFile } from "./node";
import { compress, decompress } from "./raw";
import {
Expand Down Expand Up @@ -38,4 +39,5 @@ export default {
decompressFromUTF16,
loadBinaryFile,
saveBinaryFile,
isCompressed
};
7 changes: 7 additions & 0 deletions src/isCompressed/__test__/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`isCompressed/index.ts > was the change deliberate? 1`] = `
{
"isCompressed": [Function],
}
`;
15 changes: 15 additions & 0 deletions src/isCompressed/__test__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2013 Pieroxy <pieroxy@pieroxy.net>
*
* SPDX-License-Identifier: MIT
*/

import { describe, test } from "vitest";

import * as index from "../index";

describe("isCompressed/index.ts", () => {
test("was the change deliberate?", ({ expect }) => {
expect(index).toMatchSnapshot();
});
});
40 changes: 40 additions & 0 deletions src/isCompressed/__test__/isCompressed.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, it, expect } from "vitest";
import { isCompressed } from "..";
import { compress } from "../../raw";
import { compressToBase64 } from "../../base64";
import { compressToEncodedURIComponent } from "../../encodedURIComponent";
import { compressToUTF16 } from "../../UTF16";
import { compressToUint8Array } from "../../Uint8Array";

describe("isCompressed()", () => {
const raw = "Hello World";

it("returns false for raw string", () => {
expect(isCompressed(raw)).toBe(false);
});

it("detects compress()", () => {
const c = compress(raw);
expect(isCompressed(c)).toBe(true);
});

it("detects compressToBase64()", () => {
const c = compressToBase64(raw);
expect(isCompressed(c)).toBe(true);
});

it("detects compressToEncodedURIComponent()", () => {
const c = compressToEncodedURIComponent(raw);
expect(isCompressed(c)).toBe(true);
});

it("detects compressToUTF16()", () => {
const c = compressToUTF16(raw);
expect(isCompressed(c)).toBe(true);
});

it("detects compressToUint8Array()", () => {
const c = compressToUint8Array(raw);
expect(isCompressed(c)).toBe(true);
});
});
99 changes: 99 additions & 0 deletions src/isCompressed/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* Determine whether the string likely represents `compress()` output.
* The UTF-16 compressed output produced by LZ-String commonly contains
* characters with a charCode value > 255.
*
* @param str - The input string to evaluate.
* @returns `true` if the string contains non-ASCII characters, otherwise `false`.
*/
function looksLikeUTF16(str: string): boolean {
for (let i = 0; i < str.length; i++) {
if (str.charCodeAt(i) > 255) return true;
}
return false;
}

/**
* Detect whether the string is a valid candidate for Base64-based
* compression output from `compressToBase64()`.
* This uses a strict alphabet check and enforces that the length is
* divisible by 4, as required by Base64 encoding.
*
* @param str - The string to validate.
* @returns `true` if the string matches Base64 patterns, otherwise `false`.
*/
function looksLikeBase64(str: string): boolean {
return /^[A-Za-z0-9+/=]+$/.test(str) && str.length % 4 === 0;
}

/**
* Detect whether the string matches the URI-safe alphabet used by
* `compressToEncodedURIComponent()`.
*
* @param str - The string to inspect.
* @returns `true` if it matches the URI-encoded compression alphabet.
*/
function looksLikeURIEncoded(str: string): boolean {
return /^[0-9A-Za-z\-_%!'()*]+$/.test(str);
}

/**
* Identify strings generated by `compressToUTF16()`.
* The first character always follows a predictable range:
* `32 <= code < 32 + 32` (based on the LZ-String implementation).
*
* @param str - The string to check.
* @returns `true` if the signature matches UTF-16 compressed output.
*/
function looksLikeUTF16Special(str: string): boolean {
if (str.length === 0) return false;
const code = str.charCodeAt(0);
const base = 32;
return code >= base && code < base + 32;
}

/**
* Determine whether a value appears to be LZ-String compressed data,
* covering all primary compression formats:
*
* - `compress()` → UTF-16 (non-ASCII characters)
* - `compressToBase64()` → Base64 signature
* - `compressToEncodedURIComponent()` → URI-safe alphabet
* - `compressToUTF16()` → Leading UTF-16 header range
* - `compressToUint8Array()` → `Uint8Array` instance
*
* This function does not perform decompression.
* Detection is heuristic-based but highly accurate against standard LZ-String behavior.
*
* @param input - The value to evaluate. May be a string or a Uint8Array.
* @returns `true` if the input appears to be in any LZ-String compressed format.
*/
export function isCompressed(input: unknown): boolean {
if (input instanceof Uint8Array) {
return true;
}

if (typeof input !== "string") {
return false;
}

const str = input;

if (looksLikeUTF16(str)) {
return true;
}

if (looksLikeUTF16Special(str)) {
return true;
}

if (looksLikeBase64(str)) {
return true;
}

if (looksLikeURIEncoded(str)) {
return true;
}

return false;
}