@juspay/neurolink 10.10.12 → 10.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/adapters/imageFormatSupport.d.ts +75 -0
- package/dist/adapters/imageFormatSupport.js +283 -0
- package/dist/adapters/video/ffmpegAdapter.d.ts +6 -0
- package/dist/adapters/video/ffmpegAdapter.js +1 -1
- package/dist/browser/neurolink.min.js +399 -398
- package/dist/lib/adapters/imageFormatSupport.d.ts +75 -0
- package/dist/lib/adapters/imageFormatSupport.js +284 -0
- package/dist/lib/adapters/video/ffmpegAdapter.d.ts +6 -0
- package/dist/lib/adapters/video/ffmpegAdapter.js +1 -1
- package/dist/lib/processors/config/fileExtensions.d.ts +32 -15
- package/dist/lib/processors/config/fileExtensions.js +27 -66
- package/dist/lib/processors/config/fileTypeRegistry.d.ts +106 -0
- package/dist/lib/processors/config/fileTypeRegistry.js +702 -0
- package/dist/lib/processors/config/index.d.ts +2 -1
- package/dist/lib/processors/config/index.js +5 -1
- package/dist/lib/processors/config/mimeConstants.d.ts +22 -7
- package/dist/lib/processors/config/mimeConstants.js +45 -66
- package/dist/lib/processors/media/AudioProcessor.js +16 -38
- package/dist/lib/processors/media/VideoProcessor.js +11 -32
- package/dist/lib/providers/googleAiStudio/client.js +12 -1
- package/dist/lib/providers/googleVertex/client.js +123 -66
- package/dist/lib/types/file.d.ts +41 -0
- package/dist/lib/utils/fileDetector.js +367 -251
- package/dist/lib/utils/imageProcessor.js +14 -17
- package/dist/lib/utils/markupSniff.d.ts +37 -0
- package/dist/lib/utils/markupSniff.js +125 -0
- package/dist/lib/utils/messageBuilder.d.ts +14 -0
- package/dist/lib/utils/messageBuilder.js +172 -92
- package/dist/processors/config/fileExtensions.d.ts +32 -15
- package/dist/processors/config/fileExtensions.js +27 -66
- package/dist/processors/config/fileTypeRegistry.d.ts +106 -0
- package/dist/processors/config/fileTypeRegistry.js +701 -0
- package/dist/processors/config/index.d.ts +2 -1
- package/dist/processors/config/index.js +5 -1
- package/dist/processors/config/mimeConstants.d.ts +22 -7
- package/dist/processors/config/mimeConstants.js +45 -66
- package/dist/processors/media/AudioProcessor.js +16 -38
- package/dist/processors/media/VideoProcessor.js +11 -32
- package/dist/providers/googleAiStudio/client.js +12 -1
- package/dist/providers/googleVertex/client.js +123 -66
- package/dist/types/file.d.ts +41 -0
- package/dist/utils/fileDetector.js +367 -251
- package/dist/utils/imageProcessor.js +14 -17
- package/dist/utils/markupSniff.d.ts +37 -0
- package/dist/utils/markupSniff.js +124 -0
- package/dist/utils/messageBuilder.d.ts +14 -0
- package/dist/utils/messageBuilder.js +172 -92
- package/package.json +3 -2
|
@@ -21,10 +21,15 @@ async function getArchiveProcessor() {
|
|
|
21
21
|
return mod.archiveProcessor;
|
|
22
22
|
}
|
|
23
23
|
import { tracers, ATTR, withSpan } from "../telemetry/index.js";
|
|
24
|
+
import { CONFIG_EXTENSIONS } from "../processors/config/fileExtensions.js";
|
|
25
|
+
import { fileTypeForExtension, lookupByMimeType, normalizeExtension, } from "../processors/config/fileTypeRegistry.js";
|
|
26
|
+
import { LANGUAGE_MAP } from "../processors/config/languageMap.js";
|
|
27
|
+
import { getMimeTypeForExtension, TEXT_EXTENSION_MIME_MAP, } from "../processors/config/mimeConstants.js";
|
|
24
28
|
import { CSVProcessor } from "./csvProcessor.js";
|
|
25
29
|
import { ImageProcessor } from "./imageProcessor.js";
|
|
26
30
|
import { detectIsoBmffImageMimeType, hasFtypBoxSignature } from "./isoBmff.js";
|
|
27
31
|
import { logger } from "./logger.js";
|
|
32
|
+
import { looksLikeSvgMarkup } from "./markupSniff.js";
|
|
28
33
|
import { withTimeout } from "./errorHandling.js";
|
|
29
34
|
import { normalizeUrlForCache, redactUrlForError, sanitizeErrorCause, } from "./logSanitize.js";
|
|
30
35
|
import { mimeHintToExtension, mimeHintToFileType, normalizeMimeHint, } from "./mimeTypeHints.js";
|
|
@@ -1642,15 +1647,164 @@ export class FileDetector {
|
|
|
1642
1647
|
return Buffer.from(match[2], "base64");
|
|
1643
1648
|
}
|
|
1644
1649
|
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Resolve an extension to a routing {@link FileType}.
|
|
1652
|
+
*
|
|
1653
|
+
* Consults, in order:
|
|
1654
|
+
* 1. the canonical file-type registry — every image / audio / video /
|
|
1655
|
+
* document / data / archive format;
|
|
1656
|
+
* 2. the text, markup and config MIME map;
|
|
1657
|
+
* 3. `LANGUAGE_MAP`, which already enumerates ~200 source-code extensions.
|
|
1658
|
+
*
|
|
1659
|
+
* (3) is why this is a function rather than a table: source code was
|
|
1660
|
+
* previously a third hand-written list inside the detector covering roughly a
|
|
1661
|
+
* quarter of the languages the code processor already knew about, so a `.zig`
|
|
1662
|
+
* or `.erl` file was "unknown" and fell through to content heuristics.
|
|
1663
|
+
*
|
|
1664
|
+
* Returns undefined when the extension is not recognised at all.
|
|
1665
|
+
*/
|
|
1666
|
+
function resolveFileTypeForExtension(ext) {
|
|
1667
|
+
const registryType = fileTypeForExtension(ext);
|
|
1668
|
+
if (registryType) {
|
|
1669
|
+
return registryType;
|
|
1670
|
+
}
|
|
1671
|
+
const normalized = normalizeExtension(ext);
|
|
1672
|
+
if (TEXT_EXTENSION_MIME_MAP[normalized] ||
|
|
1673
|
+
LANGUAGE_MAP[normalized] ||
|
|
1674
|
+
CONFIG_EXTENSIONS.includes(normalized)) {
|
|
1675
|
+
return "text";
|
|
1676
|
+
}
|
|
1677
|
+
return undefined;
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Bytes read from the head of a file when running magic-byte detection against
|
|
1681
|
+
* a path.
|
|
1682
|
+
*
|
|
1683
|
+
* 4 KB rather than a few dozen: the largest consumers are the M2TS sync check
|
|
1684
|
+
* (three 192-byte packets), the ASF stream-type GUID (which sits past the
|
|
1685
|
+
* header object), and the OOXML sniff, which looks for `xl/`, `word/` or
|
|
1686
|
+
* `ppt/` entry names inside a ZIP directory. Still a single small read, and
|
|
1687
|
+
* far cheaper than the whole-file read the content heuristics would otherwise
|
|
1688
|
+
* perform.
|
|
1689
|
+
*/
|
|
1690
|
+
const MAGIC_BYTES_HEADER_SIZE = 4096;
|
|
1691
|
+
/**
|
|
1692
|
+
* ASF stream-type GUIDs, little-endian as stored in the file.
|
|
1693
|
+
*
|
|
1694
|
+
* `.wmv` and `.wma` share the same container signature, so the container alone
|
|
1695
|
+
* cannot say which modality a file is. These GUIDs appear in the stream
|
|
1696
|
+
* properties object and do.
|
|
1697
|
+
*/
|
|
1698
|
+
const ASF_VIDEO_MEDIA_GUID = Buffer.from("c0ef19bc4d5bcf11a8fd00805f5c442b", "hex");
|
|
1699
|
+
const ASF_AUDIO_MEDIA_GUID = Buffer.from("409e69f84d5bcf11a8fd00805f5c442b", "hex");
|
|
1700
|
+
/**
|
|
1701
|
+
* Identify an OOXML or OpenDocument package inside a ZIP.
|
|
1702
|
+
*
|
|
1703
|
+
* Every Office format is a ZIP, so the ZIP signature alone routes .xlsx, .docx,
|
|
1704
|
+
* .pptx and .odt to the archive processor whenever no filename is available —
|
|
1705
|
+
* which is exactly the shape of a Slack or API upload that arrives as a bare
|
|
1706
|
+
* Buffer. Reading the entry names recovers the real type.
|
|
1707
|
+
*
|
|
1708
|
+
* ODF is exact: the spec requires a `mimetype` entry stored first and
|
|
1709
|
+
* uncompressed, so the media type is literally in the bytes. OOXML has no such
|
|
1710
|
+
* guarantee, so its part prefixes (`xl/`, `word/`, `ppt/`) are matched instead,
|
|
1711
|
+
* gated on `[Content_Types].xml` being present so a plain ZIP that happens to
|
|
1712
|
+
* contain a folder called `word/` is not misread.
|
|
1713
|
+
*
|
|
1714
|
+
* Returns null when the ZIP is inconclusive, leaving the archive fallback and
|
|
1715
|
+
* the extension to decide.
|
|
1716
|
+
*/
|
|
1717
|
+
function detectZipPackageType(input) {
|
|
1718
|
+
const head = input.toString("latin1", 0, Math.min(input.length, 4096));
|
|
1719
|
+
// ODF: the mimetype string follows the stored "mimetype" entry name directly.
|
|
1720
|
+
const odfMatch = head.match(/mimetype(application\/vnd\.oasis\.opendocument\.[a-z-]+)/);
|
|
1721
|
+
if (odfMatch) {
|
|
1722
|
+
const mimeType = odfMatch[1];
|
|
1723
|
+
const entry = lookupByMimeType(mimeType);
|
|
1724
|
+
if (entry) {
|
|
1725
|
+
return { type: entry.fileType, mimeType };
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (!head.includes("[Content_Types].xml")) {
|
|
1729
|
+
return null;
|
|
1730
|
+
}
|
|
1731
|
+
for (const [prefix, mimeType] of [
|
|
1732
|
+
[
|
|
1733
|
+
"xl/",
|
|
1734
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1735
|
+
],
|
|
1736
|
+
[
|
|
1737
|
+
"word/",
|
|
1738
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1739
|
+
],
|
|
1740
|
+
[
|
|
1741
|
+
"ppt/",
|
|
1742
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
1743
|
+
],
|
|
1744
|
+
]) {
|
|
1745
|
+
if (head.includes(prefix)) {
|
|
1746
|
+
const entry = lookupByMimeType(mimeType);
|
|
1747
|
+
if (entry) {
|
|
1748
|
+
return { type: entry.fileType, mimeType };
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
return null;
|
|
1753
|
+
}
|
|
1645
1754
|
/**
|
|
1646
1755
|
* Strategy 1: Magic Bytes Detection (95% confidence)
|
|
1647
1756
|
* Detects file type from binary file headers
|
|
1648
1757
|
*/
|
|
1649
1758
|
class MagicBytesStrategy {
|
|
1650
1759
|
async detect(input) {
|
|
1651
|
-
|
|
1760
|
+
const buffer = await MagicBytesStrategy.resolveBuffer(input);
|
|
1761
|
+
if (!buffer) {
|
|
1652
1762
|
return this.unknown();
|
|
1653
1763
|
}
|
|
1764
|
+
return this.detectFromBuffer(buffer);
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Obtain the header bytes to inspect.
|
|
1768
|
+
*
|
|
1769
|
+
* Buffers are used directly. For a filesystem path we read only the first
|
|
1770
|
+
* {@link MAGIC_BYTES_HEADER_SIZE} bytes — this strategy previously bailed out
|
|
1771
|
+
* for anything that was not already a Buffer, which meant a path was
|
|
1772
|
+
* classified by its extension alone and any file whose extension was missing,
|
|
1773
|
+
* wrong or ambiguous fell through to the content heuristics. Those heuristics
|
|
1774
|
+
* `readFile()` the *entire* file, so detecting a 500 MB video used to read
|
|
1775
|
+
* 500 MB into memory to conclude nothing; now it reads
|
|
1776
|
+
* {@link MAGIC_BYTES_HEADER_SIZE} bytes and returns.
|
|
1777
|
+
*
|
|
1778
|
+
* URLs and data URIs are left alone: a URL would need a network round-trip
|
|
1779
|
+
* (the MIME strategy handles it), and a data URI already carries its type.
|
|
1780
|
+
*/
|
|
1781
|
+
static async resolveBuffer(input) {
|
|
1782
|
+
if (Buffer.isBuffer(input)) {
|
|
1783
|
+
return input;
|
|
1784
|
+
}
|
|
1785
|
+
if (typeof input !== "string" ||
|
|
1786
|
+
input.startsWith("data:") ||
|
|
1787
|
+
input.startsWith("http://") ||
|
|
1788
|
+
input.startsWith("https://")) {
|
|
1789
|
+
return undefined;
|
|
1790
|
+
}
|
|
1791
|
+
let handle;
|
|
1792
|
+
try {
|
|
1793
|
+
handle = await open(input, "r");
|
|
1794
|
+
const header = Buffer.alloc(MAGIC_BYTES_HEADER_SIZE);
|
|
1795
|
+
const { bytesRead } = await handle.read(header, 0, header.length, 0);
|
|
1796
|
+
return bytesRead > 0 ? header.subarray(0, bytesRead) : undefined;
|
|
1797
|
+
}
|
|
1798
|
+
catch {
|
|
1799
|
+
// Not a readable path (nonexistent, a directory, permission denied).
|
|
1800
|
+
// Detection continues with the remaining strategies.
|
|
1801
|
+
return undefined;
|
|
1802
|
+
}
|
|
1803
|
+
finally {
|
|
1804
|
+
await handle?.close().catch(() => undefined);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
detectFromBuffer(input) {
|
|
1654
1808
|
if (this.isPNG(input)) {
|
|
1655
1809
|
return this.result("image", "image/png", 95);
|
|
1656
1810
|
}
|
|
@@ -1709,6 +1863,18 @@ class MagicBytesStrategy {
|
|
|
1709
1863
|
if (brand.startsWith("qt")) {
|
|
1710
1864
|
return this.result("video", "video/quicktime", 95);
|
|
1711
1865
|
}
|
|
1866
|
+
// 3GPP / 3GPP2 share the ftyp box. Brands are "3gp4".."3gp9", "3gr6",
|
|
1867
|
+
// "3gs7", "3ge6" … for 3GPP and "3g2a".."3g2c" for 3GPP2. A 3GPP file can
|
|
1868
|
+
// hold audio only (phone voice memos) or audio+video, and the brand does
|
|
1869
|
+
// not say which — so this reports video at a confidence *below* the
|
|
1870
|
+
// detection threshold, letting a `.3gp`/`.3g2` extension confirm it and
|
|
1871
|
+
// an explicit audio mimetype hint override it.
|
|
1872
|
+
if (brand.startsWith("3g2")) {
|
|
1873
|
+
return this.result("video", "video/3gpp2", 75);
|
|
1874
|
+
}
|
|
1875
|
+
if (brand.startsWith("3g")) {
|
|
1876
|
+
return this.result("video", "video/3gpp", 75);
|
|
1877
|
+
}
|
|
1712
1878
|
return this.result("video", "video/mp4", 95);
|
|
1713
1879
|
}
|
|
1714
1880
|
// EBML container (MKV/WebM) — both share the 0x1A45DFA3 header; the DocType
|
|
@@ -1748,6 +1914,104 @@ class MagicBytesStrategy {
|
|
|
1748
1914
|
input[11] === 0x45) {
|
|
1749
1915
|
return this.result("audio", "audio/wav", 95);
|
|
1750
1916
|
}
|
|
1917
|
+
// AIFF / AIFF-C: "FORM" + "AIFF" or "AIFC" at offset 8. Same IFF container
|
|
1918
|
+
// shape as RIFF above, big-endian.
|
|
1919
|
+
if (input.length >= 12 &&
|
|
1920
|
+
input.toString("latin1", 0, 4) === "FORM" &&
|
|
1921
|
+
(input.toString("latin1", 8, 12) === "AIFF" ||
|
|
1922
|
+
input.toString("latin1", 8, 12) === "AIFC")) {
|
|
1923
|
+
return this.result("audio", "audio/aiff", 95);
|
|
1924
|
+
}
|
|
1925
|
+
// ASF container — Windows Media. The header GUID is shared by .wmv (video)
|
|
1926
|
+
// and .wma (audio), so the stream-type GUID inside decides. A .wma read
|
|
1927
|
+
// from a bare Buffer was reported as video before this lookup existed.
|
|
1928
|
+
if (input.length >= 4 &&
|
|
1929
|
+
input[0] === 0x30 &&
|
|
1930
|
+
input[1] === 0x26 &&
|
|
1931
|
+
input[2] === 0xb2 &&
|
|
1932
|
+
input[3] === 0x75) {
|
|
1933
|
+
if (input.includes(ASF_VIDEO_MEDIA_GUID)) {
|
|
1934
|
+
return this.result("video", "video/x-ms-wmv", 92);
|
|
1935
|
+
}
|
|
1936
|
+
if (input.includes(ASF_AUDIO_MEDIA_GUID)) {
|
|
1937
|
+
return this.result("audio", "audio/x-ms-wma", 92);
|
|
1938
|
+
}
|
|
1939
|
+
// Neither GUID within the header slice — report below the detection
|
|
1940
|
+
// threshold so an extension, if there is one, still wins.
|
|
1941
|
+
return this.result("video", "video/x-ms-asf", 75);
|
|
1942
|
+
}
|
|
1943
|
+
// FLV: "FLV" + version byte.
|
|
1944
|
+
if (input.length >= 4 &&
|
|
1945
|
+
input.toString("latin1", 0, 3) === "FLV" &&
|
|
1946
|
+
input[3] === 0x01) {
|
|
1947
|
+
return this.result("video", "video/x-flv", 95);
|
|
1948
|
+
}
|
|
1949
|
+
// Rich Text Format: "{\rtf".
|
|
1950
|
+
if (input.length >= 5 && input.toString("latin1", 0, 5) === "{\\rtf") {
|
|
1951
|
+
return this.result("docx", "application/rtf", 92);
|
|
1952
|
+
}
|
|
1953
|
+
// Core Audio Format: "caff".
|
|
1954
|
+
if (input.length >= 4 && input.toString("latin1", 0, 4) === "caff") {
|
|
1955
|
+
return this.result("audio", "audio/x-caf", 95);
|
|
1956
|
+
}
|
|
1957
|
+
// Sun/NeXT audio: ".snd".
|
|
1958
|
+
if (input.length >= 4 && input.toString("latin1", 0, 4) === ".snd") {
|
|
1959
|
+
return this.result("audio", "audio/basic", 95);
|
|
1960
|
+
}
|
|
1961
|
+
// MPEG program stream (0x000001BA) and elementary video stream (0x000001B3)
|
|
1962
|
+
// — .mpg/.mpeg/.vob. Without this, an MPEG-1 file reached the content
|
|
1963
|
+
// heuristics, whose CSV check found consistent delimiter counts in the
|
|
1964
|
+
// binary and classified real video as a spreadsheet.
|
|
1965
|
+
if (input.length >= 4 &&
|
|
1966
|
+
input[0] === 0x00 &&
|
|
1967
|
+
input[1] === 0x00 &&
|
|
1968
|
+
input[2] === 0x01 &&
|
|
1969
|
+
(input[3] === 0xba || input[3] === 0xb3)) {
|
|
1970
|
+
return this.result("video", "video/mpeg", 95);
|
|
1971
|
+
}
|
|
1972
|
+
// MPEG-2 transport stream — .ts/.mts/.m2ts. There is no magic number, only
|
|
1973
|
+
// a 0x47 sync byte at a fixed stride; requiring three in a row keeps a
|
|
1974
|
+
// stray 0x47 from claiming an unrelated file. This is also what rescues
|
|
1975
|
+
// `.ts`, whose extension resolves to TypeScript source.
|
|
1976
|
+
//
|
|
1977
|
+
// Two strides, because BDAV/M2TS prefixes each packet with a 4-byte arrival
|
|
1978
|
+
// timestamp: plain TS is 188 bytes from offset 0, M2TS is 192 from offset 4.
|
|
1979
|
+
// Checking only the former left every .m2ts undetected from a Buffer.
|
|
1980
|
+
if ((input.length >= 377 &&
|
|
1981
|
+
input[0] === 0x47 &&
|
|
1982
|
+
input[188] === 0x47 &&
|
|
1983
|
+
input[376] === 0x47) ||
|
|
1984
|
+
(input.length >= 389 &&
|
|
1985
|
+
input[4] === 0x47 &&
|
|
1986
|
+
input[196] === 0x47 &&
|
|
1987
|
+
input[388] === 0x47)) {
|
|
1988
|
+
return this.result("video", "video/mp2t", 92);
|
|
1989
|
+
}
|
|
1990
|
+
// MIDI: "MThd"
|
|
1991
|
+
if (input.length >= 4 && input.toString("latin1", 0, 4) === "MThd") {
|
|
1992
|
+
return this.result("audio", "audio/midi", 95);
|
|
1993
|
+
}
|
|
1994
|
+
// Monkey's Audio: "MAC " — the trailing space is part of the signature.
|
|
1995
|
+
if (input.length >= 4 && input.toString("latin1", 0, 4) === "MAC ") {
|
|
1996
|
+
return this.result("audio", "audio/x-ape", 95);
|
|
1997
|
+
}
|
|
1998
|
+
// WavPack: "wvpk"
|
|
1999
|
+
if (input.length >= 4 && input.toString("latin1", 0, 4) === "wvpk") {
|
|
2000
|
+
return this.result("audio", "audio/x-wavpack", 95);
|
|
2001
|
+
}
|
|
2002
|
+
// AMR narrowband ("#!AMR\n") and wideband ("#!AMR-WB\n").
|
|
2003
|
+
if (input.length >= 6 && input.toString("latin1", 0, 5) === "#!AMR") {
|
|
2004
|
+
return this.result("audio", "audio/amr", 95);
|
|
2005
|
+
}
|
|
2006
|
+
// JPEG 2000: 12-byte signature box.
|
|
2007
|
+
if (input.length >= 8 &&
|
|
2008
|
+
input[0] === 0x00 &&
|
|
2009
|
+
input[1] === 0x00 &&
|
|
2010
|
+
input[2] === 0x00 &&
|
|
2011
|
+
input[3] === 0x0c &&
|
|
2012
|
+
input.toString("latin1", 4, 8) === "jP ") {
|
|
2013
|
+
return this.result("image", "image/jp2", 95);
|
|
2014
|
+
}
|
|
1751
2015
|
// MP3: ID3 tag
|
|
1752
2016
|
if (input.length >= 3 &&
|
|
1753
2017
|
input[0] === 0x49 &&
|
|
@@ -1779,25 +2043,69 @@ class MagicBytesStrategy {
|
|
|
1779
2043
|
input[1] === 0x67 &&
|
|
1780
2044
|
input[2] === 0x67 &&
|
|
1781
2045
|
input[3] === 0x53) {
|
|
2046
|
+
// Ogg is a container, not a codec — .ogv (Theora/VP8 video) and .oga/.opus
|
|
2047
|
+
// (Vorbis/Opus/FLAC audio) all start "OggS". The codec identifier lives in
|
|
2048
|
+
// the first page's payload, so read it rather than assuming audio and
|
|
2049
|
+
// routing every Ogg video to the audio processor.
|
|
2050
|
+
const firstPage = input.toString("latin1", 0, Math.min(input.length, 128));
|
|
2051
|
+
if (firstPage.includes("theora") || firstPage.includes("VP80")) {
|
|
2052
|
+
return this.result("video", "video/ogg", 92);
|
|
2053
|
+
}
|
|
2054
|
+
if (firstPage.includes("OpusHead")) {
|
|
2055
|
+
return this.result("audio", "audio/opus", 92);
|
|
2056
|
+
}
|
|
1782
2057
|
return this.result("audio", "audio/ogg", 90);
|
|
1783
2058
|
}
|
|
1784
2059
|
// ZIP: "PK\x03\x04"
|
|
1785
|
-
//
|
|
1786
|
-
//
|
|
1787
|
-
//
|
|
1788
|
-
//
|
|
1789
|
-
//
|
|
2060
|
+
// Many document formats (OOXML: .xlsx, .docx, .pptx; ODF: .odt, .ods) are
|
|
2061
|
+
// internally ZIP archives and share these magic bytes. Read the entry names
|
|
2062
|
+
// first — that identifies the real format even for a bare Buffer with no
|
|
2063
|
+
// filename, which previously routed every Office document to the archive
|
|
2064
|
+
// processor. An inconclusive ZIP still reports archive at a lower
|
|
2065
|
+
// confidence (70%) so the ExtensionStrategy (85%) can override it.
|
|
1790
2066
|
if (input.length >= 4 &&
|
|
1791
2067
|
input[0] === 0x50 &&
|
|
1792
2068
|
input[1] === 0x4b &&
|
|
1793
2069
|
input[2] === 0x03 &&
|
|
1794
2070
|
input[3] === 0x04) {
|
|
2071
|
+
const packaged = detectZipPackageType(input);
|
|
2072
|
+
if (packaged) {
|
|
2073
|
+
return this.result(packaged.type, packaged.mimeType, 92);
|
|
2074
|
+
}
|
|
1795
2075
|
return this.result("archive", "application/zip", 70);
|
|
1796
2076
|
}
|
|
1797
2077
|
// GZIP: 1F 8B
|
|
1798
2078
|
if (input.length >= 2 && input[0] === 0x1f && input[1] === 0x8b) {
|
|
1799
2079
|
return this.result("archive", "application/gzip", 90);
|
|
1800
2080
|
}
|
|
2081
|
+
// BZIP2: "BZh" + a compression-level digit.
|
|
2082
|
+
if (input.length >= 4 &&
|
|
2083
|
+
input.toString("latin1", 0, 3) === "BZh" &&
|
|
2084
|
+
input[3] >= 0x31 &&
|
|
2085
|
+
input[3] <= 0x39) {
|
|
2086
|
+
return this.result("archive", "application/x-bzip2", 95);
|
|
2087
|
+
}
|
|
2088
|
+
// XZ: FD "7zXZ" 00
|
|
2089
|
+
if (input.length >= 6 &&
|
|
2090
|
+
input[0] === 0xfd &&
|
|
2091
|
+
input.toString("latin1", 1, 5) === "7zXZ" &&
|
|
2092
|
+
input[5] === 0x00) {
|
|
2093
|
+
return this.result("archive", "application/x-xz", 95);
|
|
2094
|
+
}
|
|
2095
|
+
// Zstandard frame magic: 28 B5 2F FD
|
|
2096
|
+
if (input.length >= 4 &&
|
|
2097
|
+
input[0] === 0x28 &&
|
|
2098
|
+
input[1] === 0xb5 &&
|
|
2099
|
+
input[2] === 0x2f &&
|
|
2100
|
+
input[3] === 0xfd) {
|
|
2101
|
+
return this.result("archive", "application/zstd", 95);
|
|
2102
|
+
}
|
|
2103
|
+
// TAR: "ustar" at offset 257, inside the first header block. Checked late
|
|
2104
|
+
// because it is a weak, deep signature — anything with its own leading
|
|
2105
|
+
// magic number should have matched already.
|
|
2106
|
+
if (input.length >= 262 && input.toString("latin1", 257, 262) === "ustar") {
|
|
2107
|
+
return this.result("archive", "application/x-tar", 90);
|
|
2108
|
+
}
|
|
1801
2109
|
// 7z: 37 7A BC AF 27 1C
|
|
1802
2110
|
if (input.length >= 6 &&
|
|
1803
2111
|
input[0] === 0x37 &&
|
|
@@ -1816,6 +2124,14 @@ class MagicBytesStrategy {
|
|
|
1816
2124
|
input[3] === 0x21) {
|
|
1817
2125
|
return this.result("archive", "application/x-rar-compressed", 95);
|
|
1818
2126
|
}
|
|
2127
|
+
// SVG is text, so it has no byte signature — but it does have an
|
|
2128
|
+
// unambiguous root element. Without this an SVG supplied as a Buffer was
|
|
2129
|
+
// classified as generic XML and inlined as raw markup instead of going
|
|
2130
|
+
// through the SVG sanitizer, which is the whole reason SVG has its own
|
|
2131
|
+
// FileType. Checked last so no binary format can be beaten to it.
|
|
2132
|
+
if (looksLikeSvgMarkup(input)) {
|
|
2133
|
+
return this.result("svg", "image/svg+xml", 90);
|
|
2134
|
+
}
|
|
1819
2135
|
return this.unknown();
|
|
1820
2136
|
}
|
|
1821
2137
|
isPNG(buf) {
|
|
@@ -2024,131 +2340,9 @@ class ExtensionStrategy {
|
|
|
2024
2340
|
if (!ext) {
|
|
2025
2341
|
return this.unknown();
|
|
2026
2342
|
}
|
|
2027
|
-
const
|
|
2028
|
-
csv: "csv",
|
|
2029
|
-
tsv: "csv",
|
|
2030
|
-
jpg: "image",
|
|
2031
|
-
jpeg: "image",
|
|
2032
|
-
png: "image",
|
|
2033
|
-
gif: "image",
|
|
2034
|
-
webp: "image",
|
|
2035
|
-
bmp: "image",
|
|
2036
|
-
tiff: "image",
|
|
2037
|
-
tif: "image",
|
|
2038
|
-
// SVG is handled as text/markup, NOT as image
|
|
2039
|
-
// AI providers don't support SVG format, so we process it as sanitized text
|
|
2040
|
-
svg: "svg",
|
|
2041
|
-
avif: "image",
|
|
2042
|
-
heic: "image",
|
|
2043
|
-
heif: "image",
|
|
2044
|
-
pdf: "pdf",
|
|
2045
|
-
// Video formats
|
|
2046
|
-
mp4: "video",
|
|
2047
|
-
mkv: "video",
|
|
2048
|
-
mov: "video",
|
|
2049
|
-
avi: "video",
|
|
2050
|
-
webm: "video",
|
|
2051
|
-
wmv: "video",
|
|
2052
|
-
flv: "video",
|
|
2053
|
-
// Audio formats
|
|
2054
|
-
mp3: "audio",
|
|
2055
|
-
wav: "audio",
|
|
2056
|
-
ogg: "audio",
|
|
2057
|
-
flac: "audio",
|
|
2058
|
-
m4a: "audio",
|
|
2059
|
-
aac: "audio",
|
|
2060
|
-
wma: "audio",
|
|
2061
|
-
opus: "audio",
|
|
2062
|
-
// Archive formats
|
|
2063
|
-
zip: "archive",
|
|
2064
|
-
tar: "archive",
|
|
2065
|
-
gz: "archive",
|
|
2066
|
-
tgz: "archive",
|
|
2067
|
-
rar: "archive",
|
|
2068
|
-
"7z": "archive",
|
|
2069
|
-
jar: "archive",
|
|
2070
|
-
// Document formats (ZIP-based internally)
|
|
2071
|
-
xlsx: "xlsx",
|
|
2072
|
-
xls: "xlsx",
|
|
2073
|
-
docx: "docx",
|
|
2074
|
-
doc: "docx",
|
|
2075
|
-
pptx: "pptx",
|
|
2076
|
-
ppt: "pptx",
|
|
2077
|
-
odt: "docx", // OpenDocument text → processed like docx
|
|
2078
|
-
ods: "xlsx", // OpenDocument spreadsheet → processed like xlsx
|
|
2079
|
-
odp: "pptx", // OpenDocument presentation → processed like pptx
|
|
2080
|
-
rtf: "docx", // RTF → processed like docx (text extraction)
|
|
2081
|
-
// Text/markup formats
|
|
2082
|
-
txt: "text",
|
|
2083
|
-
md: "text",
|
|
2084
|
-
markdown: "text",
|
|
2085
|
-
json: "text",
|
|
2086
|
-
xml: "text",
|
|
2087
|
-
yaml: "text",
|
|
2088
|
-
yml: "text",
|
|
2089
|
-
html: "text",
|
|
2090
|
-
htm: "text",
|
|
2091
|
-
css: "text",
|
|
2092
|
-
log: "text",
|
|
2093
|
-
conf: "text",
|
|
2094
|
-
cfg: "text",
|
|
2095
|
-
ini: "text",
|
|
2096
|
-
env: "text",
|
|
2097
|
-
toml: "text",
|
|
2098
|
-
properties: "text",
|
|
2099
|
-
gitignore: "text",
|
|
2100
|
-
dockerignore: "text",
|
|
2101
|
-
editorconfig: "text",
|
|
2102
|
-
prettierrc: "text",
|
|
2103
|
-
eslintrc: "text",
|
|
2104
|
-
babelrc: "text",
|
|
2105
|
-
// Source code formats
|
|
2106
|
-
js: "text",
|
|
2107
|
-
mjs: "text",
|
|
2108
|
-
cjs: "text",
|
|
2109
|
-
jsx: "text",
|
|
2110
|
-
ts: "text",
|
|
2111
|
-
tsx: "text",
|
|
2112
|
-
py: "text",
|
|
2113
|
-
java: "text",
|
|
2114
|
-
go: "text",
|
|
2115
|
-
rs: "text",
|
|
2116
|
-
rb: "text",
|
|
2117
|
-
php: "text",
|
|
2118
|
-
c: "text",
|
|
2119
|
-
cpp: "text",
|
|
2120
|
-
cc: "text",
|
|
2121
|
-
h: "text",
|
|
2122
|
-
hpp: "text",
|
|
2123
|
-
cs: "text",
|
|
2124
|
-
swift: "text",
|
|
2125
|
-
kt: "text",
|
|
2126
|
-
kts: "text",
|
|
2127
|
-
scala: "text",
|
|
2128
|
-
sh: "text",
|
|
2129
|
-
bash: "text",
|
|
2130
|
-
zsh: "text",
|
|
2131
|
-
ps1: "text",
|
|
2132
|
-
sql: "text",
|
|
2133
|
-
r: "text",
|
|
2134
|
-
lua: "text",
|
|
2135
|
-
pl: "text",
|
|
2136
|
-
perl: "text",
|
|
2137
|
-
dart: "text",
|
|
2138
|
-
ex: "text",
|
|
2139
|
-
exs: "text",
|
|
2140
|
-
erl: "text",
|
|
2141
|
-
hs: "text",
|
|
2142
|
-
clj: "text",
|
|
2143
|
-
lisp: "text",
|
|
2144
|
-
vim: "text",
|
|
2145
|
-
// Additional video/image
|
|
2146
|
-
m4v: "video",
|
|
2147
|
-
ico: "image",
|
|
2148
|
-
};
|
|
2149
|
-
const type = typeMap[ext.toLowerCase()];
|
|
2343
|
+
const type = resolveFileTypeForExtension(ext);
|
|
2150
2344
|
return {
|
|
2151
|
-
type: type
|
|
2345
|
+
type: type ?? "unknown",
|
|
2152
2346
|
mimeType: this.getMimeType(ext),
|
|
2153
2347
|
extension: ext,
|
|
2154
2348
|
source: this.detectSource(input),
|
|
@@ -2201,127 +2395,7 @@ class ExtensionStrategy {
|
|
|
2201
2395
|
return "path";
|
|
2202
2396
|
}
|
|
2203
2397
|
getMimeType(ext) {
|
|
2204
|
-
|
|
2205
|
-
csv: "text/csv",
|
|
2206
|
-
tsv: "text/tab-separated-values",
|
|
2207
|
-
jpg: "image/jpeg",
|
|
2208
|
-
jpeg: "image/jpeg",
|
|
2209
|
-
png: "image/png",
|
|
2210
|
-
gif: "image/gif",
|
|
2211
|
-
webp: "image/webp",
|
|
2212
|
-
bmp: "image/bmp",
|
|
2213
|
-
tiff: "image/tiff",
|
|
2214
|
-
tif: "image/tiff",
|
|
2215
|
-
svg: "image/svg+xml",
|
|
2216
|
-
avif: "image/avif",
|
|
2217
|
-
heic: "image/heic",
|
|
2218
|
-
heif: "image/heif",
|
|
2219
|
-
pdf: "application/pdf",
|
|
2220
|
-
// Video MIME types
|
|
2221
|
-
mp4: "video/mp4",
|
|
2222
|
-
mkv: "video/x-matroska",
|
|
2223
|
-
mov: "video/quicktime",
|
|
2224
|
-
avi: "video/x-msvideo",
|
|
2225
|
-
webm: "video/webm",
|
|
2226
|
-
wmv: "video/x-ms-wmv",
|
|
2227
|
-
flv: "video/x-flv",
|
|
2228
|
-
// Audio MIME types
|
|
2229
|
-
mp3: "audio/mpeg",
|
|
2230
|
-
wav: "audio/wav",
|
|
2231
|
-
ogg: "audio/ogg",
|
|
2232
|
-
flac: "audio/flac",
|
|
2233
|
-
m4a: "audio/mp4",
|
|
2234
|
-
aac: "audio/aac",
|
|
2235
|
-
wma: "audio/x-ms-wma",
|
|
2236
|
-
opus: "audio/opus",
|
|
2237
|
-
// Archive MIME types
|
|
2238
|
-
zip: "application/zip",
|
|
2239
|
-
tar: "application/x-tar",
|
|
2240
|
-
gz: "application/gzip",
|
|
2241
|
-
tgz: "application/gzip",
|
|
2242
|
-
rar: "application/x-rar-compressed",
|
|
2243
|
-
"7z": "application/x-7z-compressed",
|
|
2244
|
-
jar: "application/java-archive",
|
|
2245
|
-
// Document MIME types
|
|
2246
|
-
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
2247
|
-
xls: "application/vnd.ms-excel",
|
|
2248
|
-
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
2249
|
-
doc: "application/msword",
|
|
2250
|
-
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
2251
|
-
ppt: "application/vnd.ms-powerpoint",
|
|
2252
|
-
odt: "application/vnd.oasis.opendocument.text",
|
|
2253
|
-
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
|
2254
|
-
odp: "application/vnd.oasis.opendocument.presentation",
|
|
2255
|
-
rtf: "application/rtf",
|
|
2256
|
-
// Text/markup MIME types
|
|
2257
|
-
txt: "text/plain",
|
|
2258
|
-
md: "text/markdown",
|
|
2259
|
-
markdown: "text/markdown",
|
|
2260
|
-
json: "application/json",
|
|
2261
|
-
xml: "application/xml",
|
|
2262
|
-
yaml: "application/yaml",
|
|
2263
|
-
yml: "application/yaml",
|
|
2264
|
-
html: "text/html",
|
|
2265
|
-
htm: "text/html",
|
|
2266
|
-
css: "text/css",
|
|
2267
|
-
log: "text/plain",
|
|
2268
|
-
conf: "text/plain",
|
|
2269
|
-
cfg: "text/plain",
|
|
2270
|
-
ini: "text/plain",
|
|
2271
|
-
env: "text/plain",
|
|
2272
|
-
toml: "text/plain",
|
|
2273
|
-
properties: "text/plain",
|
|
2274
|
-
gitignore: "text/plain",
|
|
2275
|
-
dockerignore: "text/plain",
|
|
2276
|
-
editorconfig: "text/plain",
|
|
2277
|
-
prettierrc: "application/json",
|
|
2278
|
-
eslintrc: "application/json",
|
|
2279
|
-
babelrc: "application/json",
|
|
2280
|
-
// Source code MIME types
|
|
2281
|
-
js: "text/javascript",
|
|
2282
|
-
mjs: "text/javascript",
|
|
2283
|
-
cjs: "text/javascript",
|
|
2284
|
-
jsx: "text/javascript",
|
|
2285
|
-
ts: "text/typescript",
|
|
2286
|
-
tsx: "text/typescript",
|
|
2287
|
-
py: "text/x-python",
|
|
2288
|
-
java: "text/x-java-source",
|
|
2289
|
-
go: "text/x-go",
|
|
2290
|
-
rs: "text/x-rustsrc",
|
|
2291
|
-
rb: "text/x-ruby",
|
|
2292
|
-
php: "text/x-php",
|
|
2293
|
-
c: "text/x-c",
|
|
2294
|
-
cpp: "text/x-c++",
|
|
2295
|
-
cc: "text/x-c++",
|
|
2296
|
-
h: "text/x-c",
|
|
2297
|
-
hpp: "text/x-c++",
|
|
2298
|
-
cs: "text/x-csharp",
|
|
2299
|
-
swift: "text/x-swift",
|
|
2300
|
-
kt: "text/x-kotlin",
|
|
2301
|
-
kts: "text/x-kotlin",
|
|
2302
|
-
scala: "text/x-scala",
|
|
2303
|
-
sh: "text/x-shellscript",
|
|
2304
|
-
bash: "text/x-shellscript",
|
|
2305
|
-
zsh: "text/x-shellscript",
|
|
2306
|
-
ps1: "text/x-powershell",
|
|
2307
|
-
sql: "text/x-sql",
|
|
2308
|
-
r: "text/x-r",
|
|
2309
|
-
lua: "text/x-lua",
|
|
2310
|
-
pl: "text/x-perl",
|
|
2311
|
-
perl: "text/x-perl",
|
|
2312
|
-
dart: "text/x-dart",
|
|
2313
|
-
ex: "text/x-elixir",
|
|
2314
|
-
exs: "text/x-elixir",
|
|
2315
|
-
erl: "text/x-erlang",
|
|
2316
|
-
hs: "text/x-haskell",
|
|
2317
|
-
clj: "text/x-clojure",
|
|
2318
|
-
lisp: "text/x-lisp",
|
|
2319
|
-
vim: "text/plain",
|
|
2320
|
-
// Additional video/image
|
|
2321
|
-
m4v: "video/mp4",
|
|
2322
|
-
ico: "image/x-icon",
|
|
2323
|
-
};
|
|
2324
|
-
return mimeMap[ext.toLowerCase()] || "application/octet-stream";
|
|
2398
|
+
return getMimeTypeForExtension(ext);
|
|
2325
2399
|
}
|
|
2326
2400
|
unknown() {
|
|
2327
2401
|
return {
|
|
@@ -2370,6 +2444,15 @@ class ContentHeuristicStrategy {
|
|
|
2370
2444
|
else {
|
|
2371
2445
|
return this.unknown();
|
|
2372
2446
|
}
|
|
2447
|
+
// Every check below runs on a UTF-8 *decoding* of the bytes, which succeeds
|
|
2448
|
+
// for any input — decoding a video yields a string full of replacement
|
|
2449
|
+
// characters, and that string can still satisfy a text heuristic. It did:
|
|
2450
|
+
// MPEG-1 video decoded to "lines" with a consistent delimiter count and was
|
|
2451
|
+
// classified `csv`, then handed to the CSV parser. Reject binary up front so
|
|
2452
|
+
// no text heuristic can ever see it.
|
|
2453
|
+
if (ContentHeuristicStrategy.looksBinary(buffer)) {
|
|
2454
|
+
return this.unknown();
|
|
2455
|
+
}
|
|
2373
2456
|
const sample = buffer.toString("utf-8", 0, Math.min(2000, buffer.length));
|
|
2374
2457
|
// Check for JSON first (more specific than CSV)
|
|
2375
2458
|
if (this.looksLikeJSON(sample)) {
|
|
@@ -2394,6 +2477,39 @@ class ContentHeuristicStrategy {
|
|
|
2394
2477
|
}
|
|
2395
2478
|
return this.unknown();
|
|
2396
2479
|
}
|
|
2480
|
+
/**
|
|
2481
|
+
* Whether a buffer holds binary rather than text.
|
|
2482
|
+
*
|
|
2483
|
+
* Two independent signals, both computed on the same leading sample that the
|
|
2484
|
+
* heuristics themselves inspect:
|
|
2485
|
+
*
|
|
2486
|
+
* - a NUL byte, which no text encoding this detector supports emits (UTF-16
|
|
2487
|
+
* would, but it is not among the formats handled here and would already
|
|
2488
|
+
* have been caught by its BOM);
|
|
2489
|
+
* - more than 10% control/undecodable bytes, which catches binaries that
|
|
2490
|
+
* happen not to contain a NUL in their first 2 KB.
|
|
2491
|
+
*
|
|
2492
|
+
* Tab, newline and carriage return are text and excluded from the control
|
|
2493
|
+
* count.
|
|
2494
|
+
*/
|
|
2495
|
+
static looksBinary(buffer) {
|
|
2496
|
+
const sampleLength = Math.min(2000, buffer.length);
|
|
2497
|
+
if (sampleLength === 0) {
|
|
2498
|
+
return false;
|
|
2499
|
+
}
|
|
2500
|
+
let controlBytes = 0;
|
|
2501
|
+
for (let i = 0; i < sampleLength; i++) {
|
|
2502
|
+
const byte = buffer[i];
|
|
2503
|
+
if (byte === 0x00) {
|
|
2504
|
+
return true;
|
|
2505
|
+
}
|
|
2506
|
+
const isTextWhitespace = byte === 0x09 || byte === 0x0a || byte === 0x0d;
|
|
2507
|
+
if (!isTextWhitespace && (byte < 0x20 || byte === 0x7f)) {
|
|
2508
|
+
controlBytes++;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
return controlBytes / sampleLength > 0.1;
|
|
2512
|
+
}
|
|
2397
2513
|
looksLikeCSV(text) {
|
|
2398
2514
|
const lines = text.trim().split("\n");
|
|
2399
2515
|
if (lines.length < 2) {
|