Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions src/tools/base64-string-converter/base64-string-converter.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<template>
<c-card title="String to base64">
<n-form-item label="Encode URL safe" label-placement="left">
<n-switch v-model:value="encodeUrlSafe" />
</n-form-item>
<c-input-text
v-model:value="textInput"
multiline
Expand All @@ -26,12 +29,16 @@
</c-card>

<c-card title="Base64 to string">
<n-form-item label="Decode URL safe" label-placement="left">
<n-switch v-model:value="decodeUrlSafe" />
</n-form-item>
<c-input-text
v-model:value="base64Input"
multiline
placeholder="Your base64 string..."
rows="5"
:validation-rules="b64ValidationRules"
:validation-watch="b64ValidationWatch"
label="Base64 string to decode"
mb-5
/>
Expand All @@ -58,15 +65,23 @@ import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64';
import { withDefaultOnError } from '@/utils/defaults';
import { computed, ref } from 'vue';

const encodeUrlSafe = useStorage('base64-string-converter--encode-url-safe', false);
const decodeUrlSafe = useStorage('base64-string-converter--decode-url-safe', false);

const textInput = ref('');
const base64Output = computed(() => textToBase64(textInput.value));
const base64Output = computed(() => textToBase64(textInput.value, { makeUrlSafe: encodeUrlSafe.value }));
const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' });

const base64Input = ref('');
const textOutput = computed(() => withDefaultOnError(() => base64ToText(base64Input.value.trim()), ''));
const textOutput = computed(() =>
withDefaultOnError(() => base64ToText(base64Input.value.trim(), { makeUrlSafe: decodeUrlSafe.value }), ''),
);
const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' });

const b64ValidationRules = [
{ message: 'Invalid base64 string', validator: (value: string) => isValidBase64(value.trim()) },
{
message: 'Invalid base64 string',
validator: (value: string) => isValidBase64(value.trim(), { makeUrlSafe: decodeUrlSafe.value }),
},
];
const b64ValidationWatch = [decodeUrlSafe];
</script>
4 changes: 4 additions & 0 deletions src/ui/c-input-text/c-input-text.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
<script lang="ts" setup>
import { generateRandomId } from '@/utils/random';
import { useValidation, type UseValidationRule } from '@/composable/validation';
import type { Ref } from 'vue';
import { useTheme } from './c-input-text.theme';
import { useAppTheme } from '../theme/themes';

Expand All @@ -73,6 +74,7 @@ const props = withDefaults(
readonly?: boolean;
disabled?: boolean;
validationRules?: UseValidationRule<string>[];
validationWatch?: Ref<unknown>[];
validation?: ReturnType<typeof useValidation>;
labelPosition?: 'top' | 'left';
labelWidth?: string;
Expand All @@ -97,6 +99,7 @@ const props = withDefaults(
readonly: false,
disabled: false,
validationRules: () => [],
validationWatch: undefined,
validation: undefined,
labelPosition: 'top',
labelWidth: 'auto',
Expand Down Expand Up @@ -125,6 +128,7 @@ const validation =
useValidation({
rules: validationRules,
source: value,
watch: props.validationWatch,
});

const theme = useTheme();
Expand Down
18 changes: 17 additions & 1 deletion src/utils/base64.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,34 @@ describe('base64 utils', () => {
expect(textToBase64('a')).to.eql('YQ==');
expect(textToBase64('lorem ipsum')).to.eql('bG9yZW0gaXBzdW0=');
expect(textToBase64('-1')).to.eql('LTE=');
expect(textToBase64('<<<????????>>>', { makeUrlSafe: false })).to.eql('PDw8Pz8/Pz8/Pz8+Pj4=');
});
it('should convert string into url safe base64', () => {
expect(textToBase64('', { makeUrlSafe: true })).to.eql('');
expect(textToBase64('a', { makeUrlSafe: true })).to.eql('YQ');
expect(textToBase64('lorem ipsum', { makeUrlSafe: true })).to.eql('bG9yZW0gaXBzdW0');
expect(textToBase64('<<<????????>>>', { makeUrlSafe: true })).to.eql('PDw8Pz8_Pz8_Pz8-Pj4');
});
});

describe('base64ToText', () => {
it('should convert base64 into text', () => {
expect(base64ToText('')).to.eql('');
expect(base64ToText('YQ==')).to.eql('a');
expect(base64ToText('YQ==', { makeUrlSafe: false })).to.eql('a');
expect(base64ToText('bG9yZW0gaXBzdW0=')).to.eql('lorem ipsum');
expect(base64ToText('data:text/plain;base64,bG9yZW0gaXBzdW0=')).to.eql('lorem ipsum');
expect(base64ToText('LTE=')).to.eql('-1');
});

it('should convert url safe base64 into text', () => {
expect(base64ToText('', { makeUrlSafe: true })).to.eql('');
expect(base64ToText('YQ', { makeUrlSafe: true })).to.eql('a');
expect(base64ToText('bG9yZW0gaXBzdW0', { makeUrlSafe: true })).to.eql('lorem ipsum');
expect(base64ToText('data:text/plain;base64,bG9yZW0gaXBzdW0', { makeUrlSafe: true })).to.eql('lorem ipsum');
expect(base64ToText('LTE', { makeUrlSafe: true })).to.eql('-1');
expect(base64ToText('PDw8Pz8_Pz8_Pz8-Pj4', { makeUrlSafe: true })).to.eql('<<<????????>>>');
});

it('should throw for incorrect base64 string', () => {
expect(() => base64ToText('a')).to.throw('Incorrect base64 string');
expect(() => base64ToText(' ')).to.throw('Incorrect base64 string');
Expand Down
39 changes: 32 additions & 7 deletions src/utils/base64.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
export { textToBase64, base64ToText, isValidBase64, removePotentialDataAndMimePrefix };

function textToBase64(str: string) {
return window.btoa(str);
function textToBase64(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boolean } = {}) {
const encoded = window.btoa(str);
return makeUrlSafe ? makeUriSafe(encoded) : encoded;
}

function base64ToText(str: string) {
if (!isValidBase64(str)) {
function base64ToText(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boolean } = {}) {
if (!isValidBase64(str, { makeUrlSafe: makeUrlSafe })) {
throw new Error('Incorrect base64 string');
}

const cleanStr = removePotentialDataAndMimePrefix(str);
let cleanStr = removePotentialDataAndMimePrefix(str);
if (makeUrlSafe) {
cleanStr = unURI(cleanStr);
}

try {
return window.atob(cleanStr);
Expand All @@ -22,12 +26,33 @@ function removePotentialDataAndMimePrefix(str: string) {
return str.replace(/^data:.*?;base64,/, '');
}

function isValidBase64(str: string) {
const cleanStr = removePotentialDataAndMimePrefix(str);
function isValidBase64(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boolean } = {}) {
let cleanStr = removePotentialDataAndMimePrefix(str);
if (makeUrlSafe) {
cleanStr = unURI(cleanStr);
}

try {
if (makeUrlSafe) {
return removePotentialPadding(window.btoa(window.atob(cleanStr))) === cleanStr;
}
return window.btoa(window.atob(cleanStr)) === cleanStr;
} catch (err) {
return false;
}
}

function makeUriSafe(encoded: string) {
return encoded.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}

function unURI(encoded: string): string {
return encoded
.replace(/-/g, '+')
.replace(/_/g, '/')
.replace(/[^A-Za-z0-9+/]/g, '');
}

function removePotentialPadding(str: string) {
return str.replace(/=/g, '');
}