From b7e6571a733730fe6743e4f2bb933134fa3c96aa Mon Sep 17 00:00:00 2001 From: Jan Van den Schilden Date: Tue, 16 Apr 2024 13:33:57 +0200 Subject: [PATCH 1/8] Add URL-safe codec and improve external URL handling - Import UrlsafeCodec for encoding/decoding functionality. - Implement isWebAddress utility function to validate URLs. - Adjust useEffect to fetch data only from valid web addresses. - Add new useEffect to handle non-web address URLs with UrlsafeCodec. --- src/App.tsx | 28 ++++++++++++++++- src/lib/urlsafe-codec.ts | 68 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 src/lib/urlsafe-codec.ts diff --git a/src/App.tsx b/src/App.tsx index e307c00a..8e914669 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,6 +22,7 @@ import HorizontalLine from './ui/horizontal-line'; import SampleConfigForm from './ui/sample-config-form'; import { BrowserDatabase } from './browser-log'; import legend from './legend.png'; +import UrlsafeCodec from './lib/urlsafe-codec'; const db = new Database(); const log = new BrowserDatabase(); @@ -167,8 +168,12 @@ function App(props: RouteComponentProps) { rightReads.current = []; }, [demo]); + function isWebAddress(url) { + return url.startsWith('http://') || url.startsWith('https://'); + } + useEffect(() => { - if (externalUrl) { + if (externalUrl && isWebAddress(externalUrl)) { fetch(externalUrl).then(response => response.text().then(d => { let externalDemo = JSON.parse(d); @@ -187,6 +192,27 @@ function App(props: RouteComponentProps) { ); } }, []); + + useEffect(() => { + async function fetchAndSetData() { + if (externalUrl && !isWebAddress(externalUrl)) { + let externalDemo = await UrlsafeCodec.decode(externalUrl); + if (Array.isArray(externalDemo) && externalDemo.length >= 0) { + setFilteredSamples(externalDemo); + externalDemo = externalDemo[demoIndex.current < externalDemo.length ? demoIndex.current : 0]; + } else { + setFilteredSamples([externalDemo]); + } + if (externalDemo) { + setDemo(externalDemo); + } + setShowSmallMultiples(true); + setReady(true); + } + } + + fetchAndSetData(); + }, []); useEffect(() => { prevJumpId.current = jumpButtonInfo?.id; diff --git a/src/lib/urlsafe-codec.ts b/src/lib/urlsafe-codec.ts new file mode 100644 index 00000000..1ff27a50 --- /dev/null +++ b/src/lib/urlsafe-codec.ts @@ -0,0 +1,68 @@ +import pako from 'pako'; +import base64Js from 'base64-js'; + +/** + * UrlsafeCodec provides static methods to encode and decode samples + * to and from a URL-safe base64 encoded string. It uses JSON for serialization, + * pako for compression, and base64-js for handling base64 encoding. + */ +class UrlsafeCodec { + /** + * Encodes a sample object into a URL-safe base64 string. + * + * The method serializes the sample to a JSON string, compresses it using pako, + * converts the compressed data to a base64 string, and then modifies the base64 string + * to make it URL-safe by replacing '+' with '.', '/' with '_', and '=' with '-'. + * + * @param {Object} sample - The sample object to encode. + * @returns {string} A URL-safe base64 encoded string representing the sample. + */ + static encode(sample) { + try { + const string = JSON.stringify(sample); + const encoder = new TextEncoder(); + const stringAsUint8Array = encoder.encode(string); + const compressedUint8Array = pako.deflate(stringAsUint8Array); + const base64Bytes = base64Js.fromByteArray(compressedUint8Array); + const base64Blob = base64Bytes.toString(); + const base64UrlsafeBlob = base64Blob.replace(/\+/g, '.').replace(/\//g, '_').replace(/=/g, '-'); + return base64UrlsafeBlob; + } catch (error) { + console.error('Error encoding sample:', error); + // Handle the error or rethrow, depending on your needs + throw error; + } + } + + /** + * Decodes a URL-safe base64 string back into a sample object. + * + * The method reverses the URL-safe transformation by replacing '.', '_', and '-' + * with '+', '/', and '=' respectively. It then converts the base64 string back to bytes. + * By default, the header check is turned off. If headerCheck is set to true, the method + * will perform zlib/gzip header and checksum verification during decompression using pako. + * Finally, it parses the JSON string to reconstruct the original sample object. + * + * @param {string} encodedString - The URL-safe base64 encoded string to decode. + * @param {boolean} headerCheck - Optional parameter to enable header and checksum verification. + * @returns {Object} The original sample object. + */ + static decode(encodedString, headerCheck = false) { + try { + const base64Blob = encodedString.replace(/\./g, '+').replace(/_/g, '/').replace(/-/g, '='); + const compressedUint8Array = base64Js.toByteArray(base64Blob); + // Set 'raw' to true if headerCheck is false + const bytes = pako.inflate(compressedUint8Array, {raw: !headerCheck }); + const decoder = new TextDecoder(); + const string = decoder.decode(bytes); + const sample = JSON.parse(string); + return sample; + } catch (error) { + console.error('Error decoding string:', error); + // Handle the error or rethrow, depending on your needs + throw error; + } + } +} + +export default UrlsafeCodec; From f98be8e6cca1dcd0f1a2a4d3fc1b2280bdbf55c6 Mon Sep 17 00:00:00 2001 From: Jan Van den Schilden Date: Wed, 17 Apr 2024 12:00:54 +0200 Subject: [PATCH 2/8] Reduce code duplication --- src/App.tsx | 62 ++++++++++++++++++++++------------------------------- 1 file changed, 26 insertions(+), 36 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8e914669..a9d1510c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -173,45 +173,35 @@ function App(props: RouteComponentProps) { } useEffect(() => { - if (externalUrl && isWebAddress(externalUrl)) { - fetch(externalUrl).then(response => - response.text().then(d => { - let externalDemo = JSON.parse(d); - if (Array.isArray(externalDemo) && externalDemo.length >= 0) { - setFilteredSamples(externalDemo); - externalDemo = externalDemo[demoIndex.current < externalDemo.length ? demoIndex.current : 0]; - } else { - setFilteredSamples([externalDemo]); - } - if (externalDemo) { - setDemo(externalDemo); - } - setShowSmallMultiples(true); - setReady(true); - }) - ); - } - }, []); + const fetchData = async (url) => { + let responseText; + let externalDemo; + if (isWebAddress(url)) { + responseText = await fetch(url).then(response => response.text()); + externalDemo = JSON.parse(responseText); + } else { + externalDemo = await UrlsafeCodec.decode(url); + } + processDemoData(externalDemo); + }; - useEffect(() => { - async function fetchAndSetData() { - if (externalUrl && !isWebAddress(externalUrl)) { - let externalDemo = await UrlsafeCodec.decode(externalUrl); - if (Array.isArray(externalDemo) && externalDemo.length >= 0) { - setFilteredSamples(externalDemo); - externalDemo = externalDemo[demoIndex.current < externalDemo.length ? demoIndex.current : 0]; - } else { - setFilteredSamples([externalDemo]); - } - if (externalDemo) { - setDemo(externalDemo); - } - setShowSmallMultiples(true); - setReady(true); + function processDemoData(demoData){ + if (Array.isArray(demoData) && demoData.length >= 0) { + setFilteredSamples(demoData); + demoData = demoData[demoIndex.current < demoData.length ? demoIndex.current : 0]; + } else { + setFilteredSamples([demoData]); } - } + if (demoData) { + setDemo(demoData); + } + setShowSmallMultiples(true); + setReady(true); + }; - fetchAndSetData(); + if (externalUrl) { + fetchData(externalUrl); + } }, []); useEffect(() => { From 35c79d1c88fbc8cb4e6b800122d63ef489597743 Mon Sep 17 00:00:00 2001 From: Jan Van den Schilden Date: Thu, 18 Apr 2024 09:45:05 +0200 Subject: [PATCH 3/8] Refactor encode method to support optional headerCheck parameter - Add headerCheck parameter to encode method with default value false - Modify pako.deflate call to conditionally use raw option based on headerCheck - Update JSDoc comments to reflect new parameter and its functionality --- src/lib/urlsafe-codec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/urlsafe-codec.ts b/src/lib/urlsafe-codec.ts index 1ff27a50..14d93478 100644 --- a/src/lib/urlsafe-codec.ts +++ b/src/lib/urlsafe-codec.ts @@ -11,18 +11,22 @@ class UrlsafeCodec { * Encodes a sample object into a URL-safe base64 string. * * The method serializes the sample to a JSON string, compresses it using pako, + * By default, the header check is turned off. If headerCheck is set to true, the method + * will perform zlib/gzip header and checksum verification during decompression using pako. * converts the compressed data to a base64 string, and then modifies the base64 string * to make it URL-safe by replacing '+' with '.', '/' with '_', and '=' with '-'. * * @param {Object} sample - The sample object to encode. + * @param {boolean} headerCheck - Optional parameter to enable header and checksum verification. * @returns {string} A URL-safe base64 encoded string representing the sample. */ - static encode(sample) { + static encode(sample, headerCheck = false) { try { const string = JSON.stringify(sample); const encoder = new TextEncoder(); const stringAsUint8Array = encoder.encode(string); - const compressedUint8Array = pako.deflate(stringAsUint8Array); + // Set 'raw' to true if headerCheck is false + const compressedUint8Array = pako.deflate(stringAsUint8Array, {raw: !headerCheck }); const base64Bytes = base64Js.fromByteArray(compressedUint8Array); const base64Blob = base64Bytes.toString(); const base64UrlsafeBlob = base64Blob.replace(/\+/g, '.').replace(/\//g, '_').replace(/=/g, '-'); From b2be48606a23d981875023afbcce17c9dc71b396 Mon Sep 17 00:00:00 2001 From: Jan Van den Schilden Date: Mon, 29 Apr 2024 17:04:19 +0200 Subject: [PATCH 4/8] feat: Add JsonBase64Converter component This commit introduces the JsonBase64Converter component, which allows users to encode and decode JSON data using a URL-safe base64 encoding. The component provides a user-friendly interface for handling JSON-to-base64 conversions. Changes made: - Added JsonBase64Converter component. - Integrated the component into the App and main entry files. Usage: - Navigate to "/dev/codec" to access the JsonBase64Converter. --- src/App.tsx | 4 +++ src/main.tsx | 4 ++- src/ui/json-base64-converter.tsx | 60 ++++++++++++++++++++++++++++++++ vite.config.ts | 2 +- 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/ui/json-base64-converter.tsx diff --git a/src/App.tsx b/src/App.tsx index a9d1510c..c7dcf9f0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,6 +23,7 @@ import SampleConfigForm from './ui/sample-config-form'; import { BrowserDatabase } from './browser-log'; import legend from './legend.png'; import UrlsafeCodec from './lib/urlsafe-codec'; +import JsonBase64Converter from './ui/json-base64-converter'; const db = new Database(); const log = new BrowserDatabase(); @@ -509,6 +510,7 @@ function App(props: RouteComponentProps) { currentSpec.current = JSON.stringify(spec); // console.log('spec', spec); return ( +
+ +
); // !! Removed `demo` not to update twice since `drivers` are updated right after a demo update. }, [ready, xDomain, visPanelWidth, drivers, showOverview, showPutativeDriver, selectedSvId, breakpoints, svReads]); diff --git a/src/main.tsx b/src/main.tsx index 687d69a6..0d8c7dc7 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,10 +2,12 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter, Route } from 'react-router-dom'; import App from './App'; +import JsonBase64Converter from './ui/json-base64-converter'; ReactDOM.render( - + + , document.getElementById('root') ); diff --git a/src/ui/json-base64-converter.tsx b/src/ui/json-base64-converter.tsx new file mode 100644 index 00000000..482bf73d --- /dev/null +++ b/src/ui/json-base64-converter.tsx @@ -0,0 +1,60 @@ +import React, { useState } from 'react'; +import UrlsafeCodec from '../lib/urlsafe-codec'; + +const JsonBase64Converter = () => { + const [jsonText, setJsonText] = useState(''); + const [base64Text, setBase64Text] = useState(''); + const [error, setError] = useState(''); + + const handleEncode = () => { + try { + const jsonObject = JSON.parse(jsonText); + const encoded = UrlsafeCodec.encode(jsonObject); + setBase64Text(encoded); + setError(''); + } catch (error) { + setError('Error parsing JSON. Please enter valid JSON.'); + } + }; + + const handleDecode = () => { + try { + const decoded = UrlsafeCodec.decode(base64Text); + const jsonString = JSON.stringify(decoded, null, 4); + setJsonText(jsonString); + setError(''); + } catch (error) { + setError('Error decoding base64 string. Please enter a valid base64-encoded string.'); + } + }; + + return ( +
+
+

JSON Text

+