From ea2358a315bc9e68aa5b515f84eceb9263fe1b59 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Tue, 11 Mar 2025 19:42:33 -0500 Subject: [PATCH] initial commit --- .github/FUNDING.yml | 2 ++ .github/dependabot.yml | 6 ++++ .github/workflows/check-build.yml | 23 ++++++++++++++ .github/workflows/publish-npm.yml | 26 ++++++++++++++++ .github/workflows/run-tests.yml | 25 ++++++++++++++++ .gitignore | 2 ++ .nvmrc | 1 + .prettierignore | 2 ++ .prettierrc.js | 8 +++++ .vscode/extensions.json | 3 ++ .vscode/settings.json | 8 +++++ LICENSE | 21 +++++++++++++ README.md | 8 +++++ eslint.config.mjs | 13 ++++++++ package.json | 50 +++++++++++++++++++++++++++++++ src/decoders/index.ts | 5 ++++ src/decoders/rttrp.ts | 48 +++++++++++++++++++++++++++++ src/encoders/index.ts | 5 ++++ src/encoders/rttrp.ts | 37 +++++++++++++++++++++++ src/index.ts | 3 ++ src/models.ts | 11 +++++++ tests/decode.test.js | 44 +++++++++++++++++++++++++++ tests/encode.test.js | 45 ++++++++++++++++++++++++++++ tsconfig.cjs.json | 8 +++++ tsconfig.esm.json | 8 +++++ tsconfig.json | 12 ++++++++ tsconfig.types.json | 8 +++++ 27 files changed, 432 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/check-build.yml create mode 100644 .github/workflows/publish-npm.yml create mode 100644 .github/workflows/run-tests.yml create mode 100644 .gitignore create mode 100644 .nvmrc create mode 100644 .prettierignore create mode 100644 .prettierrc.js create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 eslint.config.mjs create mode 100644 package.json create mode 100644 src/decoders/index.ts create mode 100644 src/decoders/rttrp.ts create mode 100644 src/encoders/index.ts create mode 100644 src/encoders/rttrp.ts create mode 100644 src/index.ts create mode 100644 src/models.ts create mode 100644 tests/decode.test.js create mode 100644 tests/encode.test.js create mode 100644 tsconfig.cjs.json create mode 100644 tsconfig.esm.json create mode 100644 tsconfig.json create mode 100644 tsconfig.types.json diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..5917297 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +# These are supported funding model platforms +github: [jwetzell] diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..db9ba41 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'weekly' diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml new file mode 100644 index 0000000..e5a8af6 --- /dev/null +++ b/.github/workflows/check-build.yml @@ -0,0 +1,23 @@ +name: Check rttrp-js builds +on: + pull_request: + branches: + - main +jobs: + build-webui: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - name: Set up Node.js + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 + with: + node-version-file: '.nvmrc' + cache: 'npm' + cache-dependency-path: 'package-lock.json' + + - name: Install Node.js dependencies + run: npm ci + + - run: npm run build diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml new file mode 100644 index 0000000..b1913e7 --- /dev/null +++ b/.github/workflows/publish-npm.yml @@ -0,0 +1,26 @@ +name: Publish to npmjs +on: + push: + tags: + - '*' +jobs: + publish-lib: + permissions: + id-token: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 + with: + node-version-file: '.nvmrc' + registry-url: 'https://registry.npmjs.org' + cache: 'npm' + cache-dependency-path: 'package-lock.json' + + - name: Install Node.js dependencies + run: npm ci + + - name: Publish to NPM + run: npm publish --provenance --access=public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..84d23ae --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,25 @@ +name: Run @jwetzell/rttrp tests +on: + pull_request: + branches: + - main + paths: + - 'src/**' +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + + - name: Set up Node.js + uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + cache-dependency-path: './package-lock.json' + + - name: Install Node.js dependencies + run: npm ci + + - run: npm run test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76add87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..cc7ce7f --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v22.13.1 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e12f3b9 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +*.md +*.min.* \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..5f6903a --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,8 @@ +module.exports = { + trailingComma: 'es5', + tabWidth: 2, + semi: true, + singleQuote: true, + bracketSameLine: true, + printWidth: 120, +}; diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c83e263 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["esbenp.prettier-vscode"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d4e8010 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ab61ab5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Joel Wetzell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4563218 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +![npm](https://img.shields.io/npm/v/%40jwetzell%2Frttrp) +# rttrp-js +typesecript implementation of [RTTrP](https://rttrp.github.io/RTTrP-Wiki/index.html) + +## TODO +- [x] RTTrP Header +- [ ] RTTrPM +- [ ] RTTrPL \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..c287fe6 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,13 @@ +import pluginJs from '@eslint/js'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + { files: ['**/*.{js,mjs,cjs,ts}'] }, + { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + eslintPluginPrettierRecommended, +]; diff --git a/package.json b/package.json new file mode 100644 index 0000000..7610817 --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "@jwetzell/rttrp", + "version": "0.1.0", + "description": "", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts", + "exports": { + ".": { + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js", + "types": "./dist/types/index.d.ts" + } + }, + "scripts": { + "prebuild": "rimraf dist", + "build": "npm run build:cjs && npm run build:esm && npm run build:types", + "build:cjs": "tsc -p tsconfig.cjs.json", + "build:esm": "tsc -p tsconfig.esm.json", + "build:types": "tsc -p tsconfig.types.json", + "prepublishOnly": "npm run build", + "lint:check": "eslint ./src", + "format:check": "prettier ./ --check", + "format:write": "prettier ./ --write", + "pretest": "npm run build", + "test": "node --test --experimental-test-coverage" + }, + "files": [ + "dist" + ], + "author": { + "name": "Joel Wetzell", + "email": "me@jwetzell.com", + "url": "https://jwetzell.com" + }, + "repository": "https://github.com/jwetzell/rttrp-js", + "license": "MIT", + "devDependencies": { + "@eslint/js": "9.22.0", + "@types/node": "22.13.10", + "eslint": "9.18.0", + "eslint-config-prettier": "10.1.1", + "eslint-plugin-prettier": "5.2.3", + "globals": "16.0.0", + "prettier": "3.5.3", + "rimraf": "6.0.1", + "typescript": "5.8.2", + "typescript-eslint": "8.26.0" + } +} diff --git a/src/decoders/index.ts b/src/decoders/index.ts new file mode 100644 index 0000000..1f55172 --- /dev/null +++ b/src/decoders/index.ts @@ -0,0 +1,5 @@ +import RTTrPHeader from './rttrp'; + +export const Decoders = { + RTTrPHeader, +}; diff --git a/src/decoders/rttrp.ts b/src/decoders/rttrp.ts new file mode 100644 index 0000000..0a6f2b8 --- /dev/null +++ b/src/decoders/rttrp.ts @@ -0,0 +1,48 @@ +import { RTTrPHeader } from '../models'; + +export default (bytes: Uint8Array): RTTrPHeader => { + if (bytes.length < 18) { + throw new Error('RTTrP Header must be at least 18 bytes long'); + } + + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + + let dataOffset = 0; + + let isLittleEndian = false; + const intHeader = view.getUint16(dataOffset, isLittleEndian); + dataOffset += 2; + const fltHeader = view.getUint16(dataOffset, isLittleEndian); + dataOffset += 2; + if (intHeader === 0x4154) { + isLittleEndian = false; + } + const version = view.getUint16(dataOffset, isLittleEndian); + dataOffset += 2; + if (version !== 0x0002) { + throw new Error('Only version 2 of the RTTrP header is currently supported'); + } + const pID = view.getUint32(dataOffset, isLittleEndian); + dataOffset += 4; + + const pForm = view.getUint8(dataOffset); + dataOffset += 1; + const pktSize = view.getUint16(dataOffset, isLittleEndian); + dataOffset += 2; + const context = view.getUint32(dataOffset, isLittleEndian); + dataOffset += 4; + const numMods = view.getUint8(dataOffset); + dataOffset += 1; + const data = bytes.slice(dataOffset); + return { + intHeader, + fltHeader, + version, + pID, + pForm, + pktSize, + context, + numMods, + data, + }; +}; diff --git a/src/encoders/index.ts b/src/encoders/index.ts new file mode 100644 index 0000000..15fcaa5 --- /dev/null +++ b/src/encoders/index.ts @@ -0,0 +1,5 @@ +import RTTrPHeader from './rttrp'; + +export const Encoders = { + RTTrPHeader, +}; diff --git a/src/encoders/rttrp.ts b/src/encoders/rttrp.ts new file mode 100644 index 0000000..514ae07 --- /dev/null +++ b/src/encoders/rttrp.ts @@ -0,0 +1,37 @@ +import { RTTrPHeader } from '../models'; + +export default (header: RTTrPHeader): Uint8Array => { + if (header.version !== 2) { + throw new Error('Only version 2 of the RTTrP header is currently supported'); + } + const bytes = new Uint8Array(18 + header.data.length); + bytes.set(header.data, 18); + + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + let dataOffset = 0; + + let isLittleEndian = false; + view.setUint16(dataOffset, header.intHeader, isLittleEndian); + dataOffset += 2; + view.setUint16(dataOffset, header.fltHeader, isLittleEndian); + dataOffset += 2; + if (header.intHeader === 0x4154) { + isLittleEndian = false; + } + view.setUint16(dataOffset, header.version, isLittleEndian); + dataOffset += 2; + + view.setUint32(dataOffset, header.pID, isLittleEndian); + dataOffset += 4; + + view.setUint8(dataOffset, header.pForm); + dataOffset += 1; + view.setUint16(dataOffset, header.pktSize, isLittleEndian); + dataOffset += 2; + view.setUint32(dataOffset, header.context, isLittleEndian); + dataOffset += 4; + view.setUint8(dataOffset, header.numMods); + dataOffset += 1; + + return bytes; +}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..b6c9dd7 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export { Decoders } from './decoders'; +export { Encoders } from './encoders'; +export * from './models'; diff --git a/src/models.ts b/src/models.ts new file mode 100644 index 0000000..805c4e9 --- /dev/null +++ b/src/models.ts @@ -0,0 +1,11 @@ +export type RTTrPHeader = { + intHeader: number; + fltHeader: number; + version: number; + pID: number; + pForm: number; + pktSize: number; + context: number; + numMods: number; + data: Uint8Array; +}; diff --git a/tests/decode.test.js b/tests/decode.test.js new file mode 100644 index 0000000..41314a1 --- /dev/null +++ b/tests/decode.test.js @@ -0,0 +1,44 @@ +const { deepEqual, throws } = require('assert'); +const { describe, it } = require('node:test'); +const { Decoders } = require('../dist/cjs'); +const goodTests = [ + { + description: 'RTTrP Header', + bytes: new Uint8Array([ + 0x41, 0x54, 0x43, 0x34, 0x00, 0x02, 0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x12, 0x12, 0x34, 0x56, 0x78, 0x00, + ]), + expected: { + intHeader: 0x4154, + fltHeader: 0x4334, + version: 2, + pID: 305419896, + pForm: 0, + pktSize: 18, + context: 305419896, + numMods: 0, + data: new Uint8Array(), + }, + decoder: Decoders.RTTrPHeader, + }, +]; + +describe('RTTrP Bytes Decoding', () => { + goodTests.forEach((bytesTest) => { + it(bytesTest.description, () => { + const decoded = bytesTest.decoder(bytesTest.bytes); + deepEqual(decoded, bytesTest.expected); + }); + }); +}); + +// TODO(jwetzell): add error tests +const badTests = []; +describe('RTTrP Bytes Decoding Throws', () => { + badTests.forEach((bytesTest) => { + it(bytesTest.description, () => { + throws(() => { + bytesTest.decoder(bytesTest.bytes); + }, bytesTest.throwsMessage); + }); + }); +}); diff --git a/tests/encode.test.js b/tests/encode.test.js new file mode 100644 index 0000000..c1a3922 --- /dev/null +++ b/tests/encode.test.js @@ -0,0 +1,45 @@ +const { deepEqual, throws } = require('assert'); +const { describe, it } = require('node:test'); +const { Encoders } = require('../dist/cjs/index'); + +const goodTests = [ + { + description: 'RTTrP Header', + expected: new Uint8Array([ + 0x41, 0x54, 0x43, 0x34, 0x00, 0x02, 0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x12, 0x12, 0x34, 0x56, 0x78, 0x00, + ]), + message: { + intHeader: 0x4154, + fltHeader: 0x4334, + version: 2, + pID: 305419896, + pForm: 0, + pktSize: 18, + context: 305419896, + numMods: 0, + data: new Uint8Array(), + }, + encoder: Encoders.RTTrPHeader, + }, +]; + +describe('RTTrP Message Encoding', () => { + goodTests.forEach((messageTest) => { + it(messageTest.description, () => { + const actual = messageTest.encoder(messageTest.message); + deepEqual(actual, messageTest.expected); + }); + }); +}); + +//TODO(jwetzell): add tests that handle errors +const badTests = []; + +describe('RTTrP Message Encoding Throws', () => { + badTests.forEach((messageTest) => { + it(messageTest.description, () => { + messageTest.encoder(messageTest.packet); + throws(() => {}, messageTest.throwsMessage); + }); + }); +}); diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json new file mode 100644 index 0000000..265ef58 --- /dev/null +++ b/tsconfig.cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "declaration": false, + "outDir": "./dist/cjs" + } +} diff --git a/tsconfig.esm.json b/tsconfig.esm.json new file mode 100644 index 0000000..547b0be --- /dev/null +++ b/tsconfig.esm.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "declaration": false, + "outDir": "./dist/esm" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a5d4c1d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "moduleResolution": "Node", + "lib": ["ESNext"], + "target": "ESNext", + "esModuleInterop": true, + "isolatedModules": true, + "rootDir": "src/", + "strict": true + }, + "include": ["src/"] +} diff --git a/tsconfig.types.json b/tsconfig.types.json new file mode 100644 index 0000000..2497e33 --- /dev/null +++ b/tsconfig.types.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "emitDeclarationOnly": true, + "declaration": true, + "outDir": "./dist/types" + } +}