Skip to content

Commit 32cb302

Browse files
committed
fix(tools): correctness and robustness fixes from the upstream backlog
Seven long-open upstream fixes, verified in a real browser. ASCII text drawer (CorentinTh#1774/CorentinTh#1690/CorentinTh#1685/CorentinTh#1651 - four PRs for one bug): fonts were fetched from a protocol-relative `//unpkg.com/figlet@1.6.0/fonts/` URL, pinned to a version the project no longer installs. That resolves to file:// when the build is opened from disk, is blocked by strict connect-src policies, and leaves the tool stuck on "Loading font..." with no error whenever the CDN is unreachable - which is exactly what it did here before this change. The fonts are now emitted into the bundle by a small Vite plugin and served from the deployment's own origin, so the tool works offline and has no third-party runtime dependency. They are still fetched individually on demand, so the initial payload is unchanged. text-to-binary (CorentinTh#1085): encoded with charCodeAt per character, so anything above U+00FF produced more than eight bits and broke the octet grouping, and split('') tore surrogate pairs apart. Now encodes UTF-8, so every group is exactly eight bits and any script round-trips. text-to-unicode (CorentinTh#1087): same surrogate-pair bug - an emoji became two meaningless entities. Now iterates by code point. bcrypt (CorentinTh#1152): hashSync and compareSync throw on an out-of-range salt count and on a malformed hash. Those throws escaped a computed during render and blanked the whole tool; both are now guarded. Hash text (CorentinTh#986): adds the explicit SHA3-224/256/384/512 output sizes, since crypto-js silently defaults SHA3 to 512 bits. UUID generator (CorentinTh#1441): adds v6 and v7, which required uuid 9 to 11. The node identifier option changed from number[] to Uint8Array in that major, and the validation pattern now accepts versions 1-7. Command palette (CorentinTh#1440): the per-category result cap of five was written for the original tool set; with well over a hundred tools a search for "json" or "ip" hid most of its own matches. Raised to twelve. The API's hash and UUID endpoints pick up the new algorithms and versions. Lint, 356 unit tests, both typechecks, the build and all 72 Playwright e2e tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019u4HjVENAajio7cxNSDzir
1 parent 669a38b commit 32cb302

17 files changed

Lines changed: 176 additions & 40 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@
140140
"unicode-emoji-json": "^0.4.0",
141141
"unixcrypt-browser": "^2.0.4",
142142
"unplugin-auto-import": "^0.16.4",
143-
"uuid": "^9.0.0",
143+
"uuid": "^11.1.1",
144144
"vue": "^3.3.4",
145145
"vue-i18n": "^9.9.1",
146146
"vue-router": "^4.1.6",

pnpm-lock.yaml

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

server/app.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ describe('api', () => {
144144

145145
expect(data.digests.MD5).toBe('5d41402abc4b2a76b9719d911017c592');
146146
expect(data.digests.SHA256).toBe('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
147-
expect(Object.keys(data.digests)).toHaveLength(8);
147+
expect(Object.keys(data.digests)).toHaveLength(12);
148+
expect(data.digests['SHA3-256']).toHaveLength(64);
148149
});
149150

150151
it('hashes with a single algorithm and encoding', async () => {

server/routes/crypto.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { HmacMD5, HmacRIPEMD160, HmacSHA1, HmacSHA224, HmacSHA256, HmacSHA3, HmacSHA384, HmacSHA512, enc } from 'crypto-js';
22
import bcrypt from 'bcryptjs';
33
import { ulid } from 'ulid';
4-
import { NIL as uuidNil, v1 as uuidV1, v3 as uuidV3, v4 as uuidV4, v5 as uuidV5 } from 'uuid';
4+
import { NIL as uuidNil, v1 as uuidV1, v3 as uuidV3, v4 as uuidV4, v5 as uuidV5, v6 as uuidV6, v7 as uuidV7 } from 'uuid';
55
import { booleanQuery, defineEndpoint, z } from '../endpoint';
66
import { badRequest } from '../errors';
77
import {
@@ -144,7 +144,7 @@ export const cryptoEndpoints = [
144144
tag: 'Crypto',
145145
summary: 'Generate one or more UUIDs',
146146
input: z.object({
147-
version: z.enum(['nil', 'v1', 'v3', 'v4', 'v5']).default('v4'),
147+
version: z.enum(['nil', 'v1', 'v3', 'v4', 'v5', 'v6', 'v7']).default('v4'),
148148
count: z.coerce.number().int().min(1).max(100).default(1),
149149
namespace: z.string().optional().describe('Required for v3 and v5.'),
150150
name: z.string().optional().describe('Required for v3 and v5.'),
@@ -161,6 +161,12 @@ export const cryptoEndpoints = [
161161
if (version === 'v4') {
162162
return uuidV4();
163163
}
164+
if (version === 'v6') {
165+
return uuidV6();
166+
}
167+
if (version === 'v7') {
168+
return uuidV7();
169+
}
164170

165171
if (!namespace || name === undefined) {
166172
throw badRequest(`UUID ${version} requires both "namespace" and "name".`);

src/modules/command-palette/command-palette.store.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,15 @@ export const useCommandPaletteStore = defineStore('command-palette', () => {
8181
},
8282
});
8383

84+
// Five was fine for the original tool set; with well over a hundred tools a search like "json"
85+
// or "ip" was hiding most of its own matches behind the cap.
86+
const maxSearchResultsPerCategory = 12;
87+
8488
const filteredSearchResult = computed(() =>
85-
_.chain(searchResult.value).groupBy('category').mapValues(categoryOptions => _.take(categoryOptions, 5)).value());
89+
_.chain(searchResult.value)
90+
.groupBy('category')
91+
.mapValues(categoryOptions => _.take(categoryOptions, maxSearchResultsPerCategory))
92+
.value());
8693

8794
return {
8895
filteredSearchResult,

src/modules/command-palette/command-palette.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ function activateOption(option: PaletteOption) {
128128
<c-input-text ref="inputRef" v-model:value="searchPrompt" raw-text placeholder="Type to search a tool or a command..." autofocus clearable />
129129

130130
<div v-for="(options, category) in filteredSearchResult" :key="category">
131-
<div ml-3 mt-3 text-sm font-bold text-primary op-60>
131+
<div ml-3 mt-3 text-sm text-primary font-bold op-60>
132132
{{ category }}
133133
</div>
134134
<command-palette-option v-for="option in options" :key="option.name" :option="option" :selected="selectedOptionIndex === getOptionIndex(option)" @activated="activateOption" />

src/tools/ascii-text-drawer/ascii-text-drawer.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ const output = ref('');
99
const errored = ref(false);
1010
const processing = ref(false);
1111
12-
figlet.defaults({ fontPath: '//unpkg.com/figlet@1.6.0/fonts/' });
12+
// Fonts are served from this deployment rather than from unpkg: the previous protocol-relative CDN
13+
// URL resolved to file:// when the build was opened from disk, was blocked by strict connect-src
14+
// policies, and left the tool stuck on "Loading font..." with no error whenever the CDN was
15+
// unreachable. See the figletFonts plugin in vite.config.ts.
16+
figlet.defaults({ fontPath: `${import.meta.env.BASE_URL}figlet-fonts/` });
1317
1418
watchEffect(async () => {
1519
processing.value = true;

src/tools/bcrypt/bcrypt.vue

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22
import { compareSync, hashSync } from 'bcryptjs';
33
import { useThemeVars } from 'naive-ui';
44
import { useCopy } from '@/composable/copy';
5+
import { withDefaultOnError } from '@/utils/defaults';
56
67
const themeVars = useThemeVars();
78
89
const input = ref('');
910
const saltCount = ref(10);
10-
const hashed = computed(() => hashSync(input.value, saltCount.value));
11+
12+
// bcryptjs throws on an out-of-range salt count and on a malformed hash. Left unguarded, those
13+
// throws escape a computed during render and blank the whole tool.
14+
const hashed = computed(() => withDefaultOnError(() => hashSync(input.value, saltCount.value), ''));
1115
const { copy } = useCopy({ source: hashed, text: 'Hashed string copied to the clipboard' });
1216
1317
const compareString = ref('');
1418
const compareHash = ref('');
15-
const compareMatch = computed(() => compareSync(compareString.value, compareHash.value));
19+
const compareMatch = computed(() =>
20+
withDefaultOnError(() => compareSync(compareString.value, compareHash.value), false),
21+
);
1622
</script>
1723

1824
<template>

src/tools/hash-text/hash-text.service.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,22 @@ export function convertHexToBin(hex: string) {
99
.join('');
1010
}
1111

12+
/**
13+
* SHA3 defaults to a 512-bit digest in crypto-js, so the shorter Keccak output sizes are exposed
14+
* explicitly rather than leaving callers to guess which one they got.
15+
*/
1216
export const hashAlgorithms = {
1317
MD5,
1418
SHA1,
15-
SHA256,
1619
SHA224,
17-
SHA512,
20+
SHA256,
1821
SHA384,
22+
SHA512,
1923
SHA3,
24+
'SHA3-224': (message: string) => SHA3(message, { outputLength: 224 }),
25+
'SHA3-256': (message: string) => SHA3(message, { outputLength: 256 }),
26+
'SHA3-384': (message: string) => SHA3(message, { outputLength: 384 }),
27+
'SHA3-512': (message: string) => SHA3(message, { outputLength: 512 }),
2028
RIPEMD160,
2129
} as const;
2230

src/tools/text-to-binary/text-to-binary.models.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,26 @@ describe('text-to-binary', () => {
2929
expect(() => convertAsciiBinaryToText('1')).toThrow('Invalid binary string');
3030
});
3131
});
32+
33+
describe('non-ASCII text', () => {
34+
it('encodes accented characters as UTF-8 octets', () => {
35+
expect(convertTextToAsciiBinary('é')).toBe('11000011 10101001');
36+
});
37+
38+
it('encodes characters outside the Basic Multilingual Plane', () => {
39+
expect(convertTextToAsciiBinary('😀')).toBe('11110000 10011111 10011000 10000000');
40+
});
41+
42+
it('round-trips text in any script', () => {
43+
for (const text of ['héllo', 'Привет', 'こんにちは', '👨‍👩‍👧 family', 'مرحبا']) {
44+
expect(convertAsciiBinaryToText(convertTextToAsciiBinary(text))).toBe(text);
45+
}
46+
});
47+
48+
it('keeps every group exactly eight bits wide', () => {
49+
const groups = convertTextToAsciiBinary('日本語').split(' ');
50+
51+
expect(groups.every(group => group.length === 8)).toBe(true);
52+
});
53+
});
3254
});

0 commit comments

Comments
 (0)