From 93cba8c90472b646d018d9ce8d3f6608bae0fe9c Mon Sep 17 00:00:00 2001 From: "Md. Aminur Islam" Date: Sat, 28 Mar 2026 17:00:02 +0600 Subject: [PATCH 1/2] feat(utilities): add Unserializer tool with print_r and var_dump output --- components/seo/UnserializerSEO.tsx | 68 +++ components/utils/tools-list.ts | 6 + components/utils/unserializer.utils.test.ts | 148 +++++++ components/utils/unserializer.utils.ts | 450 ++++++++++++++++++++ pages/utilities/unserializer.tsx | 164 +++++++ 5 files changed, 836 insertions(+) create mode 100644 components/seo/UnserializerSEO.tsx create mode 100644 components/utils/unserializer.utils.test.ts create mode 100644 components/utils/unserializer.utils.ts create mode 100644 pages/utilities/unserializer.tsx diff --git a/components/seo/UnserializerSEO.tsx b/components/seo/UnserializerSEO.tsx new file mode 100644 index 0000000..0b7d9df --- /dev/null +++ b/components/seo/UnserializerSEO.tsx @@ -0,0 +1,68 @@ +export default function UnserializerSEO() { + return ( +
+
+

Free Online Unserializer - Parse Serialized Data Instantly

+

+ Paste a serialized string and instantly convert it into a + human-readable format. Choose between print_r() for a quick overview + or var_dump() for type-annotated detail. Ideal for inspecting + WordPress options, transients, and metadata stored in the database. +

+
+ +
+

How to Use the Unserializer

+
    +
  • + Step 1:
    + Paste a serialized string into the input box. You can grab these + from database fields, API responses, or debug logs. +
  • +
  • + Step 2:
    + Select an output format — print_r() for a compact view or{" "} + var_dump() for detailed type and length information. +
  • +
  • + Step 3:
    + Copy the formatted output and use it in your debugging workflow. +
  • +
+
+ +
+

FAQs

+
    +
  • + What is a serialized string? +
    + Serialization converts a data structure into a compact string + representation for storage or transfer. This tool reverses that + process so you can read the original data. +
  • +
  • + Where do I find serialized data? +
    + Common sources include the WordPress wp_options table, user meta + fields, WooCommerce order meta, transient caches, and plugin + settings stored in the database. +
  • +
  • + What is the difference between print_r() and var_dump()? +
    + print_r() shows values and structure in a concise format. var_dump() + adds explicit type and length annotations for every value, which is + more useful for type-sensitive debugging. +
  • +
  • + Does this tool modify the original data? +
    + No. The tool only reads and formats the input — nothing is stored, + sent, or altered. +
  • +
+
+
+ ); +} diff --git a/components/utils/tools-list.ts b/components/utils/tools-list.ts index 36657e1..d2e8027 100644 --- a/components/utils/tools-list.ts +++ b/components/utils/tools-list.ts @@ -23,6 +23,12 @@ export const tools = [ "Format and beautify your JSON data for better readability and debugging. Quickly visualize and organize your JSON data with ease.", link: "/utilities/json-formatter", }, + { + title: "Unserializer", + description: + "Parse serialized strings and render readable output in print_r() or var_dump() format. Useful for debugging WordPress database values.", + link: "/utilities/unserializer", + }, { title: "JSONL Validator", description: diff --git a/components/utils/unserializer.utils.test.ts b/components/utils/unserializer.utils.test.ts new file mode 100644 index 0000000..d7ac0c1 --- /dev/null +++ b/components/utils/unserializer.utils.test.ts @@ -0,0 +1,148 @@ +import { + formatPrintR, + formatVarDump, + isValidSerialized, + unserialize, +} from "./unserializer.utils"; + +describe("unserializer.utils", () => { + describe("unserialize", () => { + test("parses primitive values", () => { + expect(unserialize('s:5:"hello";')).toEqual({ + type: "string", + value: "hello", + }); + expect(unserialize("i:42;")).toEqual({ type: "int", value: 42 }); + expect(unserialize("d:3.14;")).toEqual({ type: "float", value: 3.14 }); + expect(unserialize("b:1;")).toEqual({ type: "bool", value: true }); + expect(unserialize("b:0;")).toEqual({ type: "bool", value: false }); + expect(unserialize("N;")).toEqual({ type: "null" }); + }); + + test("parses multi-byte UTF-8 strings", () => { + expect(unserialize('s:4:"😊";')).toEqual({ + type: "string", + value: "😊", + }); + }); + + test("parses nested arrays from sample payload", () => { + const input = + 'a:2:{i:0;s:12:"Sample array";i:1;a:2:{i:0;s:5:"Apple";i:1;s:6:"Orange";}}'; + + expect(unserialize(input)).toEqual({ + type: "array", + entries: [ + { + key: { type: "int", value: 0 }, + value: { type: "string", value: "Sample array" }, + }, + { + key: { type: "int", value: 1 }, + value: { + type: "array", + entries: [ + { + key: { type: "int", value: 0 }, + value: { type: "string", value: "Apple" }, + }, + { + key: { type: "int", value: 1 }, + value: { type: "string", value: "Orange" }, + }, + ], + }, + }, + ], + }); + }); + + test("resolves references", () => { + const result = unserialize('a:2:{s:1:"x";s:3:"foo";s:1:"y";R:2;}'); + expect(result).toEqual({ + type: "array", + entries: [ + { + key: { type: "string", value: "x" }, + value: { type: "string", value: "foo" }, + }, + { + key: { type: "string", value: "y" }, + value: { type: "string", value: "foo" }, + }, + ], + }); + }); + + test("throws clear errors for invalid input", () => { + expect(() => unserialize("")).toThrow( + "Please enter a serialized string." + ); + expect(() => unserialize("xyz")).toThrow(); + expect(() => unserialize('a:1:{s:1:"x";R:99;}')).toThrow( + "Reference index 99 does not exist" + ); + }); + }); + + describe("isValidSerialized", () => { + test("returns true for valid payloads and false for invalid payloads", () => { + expect(isValidSerialized('s:5:"hello";')).toBe(true); + expect(isValidSerialized("i:42;")).toBe(true); + expect(isValidSerialized("")).toBe(false); + expect(isValidSerialized("not serialized")).toBe(false); + }); + }); + + describe("formatPrintR", () => { + test("matches expected output for sample payload", () => { + const input = + 'a:2:{i:0;s:12:"Sample array";i:1;a:2:{i:0;s:5:"Apple";i:1;s:6:"Orange";}}'; + const parsed = unserialize(input); + + const expected = [ + "Array", + "(", + " [0] => Sample array", + " [1] => Array", + " (", + " [0] => Apple", + " [1] => Orange", + " )", + "", + ")", + ].join("\n"); + + expect(formatPrintR(parsed)).toBe(expected); + }); + }); + + describe("formatVarDump", () => { + test("matches expected output for sample payload", () => { + const input = + 'a:2:{i:0;s:12:"Sample array";i:1;a:2:{i:0;s:5:"Apple";i:1;s:6:"Orange";}}'; + const parsed = unserialize(input); + + const expected = [ + "array(2) {", + " [0]=>", + ' string(12) "Sample array"', + " [1]=>", + " array(2) {", + " [0]=>", + ' string(5) "Apple"', + " [1]=>", + ' string(6) "Orange"', + " }", + "}", + ].join("\n"); + + expect(formatVarDump(parsed)).toBe(expected); + }); + + test("uses UTF-8 byte length for strings", () => { + const parsed = unserialize('s:4:"😊";'); + expect(formatVarDump(parsed)).toBe('string(4) "😊"'); + }); + }); +}); diff --git a/components/utils/unserializer.utils.ts b/components/utils/unserializer.utils.ts new file mode 100644 index 0000000..2c597ba --- /dev/null +++ b/components/utils/unserializer.utils.ts @@ -0,0 +1,450 @@ +export type SerializedValue = + | { type: "string"; value: string } + | { type: "int"; value: number } + | { type: "float"; value: number } + | { type: "bool"; value: boolean } + | { type: "null" } + | { + type: "array"; + entries: Array<{ key: SerializedValue; value: SerializedValue }>; + } + | { + type: "object"; + className: string; + properties: Array<{ key: SerializedValue; value: SerializedValue }>; + }; + +class UnserializeError extends Error { + constructor(message: string) { + super(message); + this.name = "UnserializeError"; + } +} + +class Parser { + private pos = 0; + private readonly parsedValues: SerializedValue[] = []; + + constructor(private input: string) {} + + parse(): SerializedValue { + const value = this.readValue(); + if (this.pos !== this.input.length) { + throw new UnserializeError( + `Unexpected trailing characters at position ${this.pos}` + ); + } + return value; + } + + private readValue(): SerializedValue { + if (this.pos >= this.input.length) { + throw new UnserializeError("Unexpected end of input"); + } + + const type = this.input[this.pos]; + + switch (type) { + case "s": + return this.readString(); + case "i": + return this.readInt(); + case "d": + return this.readFloat(); + case "b": + return this.readBool(); + case "N": + return this.readNull(); + case "a": + return this.readArray(); + case "O": + return this.readObject(); + case "r": + case "R": + return this.readReference(); + default: + throw new UnserializeError( + `Unknown type "${type}" at position ${this.pos}` + ); + } + } + + private expect(char: string) { + if (this.input[this.pos] !== char) { + throw new UnserializeError( + `Expected "${char}" at position ${this.pos}, got "${this.input[this.pos]}"` + ); + } + this.pos++; + } + + private readUntil(char: string): string { + const start = this.pos; + while (this.pos < this.input.length && this.input[this.pos] !== char) { + this.pos++; + } + + if (this.pos >= this.input.length) { + throw new UnserializeError(`Expected "${char}" but reached end of input`); + } + + return this.input.slice(start, this.pos); + } + + private readString(registerValue = true): SerializedValue { + this.expect("s"); + this.expect(":"); + const lenStr = this.readUntil(":"); + const len = parseInt(lenStr, 10); + if (Number.isNaN(len) || len < 0) { + throw new UnserializeError(`Invalid string length: "${lenStr}"`); + } + + this.expect(":"); + this.expect('"'); + const value = this.readUtf8ByteLengthString(len); + this.expect('"'); + this.expect(";"); + + const parsed: SerializedValue = { type: "string", value }; + if (registerValue) { + this.parsedValues.push(parsed); + } + return parsed; + } + + private readInt(registerValue = true): SerializedValue { + this.expect("i"); + this.expect(":"); + const numStr = this.readUntil(";"); + this.expect(";"); + const value = parseInt(numStr, 10); + if (Number.isNaN(value)) { + throw new UnserializeError(`Invalid integer value: "${numStr}"`); + } + + const parsed: SerializedValue = { type: "int", value }; + if (registerValue) { + this.parsedValues.push(parsed); + } + return parsed; + } + + private readFloat(): SerializedValue { + this.expect("d"); + this.expect(":"); + const numStr = this.readUntil(";"); + this.expect(";"); + + let value: number; + if (numStr === "INF") value = Infinity; + else if (numStr === "-INF") value = -Infinity; + else if (numStr === "NAN") value = NaN; + else { + value = parseFloat(numStr); + if (Number.isNaN(value)) { + throw new UnserializeError(`Invalid float value: "${numStr}"`); + } + } + + const parsed: SerializedValue = { type: "float", value }; + this.parsedValues.push(parsed); + return parsed; + } + + private readBool(): SerializedValue { + this.expect("b"); + this.expect(":"); + const val = this.readUntil(";"); + this.expect(";"); + if (val !== "0" && val !== "1") { + throw new UnserializeError(`Invalid boolean value: "${val}"`); + } + + const parsed: SerializedValue = { type: "bool", value: val === "1" }; + this.parsedValues.push(parsed); + return parsed; + } + + private readNull(): SerializedValue { + this.expect("N"); + this.expect(";"); + const parsed: SerializedValue = { type: "null" }; + this.parsedValues.push(parsed); + return parsed; + } + + private readArray(): SerializedValue { + this.expect("a"); + this.expect(":"); + const countStr = this.readUntil(":"); + const count = parseInt(countStr, 10); + if (Number.isNaN(count) || count < 0) { + throw new UnserializeError(`Invalid array count: "${countStr}"`); + } + this.expect(":"); + this.expect("{"); + + const parsed: { + type: "array"; + entries: Array<{ key: SerializedValue; value: SerializedValue }>; + } = { + type: "array", + entries: [], + }; + this.parsedValues.push(parsed); + + const entries: Array<{ key: SerializedValue; value: SerializedValue }> = []; + for (let i = 0; i < count; i++) { + const key = this.readArrayKey(); + const value = this.readValue(); + entries.push({ key, value }); + } + + this.expect("}"); + parsed.entries = entries; + return parsed; + } + + private readObject(): SerializedValue { + this.expect("O"); + this.expect(":"); + const classNameLenStr = this.readUntil(":"); + const classNameLen = parseInt(classNameLenStr, 10); + if (Number.isNaN(classNameLen) || classNameLen < 0) { + throw new UnserializeError( + `Invalid class name length: "${classNameLenStr}"` + ); + } + this.expect(":"); + this.expect('"'); + const className = this.readUtf8ByteLengthString(classNameLen); + this.expect('"'); + this.expect(":"); + const countStr = this.readUntil(":"); + const count = parseInt(countStr, 10); + if (Number.isNaN(count) || count < 0) { + throw new UnserializeError( + `Invalid object property count: "${countStr}"` + ); + } + this.expect(":"); + this.expect("{"); + + const parsed: { + type: "object"; + className: string; + properties: Array<{ key: SerializedValue; value: SerializedValue }>; + } = { + type: "object", + className, + properties: [], + }; + this.parsedValues.push(parsed); + + const properties: Array<{ key: SerializedValue; value: SerializedValue }> = + []; + for (let i = 0; i < count; i++) { + const key = this.readObjectKey(); + const value = this.readValue(); + properties.push({ key, value }); + } + + this.expect("}"); + parsed.properties = properties; + return parsed; + } + + private readReference(): SerializedValue { + this.pos++; // skip "r" or "R" + this.expect(":"); + const refStr = this.readUntil(";"); + this.expect(";"); + + const refIndex = parseInt(refStr, 10); + if (Number.isNaN(refIndex) || refIndex < 1) { + throw new UnserializeError(`Invalid reference index: "${refStr}"`); + } + + const referencedValue = this.parsedValues[refIndex - 1]; + if (!referencedValue) { + throw new UnserializeError(`Reference index ${refIndex} does not exist`); + } + + return referencedValue; + } + + private readUtf8ByteLengthString(byteLength: number): string { + const start = this.pos; + let consumedBytes = 0; + + while (consumedBytes < byteLength) { + if (this.pos >= this.input.length) { + throw new UnserializeError( + "Unexpected end of input while reading string bytes" + ); + } + + const codePoint = this.input.codePointAt(this.pos); + if (codePoint === undefined) { + throw new UnserializeError("Invalid string code point"); + } + + const char = String.fromCodePoint(codePoint); + const charByteLength = utf8ByteLength(char); + consumedBytes += charByteLength; + this.pos += char.length; + } + + if (consumedBytes !== byteLength) { + throw new UnserializeError( + `String byte length mismatch. Expected ${byteLength}, got ${consumedBytes}` + ); + } + + return this.input.slice(start, this.pos); + } + + private readArrayKey(): SerializedValue { + const keyType = this.input[this.pos]; + switch (keyType) { + case "i": + return this.readInt(false); + case "s": + return this.readString(false); + default: + throw new UnserializeError( + `Invalid array key type "${keyType}" at position ${this.pos}` + ); + } + } + + private readObjectKey(): SerializedValue { + const keyType = this.input[this.pos]; + if (keyType !== "s") { + throw new UnserializeError( + `Invalid object property key type "${keyType}" at position ${this.pos}` + ); + } + return this.readString(false); + } +} + +export function unserialize(input: string): SerializedValue { + if (!input || input.trim() === "") { + throw new UnserializeError("Please enter a serialized string."); + } + + const parser = new Parser(input.trim()); + return parser.parse(); +} + +export function isValidSerialized(input: string): boolean { + try { + unserialize(input); + return true; + } catch { + return false; + } +} + +function keyToString(key: SerializedValue): string { + switch (key.type) { + case "int": + return String(key.value); + case "string": + return key.value; + case "bool": + return key.value ? "1" : "0"; + case "null": + return ""; + case "float": + return String(Math.trunc(key.value)); + default: + return ""; + } +} + +function utf8ByteLength(value: string): number { + return encodeURIComponent(value).replace(/%[A-F\d]{2}/gi, "x").length; +} + +export function formatPrintR(value: SerializedValue, indent = 0): string { + const pad = " ".repeat(indent * 4); + const innerPad = " ".repeat((indent + 1) * 4); + + switch (value.type) { + case "string": + return value.value; + case "int": + case "float": + return String(value.value); + case "bool": + return value.value ? "1" : ""; + case "null": + return ""; + case "array": { + if (value.entries.length === 0) { + return `Array\n${pad}(\n${pad})`; + } + + let result = `Array\n${pad}(\n`; + for (const entry of value.entries) { + const k = keyToString(entry.key); + const v = formatPrintR(entry.value, indent + 2); + result += `${innerPad}[${k}] => ${v}\n`; + } + result += `${pad})`; + return indent > 0 ? `${result}\n` : result; + } + case "object": { + let result = `${value.className} Object\n${pad}(\n`; + for (const property of value.properties) { + const k = keyToString(property.key); + const v = formatPrintR(property.value, indent + 2); + result += `${innerPad}[${k}] => ${v}\n`; + } + result += `${pad})`; + return indent > 0 ? `${result}\n` : result; + } + } +} + +export function formatVarDump(value: SerializedValue, indent = 0): string { + const pad = " ".repeat(indent * 2); + const innerPad = " ".repeat((indent + 1) * 2); + + switch (value.type) { + case "string": + return `${pad}string(${utf8ByteLength(value.value)}) "${value.value}"`; + case "int": + return `${pad}int(${value.value})`; + case "float": + return `${pad}float(${value.value})`; + case "bool": + return `${pad}bool(${value.value ? "true" : "false"})`; + case "null": + return `${pad}NULL`; + case "array": { + let result = `${pad}array(${value.entries.length}) {\n`; + for (const entry of value.entries) { + const k = keyToString(entry.key); + const keyBracket = entry.key.type === "int" ? `[${k}]` : `["${k}"]`; + result += `${innerPad}${keyBracket}=>\n`; + result += `${formatVarDump(entry.value, indent + 1)}\n`; + } + result += `${pad}}`; + return result; + } + case "object": { + let result = `${pad}object(${value.className})#1 (${value.properties.length}) {\n`; + for (const property of value.properties) { + const k = keyToString(property.key); + result += `${innerPad}["${k}"]=>\n`; + result += `${formatVarDump(property.value, indent + 1)}\n`; + } + result += `${pad}}`; + return result; + } + } +} diff --git a/pages/utilities/unserializer.tsx b/pages/utilities/unserializer.tsx new file mode 100644 index 0000000..be2af50 --- /dev/null +++ b/pages/utilities/unserializer.tsx @@ -0,0 +1,164 @@ +import { useCallback, useState } from "react"; +import { Textarea } from "@/components/ds/TextareaComponent"; +import PageHeader from "@/components/PageHeader"; +import { Card } from "@/components/ds/CardComponent"; +import { Button } from "@/components/ds/ButtonComponent"; +import { Label } from "@/components/ds/LabelComponent"; +import Header from "@/components/Header"; +import { useCopyToClipboard } from "@/components/hooks/useCopyToClipboard"; +import { CMDK } from "@/components/CMDK"; +import CallToActionGrid from "../../components/CallToActionGrid"; +import GitHubContribution from "@/components/GitHubContribution"; +import UnserializerSEO from "@/components/seo/UnserializerSEO"; +import Meta from "@/components/Meta"; +import { + formatPrintR, + formatVarDump, + unserialize, +} from "@/components/utils/unserializer.utils"; + +type OutputMode = "print_r" | "var_dump"; + +const SAMPLE_INPUT = + 'a:2:{i:0;s:12:"Sample array";i:1;a:2:{i:0;s:5:"Apple";i:1;s:6:"Orange";}}'; + +export default function Unserializer() { + const [input, setInput] = useState(""); + const [output, setOutput] = useState(""); + const [error, setError] = useState(""); + const [mode, setMode] = useState("print_r"); + const { buttonText, handleCopy } = useCopyToClipboard(); + + const runUnserialize = useCallback( + (nextInput: string, nextMode: OutputMode) => { + if (nextInput.trim() === "") { + setOutput(""); + setError(""); + return; + } + + try { + const parsed = unserialize(nextInput); + const formatted = + nextMode === "print_r" ? formatPrintR(parsed) : formatVarDump(parsed); + setOutput(formatted); + setError(""); + } catch (err) { + const message = err instanceof Error ? err.message : "Invalid input"; + setOutput(""); + setError(message); + } + }, + [] + ); + + const handleInputChange = useCallback( + (event: React.ChangeEvent) => { + const { value } = event.currentTarget; + setInput(value); + runUnserialize(value, mode); + }, + [mode, runUnserialize] + ); + + const handleModeChange = useCallback( + (nextMode: OutputMode) => { + setMode(nextMode); + runUnserialize(input, nextMode); + }, + [input, runUnserialize] + ); + + const loadExample = useCallback(() => { + setInput(SAMPLE_INPUT); + runUnserialize(SAMPLE_INPUT, mode); + }, [mode, runUnserialize]); + + const clearAll = useCallback(() => { + setInput(""); + setOutput(""); + setError(""); + }, []); + + return ( +
+ +
+ + +
+ +
+ +
+ +
+
+ + +
+ +