feat(base64-string-converter): switch to encode and decode url safe base64 strings (#392)
* feat(base64-string-converter): switch to encode and decode url safe * feat(base64-string-converter): changes based on review comments, use config object instead of boolean argument. * feat(base64-string-converter): fix validation, add option to watch additional refs for changes which interfere with validation rules
This commit is contained in:
		
							parent
							
								
									8c92d56318
								
							
						
					
					
						commit
						0b20f1c16a
					
				| @ -1,5 +1,8 @@ | |||||||
| <template> | <template> | ||||||
|   <c-card title="String to base64"> |   <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 |     <c-input-text | ||||||
|       v-model:value="textInput" |       v-model:value="textInput" | ||||||
|       multiline |       multiline | ||||||
| @ -26,12 +29,16 @@ | |||||||
|   </c-card> |   </c-card> | ||||||
| 
 | 
 | ||||||
|   <c-card title="Base64 to string"> |   <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 |     <c-input-text | ||||||
|       v-model:value="base64Input" |       v-model:value="base64Input" | ||||||
|       multiline |       multiline | ||||||
|       placeholder="Your base64 string..." |       placeholder="Your base64 string..." | ||||||
|       rows="5" |       rows="5" | ||||||
|       :validation-rules="b64ValidationRules" |       :validation-rules="b64ValidationRules" | ||||||
|  |       :validation-watch="b64ValidationWatch" | ||||||
|       label="Base64 string to decode" |       label="Base64 string to decode" | ||||||
|       mb-5 |       mb-5 | ||||||
|     /> |     /> | ||||||
| @ -58,15 +65,23 @@ import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64'; | |||||||
| import { withDefaultOnError } from '@/utils/defaults'; | import { withDefaultOnError } from '@/utils/defaults'; | ||||||
| import { computed, ref } from 'vue'; | 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 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 { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' }); | ||||||
| 
 | 
 | ||||||
| const base64Input = ref(''); | 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 { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' }); | ||||||
| 
 |  | ||||||
| const b64ValidationRules = [ | 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> | </script> | ||||||
|  | |||||||
| @ -61,6 +61,7 @@ | |||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { generateRandomId } from '@/utils/random'; | import { generateRandomId } from '@/utils/random'; | ||||||
| import { useValidation, type UseValidationRule } from '@/composable/validation'; | import { useValidation, type UseValidationRule } from '@/composable/validation'; | ||||||
|  | import type { Ref } from 'vue'; | ||||||
| import { useTheme } from './c-input-text.theme'; | import { useTheme } from './c-input-text.theme'; | ||||||
| import { useAppTheme } from '../theme/themes'; | import { useAppTheme } from '../theme/themes'; | ||||||
| 
 | 
 | ||||||
| @ -73,6 +74,7 @@ const props = withDefaults( | |||||||
|     readonly?: boolean; |     readonly?: boolean; | ||||||
|     disabled?: boolean; |     disabled?: boolean; | ||||||
|     validationRules?: UseValidationRule<string>[]; |     validationRules?: UseValidationRule<string>[]; | ||||||
|  |     validationWatch?: Ref<unknown>[]; | ||||||
|     validation?: ReturnType<typeof useValidation>; |     validation?: ReturnType<typeof useValidation>; | ||||||
|     labelPosition?: 'top' | 'left'; |     labelPosition?: 'top' | 'left'; | ||||||
|     labelWidth?: string; |     labelWidth?: string; | ||||||
| @ -97,6 +99,7 @@ const props = withDefaults( | |||||||
|     readonly: false, |     readonly: false, | ||||||
|     disabled: false, |     disabled: false, | ||||||
|     validationRules: () => [], |     validationRules: () => [], | ||||||
|  |     validationWatch: undefined, | ||||||
|     validation: undefined, |     validation: undefined, | ||||||
|     labelPosition: 'top', |     labelPosition: 'top', | ||||||
|     labelWidth: 'auto', |     labelWidth: 'auto', | ||||||
| @ -125,6 +128,7 @@ const validation = | |||||||
|   useValidation({ |   useValidation({ | ||||||
|     rules: validationRules, |     rules: validationRules, | ||||||
|     source: value, |     source: value, | ||||||
|  |     watch: props.validationWatch, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
| const theme = useTheme(); | const theme = useTheme(); | ||||||
|  | |||||||
| @ -8,18 +8,34 @@ describe('base64 utils', () => { | |||||||
|       expect(textToBase64('a')).to.eql('YQ=='); |       expect(textToBase64('a')).to.eql('YQ=='); | ||||||
|       expect(textToBase64('lorem ipsum')).to.eql('bG9yZW0gaXBzdW0='); |       expect(textToBase64('lorem ipsum')).to.eql('bG9yZW0gaXBzdW0='); | ||||||
|       expect(textToBase64('-1')).to.eql('LTE='); |       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', () => { |   describe('base64ToText', () => { | ||||||
|     it('should convert base64 into text', () => { |     it('should convert base64 into text', () => { | ||||||
|       expect(base64ToText('')).to.eql(''); |       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('bG9yZW0gaXBzdW0=')).to.eql('lorem ipsum'); | ||||||
|       expect(base64ToText('data:text/plain;base64,bG9yZW0gaXBzdW0=')).to.eql('lorem ipsum'); |       expect(base64ToText('data:text/plain;base64,bG9yZW0gaXBzdW0=')).to.eql('lorem ipsum'); | ||||||
|       expect(base64ToText('LTE=')).to.eql('-1'); |       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', () => { |     it('should throw for incorrect base64 string', () => { | ||||||
|       expect(() => base64ToText('a')).to.throw('Incorrect base64 string'); |       expect(() => base64ToText('a')).to.throw('Incorrect base64 string'); | ||||||
|       expect(() => base64ToText(' ')).to.throw('Incorrect base64 string'); |       expect(() => base64ToText(' ')).to.throw('Incorrect base64 string'); | ||||||
|  | |||||||
| @ -1,15 +1,19 @@ | |||||||
| export { textToBase64, base64ToText, isValidBase64, removePotentialDataAndMimePrefix }; | export { textToBase64, base64ToText, isValidBase64, removePotentialDataAndMimePrefix }; | ||||||
| 
 | 
 | ||||||
| function textToBase64(str: string) { | function textToBase64(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boolean } = {}) { | ||||||
|   return window.btoa(str); |   const encoded = window.btoa(str); | ||||||
|  |   return makeUrlSafe ? makeUriSafe(encoded) : encoded; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function base64ToText(str: string) { | function base64ToText(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boolean } = {}) { | ||||||
|   if (!isValidBase64(str)) { |   if (!isValidBase64(str, { makeUrlSafe: makeUrlSafe })) { | ||||||
|     throw new Error('Incorrect base64 string'); |     throw new Error('Incorrect base64 string'); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const cleanStr = removePotentialDataAndMimePrefix(str); |   let cleanStr = removePotentialDataAndMimePrefix(str); | ||||||
|  |   if (makeUrlSafe) { | ||||||
|  |     cleanStr = unURI(cleanStr); | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   try { |   try { | ||||||
|     return window.atob(cleanStr); |     return window.atob(cleanStr); | ||||||
| @ -22,12 +26,33 @@ function removePotentialDataAndMimePrefix(str: string) { | |||||||
|   return str.replace(/^data:.*?;base64,/, ''); |   return str.replace(/^data:.*?;base64,/, ''); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function isValidBase64(str: string) { | function isValidBase64(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boolean } = {}) { | ||||||
|   const cleanStr = removePotentialDataAndMimePrefix(str); |   let cleanStr = removePotentialDataAndMimePrefix(str); | ||||||
|  |   if (makeUrlSafe) { | ||||||
|  |     cleanStr = unURI(cleanStr); | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   try { |   try { | ||||||
|  |     if (makeUrlSafe) { | ||||||
|  |       return removePotentialPadding(window.btoa(window.atob(cleanStr))) === cleanStr; | ||||||
|  |     } | ||||||
|     return window.btoa(window.atob(cleanStr)) === cleanStr; |     return window.btoa(window.atob(cleanStr)) === cleanStr; | ||||||
|   } catch (err) { |   } catch (err) { | ||||||
|     return false; |     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, ''); | ||||||
|  | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user