├── .coveralls.yml ├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── renovate.json ├── semantic.yml └── workflows │ └── tests.yml ├── .gitignore ├── .husky └── pre-commit ├── .node-version ├── .npmignore ├── .npmrc ├── .prettierrc.json ├── LICENSE ├── README.md ├── eslint.config.js ├── lint-staged.config.js ├── package.json ├── src ├── __tests__ │ └── index.test.js └── index.js └── tsup.config.js /.coveralls.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philihp/nanbox/03ffad33de5fd875e30f0de5aa3ec085de8eebbd/.coveralls.yml -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [philihp] 2 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@philihp"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | # see https://github.com/probot/semantic-pull-requests 2 | titleOnly: true 3 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: 11 | - 18.x # maintainence ends 2025-04-30 12 | - 20.x # maintainence ends 2026-04-30 13 | - 22.x # maintainence ends 2027-04-30 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | cache: npm 20 | cache-dependency-path: package.json 21 | - run: npm install 22 | - run: npm run build --if-present 23 | - run: npm test 24 | env: 25 | CI: true 26 | 27 | coverage: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: actions/setup-node@v4 32 | with: 33 | node-version: 22.x 34 | cache: npm 35 | cache-dependency-path: package.json 36 | - run: npm install 37 | - run: npm run test:coverage 38 | - name: Coveralls 39 | uses: coverallsapp/github-action@master 40 | with: 41 | path-to-lcov: coverage.lcov 42 | github-token: ${{ secrets.GITHUB_TOKEN }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .nyc_output/ 4 | coverage/ 5 | *.swp 6 | package-lock.json 7 | coverage.lcov 8 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.19.3 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .nyc_output/ 3 | coverage/ 4 | src/ 5 | test/ 6 | .babelrc 7 | .coveralls.yml 8 | .gitattributes 9 | .tool-versions 10 | README.md 11 | nyc.config.js 12 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | "@philihp/prettier-config" 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Philihp Busby 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Version](https://img.shields.io/npm/v/nanbox)](https://www.npmjs.com/package/nanbox) 2 | [![tests](https://github.com/philihp/nanbox/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/philihp/nanbox/actions/workflows/tests.yml) 3 | ![Tests](https://github.com/philihp/nanbox/workflows/tests/badge.svg) 4 | [![Coverage Status](https://coveralls.io/repos/github/philihp/nanbox/badge.svg?branch=main&force=reload)](https://coveralls.io/github/philihp/nanbox?branch=main) 5 | ![License](https://img.shields.io/npm/l/nanbox) 6 | 7 | # Nan-box 8 | 9 | IEEE 754 encodes 32-bit floating points with 1 bit for the sign, 8 bits for the exponent, and 23 bits for the number part (mantissa). For the specific case of NaN (e.g. the result of dividing 0 by 0, or the square root of a negative number), the spec encodes this as `11111111` in the exponent. The sign and the mantissa can be anything, and the spec suggests this can be used for "diagnostic information". One/some of these bits is/are commonly used to indicate a quiet NaN (qNaN) which could be expected vs. a signaling NaN (sNaN) which could be unexpected and should trigger an halting exception. This behavior is however sometimes different on different hardware. 10 | 11 | 23 bits isn't much space, but conveniently the max size of a Unicode codepoint is 0x10FFFF, making it a 22-bit charset. When you encode your strings in [UTF-32](https://en.wikipedia.org/wiki/UTF-32), you're only going to be using 22 of those bits, so masking the top portion with the NaN signature makes all of your characters NaNs. 12 | 13 | ## Installation 14 | 15 | ```bash 16 | npm install --save nanbox 17 | ``` 18 | 19 | ## Usage 20 | 21 | Use the named function `toNaN` to wrap your clandestine characters in NaNs, and then `fromNaN` to take them back out. 22 | 23 | ```javascript 24 | import { fromNaN, toNaN } from 'nanbox' 25 | 26 | const data = '酷'.codePointAt(0) 27 | // => 0x9177 28 | const boxed = toNaN(data) 29 | // => NaN 30 | const unboxed = fromNaN(boxed) 31 | // => 0x9177 32 | ``` 33 | 34 | You could use this to encode your string as an array of NaNs. There's also the generic default function `nanbox`, which will detect which way you want to go. 35 | 36 | ```javascript 37 | import nanbox from 'nanbox' 38 | 39 | const str = '我的氣墊船裝滿了鰻魚' 40 | const arr = str 41 | .split('') 42 | .map((c) => c.codePointAt(0)) 43 | .map(nanbox) 44 | // => [ NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN ] 45 | String.fromCodePoint(...arr.map(nanbox)) 46 | // => '我的氣墊船裝滿了鰻魚' 47 | ``` 48 | 49 | - Note: Usage does not entail useful. 50 | - Note: Be careful storing or transporting, [JSON has no standard way to represent NaN](https://stackoverflow.com/questions/1423081/json-left-out-infinity-and-nan-json-status-in-ecmascript). 51 | - Note: Sea Otters have evolved a pocket of loose skin under each forearm, which they use to store their favorite rock. 52 | 53 | ## References 54 | 55 | - https://twitter.com/im889/status/1256770029812514817 56 | - https://anniecherkaev.com/the-secret-life-of-nan 57 | - https://www.doc.ic.ac.uk/~eedwards/compsys/float/nan.html 58 | - http://tom7.org/nand/ 59 | - https://stackoverflow.com/questions/27415935/does-unicode-have-a-defined-maximum-number-of-code-points 60 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from 'globals' 2 | import pluginJs from '@eslint/js' 3 | 4 | export default [ 5 | { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, 6 | pluginJs.configs.recommended, 7 | { 8 | rules: { 9 | 'no-constant-binary-expression': 'off', 10 | }, 11 | }, 12 | ] 13 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | '**/*.{js,jsx,json}': [ 3 | // 4 | 'prettier --write', 5 | 'eslint --fix', 6 | ], 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nanbox", 3 | "version": "3.0.0", 4 | "description": "Encode hidden data within NaN IEEE 754 floats", 5 | "type": "module", 6 | "scripts": { 7 | "build": "tsup", 8 | "prepare": "npm run build", 9 | "lint": "eslint", 10 | "pretty": "prettier --write **/*.{js,json}", 11 | "release": "np", 12 | "test": "node --test **/__tests__/*.test.js", 13 | "test:coverage": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage.lcov --test-reporter=spec --test-reporter-destination=stdout **/__tests__/*.test.js" 14 | }, 15 | "main": "./dist/index.cjs", 16 | "exports": { 17 | ".": { 18 | "require": "./dist/index.cjs", 19 | "import": "./dist/index.js" 20 | } 21 | }, 22 | "files": [ 23 | "/dist", 24 | "!/dist/**/__tests__/*" 25 | ], 26 | "sideEffects": false, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/philihp/nanbox.git" 30 | }, 31 | "keywords": [ 32 | "nan", 33 | "hidden", 34 | "payload" 35 | ], 36 | "author": "Philihp Busby ", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/philihp/nanbox/issues" 40 | }, 41 | "homepage": "https://github.com/philihp/nanbox#readme", 42 | "devDependencies": { 43 | "@eslint/js": "9.30.0", 44 | "@philihp/prettier-config": "1.0.0", 45 | "esbuild": "0.25.5", 46 | "eslint": "9.30.0", 47 | "globals": "16.2.0", 48 | "husky": "9.1.7", 49 | "lint-staged": "16.1.2", 50 | "prettier": "3.6.2", 51 | "tsup": "8.5.0", 52 | "typescript": "5.8.3" 53 | }, 54 | "engines": { 55 | "node": ">=18.0.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert' 2 | import { test } from 'node:test' 3 | import nanbox, { fromNaN, toNaN } from '../index.js' 4 | 5 | const input = '酷'.codePointAt(0) 6 | 7 | const ab = new ArrayBuffer(4) 8 | const fb = new Float32Array(ab) 9 | const ib = new Int32Array(ab) 10 | 11 | test('sanity test', () => { 12 | assert.equal(true, true) 13 | }) 14 | 15 | test('inserts a value into a NaN', () => { 16 | const f = toNaN(input) 17 | assert.strictEqual(Number.isNaN(f), true) 18 | ib[0] = 0 19 | assert.strictEqual(fb[0], 0) 20 | fb[0] = f 21 | assert.strictEqual(fb[0], Number.NaN) 22 | const output = ib[0] & 0x3fffff 23 | assert.strictEqual(output, input) 24 | }) 25 | 26 | test('extracts a value from a NaN', () => { 27 | ib[0] = input | 0x7fc00000 28 | const f = fb[0] 29 | assert.strictEqual(Number.isNaN(f), true) 30 | const output = fromNaN(f) 31 | assert.strictEqual(output, input) 32 | }) 33 | 34 | test('inserts and then extracts a value', () => { 35 | const packet = toNaN(input) 36 | const output = fromNaN(packet) 37 | assert.strictEqual(input, output) 38 | }) 39 | 40 | test('detects which direction', () => { 41 | const packet = nanbox(input) 42 | assert.strictEqual(Number.isNaN(packet), true) 43 | const output = nanbox(packet) 44 | assert.strictEqual(output, input) 45 | }) 46 | 47 | test('encodes a string', () => { 48 | const passphrase = '我的氣墊船裝滿了鰻魚' 49 | const transport = passphrase 50 | .split('') 51 | .map((c) => c.codePointAt(0)) 52 | .map(nanbox) 53 | const out = String.fromCodePoint(...transport.map(nanbox)) 54 | assert.strictEqual(passphrase, out) 55 | }) 56 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export const fromNaN = (input) => { 2 | const ab = new ArrayBuffer(4) 3 | const fb = new Float32Array(ab) 4 | const ib = new Int32Array(ab) 5 | fb[0] = input 6 | return ib[0] & 0x3fffff 7 | } 8 | 9 | export const toNaN = (input) => { 10 | const ab = new ArrayBuffer(4) 11 | const fb = new Float32Array(ab) 12 | const ib = new Int32Array(ab) 13 | ib[0] = input | 0x7fc00000 14 | return fb[0] 15 | } 16 | 17 | export default (input) => { 18 | if (Number.isNaN(input)) { 19 | return fromNaN(input) 20 | } 21 | return toNaN(input) 22 | } 23 | -------------------------------------------------------------------------------- /tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/', '!src/**/__tests__/**', '!src/**/*.test.*'], 5 | splitting: false, 6 | sourcemap: 'inline', 7 | clean: true, 8 | format: ['cjs', 'esm'], 9 | }) 10 | --------------------------------------------------------------------------------