Skip to content

Commit

Permalink
increase coverage, add header test, allow octet-stream as a mimetype
Browse files Browse the repository at this point in the history
  • Loading branch information
mdtanrikulu committed Sep 23, 2024
1 parent 88164bc commit 9a20c9f
Show file tree
Hide file tree
Showing 7 changed files with 560 additions and 18 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "1.0.0",
"version": "1.0.1",
"license": "MIT",
"main": "dist/index.js",
"module": "dist/index.esm.js",
Expand Down Expand Up @@ -57,9 +57,11 @@
"@size-limit/preset-small-lib": "^11.0.1",
"@types/dompurify": "^3.0.5",
"@types/jsdom": "^21.1.6",
"@types/moxios": "^0.4.17",
"@types/url-join": "^4.0.1",
"dotenv": "^16.3.1",
"esbuild": "^0.14.21",
"moxios": "^0.4.0",
"nock": "^13.2.2",
"rollup": "^4.9.1",
"size-limit": "^11.0.1",
Expand Down
28 changes: 18 additions & 10 deletions src/utils/isImageURI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Buffer } from 'buffer/';
import { fetch } from './fetch';

export const ALLOWED_IMAGE_MIMETYPES = [
'application/octet-stream',
'image/jpeg',
'image/png',
'image/gif',
Expand All @@ -16,6 +17,14 @@ export const ALLOWED_IMAGE_MIMETYPES = [
'image/jxl',
];

export const IMAGE_SIGNATURES = {
'FFD8FF': 'image/jpeg',

Check failure on line 21 in src/utils/isImageURI.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and ubuntu-latest

Replace `'FFD8FF'` with `FFD8FF`

Check failure on line 21 in src/utils/isImageURI.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and macOS-latest

Replace `'FFD8FF'` with `FFD8FF`

Check failure on line 21 in src/utils/isImageURI.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and ubuntu-latest

Replace `'FFD8FF'` with `FFD8FF`

Check failure on line 21 in src/utils/isImageURI.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and macOS-latest

Replace `'FFD8FF'` with `FFD8FF`
'89504E47': 'image/png',
'47494638': 'image/gif',
'424D': 'image/bmp',
'FF0A': 'image/jxl',

Check failure on line 25 in src/utils/isImageURI.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and ubuntu-latest

Replace `'FF0A'` with `FF0A`

Check failure on line 25 in src/utils/isImageURI.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and macOS-latest

Replace `'FF0A'` with `FF0A`

Check failure on line 25 in src/utils/isImageURI.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and ubuntu-latest

Replace `'FF0A'` with `FF0A`

Check failure on line 25 in src/utils/isImageURI.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and macOS-latest

Replace `'FF0A'` with `FF0A`
};

const MAX_FILE_SIZE = 300 * 1024 * 1024; // 300 MB

function isURIEncoded(uri: string): boolean {
Expand Down Expand Up @@ -56,19 +65,18 @@ async function isStreamAnImage(url: string): Promise<boolean> {
if (response.data instanceof ArrayBuffer) {
magicNumbers = new DataView(response.data).getUint32(0).toString(16);
} else {
if (
!response.data ||
typeof response.data === 'string' ||
!('readUInt32BE' in response.data)
) {
throw 'isStreamAnImage: unsupported data, instance is not BufferLike';

Check warning on line 73 in src/utils/isImageURI.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and ubuntu-latest

Expected an error object to be thrown

Check warning on line 73 in src/utils/isImageURI.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and macOS-latest

Expected an error object to be thrown

Check warning on line 73 in src/utils/isImageURI.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and ubuntu-latest

Expected an error object to be thrown

Check warning on line 73 in src/utils/isImageURI.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and macOS-latest

Expected an error object to be thrown
}
magicNumbers = response.data.readUInt32BE(0).toString(16);
}

const imageSignatures = [
'ffd8ff', // JPEG
'89504e47', // PNG
'47494638', // GIF
'424d', // BMP
'ff0a', // JPEG XL
];

const isBinaryImage = imageSignatures.some(signature =>
magicNumbers.startsWith(signature)
const isBinaryImage = Object.keys(IMAGE_SIGNATURES).some(signature =>
magicNumbers.toUpperCase().startsWith(signature)
);

// Check for SVG image
Expand Down
64 changes: 63 additions & 1 deletion src/utils/resolveURI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,75 @@ import urlJoin from 'url-join';

import { Gateways } from '../types';
import { isCID } from './isCID';
import { IMAGE_SIGNATURES } from './isImageURI';

const IPFS_SUBPATH = '/ipfs/';
const IPNS_SUBPATH = '/ipns/';
const networkRegex = /(?<protocol>ipfs:\/|ipns:\/|ar:\/)?(?<root>\/)?(?<subpath>ipfs\/|ipns\/)?(?<target>[\w\-.]+)(?<subtarget>\/.*)?/;
const base64Regex = /^data:([a-zA-Z\-/+]*);base64,([^"].*)/;
const dataURIRegex = /^data:([a-zA-Z\-/+]*)?(;[a-zA-Z0-9].*?)?(,)/;

function _getImageMimeType(uri: string) {
const base64Data = uri.replace(base64Regex, '$2');
const buffer = Buffer.from(base64Data, 'base64');

if (buffer.length < 12) {
return null; // not enough data to determine the type
}

// get the hex representation of the first 12 bytes
const hex = buffer.toString('hex', 0, 12).toUpperCase();

// check against magic number mapping
for (const [magicNumber, mimeType] of Object.entries({
...IMAGE_SIGNATURES,
'52494646': 'special_webp_check',
'3C737667': 'image/svg+xml',
})) {
if (hex.startsWith(magicNumber.toUpperCase())) {
if (mimeType === 'special_webp_check') {
return hex.slice(8, 12) === '5745' ? 'image/webp' : null;
}
return mimeType;
}
}

return null;
}

function _isValidBase64(uri: string) {
if (typeof uri !== 'string') {
return false;
}

// check if the string matches the Base64 pattern
if (!base64Regex.test(uri)) {
return false;
}

const [header, str] = uri.split('base64,');

const mimeType = _getImageMimeType(uri);

if (!mimeType || !header.includes(mimeType)) {
return false;
}

// length must be multiple of 4
if (str.length % 4 !== 0) {
return false;
}

try {
// try to encode/decode the string, to see if matches
const buffer = Buffer.from(str, 'base64');
const encoded = buffer.toString('base64');
return encoded === str;
} catch (e) {
return false;
}
}

function _replaceGateway(uri: string, source: string, target?: string) {
if (uri.startsWith(source) && target) {
try {
Expand All @@ -28,7 +90,7 @@ export function resolveURI(
customGateway?: string
): { uri: string; isOnChain: boolean; isEncoded: boolean } {
// resolves uri based on its' protocol
const isEncoded = base64Regex.test(uri);
const isEncoded = _isValidBase64(uri);
if (isEncoded || uri.startsWith('http')) {
uri = _replaceGateway(uri, 'https://ipfs.io/', gateways?.ipfs);
uri = _replaceGateway(uri, 'https://arweave.net/', gateways?.arweave);
Expand Down
126 changes: 120 additions & 6 deletions test/resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,14 @@ function jsonRpcResult(result: string): JsonRpcResult {
};
}

const provider = new JsonRpcProvider(INFURA_URL.toString(), 'mainnet');
const avt = new AvatarResolver(provider, {
apiKey: {
opensea: 'api-key',
},
});

describe('get avatar', () => {
const provider = new JsonRpcProvider(INFURA_URL.toString(), 'mainnet');
const avt = new AvatarResolver(provider, {
apiKey: {
opensea: 'a2b184238ee8460d9d2f58b0d3177c23',
},
});
it('retrieves image uri with erc721 spec', async () => {
mockInfuraChainId();

Expand Down Expand Up @@ -518,3 +519,116 @@ describe('get avatar', () => {
// expect(await avt.getAvatar({ ens: 'testname.eth' })).toMatch(/^(data:image\/svg\+xml;base64,).*$/);
// });
});

describe('get banner/header', () => {
it('retrieves image uri with custom spec', async () => {
mockInfuraChainId();

nockInfuraBatch(
[
ethCallParams(
ENSRegistryWithFallback.toString(),
'0x0178b8bfb47a0edaf3c702800c923ca4c44a113d0d718cb1f42ecdce70c5fd05fa36a63f'
),
chainIdParams(),
ethCallParams(
ENSRegistryWithFallback.toString(),
'0x0178b8bfb47a0edaf3c702800c923ca4c44a113d0d718cb1f42ecdce70c5fd05fa36a63f'
),
],
[
jsonRpcResult(
'0x0000000000000000000000004976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41'
),
jsonRpcResult('0x1'),
jsonRpcResult(
'0x0000000000000000000000004976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41'
),
]
);
nockInfuraBatch(
[
ethCallParams(
PublicResolver.toLowerCase(),
'0x01ffc9a79061b92300000000000000000000000000000000000000000000000000000000'
),
chainIdParams(),
],
[
jsonRpcResult(
'0x0000000000000000000000000000000000000000000000000000000000000000'
),
jsonRpcResult('0x1'),
]
);
nockInfuraBatch(
[
ethCallParams(
PublicResolver.toLowerCase(),
'0x3b3b57deb47a0edaf3c702800c923ca4c44a113d0d718cb1f42ecdce70c5fd05fa36a63f'
),
chainIdParams(),
],
[
jsonRpcResult(
'0x0000000000000000000000000d59d0f7dcc0fbf0a3305ce0261863aaf7ab685c'
),
jsonRpcResult('0x1'),
]
);
nockInfuraBatch(
[
ethCallParams(
PublicResolver.toLowerCase(),
'0x01ffc9a79061b92300000000000000000000000000000000000000000000000000000000'
),
chainIdParams(),
],
[
jsonRpcResult(
'0x0000000000000000000000000000000000000000000000000000000000000000'
),
jsonRpcResult('0x1'),
]
);

nockInfuraBatch(
[
ethCallParams(
PublicResolver.toLowerCase(),
'0x59d1d43cb47a0edaf3c702800c923ca4c44a113d0d718cb1f42ecdce70c5fd05fa36a63f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000066865616465720000000000000000000000000000000000000000000000000000'
),
chainIdParams(),
],
[
jsonRpcResult(
'0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004368747470733a2f2f697066732e696f2f697066732f516d55536867666f5a5153484b3354517975546655707363385566654e6644384b77505576444255645a346e6d520000000000000000000000000000000000000000000000000000000000'
), // Encoded HEADER URI
jsonRpcResult('0x1'),
]
);


Check failure on line 611 in test/resolver.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and ubuntu-latest

Delete `⏎`

Check failure on line 611 in test/resolver.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and macOS-latest

Delete `⏎`

Check failure on line 611 in test/resolver.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and ubuntu-latest

Delete `⏎`

Check failure on line 611 in test/resolver.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and macOS-latest

Delete `⏎`
const HEADER_URI_TANRIKULU = new URL(
'https://ipfs.io/ipfs/QmUShgfoZQSHK3TQyuTfUpsc8UfeNfD8KwPUvDBUdZ4nmR'
);

/* mock head call */
nock(HEADER_URI_TANRIKULU.origin)
.head(HEADER_URI_TANRIKULU.pathname)
.reply(200, {}, {
...CORS_HEADERS,
'content-type': 'image/png',
} as any);
/* mock get call */
nock(HEADER_URI_TANRIKULU.origin)
.get(HEADER_URI_TANRIKULU.pathname)
.reply(200, {}, {
...CORS_HEADERS,
'content-type': 'image/png',
} as any);
expect(await avt.getHeader('tanrikulu.eth')).toEqual(
'https://ipfs.io/ipfs/QmUShgfoZQSHK3TQyuTfUpsc8UfeNfD8KwPUvDBUdZ4nmR'
);
});
});
Loading

0 comments on commit 9a20c9f

Please sign in to comment.