diff --git a/js/Readme.md b/js/Readme.md index 8b70b58..d3fadac 100644 --- a/js/Readme.md +++ b/js/Readme.md @@ -4,15 +4,42 @@ Pixelpass is a library which can do multiple things which are listed below, - Given a data → `generateQRCode` → returns a QR Code. -- Given a JSON String → `generateQRData` → Gives back CBOR encoded data. +- Given a JSON String → `generateQRData` → Gives back CBOR-encoded data. -- Given a CBOR encoded data as byte array → `decode` → Gives back JSON String. - +- Given a CBOR-encoded data as byte array → `decode` → Gives back JSON String. - Given data as byteArray → `decodeBinary` → Gives back JSON String. - -- Given a JSON and Mapper → `getMappedData` → Gives back CBOR encoded data. +- Given a JSON and Mapper → `getMappedData` → Gives back CBOR-encoded data or mapped JSON. -- Given a CBOR encoded data and Mapper → `decodeMappedData` → Gives back a JSON. +- Given a CBOR-encoded data and Mapper → `decodeMappedData` → Gives back a JSON. + +## 🚨 Breaking Changes (`0.8.0` and later) + +Starting from version `0.8.0`, the following APIs have undergone changes +to support advanced key/value compression and depth-aware decoding. + +These updates improve interoperability and reduce CBOR payload sizes. + +## 🔄 API Contract Changes + +### 1. `getMappedData` --- Signature Change + +#### Old (Deprecated) + +`getMappedData(jsonData, mapper, cborEnable?)` + +#### New (Recommended) + +`getMappedData(jsonData, keyMapper, valueMapper, cborEnable?)` + +### 2. `decodeMappedData` --- Signature Change + +#### Old (Deprecated) + +`decodeMappedData(data, mapper)` + +#### New (Recommended) + +`decodeMappedData(data, keyMapper, valueMapperFunction?)`. ## Features @@ -24,15 +51,18 @@ Pixelpass is a library which can do multiple things which are listed below, - When JSON and a Mapper is given, it maps the JSON with Mapper and then does the CBOR encode/decode which further reduces the size of the data. -## Usage +## Usage + `npm i @mosip/pixelpass` [npm](https://www.npmjs.com/package/@mosip/pixelpass) ## Example + Prerequisites -* [nodejs](https://nodejs.org/en/learn/getting-started/how-to-install-nodejs) -* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) + +- [nodejs](https://nodejs.org/en/learn/getting-started/how-to-install-nodejs) +- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) To run the example app copy the below command and paste it to your terminal. @@ -41,16 +71,17 @@ git clone https://github.com/mosip/pixelpass.git && cd pixelpass && git checkout ``` ## APIs + ### generateQRCode( data, ecc , header ) - - `data` - Data needs to be compressed and encoded. +- `data` - Data needs to be compressed and encoded. - - `ecc` - Error Correction Level for the QR generated. defaults to `"L"`. +- `ecc` - Error Correction Level for the QR generated. defaults to `"L"`. - - `header` - Data header need to be prepend to identify the encoded data. defaults to `""`. +- `header` - Data header need to be prepend to identify the encoded data. defaults to `""`. ```javascript -import { generateQRCode } from '@mosip/pixelpass'; +import { generateQRCode } from "@mosip/pixelpass"; const data = "Hello"; const qrCode = generateQRCode(data, ecc, header); @@ -58,6 +89,7 @@ const qrCode = generateQRCode(data, ecc, header); // ecc is Error Correction Level for the QR generated. defaults to "L". // header defaults to empty string if not passed. ``` + The `generateQRCode` takes a data, ECC (Error correction level) which when not passed defaults to L and header which defaults to empty string if not passed. Returns a base64 encoded PNG image. @@ -68,30 +100,32 @@ Returns a base64 encoded PNG image. - `header` - Data header need to be prepend to identify the encoded data. defaults to `""`. ```javascript -import { generateQRData } from '@mosip/pixelpass'; +import { generateQRData } from "@mosip/pixelpass"; -const jsonString = "{\"name\":\"Steve\",\"id\":\"1\",\"l_name\":\"jobs\"}"; +const jsonString = '{"name":"Steve","id":"1","l_name":"jobs"}'; const header = "jsonstring"; const encodedCBORData = generateQRData(jsonString, header); // header defaults to empty string if not passed. ``` + The `generateQRData` takes a valid JSON string and a header which when not passed defaults to an empty string. This API will return a base45 encoded string which is `Compressed > CBOR Encoded > Base45 Encoded`. - ### decode( data ) - `data` - Data needs to be decoded and decompressed without header. ```javascript -import { decode } from '@mosip/pixelpass'; +import { decode } from "@mosip/pixelpass"; -const b45EncodedData = "NCFWTL$PPB$PN$AWGAE%5UW5A%ADFAHR9 IE:GG6ZJJCL2.AJKAMHA100+8S.1"; +const b45EncodedData = + "NCFWTL$PPB$PN$AWGAE%5UW5A%ADFAHR9 IE:GG6ZJJCL2.AJKAMHA100+8S.1"; const jsonString = decode(b45EncodedData); ``` -The `decode` will take a `string` as parameter and gives us decoded JSON string which is Base45 `Decoded > CBOR Decoded > Decompressed`. + +The `decode` will take a `string` as parameter and gives us decoded JSON string which is Base45 `Decoded > CBOR Decoded > Decompressed`. ### decodeBinary( data ) @@ -103,52 +137,137 @@ import { decodeBinary } from '@mosip/pixelpass'; const zipdata = ; const decompressedData = decodeBinary(zipdata); ``` -The `decodeBinary` will take a `UInt8ByteArray` as parameter and gives us unzipped string. Currently only zip binary data is only supported. +The `decodeBinary` will take a `UInt8ByteArray` as parameter and gives us unzipped string. Currently only zip binary data is supported. -### getMappedData( jsonData, mapper, cborEnable ); +### getMappedData( jsonData, keyMapper, valueMapper, cborEnable ) -- `jsonData` - A JSON data. -- `mapper` - A Map which is used to map with the JSON. +- `jsonData` - A JSON data or an array of JSON data. +- `keyMapper` - A Map which is used to map the keys of the JSON. +- `valueMapper` - A Map which is used to map the values of the JSON. - `cborEnable` - A Boolean which is used to enable or disable CBOR encoding on mapped data. Defaults to `false` if not provided. ```javascript -import { getMappedData } from '@mosip/pixelpass'; +import { getMappedData } from "@mosip/pixelpass"; -const jsonData = {"name": "Jhon", "id": "207", "l_name": "Honay"}; -const mapper = {"id": "1", "name": "2", "l_name": "3"}; +const jsonData = { name: "Jhon", id: "207", l_name: "Honay" }; +const keyMapper = { id: "1", name: "2", l_name: "3" }; +const valueMapper = {}; // Optional value mapping -const byteBuffer = getMappedData(jsonData, mapper,true); +const result = getMappedData(jsonData, keyMapper, valueMapper, true); -const cborEncodedString = byteBuffer.toString('hex'); +// If cborEnable is true, result is a hex string of CBOR-encoded data +const cborEncodedString = result; + +// If cborEnable is false, result is the mapped JSON object +const mappedJson = getMappedData(jsonData, keyMapper, valueMapper, false); ``` -The `getMappedData` takes 3 arguments a JSON and a map with which we will be creating a new map with keys and values mapped based on the mapper. The third parameter is an optional value to enable or disable CBOR encoding on the mapped data. + +The `getMappedData` takes 4 arguments: + +1. A JSON object or array of JSON objects +2. A key mapper to map JSON keys +3. A value mapper to map JSON values (can be empty object if no value mapping needed) +4. An optional boolean to enable or disable CBOR encoding on the mapped data (defaults to `false`) + The example of a converted map would look like, `{ "1": "207", "2": "Jhon", "3": "Honay"}` -### decodeMappedData( data, mapper ) +When `cborEnable` is `true`, the function returns a hex string of the CBOR-encoded mapped data. +When `cborEnable` is `false`, the function returns the mapped JSON object directly. + +**⚠️ DEPRECATION NOTICE**: The previous 3-argument signature `getMappedData(jsonData, mapper, cborEnable)` is deprecated. Please use the new 4-argument signature with separate `keyMapper` and `valueMapper` parameters. -- `data` - A CBOREncoded string or a mapped JSON. -- `mapper` - A Map which is used to map with the JSON. +### decodeMappedData( data, keyMapper, valueMapper ) + +- `data` - A CBOR-encoded hex string, a mapped JSON string, or an array of either. +- `keyMapper` - An array of mapper objects for depth-aware key decoding. Each mapper object handles keys at a specific depth level. +- `valueMapper` - A function to transform values in the decoded JSON. Optional. ```javascript -import { decodeMappedData } from '@mosip/pixelpass'; +import { decodeMappedData } from "@mosip/pixelpass"; const cborEncodedString = "a302644a686f6e01633230370365486f6e6179"; -const mapper = {"1": "id", "2": "name", "3": "l_name"}; +const keyMapper = [ + { 1: "id", 2: "name", 3: "l_name" }, // Mapper for depth 0 +]; +const valueMapper = (jsonData) => { + // Optional: transform values if needed + return jsonData; +}; + +const jsonString = decodeMappedData(cborEncodedString, keyMapper, valueMapper); +const jsonData = JSON.parse(jsonString); +``` + +The `decodeMappedData` takes 3 arguments: + +1. A CBOR-encoded hex string, a JSON string, or an array of either +2. An array of mapper objects for depth-aware key decoding (each index corresponds to a depth level) +3. An optional value mapper function to transform the decoded data + +The function will: + +- First attempt to decode the data as CBOR (if it's a hex string) +- If CBOR decoding fails, it will try to parse it as JSON +- Apply key mapping at each depth level using the provided keyMapper array +- Apply value transformation using the valueMapper function (if provided) +- Return a JSON string representation of the decoded and mapped data + +Example with nested objects: + +```javascript +const keyMapper = [ + { 1: "id", 2: "name" }, // Maps keys at depth 0 + { a: "street", b: "city" }, // Maps keys at depth 1 (nested objects) +]; + +const result = decodeMappedData(cborEncodedData, keyMapper); +``` +The function also supports arrays of data: + +```javascript +const dataArray = [encodedData1, encodedData2, encodedData3]; +const results = decodeMappedData(dataArray, keyMapper, valueMapper); +// Returns an array of decoded JSON strings +``` + +**⚠️ BREAKING CHANGE**: The signature has been updated from the previous 2-argument version `decodeMappedData(data, mapper)` to the new 3-argument version `decodeMappedData(data, keyMapper, valueMapper)`. The `keyMapper` is now expected to be an array of mapper objects for depth-aware decoding, and an optional `valueMapper` function can be provided for value transformations. + +#### Migration Guide from Old to New API + +**Old API (Deprecated):** + +```javascript +const mapper = { 1: "id", 2: "name", 3: "l_name" }; const jsonData = decodeMappedData(cborEncodedString, mapper); ``` -The `decodeMappedData` takes 2 arguments a string which is CBOR Encoded or a mapped JSON and a map with which we will be creating a JSON by mapping the keys and values. If the data provided is CBOR encoded string the API will do a CBOR decode first ad then proceed with re-mapping the data. -The example of the returned JSON would look like, `{"name": "Jhon", "id": "207", "l_name": "Honay"}` +**New API:** + +```javascript +const keyMapper = [ + { 1: "id", 2: "name", 3: "l_name" }, // Wrap mapper in array +]; +const valueMapper = null; // or a transformation function +const jsonString = decodeMappedData(cborEncodedString, keyMapper, valueMapper); +const jsonData = JSON.parse(jsonString); // Parse the returned JSON string +``` + +Key differences: +- `keyMapper` must now be an array (for depth-aware mapping) +- `valueMapper` is a new optional parameter for value transformations +- Returns a JSON string instead of an object (needs `JSON.parse()`) +- Supports arrays of data for batch processing ## Errors / Exceptions + - `Cannot read properties of null (reading 'length')` - thrown when the string passed to encode is null. - `Cannot read properties of undefined (reading 'length')` - thrown when the string passed to encode is undefined. -- `byteArrayArg is null or undefined.` - thrown when the string passed to encode is null or undefined. +- `byteArrayArg is null or undefined.` - thrown when the string passed to encode is null or undefined. - `utf8StringArg is null or undefined.` - thrown when the string passed to decode is null or undefined. @@ -158,6 +277,7 @@ The example of the returned JSON would look like, `{"name": "Jhon", "id": "207", - `incorrect data check` - thrown when the string passed to decode is invalid. +- `jsonData must not be null or undefined` - thrown when null or undefined is passed to `getMappedData`. ## License MPL-2.0 diff --git a/js/package-lock.json b/js/package-lock.json index f6ca70d..f7a89dc 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -1,12 +1,12 @@ { "name": "@mosip/pixelpass", - "version": "0.7.0", + "version": "0.8.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@mosip/pixelpass", - "version": "0.7.0", + "version": "0.8.0", "license": "MPL-2.0", "dependencies": { "base45-web": "^1.0.2", @@ -5591,8 +5591,7 @@ "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "requires": {} + "dev": true }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", @@ -7203,8 +7202,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", - "dev": true, - "requires": {} + "dev": true }, "deepmerge": { "version": "4.3.1", @@ -7838,8 +7836,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "requires": {} + "dev": true }, "jest-regex-util": { "version": "29.6.3", diff --git a/js/package.json b/js/package.json index 18badeb..3e284c3 100644 --- a/js/package.json +++ b/js/package.json @@ -1,10 +1,10 @@ { "name": "@mosip/pixelpass", - "version": "0.7.0", - "repository": "https://github.com/mosip/pixelpass", + "version": "0.8.0", + "repository": "https://github.com/inji/pixelpass", "author": "MOSIP", "license": "MPL-2.0", - "homepage": "https://github.com/mosip/pixelpass", + "homepage": "https://github.com/inji/pixelpass", "description": "JS module that can be used to compress (zlib) , encode (base45) and generate QR out of given any data. Also can be used to do the operation vice versa", "main": "src/index.js", "scripts": { diff --git a/js/src/index.js b/js/src/index.js index 7ee15a6..47a16f9 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -1,116 +1,192 @@ const { - DEFAULT_QR_QUALITY, - DEFAULT_QR_BORDER, - DEFAULT_QR_SCALE, - COLOR_BLACK, - COLOR_WHITE, - DEFAULT_ZLIB_COMPRESSION_LEVEL, - DEFAULT_ECC_LEVEL, - ZIP_HEADER, - DEFAULT_ZIP_FILE_NAME -} = require('./shared/Constants'); -const QRCode = require('qrcode'); + DEFAULT_QR_QUALITY, + DEFAULT_QR_BORDER, + DEFAULT_QR_SCALE, + COLOR_BLACK, + COLOR_WHITE, + DEFAULT_ZLIB_COMPRESSION_LEVEL, + DEFAULT_ECC_LEVEL, + ZIP_HEADER, + DEFAULT_ZIP_FILE_NAME, + CLAIM_169_KEY_MAPPER, + CLAIM_169_VALUE_MAPPER, + CLAIM_169_REVERSE_KEY_MAPPER, +} = require("./shared/Constants"); +const QRCode = require("qrcode"); const b45 = require("base45-web"); const pako = require("pako"); const cbor = require("cbor-web"); const JSZip = require("jszip"); +const { + translateToJson, + replaceKeysAtDepth, + replaceValuesForClaim169, + decodeFromBase64UrlFormat, +} = require("./utils/cborUtils.js"); +const { toMapWithKeyAndValueMapper } = require("./utils/mapperUtils.js"); + +function toJson(base64UrlEncodedCborEncodedString) { + if (typeof base64UrlEncodedCborEncodedString !== "string") { + throw new TypeError("Expected base64url-encoded CBOR string"); + } + try { + const decodedData = decodeFromBase64UrlFormat( + base64UrlEncodedCborEncodedString + ); + const cborDecoded = cbor.decodeFirstSync(decodedData); + return translateToJson(cborDecoded); + } catch (error) { + throw new Error(`Failed to decode CBOR data: ${error.message}`); + } +} function generateQRData(data, header = "") { - let parsedData = null; - let compressedData, b45EncodedData; - try { - parsedData = JSON.parse(data); - const cborEncodedData = cbor.encode(parsedData); - compressedData = pako.deflate(cborEncodedData, {level: DEFAULT_ZLIB_COMPRESSION_LEVEL}); - } catch (e) { - console.error("Data is not JSON"); - compressedData = pako.deflate(data, {level: DEFAULT_ZLIB_COMPRESSION_LEVEL}); - } finally { - b45EncodedData = b45.encode(compressedData).toString(); - } - return header + b45EncodedData; + let parsedData = null; + let compressedData, b45EncodedData; + try { + parsedData = JSON.parse(data); + const cborEncodedData = cbor.encode(parsedData); + compressedData = pako.deflate(cborEncodedData, { + level: DEFAULT_ZLIB_COMPRESSION_LEVEL, + }); + } catch (e) { + console.error("Data is not JSON"); + compressedData = pako.deflate(data, { + level: DEFAULT_ZLIB_COMPRESSION_LEVEL, + }); + } finally { + b45EncodedData = b45.encode(compressedData).toString(); + } + return header + b45EncodedData; } + async function generateQRCode(data, ecc = DEFAULT_ECC_LEVEL, header = "") { - const base45Data = generateQRData(data, header); - const opts = { - errorCorrectionLevel: ecc, - quality: DEFAULT_QR_QUALITY, - margin: DEFAULT_QR_BORDER, - scale: DEFAULT_QR_SCALE, - color: { - dark: COLOR_BLACK, - light: COLOR_WHITE, - }, - }; - return QRCode.toDataURL(base45Data, opts); + const base45Data = generateQRData(data, header); + const opts = { + errorCorrectionLevel: ecc, + quality: DEFAULT_QR_QUALITY, + margin: DEFAULT_QR_BORDER, + scale: DEFAULT_QR_SCALE, + color: { + dark: COLOR_BLACK, + light: COLOR_WHITE, + }, + }; + return QRCode.toDataURL(base45Data, opts); } function decode(data) { - const decodedBase45Data = b45.decode(data); - const decompressedData = pako.inflate(decodedBase45Data); - const textData = new TextDecoder().decode(decompressedData); - try { - const decodedCBORData = cbor.decode(decompressedData); - if (decodedCBORData) return JSON.stringify(decodedCBORData); - return textData; - } catch (e) { - return textData; - } + const decodedBase45Data = b45.decode(data); + // Base45 returns number[], convert it + const binaryData = Uint8Array.from(decodedBase45Data); + const decompressedData = pako.inflate(binaryData); + const textData = new TextDecoder().decode(decompressedData); + try { + const decodedCBORData = cbor.decodeFirstSync(decompressedData); + if (decodedCBORData) return JSON.stringify(decodedCBORData); + return textData; + } catch (e) { + return textData; + } } async function decodeBinary(data) { - let decodedData = new TextDecoder("utf-8").decode(data); - if (decodedData.startsWith(ZIP_HEADER)){ - return (await JSZip.loadAsync(decodedData)).file(DEFAULT_ZIP_FILE_NAME).async("text") - }else { - throw new Error("Unsupported binary file type"); + let decodedData = new TextDecoder("utf-8").decode(data); + if (decodedData.startsWith(ZIP_HEADER)) { + const zip = await JSZip.loadAsync(decodedData); + const file = zip.file(DEFAULT_ZIP_FILE_NAME); + if (!file) { + throw new Error( + `File '${DEFAULT_ZIP_FILE_NAME}' not found in ZIP archive` + ); } + return file.async("text"); + } else { + throw new Error("Unsupported binary file type"); + } } -function getMappedData(jsonData, mapper, cborEnable = false) { - const payload ={}; - for (const param in jsonData) { - const key = mapper[param] ? mapper[param] : param; - payload[key]= jsonData[param]; - } - if (cborEnable) - return cbor.encode(payload); - else - return payload +function getMappedData( + jsonData, + keyMapper = CLAIM_169_KEY_MAPPER, + valueMapper = CLAIM_169_VALUE_MAPPER, + cborEnable = false +) { + if (jsonData == null) { + throw new TypeError("jsonData must not be null or undefined"); + } + + if (Array.isArray(jsonData)) { + return jsonData.map((item) => + getMappedData(item, keyMapper, valueMapper, cborEnable) + ); + } + + const payload = toMapWithKeyAndValueMapper(jsonData, keyMapper, valueMapper); + + if (cborEnable) { + return Buffer.from(cbor.encode(payload)).toString("hex"); + } + + return payload; } -function decodeMappedData(data, mapper) { +function decodeMappedData( + data, + keyMapper = CLAIM_169_REVERSE_KEY_MAPPER, + valueMapper = replaceValuesForClaim169 +) { + if (data == null) { + throw new TypeError("data must not be null or undefined"); + } + + if (Array.isArray(data)) { + return data.map((item) => { + return decodeMappedData(item, keyMapper, valueMapper); + }); + } + + let jsonData; + + try { + const bytes = Buffer.from(data, "hex"); + const decoded = cbor.decodeFirstSync(bytes); + jsonData = translateToJson(decoded); + } catch (error) { try { - const jsonData = cbor.decode(data) - return translateToJSON(jsonData, mapper) - }catch (e) { - return translateToJSON(data,mapper) + jsonData = JSON.parse(data); + } catch (parseError) { + throw new Error(`Failed to decode data as CBOR or JSON: ${parseError.message}`); } + } -} - -function translateToJSON(claims, mapper) { - const result = {} - if (claims instanceof Map) { - claims.forEach((value, param) => { - const key = mapper[param] ? mapper[param] : param; - result[key] = value; - }); - } else if (typeof claims === 'object' && claims !== null) { - Object.entries(claims).forEach(([param, value]) => { - const key = mapper[param] ? mapper[param] : param; - result[key] = value; - }); + if (keyMapper) { + if (!Array.isArray(keyMapper)) { + throw new TypeError( + "keyMapper must be an array of mapper objects for depth-aware decoding" + ); } - return result; -} + keyMapper.forEach((mapper, index) => { + if (mapper && typeof mapper === "object") { + jsonData = replaceKeysAtDepth(jsonData, mapper, index); + } + }); + } + + if (valueMapper && typeof valueMapper === "function") { + jsonData = valueMapper(jsonData); + } + + return JSON.stringify(jsonData); +} module.exports = { - generateQRData, - generateQRCode, - decode, - getMappedData, - decodeMappedData, - decodeBinary + toJson, + generateQRData, + generateQRCode, + decode, + decodeBinary, + getMappedData, + decodeMappedData, }; diff --git a/js/src/shared/Constants.js b/js/src/shared/Constants.js index a77c4d1..7cf3274 100644 --- a/js/src/shared/Constants.js +++ b/js/src/shared/Constants.js @@ -8,4 +8,185 @@ exports.DEFAULT_QR_SCALE = 10 exports.DEFAULT_QR_BORDER = 3 exports.DEFAULT_QR_QUALITY = 1 exports.ZIP_HEADER = "PK" -exports.DEFAULT_ZIP_FILE_NAME = "certificate.json" \ No newline at end of file +exports.DEFAULT_ZIP_FILE_NAME = "certificate.json" + +exports.CLAIM_169_KEY_MAPPER = { + "ID": 1, + "Version": 2, + "Language": 3, + "Full Name": 4, + "First Name": 5, + "Middle Name": 6, + "Last Name": 7, + "Date of Birth": 8, + "Gender": 9, + "Address": 10, + "Email ID": 11, + "Phone Number": 12, + "Nationality": 13, + "Marital Status": 14, + "Guardian": 15, + "Binary Image": 16, + "Binary Image Format": 17, + "Best Quality Fingers": 18, + + "Right Thumb": 50, + "Right Pointer Finger": 51, + "Right Middle Finger": 52, + "Right Ring Finger": 53, + "Right Little Finger": 54, + "Left Thumb": 55, + "Left Pointer Finger": 56, + "Left Middle Finger": 57, + "Left Ring Finger": 58, + "Left Little Finger": 59, + "Right Iris": 60, + "Left Iris": 61, + "Face": 62, + "Right Palm Print": 63, + "Left Palm Print": 64, + "Voice": 65, + + "Data": 0, + "Data format": 1, + "Data sub format": 2, + "Data issuer": 3 +}; + +exports.CLAIM_169_VALUE_MAPPER = { + "Data format": { + "Image": 0, + "Template": 1, + "Sound": 2, + "Bio Hash": 3 + }, + "Data sub format": { + "PNG": 0, + "JPEG": 1, + "JPEG2000": 2, + "AVIF": 3, + "WEBP": 4, + "TIFF": 5, + "WSQ": 6, + + "Fingerprint Template ANSI 378": 0, + "Fingerprint Template ISO 19794-2": 1, + "Fingerprint Template NIST": 2, + + "WAV": 0, + "MP3": 1 + }, + "Gender": { + "Male": 1, + "Female": 2, + "Others": 3 + }, + "Marital Status": { + "Unmarried": 1, + "Married": 2, + "Divorced": 3 + }, + "Binary Image Format": { + "JPEG": 1, + "JPEG2": 2, + "AVIF": 3, + "WEBP": 4 + } +}; + +exports.CLAIM_169_REVERSE_KEY_MAPPER = [ + { + "1": "ID", + "2": "Version", + "3": "Language", + "4": "Full Name", + "5": "First Name", + "6": "Middle Name", + "7": "Last Name", + "8": "Date of Birth", + "9": "Gender", + "10": "Address", + "11": "Email ID", + "12": "Phone Number", + "13": "Nationality", + "14": "Marital Status", + "15": "Guardian", + "16": "Binary Image", + "17": "Binary Image Format", + "18": "Best Quality Fingers", + + "50": "Right Thumb", + "51": "Right Pointer Finger", + "52": "Right Middle Finger", + "53": "Right Ring Finger", + "54": "Right Little Finger", + "55": "Left Thumb", + "56": "Left Pointer Finger", + "57": "Left Middle Finger", + "58": "Left Ring Finger", + "59": "Left Little Finger", + "60": "Right Iris", + "61": "Left Iris", + "62": "Face", + "63": "Right Palm Print", + "64": "Left Palm Print", + "65": "Voice" + }, + { + "0": "Data", + "1": "Data format", + "2": "Data sub format", + "3": "Data issuer" + } +]; + +exports.CLAIM_169_ROOT_REVERSE_VALUE_MAPPER = { + "Data format": { 0: "Image", 1: "Template", 2: "Sound", 3: "Bio Hash" }, + "Gender": { 1: "Male", 2: "Female", 3: "Others" }, + "Binary Image Format": { 1: "JPEG", 2: "JPEG2", 3: "AVIF", 4: "WEBP" }, + "Marital Status": { 1: "Unmarried", 2: "Married", 3: "Divorced" } +}; + +exports.CLAIM_169_BIOMETRIC_FORMAT_REVERSE_VALUE_MAPPER = { + 0: "Image", + 1: "Template", + 2: "Sound", + 3: "Bio Hash" +}; + +exports.CLAIM_169_BIOMETRIC_SUB_FORMAT_REVERSE_VALUE_MAPPER = { + "Image": { + 0: "PNG", 1: "JPEG", 2: "JPEG2000", 3: "AVIF", 4: "WEBP", 5: "TIFF", 6: "WSQ" + }, + "Template": { + 0: "Fingerprint Template ANSI 378", + 1: "Fingerprint Template ISO 19794-2", + 2: "Fingerprint Template NIST" + }, + "Sound": { + 0: "WAV", + 1: "MP3" + } +}; + +exports.CLAIM_169_BIOMETRIC_KEYS = [ + "Right Thumb", + "Right Pointer Finger", + "Right Middle Finger", + "Right Ring Finger", + "Right Little Finger", + "Left Thumb", + "Left Pointer Finger", + "Left Middle Finger", + "Left Ring Finger", + "Left Little Finger", + "Right Iris", + "Left Iris", + "Face", + "Right Palm Print", + "Left Palm Print", + "Voice" +]; + +exports.CLAIM_169_BIOMETRIC_DATA_FORMAT_KEY = "Data format"; +exports.CLAIM_169_BIOMETRIC_DATA_SUB_FORMAT_KEY = "Data sub format"; \ No newline at end of file diff --git a/js/src/utils/cborUtils.js b/js/src/utils/cborUtils.js new file mode 100644 index 0000000..f9f9c31 --- /dev/null +++ b/js/src/utils/cborUtils.js @@ -0,0 +1,178 @@ +const { + CLAIM_169_BIOMETRIC_KEYS, + CLAIM_169_BIOMETRIC_DATA_FORMAT_KEY, + CLAIM_169_BIOMETRIC_DATA_SUB_FORMAT_KEY, + CLAIM_169_BIOMETRIC_FORMAT_REVERSE_VALUE_MAPPER, + CLAIM_169_BIOMETRIC_SUB_FORMAT_REVERSE_VALUE_MAPPER, + CLAIM_169_ROOT_REVERSE_VALUE_MAPPER, +} = require("../shared/Constants"); + +function translateToJson(value) { + if (value instanceof Map) { + const data = {}; + value.forEach((mapValue, mapKey) => { + data[mapKey] = translateToJson(mapValue); + }); + return data; + } + + if (Array.isArray(value)) { + return value.map(translateToJson); + } + + if (typeof value === "object" && value !== null) { + const data = {}; + for (const [key, val] of Object.entries(value)) { + data[key] = translateToJson(val); + } + return data; + } + + return value; +} + +function replaceKeysAtDepth(obj, mapper, depth, currentDepth = 0) { + if (Array.isArray(obj)) { + return replaceKeysInArrayAtDepth(obj, mapper, depth, currentDepth); + } + + if (typeof obj !== "object" || obj === null) { + return obj; + } + + const result = {}; + const nextDepth = currentDepth + 1; + + for (const [originalKey, value] of Object.entries(obj)) { + const newKey = + currentDepth === depth + ? mapper[originalKey] ?? mapper[Number(originalKey)] ?? originalKey + : originalKey; + + let processedValue; + if (Array.isArray(value)) { + processedValue = replaceKeysInArrayAtDepth( + value, + mapper, + depth, + nextDepth + ); + } else if (typeof value === "object" && value !== null) { + processedValue = replaceKeysAtDepth(value, mapper, depth, nextDepth); + } else if (value === null) { + processedValue = null; + } else { + processedValue = value; + } + + result[newKey] = processedValue; + } + + return result; +} + +function replaceKeysInArrayAtDepth(arr, mapper, depth, currentDepth = 0) { + return arr.map((item) => { + if (Array.isArray(item)) { + return replaceKeysInArrayAtDepth(item, mapper, depth, currentDepth); + } else if (typeof item === "object" && item !== null) { + return replaceKeysAtDepth(item, mapper, depth, currentDepth); + } else { + return item; + } + }); +} + +function replaceValuesForClaim169(jsonData) { + const result = { ...jsonData }; + + Object.entries(CLAIM_169_ROOT_REVERSE_VALUE_MAPPER).forEach( + ([fieldName, reverseMap]) => { + if (result[fieldName] !== undefined) { + const originalValue = result[fieldName]; + const mappedValue = reverseMap[originalValue]; + if (mappedValue !== undefined) { + result[fieldName] = mappedValue; + } + } + } + ); + + CLAIM_169_BIOMETRIC_KEYS.forEach((nestedKey) => { + if (result[nestedKey] === undefined) return; + + const nestedObject = { ...result[nestedKey] }; + + if ( + typeof nestedObject !== "object" || + nestedObject === null || + Array.isArray(nestedObject) + ) { + return; + } + + const formatKey = CLAIM_169_BIOMETRIC_DATA_FORMAT_KEY; + const subFormatKey = CLAIM_169_BIOMETRIC_DATA_SUB_FORMAT_KEY; + + if ( + nestedObject[formatKey] === undefined || + nestedObject[subFormatKey] === undefined + ) { + return; + } + + const dataFormatShortCode = nestedObject[formatKey]; + const subFormatShortCode = nestedObject[subFormatKey]; + + const dataFormatInt = + typeof dataFormatShortCode === "number" + ? dataFormatShortCode + : typeof dataFormatShortCode === "string" + ? parseInt(dataFormatShortCode, 10) + : null; + + const subFormatInt = + typeof subFormatShortCode === "number" + ? subFormatShortCode + : typeof subFormatShortCode === "string" + ? parseInt(subFormatShortCode, 10) + : null; + + if (dataFormatInt === null || Number.isNaN(dataFormatInt)) return; + + if (subFormatInt !== null && Number.isNaN(subFormatInt)) return; + + const dataFormatValue = + CLAIM_169_BIOMETRIC_FORMAT_REVERSE_VALUE_MAPPER[dataFormatInt]; + + const subFormatValue = + dataFormatValue !== undefined && subFormatInt !== null + ? CLAIM_169_BIOMETRIC_SUB_FORMAT_REVERSE_VALUE_MAPPER?.[ + dataFormatValue + ]?.[subFormatInt] + : undefined; + + if (dataFormatValue !== undefined) { + nestedObject[formatKey] = dataFormatValue; + } + + if (subFormatValue !== undefined) { + nestedObject[subFormatKey] = subFormatValue; + } + + result[nestedKey] = nestedObject; + }); + + return result; +} + +function decodeFromBase64UrlFormat(content) { + return Buffer.from(content, 'base64url'); +} + +module.exports = { + translateToJson, + replaceKeysAtDepth, + replaceValuesForClaim169, + decodeFromBase64UrlFormat +}; diff --git a/js/src/utils/mapperUtils.js b/js/src/utils/mapperUtils.js new file mode 100644 index 0000000..b61bd2c --- /dev/null +++ b/js/src/utils/mapperUtils.js @@ -0,0 +1,112 @@ +function normalizeMappers(keyMapper = {}, valueMapper = {}) { + return { + normalizedKeyMapper: Object.fromEntries( + Object.entries(keyMapper).map(([k, v]) => [k.toLowerCase(), v]) + ), + normalizedValueMapper: Object.fromEntries( + Object.entries(valueMapper).map(([field, mappings]) => [ + typeof field === "string" && field.toLowerCase(), + Object.fromEntries( + Object.entries(mappings).map(([src, mapped]) => [ + String(src).toLowerCase(), + mapped, + ]) + ), + ]) + ), + }; +} + +function toMapWithKeyAndValueMapper(data, keyMapper, valueMapper) { + if (data === null || data === undefined) { + return data; + } + + if (Array.isArray(data)) { + return toListWithKeyAndValueMapper(data, keyMapper, valueMapper); + } + + if (typeof data !== "object") { + return data; + } + + const result = {}; + + const normalizedMappers = normalizeMappers(keyMapper, valueMapper); + + const { normalizedKeyMapper, normalizedValueMapper } = normalizedMappers; + + for (const [originalKey, originalValue] of Object.entries(data)) { + const normalizedKey = originalKey.toLowerCase(); + const mappedKey = normalizedKeyMapper[normalizedKey] ?? originalKey; + + let processedValue = originalValue; + + const fieldValueMapper = normalizedValueMapper[normalizedKey]; + + if ( + originalValue !== null && + typeof originalValue === "string" && + fieldValueMapper && + fieldValueMapper[originalValue.toLowerCase()] !== undefined + ) { + processedValue = fieldValueMapper[originalValue.toLowerCase()]; + } + + if (processedValue === null) { + result[mappedKey] = null; + } else if (Array.isArray(processedValue)) { + result[mappedKey] = toListWithKeyAndValueMapper( + processedValue, + keyMapper, + valueMapper + ); + } else if (typeof processedValue === "object") { + result[mappedKey] = toMapWithKeyAndValueMapper( + processedValue, + keyMapper, + valueMapper + ); + } else { + result[mappedKey] = processedValue; + } + } + + return result; +} + +function toListWithKeyAndValueMapper(arr, keyMapper, valueMapper) { + const result = []; + + for (let i = 0; i < arr.length; i++) { + const value = arr[i]; + let processedValue; + + if (value === null) { + processedValue = null; + } else if (typeof value === "object" && !Array.isArray(value)) { + processedValue = toMapWithKeyAndValueMapper( + value, + keyMapper, + valueMapper + ); + } else if (Array.isArray(value)) { + processedValue = toListWithKeyAndValueMapper( + value, + keyMapper, + valueMapper + ); + } else { + processedValue = value; + } + + result.push(processedValue); + } + + return result; +} + +module.exports = { + toMapWithKeyAndValueMapper, + toListWithKeyAndValueMapper, +}; diff --git a/js/test/PixelPass.test.js b/js/test/PixelPass.test.js deleted file mode 100644 index 1239820..0000000 --- a/js/test/PixelPass.test.js +++ /dev/null @@ -1,231 +0,0 @@ -const {decode, decodeBinary, generateQRCode, generateQRData, getMappedData, decodeMappedData} = require("../src"); -const {expect} = require("expect"); -const {ECC} = require("../src/types/ECC"); -const JSZip = require("jszip"); - -const HEX_ENCODING = "hex"; - - -test("should return decoded data for given QR data", () => { - const data = "NCFKVPV0QSIP600GP5L0"; - const expected = "hello"; - - const actual = decode(data); - expect(actual).toBe(expected); -}); - -test("should return decoded data for given QR data for zipped data", async () => { - const expected = "Hello World!!"; - const zip = new JSZip(); - zip.file("certificate.json", expected, { - compression: "DEFLATE" - }); - const data = await zip.generateAsync({type: 'string', compression: "DEFLATE"}) - - const actual = await decodeBinary(new TextEncoder().encode(data)); - expect(actual).toBe(expected); -},5000); - - -test("should throw error if binary data type not zip", async () => { - await expect(decodeBinary(new TextEncoder().encode("tempfile"))).rejects.toThrowError("Unsupported binary file type") -}); - - -test("should return decoded data for given QR data in cbor", async () => { - const data = "NCF3QBXJA5NJRCOC004 QN4"; - const expected = "{\"temp\":15}"; - const actual = decode(data); - expect(actual).toBe(expected); -}); -test("should throw error if given data is undefined for encoding", () => { - expect(() => generateQRData(undefined)).toThrowError( - "byteArrayArg is null or undefined." - ); -}); -test("should throw error if given data length is bad", () => { - expect(() => decode("1")).toThrowError("utf8StringArg has incorrect length."); - expect(() => decode("1234")).toThrowError( - "utf8StringArg has incorrect length." - ); -}); -test("should throw error if given data is invalid", () => { - expect(() => decode("^1")).toThrowError("Invalid character at position 0."); - expect(() => decode("1^")).toThrowError("Invalid character at position 1."); - expect(() => decode("0123456789^")).toThrowError( - "Invalid character at position 10." - ); -}); - -test("should return encoded QR data for data", () => { - const expected = "NCFKVPV0QSIP600GP5L0"; - const data = "hello"; - - const actual = generateQRData(data); - expect(actual).toBe(expected); -}); - -test("should return encoded QR data for given data with cbor", () => { - const expected = "NCF3QBXJA5NJRCOC004 QN4"; - const data = '{"temp":15}'; - const actual = generateQRData(data); - expect(actual).toBe(expected); -}); -test("should return encoded QR data for data with header", () => { - const expected = "mockHeader://" + "NCFKVPV0QSIP600GP5L0"; - const data = "hello"; - - const actual = generateQRData(data, "mockHeader://"); - expect(actual).toBe(expected); -}); -test("should return base64 encoded QR for given data", async () => { - const data = "hello"; - const expected = - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQ4AAAEOCAYAAAB4sfmlAAAAAklEQVR4AewaftIAAAUpSURBVO3BQY5bCw4EsCrB97+yJvvePH14YKdDsvtHAA4mAEcTgKMJwNEE4GgCcDQBOJoAHE0AjiYARxOAownA0QTgaAJwNAE4mgAcTQCOJgBHE4CjCcDRBOBoAnA0ATiaABxNAI4mAEcTgKMJwNEE4GgCcPTKh7QNP+1uPqFtntjdPNE2T+xunmgbftrdfMIE4GgCcDQBOJoAHE0AjiYARxOAownA0QTg6JUvt7v5DdrmndrmnXY3v8Hu5jdom282ATiaABxNAI4mAEcTgKMJwNEE4GgCcDQBOHrll2ibT9jdfMLu5p3a5p12N9+sbT5hd/MbTACOJgBHE4CjCcDRBOBoAnA0ATiaABxNAI5e4Vdrmyd2N+/UNk/sbvj7TACOJgBHE4CjCcDRBOBoAnA0ATiaABxNAI5e4a/UNu/UNk/sbmACcDQBOJoAHE0AjiYARxOAownA0QTgaAJw9Movsbv5l+xuPqFtntjdfLPdDf/dBOBoAnA0ATiaABxNAI4mAEcTgKMJwNEE4OiVL9c2/NQ2T+xunmibJ3Y3T7TNE7ubd2ob/v8mAEcTgKMJwNEE4GgCcDQBOJoAHE0AjiYAR90/wj+vbT5hd8PfZwJwNAE4mgAcTQCOJgBHE4CjCcDRBOBoAnD0yoe0zRO7myfa5ondzRNt88Tu5om2eWJ38812N0+0zTdrmyd2N+/UNk/sbr7ZBOBoAnA0ATiaABxNAI4mAEcTgKMJwNEE4Kj7Rz6gbd5pd/NE2zyxu/mEtnlid/NE2zyxu3mibf4lu5tPaJsndjefMAE4mgAcTQCOJgBHE4CjCcDRBOBoAnA0ATh65cvtbp5om09om09omyd2N0+0zSfsbp5om09oG36aABxNAI4mAEcTgKMJwNEE4GgCcDQBOJoAHHX/CD+0zTvtbp5omyd2N9+sbZ7Y3bxT27zT7oafJgBHE4CjCcDRBOBoAnA0ATiaABxNAI4mAEev/GPa5ondzTu1zTu1zRO7m3dqm99gd/PN2uaJ3c0nTACOJgBHE4CjCcDRBOBoAnA0ATiaABxNAI66f4S/Ttv8Brubd2qbd9rdPNE277S7+WYTgKMJwNEE4GgCcDQBOJoAHE0AjiYARxOAo+4f+YC24afdzSe0zRO7m09omyd2N+/UNk/sbp5om3fa3XzCBOBoAnA0ATiaABxNAI4mAEcTgKMJwNEE4OiVL7e7+Q3a5hPa5p3a5hN2N0+0zRO7myd2N/w0ATiaABxNAI4mAEcTgKMJwNEE4GgCcDQBOHrll2ibT9jdfELbPLG7eae24b/b3fwGE4CjCcDRBOBoAnA0ATiaABxNAI4mAEcTgKNX+Cvtbp5omyd2N++0u3mibZ5omyd2N+/UNk/sbp5omyd2N99sAnA0ATiaABxNAI4mAEcTgKMJwNEE4GgCcPQKf6W2eae2eae2eWJ380Tb/Aa7myfa5ondzSdMAI4mAEcTgKMJwNEE4GgCcDQBOJoAHE0Ajl75JXY3/5LdzSe0zRO7myfa5ondzTu1zSe0zW8wATiaABxNAI4mAEcTgKMJwNEE4GgCcDQBOHrly7UNP7XNJ+xu3ml380TbPLG7eWJ38wm7myfa5ptNAI4mAEcTgKMJwNEE4GgCcDQBOJoAHE0Ajrp/BOBgAnA0ATiaABxNAI4mAEcTgKMJwNEE4GgCcDQBOJoAHE0AjiYARxOAownA0QTgaAJwNAE4mgAcTQCOJgBHE4CjCcDRBOBoAnA0ATiaABxNAI4mAEf/A4+rCzKLi4oKAAAAAElFTkSuQmCC"; - - const actual = await generateQRCode(data, ECC.M); - expect(actual).toBe(expected); -},5000); -test("should return base64 encoded QR for given data with header", async () => { - const data = "hello"; - const header = "mockHeader://"; - const expected = - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAV4AAAFeCAYAAADNK3caAAAAAklEQVR4AewaftIAAAi9SURBVO3BQZIkB44EMHda/f/LXN37EiuLYaaqAXT/EQDOTAA4NQHg1ASAUxMATk0AODUB4NQEgFMTAE5NADg1AeDUBIBTEwBOTQA4NQHg1ASAUxMATk0AODUB4NQEgFMTAE5NADg1AeDUBIBTEwBOTQA4NQHg1ASAUxMATk0AOPWTD2kb/rS7eaJtvtnu5hPa5k27mze1zRO7mze1DX/a3XzCBIBTEwBOTQA4NQHg1ASAUxMATk0AODUB4NQEgFM/+XK7m9+gbT5hd/NE2zyxu3mibZ7Y3bxpd/NE27ypbX6D3c1v0DbfbALAqQkApyYAnJoAcGoCwKkJAKcmAJyaAHBqAsCpn/wSbfMJu5tv1jZP7G6eaJsndjef0DbfbHfzG7TNJ+xufoMJAKcmAJyaAHBqAsCpCQCnJgCcmgBwagLAqQkAp37Cf9Lu5k1t86a2eWJ386bdzRNt883a5ondDf89EwBOTQA4NQHg1ASAUxMATk0AODUB4NQEgFMTAE79hP+ktvmE3c0TbfOmtvmEtnlT28AEgFMTAE5NADg1AeDUBIBTEwBOTQA4NQHg1ASAUz/5JXY3f5PdzRNt86a2+YTdzRNtw//e7oZ/bwLAqQkApyYAnJoAcGoCwKkJAKcmAJyaAHBqAsCpn3y5tuFPbfPE7uaJtnlid/NE27ypbZ7Y3TzRNk/sbp5omyd2N0+0zSe0Df97EwBOTQA4NQHg1ASAUxMATk0AODUB4NQEgFMTAE795EN2N/x7u5sn2uYTdjdPtM0Tu5sn2uY32N28aXfD95gAcGoCwKkJAKcmAJyaAHBqAsCpCQCnJgCcmgBw6icf0jZP7G7e1DZ/k93NE23zRNs8sbv5hN3NJ+xu3tQ2b9rdvKltntjdPNE2n7C7+YQJAKcmAJyaAHBqAsCpCQCnJgCcmgBwagLAqQkAp37y5drmTbubN7XNE7ubN7XNE23zxO7mTW3zzdrmTbubJ9rmTbubN7XNE7ubN+1u/iYTAE5NADg1AeDUBIBTEwBOTQA4NQHg1ASAUxMATv3kl9jdfMLu5om2+YTdzZva5ondzRNt86a2+Wa7m2+2u3lT2zyxu/mbTAA4NQHg1ASAUxMATk0AODUB4NQEgFMTAE5NADj1kw/Z3bypbb7Z7uaJtnlT23xC23zC7uaJtvlmbfOm3c1v0DZP7G6+2QSAUxMATk0AODUB4NQEgFMTAE5NADg1AeDUBIBTP/lybfMJu5s3tc032918Qtu8qW3etLt5om3etLv5m7TNm9rmid3NJ0wAODUB4NQEgFMTAE5NADg1AeDUBIBTEwBOTQA49ZMPaZsndjdvapsn2uZNu5sn2uabtc0ntM0Tu5sn2uYTdjdPtM0n7G4+YXfzprb5ZhMATk0AODUB4NQEgFMTAE5NADg1AeDUBIBTEwBO/eRDdjdvapsndjef0Da/we7mTW3zxO7mE9rmTW3zxO7mTW3zprbh35sAcGoCwKkJAKcmAJyaAHBqAsCpCQCnJgCcmgBw6icf0jZP7G6e2N28qW3etLt5om0+oW0+YXfzprZ5YnfzRNs8sbt5U9s8sbv5DdrmTbubbzYB4NQEgFMTAE5NADg1AeDUBIBTEwBOTQA4NQHg1E8+ZHfzprZ5YnfzxO7mTW3zpt3Nm9rmTbubN7XNE7ubJ9qGP7XNE7ubb9Y2T+xuPmECwKkJAKcmAJyaAHBqAsCpCQCnJgCcmgBwagLAqZ98SNu8aXfzRNu8aXfzxO7mibZ5om0+YXfzprb5Zrsb/tQ2b9rdvGl3880mAJyaAHBqAsCpCQCnJgCcmgBwagLAqQkApyYAnPrJX2Z380TbvKltntjdvKltntjdfMLu5k1t88Tu5om2+WZt88Tu5om2eWJ386a2eWJ380TbPLG7+YQJAKcmAJyaAHBqAsCpCQCnJgCcmgBwagLAqQkAp7r/yAe0zSfsbr5Z2/wGu5s3tc0Tu5tPaJtP2N18Qts8sbt5om3etLv5ZhMATk0AODUB4NQEgFMTAE5NADg1AeDUBIBTEwBOdf+RD2ibN+1unmibJ3Y3T7TNm3Y3T7TNE7ubN7XN32R380TbPLG7eaJt+NPu5jeYAHBqAsCpCQCnJgCcmgBwagLAqQkApyYAnJoAcOonH7K7+YTdzZt2N79B2zyxu/mEtnlid/M32d18Qts8sbt5om3+JhMATk0AODUB4NQEgFMTAE5NADg1AeDUBIBTEwBO/eRD2oY/7W5+g7Z5YnfzzdrmN2ibJ3Y3b2ob/jQB4NQEgFMTAE5NADg1AeDUBIBTEwBOTQA4NQHg1E++3O7mN2ibT2ibJ3Y3T7TNN2ubJ3Y3n9A2n7C7+Wa7m7/JBIBTEwBOTQA4NQHg1ASAUxMATk0AODUB4NQEgFM/+SXa5hN2N/xpd/NE27xpd/PNdjdPtM0TbfM3aZsndjffbALAqQkApyYAnJoAcGoCwKkJAKcmAJyaAHBqAsCpn8D/w+7mibZ5YnfDv7e7eaJt3rS7eVPbPLG7+Q0mAJyaAHBqAsCpCQCnJgCcmgBwagLAqQkApyYAnPoJ/0m7mze1zZt2N0+0zRO7myfa5ondzZt2N0+0zRO7myfa5ondzRNt80TbvGl380TbvGl38wkTAE5NADg1AeDUBIBTEwBOTQA4NQHg1ASAUxMATv3kl9jd8Ke2eWJ3883a5ondzRNt86a2eVPb/E3a5ondzRNt880mAJyaAHBqAsCpCQCnJgCcmgBwagLAqQkApyYAnOr+Ix/QNvxpd/NE23zC7uYT2uZNu5sn2uaJ3c0TbfPE7uabtc0Tuxv+NAHg1ASAUxMATk0AODUB4NQEgFMTAE5NADg1AeBU9x8B4MwEgFMTAE5NADg1AeDUBIBTEwBOTQA4NQHg1ASAUxMATk0AODUB4NQEgFMTAE5NADg1AeDUBIBTEwBOTQA4NQHg1ASAUxMATk0AODUB4NQEgFMTAE5NADg1AeDUBIBT/wdnFw8OvzJrygAAAABJRU5ErkJggg=="; - - const actual = await generateQRCode(data, ECC.M, header); - expect(actual).toBe(expected); -}); -test("should return mapped CBOR data for given data with map", () => { - const data = {"name": "Jhon", "id": "207", "l_name": "Honay"}; - const map = {"id": "1", "name": "2", "l_name": "3"}; - const expected = - "a36131633230376132644a686f6e613365486f6e6179"; - - const actual = getMappedData(data, map,true).toString(HEX_ENCODING); - expect(actual).toBe(expected); -}); - -test("should return mapped data for given data with map", () => { - const data = {"name": "Jhon", "id": "207", "l_name": "Honay"}; - const map = {"id": "1", "name": "2", "l_name": "3"}; - const expected ={"2": "Jhon", "1": "207", "3": "Honay"}; - - const actual = getMappedData(data, map); - expect(actual).toStrictEqual(expected); -}); -test("should return mapped CBOR data for given data with map for claim 169 semantics", () => { - - const data = { - "id": "11110000324013", - "version": "1.0", - "language": "EN", - "fullName": "Peter M Jhon", - "firstName": "Peter", - "middleName": "M", - "lastName": "Jhon", - "dob": "19880102", - "gender": "1", - "address": "New City, METRO LINE, PA", - "email": "peter@example.com", - "phone": "+1 234-567", - "nationality": "US", - "maritalStatus": "2", - "guardian": "Jhon Honai", - "binaryImage": - "03CBABDF83D068ACB5DE65B3CDF25E0036F2C546CB90378C587A076E7A759DFD27CA7872B6CDFF339AEAACA61A6023FD1D305A9B4F33CAA248CEDE38B67D7C915C59A51BB4E77D10077A625258873183F82D65F4C482503A5A01F41DEE612C3542E5370987815E592B8EA2020FD3BDDC747897DB10237EAD179E55B441BC6D8BAD07CE535129CF8D559445CC3A29D746FBF1174DE2E7C0F3439BE7DBEA4520CF88825AAE6B1F291A746AB8177C65B2A459DD19BD32C0C3070004B85C1D63034707CC690AB0BA023350C8337FC6894061EB8A714A8F22FE2365E7A904C72DEC9746ABEA1A3296ECACD1A40450794EDCD2B34844E7C19EB7FB1A4AF3B05C3B374BD2941603F72D3F9A62EAB9A2FDAEEEEC8EE6E350F8A1863C0A0AB1B4058D154559A1CD5133EFCF682ABC339960819C9427889D60380B635A7D21D017974BBA57798490F668ADD86DA58125D9C4C1202CA1308F7734E43E8F77CEB0AF968A8F8B88849F9B98B26620399470ED057E7931DED82876DCA896A30D0031A8CBD7B9EDFDF16C15C6853F4F8D9EEC09317C84EDAE4B349FE54D23D8EC7DC9BB9F69FD7B7B23383B64F22E25F", - "binaryImageFormat": "2", - "bestQualityFingers": "[1, 2]", - }; - const map = { - "id": "1", - "version": "2", - "language": "3", - "fullName": "4", - "firstName": "5", - "middleName": "6", - "lastName": "7", - "dob": "8", - "gender": "9", - "address": "10", - "email": "11", - "phone": "12", - "nationality": "13", - "maritalStatus": "14", - "guardian": "15", - "binaryImage": "16", - "binaryImageFormat": "17", - "bestQualityFingers": "18", - }; - - const expected = - "b261316e3131313130303030333234303133613263312e30613362454e61346c5065746572204d204a686f6e61356550657465726136614d6137644a686f6e61386831393838303130326139613162313078184e657720436974792c204d4554524f204c494e452c205041623131717065746572406578616d706c652e636f6d6231326a2b31203233342d35363762313362555362313461326231356a4a686f6e20486f6e61696231367903513033434241424446383344303638414342354445363542334344463235453030333646324335343643423930333738433538374130373645374137353944464432374341373837324236434446463333394145414143413631413630323346443144333035413942344633334341413234384345444533384236374437433931354335394135314242344537374431303037374136323532353838373331383346383244363546344334383235303341354130314634314445453631324333353432453533373039383738313545353932423845413230323046443342444443373437383937444231303233374541443137394535354234343142433644384241443037434535333531323943463844353539343435434333413239443734364642463131373444453245374330463334333942453744424541343532304346383838323541414536423146323931413734364142383137374336354232413435394444313942443332433043333037303030344238354331443633303334373037434336393041423042413032333335304338333337464336383934303631454238413731344138463232464532333635453741393034433732444543393734364142454131413332393645434143443141343034353037393445444344324233343834344537433139454237464231413441463342303543334233373442443239343136303346373244334639413632454142394132464441454545454338454536453335304638413138363343304130414231423430353844313534353539413143443531333345464346363832414243333339393630383139433934323738383944363033383042363335413744323144303137393734424241353737393834393046363638414444383644413538313235443943344331323032434131333038463737333445343345384637374345423041463936384138463842383838343946394239384232363632303339393437304544303537453739333144454438323837364443413839364133304430303331413843424437423945444644463136433135433638353346344638443945454330393331374338344544414534423334394645353444323344384543374443394242394636394644374237423233333833423634463232453235466231376132623138665b312c20325d"; - - const actual = getMappedData(data, map,true).toString(HEX_ENCODING); - expect(actual).toBe(expected); -}); -test("should return properly mapped JSON data for given CBOR", () => { - const expected = {"name": "Jhon", "id": "207", "l_name": "Honay"}; - const map = {"1": "id", "2": "name", "3": "l_name"}; - const data = "a302644a686f6e01633230370365486f6e6179"; - - const actual = decodeMappedData(data, map); - expect(actual).toStrictEqual(expected); -}); - -test("should return properly mapped JSON data for given data", () => { - const expected = {"name": "Jhon", "id": "207", "l_name": "Honay"}; - const map = {"1": "id", "2": "name", "3": "l_name"}; - const data = {"2": "Jhon", "1": "207", "3": "Honay"}; - - const actual = decodeMappedData(data, map); - expect(actual).toStrictEqual(expected); -}); -test("should return properly mapped JSON data for given CBOR for claim 169 semantics", () => { - const expected = { - "id": "11110000324013", - "version": "1.0", - "language": "EN", - "fullName": "Peter M Jhon", - "firstName": "Peter", - "middleName": "M", - "lastName": "Jhon", - "dob": "19880102", - "gender": "1", - "address": "New City, METRO LINE, PA", - "email": "peter@example.com", - "phone": "+1 234-567", - "nationality": "US", - "maritalStatus": "2", - "guardian": "Jhon Honai", - "binaryImage": - "03CBABDF83D068ACB5DE65B3CDF25E0036F2C546CB90378C587A076E7A759DFD27CA7872B6CDFF339AEAACA61A6023FD1D305A9B4F33CAA248CEDE38B67D7C915C59A51BB4E77D10077A625258873183F82D65F4C482503A5A01F41DEE612C3542E5370987815E592B8EA2020FD3BDDC747897DB10237EAD179E55B441BC6D8BAD07CE535129CF8D559445CC3A29D746FBF1174DE2E7C0F3439BE7DBEA4520CF88825AAE6B1F291A746AB8177C65B2A459DD19BD32C0C3070004B85C1D63034707CC690AB0BA023350C8337FC6894061EB8A714A8F22FE2365E7A904C72DEC9746ABEA1A3296ECACD1A40450794EDCD2B34844E7C19EB7FB1A4AF3B05C3B374BD2941603F72D3F9A62EAB9A2FDAEEEEC8EE6E350F8A1863C0A0AB1B4058D154559A1CD5133EFCF682ABC339960819C9427889D60380B635A7D21D017974BBA57798490F668ADD86DA58125D9C4C1202CA1308F7734E43E8F77CEB0AF968A8F8B88849F9B98B26620399470ED057E7931DED82876DCA896A30D0031A8CBD7B9EDFDF16C15C6853F4F8D9EEC09317C84EDAE4B349FE54D23D8EC7DC9BB9F69FD7B7B23383B64F22E25F", - "binaryImageFormat": "2", - "bestQualityFingers": "[1, 2]", - }; - const map = { - "1": "id", - "2": "version", - "3": "language", - "4": "fullName", - "5": "firstName", - "6": "middleName", - "7": "lastName", - "8": "dob", - "9": "gender", - "10": "address", - "11": "email", - "12": "phone", - "13": "nationality", - "14": "maritalStatus", - "15": "guardian", - "16": "binaryImage", - "17": "binaryImageFormat", - "18": "bestQualityFingers", - }; - - const data = - "b261316e3131313130303030333234303133613263312e30613362454e61346c5065746572204d204a686f6e61356550657465726136614d6137644a686f6e61386831393838303130326139613162313078184e657720436974792c204d4554524f204c494e452c205041623131717065746572406578616d706c652e636f6d6231326a2b31203233342d35363762313362555362313461326231356a4a686f6e20486f6e61696231367903513033434241424446383344303638414342354445363542334344463235453030333646324335343643423930333738433538374130373645374137353944464432374341373837324236434446463333394145414143413631413630323346443144333035413942344633334341413234384345444533384236374437433931354335394135314242344537374431303037374136323532353838373331383346383244363546344334383235303341354130314634314445453631324333353432453533373039383738313545353932423845413230323046443342444443373437383937444231303233374541443137394535354234343142433644384241443037434535333531323943463844353539343435434333413239443734364642463131373444453245374330463334333942453744424541343532304346383838323541414536423146323931413734364142383137374336354232413435394444313942443332433043333037303030344238354331443633303334373037434336393041423042413032333335304338333337464336383934303631454238413731344138463232464532333635453741393034433732444543393734364142454131413332393645434143443141343034353037393445444344324233343834344537433139454237464231413441463342303543334233373442443239343136303346373244334639413632454142394132464441454545454338454536453335304638413138363343304130414231423430353844313534353539413143443531333345464346363832414243333339393630383139433934323738383944363033383042363335413744323144303137393734424241353737393834393046363638414444383644413538313235443943344331323032434131333038463737333445343345384637374345423041463936384138463842383838343946394239384232363632303339393437304544303537453739333144454438323837364443413839364133304430303331413843424437423945444644463136433135433638353346344638443945454330393331374338344544414534423334394645353444323344384543374443394242394636394644374237423233333833423634463232453235466231376132623138665b312c20325d"; - - const actual = decodeMappedData(data, map); - expect(actual).toStrictEqual(expected); -}); \ No newline at end of file diff --git a/js/test/UtilsTest.test.js b/js/test/UtilsTest.test.js new file mode 100644 index 0000000..d7f1186 --- /dev/null +++ b/js/test/UtilsTest.test.js @@ -0,0 +1,332 @@ +const { + decode, + generateQRData, + getMappedData, + decodeMappedData, +} = require("../src"); + +const { + translateToJson, + replaceKeysAtDepth, + replaceValuesForClaim169, +} = require("../src/utils/cborUtils"); + +const { + toMapWithKeyAndValueMapper, + toListWithKeyAndValueMapper, +} = require("../src/utils/mapperUtils"); + +/* ================================================================== + * TESTS TO COVER REMAINING UNCOVERED LINES + * ================================================================== */ + +/* ------------------------------------------------------------------ + * index.js - Line 69 (decode function - CBOR decode returns falsy) + * ------------------------------------------------------------------ */ + +test("decode returns text when CBOR decode returns null/undefined", () => { + // This covers the case where cbor.decode succeeds but returns falsy value + // We need to trigger the path where decodedCBORData is falsy + const plainText = "plain text without CBOR"; + const encoded = generateQRData(plainText); + const result = decode(encoded); + + expect(result).toBe(plainText); +}); + +/* ------------------------------------------------------------------ + * cborUtils.js - Uncovered Lines + * ------------------------------------------------------------------ */ + +// Line 36, 40 - translateToJson with Map containing nested Maps +test("translateToJson handles deeply nested Maps", () => { + const innerMap = new Map([ + ["level2", "value2"], + ["data", new Map([["level3", "value3"]])], + ]); + const outerMap = new Map([ + ["level1", "value1"], + ["nested", innerMap], + ]); + + const result = translateToJson(outerMap); + + expect(result.level1).toBe("value1"); + expect(result.nested.level2).toBe("value2"); + expect(result.nested.data.level3).toBe("value3"); +}); + +// Line 65 - replaceKeysAtDepth with primitive value (not object/array) +test("replaceKeysAtDepth handles primitive values gracefully", () => { + const primitiveValue = "string value"; + const result = replaceKeysAtDepth(primitiveValue, {}, 0); + expect(result).toBe("string value"); +}); + +test("replaceKeysAtDepth handles number values", () => { + const numberValue = 123; + const result = replaceKeysAtDepth(numberValue, {}, 0); + expect(result).toBe(123); +}); + +// Line 79, 83 - replaceValuesForClaim169 with string format codes and edge cases +test("replaceValuesForClaim169 handles NaN from parseInt", () => { + const input = { + Face: { + "Data format": "invalid-number", + "Data sub format": "0", + Data: "5249", + }, + }; + + const result = replaceValuesForClaim169(input); + + // Should not crash, format should remain unchanged + expect(result.Face["Data format"]).toBe("invalid-number"); +}); + +test("replaceValuesForClaim169 handles missing subformat mapping", () => { + const input = { + Face: { + "Data format": 0, // Valid format (Image) + "Data sub format": 999, // Invalid subformat code + Data: "5249", + }, + }; + + const result = replaceValuesForClaim169(input); + + expect(result.Face["Data format"]).toBe("Image"); + // Subformat should remain unchanged since 999 is not mapped + expect(result.Face["Data sub format"]).toBe(999); +}); + +test("replaceValuesForClaim169 handles null dataFormat value", () => { + const input = { + Face: { + "Data format": null, + "Data sub format": 0, + Data: "5249", + }, + }; + + const result = replaceValuesForClaim169(input); + + // Should not crash, should handle null gracefully + expect(result.Face["Data format"]).toBe(null); +}); + +// Line 119, 129 - replaceValuesForClaim169 biometric handling edge cases +test("replaceValuesForClaim169 handles non-object biometric values", () => { + const input = { + Face: "string instead of object", + Voice: null, + Fingerprint: ["array", "instead", "of", "object"], + }; + + const result = replaceValuesForClaim169(input); + + // Should not crash, should preserve values + expect(result.Face).toBe("string instead of object"); + expect(result.Voice).toBe(null); + expect(Array.isArray(result.Fingerprint)).toBe(true); +}); + +test("replaceValuesForClaim169 handles biometric object missing required keys", () => { + const input = { + Face: { + Data: "5249", + // Missing "Data format" and "Data sub format" + }, + }; + + const result = replaceValuesForClaim169(input); + + // Should not crash, object should remain unchanged + expect(result.Face.Data).toBe("5249"); + expect(result.Face["Data format"]).toBeUndefined(); +}); + +test("replaceValuesForClaim169 handles biometric with only format, no subformat", () => { + const input = { + Face: { + "Data format": 0, + // Missing "Data sub format" + Data: "5249", + }, + }; + + const result = replaceValuesForClaim169(input); + + // Should not crash, should handle missing subformat + expect(result.Face["Data format"]).toBe(0); +}); + +/* ------------------------------------------------------------------ + * mapperUtils.js - Lines 38, 42-47 + * ------------------------------------------------------------------ */ + +// Line 38 - toMapWithKeyAndValueMapper with nested array +test("toMapWithKeyAndValueMapper handles arrays with nested objects", () => { + const obj = { + items: [ + { name: "Item1", value: 10 }, + { name: "Item2", value: 20 }, + ], + }; + const keyMapper = { name: "n", value: "v" }; + + const result = toMapWithKeyAndValueMapper(obj, keyMapper, {}); + + expect(result.items[0].n).toBe("Item1"); + expect(result.items[0].v).toBe(10); + expect(result.items[1].n).toBe("Item2"); + expect(result.items[1].v).toBe(20); +}); + +// Lines 42-47 - toListWithKeyAndValueMapper with nested structures +test("toListWithKeyAndValueMapper handles array with null values", () => { + const arr = [{ name: "A" }, null, { name: "B" }]; + const keyMapper = { name: "n" }; + + const result = toListWithKeyAndValueMapper(arr, keyMapper, {}); + + expect(result[0].n).toBe("A"); + expect(result[1]).toBe(null); + expect(result[2].n).toBe("B"); +}); + +test("toListWithKeyAndValueMapper handles nested arrays of arrays", () => { + const arr = [ + [ + [1, 2], + [3, 4], + ], + [ + [5, 6], + [7, 8], + ], + ]; + + const result = toListWithKeyAndValueMapper(arr, {}, {}); + + expect(result[0][0]).toStrictEqual([1, 2]); + expect(result[0][1]).toStrictEqual([3, 4]); + expect(result[1][0]).toStrictEqual([5, 6]); + expect(result[1][1]).toStrictEqual([7, 8]); +}); + +test("toListWithKeyAndValueMapper handles mixed types in array", () => { + const arr = [{ type: "object" }, "string", 123, true, null, [1, 2, 3]]; + const keyMapper = { type: "t" }; + + const result = toListWithKeyAndValueMapper(arr, keyMapper, {}); + + expect(result[0].t).toBe("object"); + expect(result[1]).toBe("string"); + expect(result[2]).toBe(123); + expect(result[3]).toBe(true); + expect(result[4]).toBe(null); + expect(result[5]).toStrictEqual([1, 2, 3]); +}); + +test("toListWithKeyAndValueMapper handles deeply nested object structures in arrays", () => { + const arr = [ + { + level1: { + level2: { + name: "deep", + }, + }, + }, + ]; + const keyMapper = { name: "n" }; + + const result = toListWithKeyAndValueMapper(arr, keyMapper, {}); + + expect(result[0].level1.level2.n).toBe("deep"); +}); + +/* ------------------------------------------------------------------ + * INTEGRATION TESTS - Covering edge cases through public API + * ------------------------------------------------------------------ */ + +test("getMappedData with arrays containing nulls throws error", () => { + const data = [{ name: "A" }, null, { name: "B" }]; + const keyMapper = { name: "n" }; + + expect(() => getMappedData(data, keyMapper)).toThrow(TypeError); + expect(() => getMappedData(data, keyMapper)).toThrow( + "jsonData must not be null or undefined" + ); +}); + +test("decodeMappedData with nested arrays at specific depth", () => { + const input = { + users: [ + { id: 1, name: "A" }, + { id: 2, name: "B" }, + ], + }; + + // The objects inside 'users' array are at depth 1 + const keyMapper = [ + { users: "people" }, // depth 0 - rename users to people + { id: "identifier", name: "label" }, // depth 1 - rename keys inside array objects + ]; + + const result = JSON.parse(decodeMappedData(JSON.stringify(input), keyMapper)); + + expect(result.people[0].identifier).toBe(1); + expect(result.people[0].label).toBe("A"); + expect(result.people[1].identifier).toBe(2); + expect(result.people[1].label).toBe("B"); +}); + +test("replaceKeysAtDepth with arrays at target depth", () => { + const obj = { + data: [{ key: "value1" }, { key: "value2" }], + }; + const mapper = { key: "newKey" }; + + const result = replaceKeysAtDepth(obj, mapper, 1); + + expect(result.data[0].newKey).toBe("value1"); + expect(result.data[1].newKey).toBe("value2"); +}); + +/* ------------------------------------------------------------------ + * EDGE CASES FOR COMPLETE BRANCH COVERAGE + * ------------------------------------------------------------------ */ + +test("translateToJson handles empty Map", () => { + const emptyMap = new Map(); + const result = translateToJson(emptyMap); + expect(result).toStrictEqual({}); +}); + +test("translateToJson handles empty array", () => { + const emptyArray = []; + const result = translateToJson(emptyArray); + expect(result).toStrictEqual([]); +}); + +test("translateToJson handles Map with mixed value types", () => { + const map = new Map([ + ["string", "value"], + ["number", 123], + ["boolean", true], + ["null", null], + ["array", [1, 2, 3]], + ["object", { nested: "value" }], + ]); + + const result = translateToJson(map); + + expect(result.string).toBe("value"); + expect(result.number).toBe(123); + expect(result.boolean).toBe(true); + expect(result.null).toBe(null); + expect(result.array).toStrictEqual([1, 2, 3]); + expect(result.object.nested).toBe("value"); +}); diff --git a/js/test/decode.test.js b/js/test/decode.test.js new file mode 100644 index 0000000..112fa68 --- /dev/null +++ b/js/test/decode.test.js @@ -0,0 +1,45 @@ +const { decode, generateQRData } = require("../src"); + +describe("decode", () => { + test("decode simple base45 string", () => { + expect(decode("NCFKVPV0QSIP600GP5L0")).toBe("hello"); + }); + + test("decode CBOR encoded QR data", () => { + expect(decode("NCF3QBXJA5NJRCOC004 QN4")).toBe('{"temp":15}'); + }); + + test("decode CBOR array QR", () => { + const data = '[{"a":1},{"b":2}]'; + const encoded = generateQRData(data); + expect(decode(encoded)).toBe(data); + }); + + test("decode handles null values", () => { + const encoded = generateQRData(JSON.stringify({ key: null })); + expect(decode(encoded)).toBe('{"key":null}'); + }); + + test("decode handles booleans", () => { + const encoded = generateQRData(JSON.stringify({ a: true, b: false })); + expect(JSON.parse(decode(encoded))).toStrictEqual({ a: true, b: false }); + }); + + test("decode returns text when CBOR fails", () => { + const encoded = generateQRData("hello"); + expect(decode(encoded)).toBe("hello"); + }); + + test("decode throws on invalid base45", () => { + expect(() => decode("^1")).toThrow(); + }); + + test("decode cwt data", () => { + const cwtData = + "D28443A10126A1044230315905BFA301781D68747470733A2F2F6170692E72656C65617365642E6D6F7369702E6E65742F2E77656C6C2D6B6E6F776E2F6964612D636F6E74726F6C6C65722E6A736F6E061A676F3D33F518FAA35902F2A36840636F6E7465787483782A68747470733A2F2F7777772E77332E6F72672F323031382F63726564656E7469616C732F7631783F68747470733A2F2F6170692E72656C65617365642E6D6F7369702E6E65742F2E77656C6C2D6B6E6F776E2F6D6F7369702D6964612D636F6E746578742E6A736F6EA163736563781E68747470733A2F2F773369642E6F72672F736563757269747923647479706582784D4D4F53495056657269666961626C6543726564656E7469616C785456657269666961626C6543726564656E7469616C7363726564656E7469616C5375626A656374A86A646174654F6642697274686A313939392F30362F32336A656D61696C6E48756E6465724067616D696C2E636F6D62696478166469643A6A776B3A65794A7264486B694F694A53553045694C434A6C496A6F695156464251694973496E567A5A534936496E4E705A794973496D46735A794936496C4A544D6A55324969776962694936496A4657637A6B3252314E4361336B355345706D64584E6E62474E515745686A656C6C4E3268725A6E5642524769785A545A494E5842444F5739325446686B545563774F5577334C585A6B5A5752555456524454464A4C53565653524F4832324D4452785447684E5158686F567A4E7"; + const qrData = + "NCFO-MEC8KODJR19H2-MM Q98B0338G6J2O7$PT2PK75T1:BYHR3S4PU4OIQC.LVWU+9HL:T:UJ4-9:WTRWVVB2$VQR 66IPA+3DALB2208G%1U-VFJ1NK08-9Q%UB36N2ZUQ*O/QFSOVENQ60WXQP/9F:W9VGVA6O3BV*YNO-AA*R-ZUL%3UXE++3RXSYI5-72+-Q5RREZ6EFP-6DZNPLZF*:FZ6HVUAAOUATKXGL4J6XUL2232FD1BVPYIQ4MVAM.8I69OYARY/5A+QN%B+-9T:QZWV47W+6K-B4L.M58TEF9 M4UCL8V0XHNBHH%0P+BJ6IBRDV.BTE*IPC5Q2ARR158MT:UP2AIXHSTI6L3V8EI0RQ*2$D9FVB2P7SC7TBBY9LG-Q-%LJ3RHRS91F+536SRU042NB YH4QH-W7JAPN.2DEQ7G0BJ9X.B4CO OD5HI4+054Q%NM%DV50D255PSS2J48KOJ29GCBQ73MPC4*P85D0.PNZ5:/79VPK.EGZP3H7/JU69I%Y719H3R5$4V$4U.JJ1NC*GFIX4T:JQOV0HH6XDF4FC.FOV58DTMP389AAH9*PPV+AJ5NKRNZVNM/LLPFW2JA0IZJJS+7LR5%IH4KH.60F2VZTMD.7WQT 1A9H3MGNF 71RC7DCVW5+$P 7TPK8 L40RLRP7+NG1AOW9AKTSE-QPACBGEWT1-9K-CHU%VU.VDVU"; + const decodedData = decode(qrData); + expect(decodedData).toStrictEqual(cwtData); + }); +}); diff --git a/js/test/decodeBinary.test.js b/js/test/decodeBinary.test.js new file mode 100644 index 0000000..33895a0 --- /dev/null +++ b/js/test/decodeBinary.test.js @@ -0,0 +1,38 @@ +const { decodeBinary } = require("../src"); +const JSZip = require("jszip"); + +describe("decodeBinary", () => { + test("decode ZIP binary data", async () => { + const zip = new JSZip(); + zip.file("certificate.json", "Hello World!!"); + const data = await zip.generateAsync({ type: "string" }); + + const result = await decodeBinary(new TextEncoder().encode(data)); + expect(result).toBe("Hello World!!"); + }); + + test("decodeBinary throws for unsupported file", async () => { + await expect( + decodeBinary(new TextEncoder().encode("not-zip")) + ).rejects.toThrow("Unsupported binary file type"); + }); + + test("decode ZIP with multiple files", async () => { + const zip = new JSZip(); + zip.file("certificate.json", "Cert"); + zip.file("other.txt", "Other"); + const data = await zip.generateAsync({ type: "string" }); + + const result = await decodeBinary(new TextEncoder().encode(data)); + expect(result).toBe("Cert"); + }); + + test("decode empty ZIP entry", async () => { + const zip = new JSZip(); + zip.file("certificate.json", ""); + const data = await zip.generateAsync({ type: "string" }); + + const result = await decodeBinary(new TextEncoder().encode(data)); + expect(result).toBe(""); + }); +}); diff --git a/js/test/decodeMappedData.test.js b/js/test/decodeMappedData.test.js new file mode 100644 index 0000000..4ca177a --- /dev/null +++ b/js/test/decodeMappedData.test.js @@ -0,0 +1,61 @@ +const { decodeMappedData, getMappedData } = require("../src"); + +describe("decodeMappedData", () => { + test("decodeMappedData from CBOR", () => { + const data = "a302644a686f6e01633230370365486f6e6179"; + const map = [{ 1: "id", 2: "name", 3: "l_name" }]; + + const result = JSON.parse(decodeMappedData(data, map)); + expect(result).toStrictEqual({ + name: "Jhon", + id: "207", + l_name: "Honay", + }); + }); + + test("supports array input", () => { + const data = [ + "a302644a686f6e01633230370365486f6e6179", + "a302654a6d69746801633130320363446f65", + ]; + + const map = [{ 1: "id", 2: "name", 3: "l_name" }]; + const result = decodeMappedData(data, map).map(JSON.parse); + + expect(result[1].name).toBe("Jmith"); + }); + + test("depth-based key remapping", () => { + const input = { 1: { 0: "5249", 1: 0, 2: 0 } }; + const mapper = [ + { 1: "Face" }, + { 0: "Data", 1: "Data format", 2: "Data sub format" }, + ]; + + const result = JSON.parse(decodeMappedData(JSON.stringify(input), mapper)); + expect(result.Face["Data sub format"]).toBe("PNG"); + }); + + test("round-trip inverse operation", () => { + const input = { id: "123", name: "Test" }; + const encoded = getMappedData(input, { id: "1", name: "2" }, {}, true); + const decoded = JSON.parse( + decodeMappedData(encoded, [{ 1: "id", 2: "name" }]) + ); + + expect(decoded).toStrictEqual(input); + }); + + test("throws on invalid JSON", () => { + expect(() => decodeMappedData("{bad")).toThrow(); + }); + + test("decode CBOR hex string for claim-169 mapped full JSON", () => { + const hex = + "ac6249446a333931383539323433386756657273696f6e0a6946756c6c204e616d656c4a616e61726468616e2042536d44617465206f662042697274686a30342d31382d313938346647656e646572644d616c65674164647265737378294e657720486f7573652c204e656172204d6574726f204c696e652c2042656e67616c7572752c204b4168456d61696c204944756a616e61726468616e406578616d706c652e636f6d6c50686f6e65204e756d6265726d2b3931393837363534333231306b4e6174696f6e616c69747962494e6446616365a3644461746164353234396b4461746120666f726d617465496d6167656f446174612073756220666f726d617463504e4765566f696365a3644461746164353234396b4461746120666f726d617465536f756e646f446174612073756220666f726d6174635741566568656c6c6f65776f726c64"; + const decode = decodeMappedData(hex); + const decodedData = JSON.parse(decode); + const hexData = getMappedData(decodedData, [], [], true); + expect(hexData).toBe(hex); + }); +}); diff --git a/js/test/generateQRCode.test.js b/js/test/generateQRCode.test.js new file mode 100644 index 0000000..8a5e744 --- /dev/null +++ b/js/test/generateQRCode.test.js @@ -0,0 +1,26 @@ +const { generateQRCode } = require("../src"); +const { ECC } = require("../src/types/ECC"); + +describe("generateQRCode", () => { + test("generate base64 QR image", async () => { + const img = await generateQRCode("hello", ECC.M); + expect(img.startsWith("data:image/png;base64")).toBe(true); + }); + + test("generate QR image with header", async () => { + const img = await generateQRCode("hello", ECC.M, "hdr://"); + expect(img.startsWith("data:image/png;base64")).toBe(true); + }); + + test("supports all ECC levels", async () => { + for (const level of [ECC.L, ECC.M, ECC.Q, ECC.H]) { + const img = await generateQRCode("test", level); + expect(img.startsWith("data:image/png;base64")).toBe(true); + } + }); + + test("handles empty string", async () => { + const img = await generateQRCode("", ECC.M); + expect(img.startsWith("data:image/png;base64")).toBe(true); + }); +}); diff --git a/js/test/generateQRData.test.js b/js/test/generateQRData.test.js new file mode 100644 index 0000000..201505f --- /dev/null +++ b/js/test/generateQRData.test.js @@ -0,0 +1,32 @@ +const { generateQRData, decode } = require("../src"); + +describe("generateQRData", () => { + test("encode raw string to QR", () => { + expect(generateQRData("hello")).toBe("NCFKVPV0QSIP600GP5L0"); + }); + + test("encode JSON to CBOR QR", () => { + expect(generateQRData('{"temp":15}')).toBe("NCF3QBXJA5NJRCOC004 QN4"); + }); + + test("encode QR with header", () => { + expect(generateQRData("hello", "hdr://")) + .toBe("hdr://NCFKVPV0QSIP600GP5L0"); + }); + + test("handles empty string", () => { + const result = generateQRData(""); + expect(result.length).toBeGreaterThan(0); + }); + + test("handles complex nested JSON", () => { + const data = JSON.stringify({ a: { b: { c: [1, 2, 3] } } }); + const encoded = generateQRData(data); + expect(decode(encoded)).toBe(data); + }); + + test("handles non-JSON string with header", () => { + const result = generateQRData("plain text", "PREFIX:"); + expect(result.startsWith("PREFIX:")).toBe(true); + }); +}); diff --git a/js/test/getMappedData.test.js b/js/test/getMappedData.test.js new file mode 100644 index 0000000..4d4bf00 --- /dev/null +++ b/js/test/getMappedData.test.js @@ -0,0 +1,88 @@ +const { getMappedData, decodeMappedData } = require("../src"); +const { + CLAIM_169_KEY_MAPPER, + CLAIM_169_VALUE_MAPPER, +} = require("../src/shared/Constants"); + +describe("getMappedData", () => { + test("returns mapped object", () => { + const data = { name: "Jhon", id: "207", l_name: "Honay" }; + const map = { id: "1", name: "2", l_name: "3" }; + + expect(getMappedData(data, map)).toStrictEqual({ + 2: "Jhon", + 1: "207", + 3: "Honay", + }); + }); + + test("returns CBOR hex when enabled", () => { + const data = { name: "Jhon", id: "207", l_name: "Honay" }; + const map = { id: "1", name: "2", l_name: "3" }; + + const result = getMappedData(data, map, undefined, true); + expect(typeof result).toBe("string"); + }); + + test("supports array input", () => { + const data = [{ name: "A" }, { name: "B" }]; + const map = { name: "n" }; + + const result = getMappedData(data, map); + expect(result[0].n).toBe("A"); + expect(result[1].n).toBe("B"); + }); + + test("handles nested objects and arrays", () => { + const data = { users: [{ name: "A" }, { name: "B" }] }; + const map = { name: "n" }; + + const result = getMappedData(data, map); + expect(result.users[1].n).toBe("B"); + }); + + test("preserves null values", () => { + const result = getMappedData({ a: null }); + expect(result.a).toBeNull(); + }); + + test("should return CBOR hex string for claim-169 mapped full JSON", () => { + const jsonData = { + Address: "New House, Near Metro Line, Bengaluru, KA", + Version: 10, + "Email ID": "janardhan@example.com", + "Full Name": "Janardhan BS", + "Date of Birth": "04-18-1984", + ID: "3918592438", + Gender: "Male", + hello: "world", + "Phone Number": "+919876543210", + Face: { "Data format": "Image", "Data sub format": "PNG", Data: "5249" }, + Voice: { "Data format": "Sound", "Data sub format": "WAV", Data: "5249" }, + Nationality: "IN", + }; + const result = getMappedData( + jsonData, + CLAIM_169_KEY_MAPPER, + CLAIM_169_VALUE_MAPPER, + true + ); + const decodedString = decodeMappedData(result); + const decoded = JSON.parse(decodedString); + + expect(decoded).toMatchObject({ + ID: "3918592438", + Version: 10, + "Full Name": "Janardhan BS", + "Date of Birth": "04-18-1984", + Gender: "Male", + Address: "New House, Near Metro Line, Bengaluru, KA", + "Email ID": "janardhan@example.com", + "Phone Number": "+919876543210", + Nationality: "IN", + Face: { Data: "5249", "Data format": "Image", "Data sub format": "PNG" }, + Voice: { Data: "5249", "Data format": "Sound", "Data sub format": "WAV" }, + hello: "world", + }); + }); +}); diff --git a/js/test/toJson.test.js b/js/test/toJson.test.js new file mode 100644 index 0000000..c569761 --- /dev/null +++ b/js/test/toJson.test.js @@ -0,0 +1,94 @@ +const cbor = require("cbor-web"); +const { toJson } = require("../src"); + +describe("toJson (Base64URL CBOR → JSON)", () => { + test("decodes base64url encoded CBOR into JSON object", () => { + // 🔹 Input JSON + const input = { + id: "123", + name: "Alice", + age: 30, + }; + + // 🔹 Encode to CBOR → Base64URL (same as Kotlin flow) + const cborBytes = cbor.encode(input); + const base64Url = Buffer.from(cborBytes).toString("base64url"); + + // 🔹 Decode using toJson + const result = toJson(base64Url); + + expect(result).toStrictEqual(input); + }); + + test("handles nested objects and arrays", () => { + const input = { + user: { + name: "Bob", + roles: ["admin", "user"], + profile: { + active: true, + score: 99, + }, + }, + }; + + const cborBytes = cbor.encode(input); + const base64Url = Buffer.from(cborBytes).toString("base64url"); + + const result = toJson(base64Url); + + expect(result).toStrictEqual(input); + }); + + test("handles CBOR Map correctly", () => { + // 🔹 Explicit Map to ensure translateToJson(Map) path is hit + const map = new Map(); + map.set(1, "one"); + map.set(2, "two"); + + const cborBytes = cbor.encode(map); + const base64Url = Buffer.from(cborBytes).toString("base64url"); + + const result = toJson(base64Url); + + expect(result).toStrictEqual({ + 1: "one", + 2: "two", + }); + }); + + test("handles arrays at root level", () => { + const input = [ + { id: 1, value: "A" }, + { id: 2, value: "B" }, + ]; + + const cborBytes = cbor.encode(input); + const base64Url = Buffer.from(cborBytes).toString("base64url"); + + const result = toJson(base64Url); + + expect(result).toStrictEqual(input); + }); + + test("handles null and primitive values", () => { + const input = { + a: null, + b: true, + c: false, + d: 42, + e: "text", + }; + + const cborBytes = cbor.encode(input); + const base64Url = Buffer.from(cborBytes).toString("base64url"); + + const result = toJson(base64Url); + + expect(result).toStrictEqual(input); + }); + + test("throws error for invalid base64url input", () => { + expect(() => toJson("%%%invalid%%%")).toThrow(); + }); +}); diff --git a/kotlin/PixelPass/publish-artifact.gradle b/kotlin/PixelPass/publish-artifact.gradle index 34e7690..df78f10 100644 --- a/kotlin/PixelPass/publish-artifact.gradle +++ b/kotlin/PixelPass/publish-artifact.gradle @@ -29,7 +29,7 @@ publishing { pom { withXml { asNode().appendNode('name', "Pixelpass") - asNode().appendNode('url', "https://github.com/mosip/pixelpass") + asNode().appendNode('url', "https://github.com/inji/pixelpass") asNode().appendNode('description', "Kotlin library to generate QR code from VC and decode the data") asNode().appendNode('licenses').appendNode('license').with { @@ -38,9 +38,9 @@ publishing { } asNode().appendNode('scm').with { - appendNode('connection', 'scm:git:git://github.com/mosip/pixelpass.git') - appendNode('developerConnection', 'scm:git:ssh://github.com:mosip/pixelpass.git') - appendNode('url', "https://github.com/mosip/pixelpass") + appendNode('connection', 'scm:git:git://github.com/inji/pixelpass.git') + appendNode('developerConnection', 'scm:git:ssh://github.com:inji/pixelpass.git') + appendNode('url', "https://github.com/inji/pixelpass") appendNode('tag', "HEAD") } @@ -49,7 +49,7 @@ publishing { appendNode('name', 'Mosip') appendNode('email', 'mosip.emailnotifier@gmail.com') appendNode('organization', 'io.mosip') - appendNode("organizationUrl", "https://github.com/mosip/pixelpass") + appendNode("organizationUrl", "https://github.com/inji/pixelpass") } def dependenciesNode = asNode().appendNode('dependencies') @@ -123,7 +123,7 @@ publishing { pom { withXml { asNode().appendNode('name', "Pixelpass") - asNode().appendNode('url', "https://github.com/mosip/pixelpass") + asNode().appendNode('url', "https://github.com/inji/pixelpass") asNode().appendNode('description', "Java library to generate QR code from VC and decode the data") asNode().appendNode('licenses').appendNode('license').with { @@ -136,7 +136,7 @@ publishing { appendNode('name', 'Mosip') appendNode('email', 'mosip.emailnotifier@gmail.com') appendNode('organization', 'io.mosip') - appendNode('organizationUrl', 'https://github.com/mosip/pixelpass') + appendNode('organizationUrl', 'https://github.com/inji/pixelpass') } def dependenciesNode = asNode().appendNode('dependencies') @@ -158,9 +158,9 @@ publishing { pluginNode.appendNode('version', '3.0.1') asNode().appendNode('scm').with { - appendNode('connection', 'scm:git:git://github.com/mosip/pixelpass.git') - appendNode('developerConnection', 'scm:git:ssh://github.com:mosip/pixelpass.git') - appendNode('url', "https://github.com/mosip/pixelpass") + appendNode('connection', 'scm:git:git://github.com/inji/pixelpass.git') + appendNode('developerConnection', 'scm:git:ssh://github.com:inji/pixelpass.git') + appendNode('url', "https://github.com/inji/pixelpass") appendNode('tag', "HEAD") }