Skip to content

Commit fd48016

Browse files
committed
feat(new tool): File Hasher
Fix CorentinTh#528
1 parent b430bae commit fd48016

File tree

7 files changed

+331
-18
lines changed

7 files changed

+331
-18
lines changed

components.d.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ declare module '@vue/runtime-core' {
7878
Encryption: typeof import('./src/tools/encryption/encryption.vue')['default']
7979
EtaCalculator: typeof import('./src/tools/eta-calculator/eta-calculator.vue')['default']
8080
FavoriteButton: typeof import('./src/components/FavoriteButton.vue')['default']
81+
FileHasher: typeof import('./src/tools/file-hasher/file-hasher.vue')['default']
8182
FormatTransformer: typeof import('./src/components/FormatTransformer.vue')['default']
8283
GitMemo: typeof import('./src/tools/git-memo/git-memo.vue')['default']
8384
'GitMemo.content': typeof import('./src/tools/git-memo/git-memo.content.md')['default']
@@ -127,23 +128,18 @@ declare module '@vue/runtime-core' {
127128
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
128129
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
129130
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
130-
NCode: typeof import('naive-ui')['NCode']
131131
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
132132
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
133133
NDivider: typeof import('naive-ui')['NDivider']
134134
NEllipsis: typeof import('naive-ui')['NEllipsis']
135-
NFormItem: typeof import('naive-ui')['NFormItem']
136-
NGi: typeof import('naive-ui')['NGi']
137-
NGrid: typeof import('naive-ui')['NGrid']
138135
NH1: typeof import('naive-ui')['NH1']
139136
NH3: typeof import('naive-ui')['NH3']
140137
NIcon: typeof import('naive-ui')['NIcon']
141-
NInputNumber: typeof import('naive-ui')['NInputNumber']
142-
NLabel: typeof import('naive-ui')['NLabel']
138+
NInputGroup: typeof import('naive-ui')['NInputGroup']
139+
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
143140
NLayout: typeof import('naive-ui')['NLayout']
144141
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
145142
NMenu: typeof import('naive-ui')['NMenu']
146-
NScrollbar: typeof import('naive-ui')['NScrollbar']
147143
NSpin: typeof import('naive-ui')['NSpin']
148144
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
149145
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,11 @@
6161
"figlet": "^1.7.0",
6262
"figue": "^1.2.0",
6363
"fuse.js": "^6.6.2",
64+
"hash-wasm": "^4.11.0",
6465
"highlight.js": "^11.7.0",
6566
"iarna-toml-esm": "^3.0.5",
6667
"ibantools": "^4.3.3",
67-
"js-base64": "^3.7.6",
68+
"js-base64": "^3.7.7",
6869
"json5": "^2.2.3",
6970
"jwt-decode": "^3.1.2",
7071
"libphonenumber-js": "^1.10.28",

pnpm-lock.yaml

Lines changed: 16 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/composable/queryParams.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useRouteQuery } from '@vueuse/router';
22
import { computed } from 'vue';
3+
import { useStorage } from '@vueuse/core';
34

4-
export { useQueryParam };
5+
export { useQueryParam, useQueryParamOrStorage };
56

67
const transformers = {
78
number: {
@@ -33,3 +34,31 @@ function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue:
3334
},
3435
});
3536
}
37+
38+
function useQueryParamOrStorage<T>({ name, storageName, defaultValue }: { name: string; storageName: string; defaultValue?: T }) {
39+
const type = typeof defaultValue;
40+
const transformer = transformers[type as keyof typeof transformers] ?? transformers.string;
41+
42+
const storageRef = useStorage(storageName, defaultValue);
43+
const storageDefaultValue = storageRef.value ?? defaultValue;
44+
45+
const proxy = useRouteQuery(name, transformer.toQuery(storageDefaultValue as never));
46+
47+
const ref = computed<T>({
48+
get() {
49+
return transformer.fromQuery(proxy.value) as unknown as T;
50+
},
51+
set(value) {
52+
proxy.value = transformer.toQuery(value as never);
53+
},
54+
});
55+
56+
watch(
57+
ref,
58+
(newValue) => {
59+
storageRef.value = newValue;
60+
},
61+
);
62+
63+
return ref;
64+
}

src/tools/file-hasher/file-hasher.vue

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
<script setup lang="ts">
2+
import {
3+
createAdler32, // (): Promise<IHasher>
4+
createBLAKE2b, // (bits?: number, key?: IDataType): Promise<IHasher> // default is 512 bits
5+
createBLAKE2s, // (bits?: number, key?: IDataType): Promise<IHasher> // default is 256 bits
6+
createBLAKE3, // (bits?: number, key?: IDataType): Promise<IHasher> // default is 256 bits
7+
createCRC32, // (): Promise<IHasher>
8+
createCRC32C, // (): Promise<IHasher>
9+
createKeccak, // (bits?: 224 | 256 | 384 | 512): Promise<IHasher> // default is 512 bits
10+
createMD4, // (): Promise<IHasher>
11+
createMD5, // (): Promise<IHasher>
12+
createRIPEMD160, // (): Promise<IHasher>
13+
createSHA1, // (): Promise<IHasher>
14+
createSHA224, // (): Promise<IHasher>
15+
createSHA256, // (): Promise<IHasher>
16+
createSHA3, // (bits?: 224 | 256 | 384 | 512): Promise<IHasher> // default is 512 bits
17+
createSHA384, // (): Promise<IHasher>
18+
createSHA512, // (): Promise<IHasher>
19+
createSM3, // (): Promise<IHasher>
20+
createWhirlpool, // (): Promise<IHasher>
21+
// createXXHash32, //(seed: number): Promise<IHasher>
22+
// createXXHash64, //(seedLow: number, seedHigh: number): Promise<IHasher>
23+
// createXXHash3, //(seedLow: number, seedHigh: number): Promise<IHasher>
24+
// createXXHash128, //(seedLow: number, seedHigh: number): Promise<IHasher>
25+
} from 'hash-wasm';
26+
import type { lib } from 'crypto-js';
27+
import { enc } from 'crypto-js';
28+
29+
import type { IHasher } from 'hash-wasm/dist/lib/WASMInterface';
30+
import InputCopyable from '../../components/InputCopyable.vue';
31+
import { convertHexToBin } from '../hash-text/hash-text.service';
32+
import { useQueryParamOrStorage } from '@/composable/queryParams';
33+
import { withDefaultOnError } from '@/utils/defaults';
34+
35+
const status = ref<'idle' | 'done' | 'error' | 'processing'>('idle');
36+
const file = ref<File | null>(null);
37+
38+
async function getHashersAsync() {
39+
return {
40+
adler32: await createAdler32(), // (): Promise<IHasher>
41+
BLAKE2b: await createBLAKE2b(), // (bits?: number, key?: IDataType): Promise<IHasher> // default is 512 bits
42+
BLAKE2s: await createBLAKE2s(), // (bits?: number, key?: IDataType): Promise<IHasher> // default is 256 bits
43+
BLAKE3: await createBLAKE3(), // (bits?: number, key?: IDataType): Promise<IHasher> // default is 256 bits
44+
CRC32: await createCRC32(), // (): Promise<IHasher>
45+
CRC32C: await createCRC32C(), // (): Promise<IHasher>
46+
Keccak_224: await createKeccak(224), // (bits?: 224 | 256 | 384 | 512): Promise<IHasher> // default is 512 bits
47+
Keccak_256: await createKeccak(256), // (bits?: 224 | 256 | 384 | 512): Promise<IHasher> // default is 512 bits
48+
Keccak_384: await createKeccak(384), // (bits?: 224 | 256 | 384 | 512): Promise<IHasher> // default is 512 bits
49+
Keccak_512: await createKeccak(512), // (bits?: 224 | 256 | 384 | 512): Promise<IHasher> // default is 512 bits
50+
MD4: await createMD4(), // (): Promise<IHasher>
51+
MD5: await createMD5(), // (): Promise<IHasher>
52+
RIPEMD160: await createRIPEMD160(), // (): Promise<IHasher>
53+
SHA1: await createSHA1(), // (): Promise<IHasher>
54+
SHA224: await createSHA224(), // (): Promise<IHasher>
55+
SHA256: await createSHA256(), // (): Promise<IHasher>
56+
SHA3_224: await createSHA3(224), // (bits?: 224 | 256 | 384 | 512): Promise<IHasher> // default is 512 bits
57+
SHA3_256: await createSHA3(256), // (bits?: 224 | 256 | 384 | 512): Promise<IHasher> // default is 512 bits
58+
SHA3_384: await createSHA3(384), // (bits?: 224 | 256 | 384 | 512): Promise<IHasher> // default is 512 bits
59+
SHA3_512: await createSHA3(512), // (bits?: 224 | 256 | 384 | 512): Promise<IHasher> // default is 512 bits
60+
SHA384: await createSHA384(), // (): Promise<IHasher>
61+
SHA512: await createSHA512(), // (): Promise<IHasher>
62+
SM3: await createSM3(), // (): Promise<IHasher>
63+
Whirlpool: await createWhirlpool(), // (): Promise<IHasher>
64+
};
65+
}
66+
67+
const defaultHashWasmValues = {
68+
adler32: '',
69+
BLAKE2b: '',
70+
BLAKE2s: '',
71+
BLAKE3: '',
72+
CRC32: '',
73+
CRC32C: '',
74+
Keccak_224: '',
75+
Keccak_256: '',
76+
Keccak_384: '',
77+
Keccak_512: '',
78+
MD4: '',
79+
MD5: '',
80+
RIPEMD160: '',
81+
SHA1: '',
82+
SHA224: '',
83+
SHA256: '',
84+
SHA3_224: '',
85+
SHA3_256: '',
86+
SHA3_384: '',
87+
SHA3_512: '',
88+
SHA384: '',
89+
SHA512: '',
90+
SM3: '',
91+
Whirlpool: '',
92+
};
93+
const chunkSize = 64 * 1024 * 1024;
94+
const fileReader = new FileReader();
95+
96+
async function hashChunkAsync(chunk: Blob, hashers: IHasher[]) {
97+
return new Promise<void>((resolve, _reject) => {
98+
fileReader.onload = async (e) => {
99+
const view = new Uint8Array((e.target?.result as ArrayBuffer)!);
100+
for (const hasher of hashers) {
101+
hasher.update(view);
102+
}
103+
resolve();
104+
};
105+
106+
fileReader.readAsArrayBuffer(chunk);
107+
});
108+
}
109+
110+
type AlgoWasmNames = keyof typeof defaultHashWasmValues;
111+
const algoWasmNames = Object.keys(defaultHashWasmValues) as AlgoWasmNames[];
112+
113+
async function hashFileAsync(file: File) {
114+
const chunkNumber = Math.floor(file.size / chunkSize);
115+
116+
const hashers = await getHashersAsync();
117+
for (let i = 0; i <= chunkNumber; i++) {
118+
const chunk = file.slice(
119+
chunkSize * i,
120+
Math.min(chunkSize * (i + 1), file.size),
121+
);
122+
await hashChunkAsync(chunk, Object.values(hashers));
123+
}
124+
125+
const ret = { ...defaultHashWasmValues };
126+
for (const algo of algoWasmNames) {
127+
ret[algo as AlgoWasmNames] = hashers[algo as AlgoWasmNames].digest();
128+
}
129+
return Promise.resolve(ret);
130+
}
131+
132+
const hashes = ref(defaultHashWasmValues);
133+
async function onUpload(uploadedFile: File) {
134+
file.value = uploadedFile;
135+
136+
status.value = 'processing';
137+
try {
138+
status.value = 'done';
139+
140+
hashes.value = await hashFileAsync(uploadedFile);
141+
}
142+
catch (e) {
143+
status.value = 'error';
144+
}
145+
}
146+
147+
type Encoding = keyof typeof enc | 'Bin';
148+
149+
const encoding = useQueryParamOrStorage<Encoding>({ defaultValue: 'Hex', storageName: 'hash-text:encoding', name: 'encoding' });
150+
151+
function formatWithEncoding(words: lib.WordArray, encoding: Encoding) {
152+
if (encoding === 'Bin') {
153+
return convertHexToBin(words.toString(enc.Hex));
154+
}
155+
156+
return words.toString(enc[encoding]);
157+
}
158+
159+
const hashWasmValues = computed(() => withDefaultOnError(() => {
160+
const encodingValue = encoding.value;
161+
const hashesValue = hashes.value;
162+
163+
const ret = defaultHashWasmValues;
164+
for (const algo of algoWasmNames) {
165+
ret[algo] = formatWithEncoding(enc.Hex.parse(hashesValue[algo]), encodingValue);
166+
}
167+
return ret;
168+
}, defaultHashWasmValues));
169+
</script>
170+
171+
<template>
172+
<div>
173+
<c-card>
174+
<c-file-upload
175+
title="Drag and drop a file here, or click to select a file"
176+
@file-upload="onUpload"
177+
/>
178+
179+
<n-divider />
180+
181+
<c-select
182+
v-model:value="encoding"
183+
mb-4
184+
label="Digest encoding"
185+
:options="[
186+
{
187+
label: 'Binary (base 2)',
188+
value: 'Bin',
189+
},
190+
{
191+
label: 'Hexadecimal (base 16)',
192+
value: 'Hex',
193+
},
194+
{
195+
label: 'Base64 (base 64)',
196+
value: 'Base64',
197+
},
198+
{
199+
label: 'Base64url (base 64 with url safe chars)',
200+
value: 'Base64url',
201+
},
202+
]"
203+
/>
204+
</c-card>
205+
206+
<div mt-3 flex justify-center>
207+
<c-alert v-if="status === 'error'" type="error">
208+
An error occured hashing file '{{ file?.name }}'.
209+
</c-alert>
210+
<n-spin
211+
v-if="status === 'processing'"
212+
size="small"
213+
/>
214+
</div>
215+
216+
<c-card v-if="status === 'done'" :title="`Hashes of ${file?.name}`">
217+
<div v-for="algo in algoWasmNames" :key="algo" style="margin: 5px 0">
218+
<n-input-group>
219+
<n-input-group-label style="flex: 0 0 120px">
220+
{{ algo.toUpperCase() }}
221+
</n-input-group-label>
222+
<InputCopyable :value="hashWasmValues[algo]" readonly />
223+
</n-input-group>
224+
</div>
225+
</c-card>
226+
</div>
227+
</template>

0 commit comments

Comments
 (0)