From 3784b29f148b706d5bc40b69b5ae898efa2c1990 Mon Sep 17 00:00:00 2001 From: Vojtech Simetka Date: Mon, 25 Apr 2022 20:38:36 +0500 Subject: [PATCH] feat: support for bzz.link cids when downloading files (#350) * feat: detect and extract bzz.link cids into hash when downloading files * test: add quite thorough testsuite --- src/constants.ts | 1 + src/pages/files/Download.tsx | 16 +--- src/utils/index.test.ts | 138 +++++++++++++++++++++++++++++++++++ src/utils/index.ts | 39 +++++++++- 4 files changed, 176 insertions(+), 18 deletions(-) create mode 100644 src/utils/index.test.ts diff --git a/src/constants.ts b/src/constants.ts index 1051dd9..e04ec12 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,4 @@ export const META_FILE_NAME = '.swarmgatewaymeta.json' export const PREVIEW_FILE_NAME = '.swarmgatewaypreview.jpeg' export const PREVIEW_DIMENSIONS = { maxWidth: 250, maxHeight: 175 } +export const BZZ_LINK_DOMAIN = process.env.REACT_APP_BZZ_LINK_DOMAIN || 'bzz.link' diff --git a/src/pages/files/Download.tsx b/src/pages/files/Download.tsx index 0ae913b..71718bd 100644 --- a/src/pages/files/Download.tsx +++ b/src/pages/files/Download.tsx @@ -8,7 +8,7 @@ import { History } from '../../components/History' import { Context, defaultUploadOrigin } from '../../providers/File' import { Context as SettingsContext } from '../../providers/Settings' import { ROUTES } from '../../routes' -import { extractSwarmHash } from '../../utils' +import { recognizeSwarmHash } from '../../utils' import { determineHistoryName, HISTORY_KEYS, putHistory } from '../../utils/local-storage' import { FileNavigation } from './FileNavigation' @@ -71,20 +71,6 @@ export function Download(): ReactElement { } } - function recognizeSwarmHash(value: string) { - if (value.length < 64) { - return value - } - - const hash = extractSwarmHash(value) - - if (hash) { - return hash - } - - return value - } - return ( <> diff --git a/src/utils/index.test.ts b/src/utils/index.test.ts new file mode 100644 index 0000000..24db426 --- /dev/null +++ b/src/utils/index.test.ts @@ -0,0 +1,138 @@ +import { extractSwarmHash, extractSwarmCid, recognizeSwarmHash } from './index' + +interface TestObject { + input: string + expectedOutput: string | undefined +} + +const correctHashes: TestObject[] = [ + // non-encrypted + { + input: 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f', + expectedOutput: 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f', + }, + { + input: 'http://gateway.org/bzz/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f', + expectedOutput: 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f', + }, + { + input: 'https://gateway.org/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f', + expectedOutput: 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f', + }, + { + input: 'http://gateway.org/bzz/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f/', + expectedOutput: 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f', + }, + { + input: 'https://gateway.org/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f/', + expectedOutput: 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f', + }, + // encrypted + { + input: + 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f', + expectedOutput: + 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f', + }, + { + input: + 'http://gateway.org/bzz/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f', + expectedOutput: + 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f', + }, + { + input: + 'https://gateway.org/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f', + expectedOutput: + 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f', + }, + { + input: + 'http://gateway.org/bzz/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f/', + expectedOutput: + 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f', + }, + { + input: + 'https://gateway.org/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f/', + expectedOutput: + 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fb7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3f', + }, +] + +const wrongHashes: string[] = [ + // one character too long + 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fa', + 'http://gateway.org/bzz/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fa', + 'https://gateway.org/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fa', + 'http://gateway.org/bzz/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fa/', + 'https://gateway.org/b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d3fa/', + + // a bit shorter + 'b7e53783114e4555384d7fd7154eb8c2e3f7c749c176dcb8f4015b08161b3d', +] + +describe('extractSwarmHash', () => { + test('should correctly extract hash', () => { + correctHashes.forEach(({ input, expectedOutput }) => { + const hash = extractSwarmHash(input) + expect(hash).toBe(expectedOutput) + }) + }) + test('should not extract hash from incorrect inputs', () => { + wrongHashes.forEach(input => { + const hash = extractSwarmHash(input) + expect(hash).toBe(undefined) + }) + }) +}) + +const correctCids: TestObject[] = [ + { + input: 'https://bah5acgza5afd34lfvo7sowxfjahj4ujeduxgg2ge5u3zo4kcjlzjzi23fhka.bzz.link', + expectedOutput: 'e80a3df165abbf275ae5480e9e51241d2e6368c4ed379771424af29ca35b29d4', + }, + { + input: 'https://bah5acgza2lzgtqfztvn3xtnzhv7qvbmblljd7bi52l5jiueretcad55vookq.bzz.link', + expectedOutput: 'd2f269c0b99d5bbbcdb93d7f0a85815ad23f851dd2fa94509124c401f7b57395', + }, +] +const wrongCids: string[] = [ + 'https://bah5acgza5afd34lfvo7sowxfjahj4ujeduxgg2ge5u3zo4kcjlzjzi23fhka.another.domain', + 'http://bah5acgza2lzgtqfztvn3xtnzhv7qvbmblljd7bi52l5jiueretcad55vookq.bzz.link', + 'https://not_cid.bzz.link', + 'https://bah5acgza5afd34lfvo7sowxfjahj4ujeduxgg2ge5u3zo4kcjlzjzi23fhka.subdomain.bzz.link', + 'https://bah5acgza5afd34lfvo7sowxfjahj4ujeduxgg2ge5u3zo4kcjlzjzi23fhka.subdomain.bzz.link', + 'https://bah5acgza2lzgtqfztvn3xtnzhv7qvbmblljd7bi52l5jiueretcad55vook.bzz.link', + 'https://aah5acgza2lzgtqfztvn3xtnzhv7qvbmblljd7bi52l5jiueretcad55vookq.bzz.link', +] + +describe('extractSwarmCid', () => { + test('should correctly extract hash', () => { + correctCids.forEach(({ input, expectedOutput }) => { + const hash = extractSwarmCid(input) + expect(hash).toBe(expectedOutput) + }) + }) + test('should not extract cid from incorrect urls', () => { + wrongCids.forEach(url => { + const hash = extractSwarmCid(url) + expect(hash).toBe(undefined) + }) + }) +}) + +describe('recognizeSwarmHash', () => { + test('should correctly extract hash', () => { + ;[...correctHashes, ...correctCids].forEach(({ input, expectedOutput }) => { + const hash = recognizeSwarmHash(input) + expect(hash).toBe(expectedOutput) + }) + }) + test('should not extract hash from incorrect inputs but instead return them', () => { + ;[...wrongHashes, ...wrongCids].forEach(url => { + const hash = recognizeSwarmHash(url) + expect(hash).toBe(url) + }) + }) +}) diff --git a/src/utils/index.ts b/src/utils/index.ts index 2d8e766..ded258f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,7 @@ import { BigNumber } from 'bignumber.js' import { Token } from '../models/Token' +import { decodeCid } from '@ethersphere/swarm-cid' +import { BZZ_LINK_DOMAIN } from '../constants' /** * Test if value is an integer @@ -108,10 +110,41 @@ export function makeRetriablePromise(fn: () => Promise, maxRetries = 3, de }) } -export function extractSwarmHash(string: string): string | null { - const matches = string.match(/[a-fA-F0-9]{64,128}/) +// Matches exactly 64 or 128 caracters alphanumeric characters that are surrounded by non-alpha num characters +const regexpMatchHash = /(?:^|[^a-f0-9]+)([a-f0-9]{64}|[a-f0-9]{128})(?:$|[^a-f0-9]+)/i - return (matches && matches[0]) || null +export function extractSwarmHash(string: string): string | undefined { + const matches = string.match(regexpMatchHash) + + return (matches && matches[1]) || undefined +} + +// Matches the CID from bzz-link subdomain +const regexpMatchCID = new RegExp(`https://(bah5acgza[a-z0-9]{52})\\.${BZZ_LINK_DOMAIN}`, 'i') + +export function extractSwarmCid(s: string): string | undefined { + const matches = s.match(regexpMatchCID) + + if (!matches || !matches[1]) { + return + } + + const cid = matches[1] + try { + const decodeResult = decodeCid(cid) + + if (!decodeResult.type) { + return + } + + return decodeResult.reference + } catch (e) { + return + } +} + +export function recognizeSwarmHash(value: string): string { + return extractSwarmHash(value) || extractSwarmCid(value) || value } export function uuidV4(): string {