16 Commits

Author SHA1 Message Date
68f31018ef style: standardize quotation marks and formatting in README.md
All checks were successful
CI / build (push) Successful in 17s
CI / publish (push) Successful in 15s
2025-09-06 19:36:14 +01:00
356b70474c feat: enhance BenncClient with websocket-ts integration and improved reconnection logic
Some checks failed
CI / build (push) Failing after 12s
CI / publish (push) Has been skipped
2025-09-06 19:34:23 +01:00
ed1dc6870e feat: implement BenncClient class with connection management and messaging capabilities 2025-09-06 19:24:16 +01:00
22999c34f9 style: remove unnecessary whitespace in packet.ts and basic.test.ts
All checks were successful
CI / build (push) Successful in 17s
CI / publish (push) Successful in 14s
2025-09-06 19:07:27 +01:00
61b7c50868 refactor: update romulus-js import path to @3t/romulus and adjust tests accordingly
Some checks failed
CI / build (push) Failing after 12s
CI / publish (push) Has been skipped
2025-09-06 19:06:08 +01:00
15fa2b1608 refactor: rename package to @3t/bennc and update repository URL
Some checks failed
CI / build (push) Failing after 10s
CI / publish (push) Has been skipped
- Changed package name from "bennc-js" to "@3t/bennc"
- Updated repository URL to point to the new location
- Added publishConfig for npm registry
- Updated devDependencies to newer versions
2025-09-06 19:03:04 +01:00
61082d32e7 chore: update dependencies in package.json
- Added @3t/romulus dependency with version ^1.0.2
- Removed romulus-js file dependency
2025-09-06 18:57:58 +01:00
dd8e6ee49f Improve library 2025-09-06 18:50:08 +01:00
02c9cfdabc Update romulus-js path in package.json and tsconfig.json 2025-09-03 20:22:30 +01:00
695964a636 Prettier 2025-09-03 19:18:44 +01:00
eb620087c9 Initial updates 2025-09-03 19:18:19 +01:00
Jack Hadrill
8a6a73206e Get history fix 2024-11-20 00:25:21 +00:00
Jack Hadrill
26af3b8b69 Fix get history 2024-11-19 23:46:23 +00:00
Jack Hadrill
386e8c3ed1 Add get history message 2024-11-19 23:15:50 +00:00
Jack Hadrill
3a163df991 Fix outbound user data request/response 2022-03-18 22:31:50 +00:00
Jack Hadrill
0b19b83271 Fixed user data request/response definitions 2022-03-18 17:33:10 +00:00
37 changed files with 5452 additions and 8189 deletions

View File

@@ -1,52 +0,0 @@
kind: pipeline
type: docker
name: build
steps:
- name: install
image: node:lts-alpine
commands:
- apk add git
- npm install
- name: lint
image: node:lts-alpine
commands:
- npm run lint
depends_on:
- install
- name: test
image: node:lts-alpine
commands:
- npm run test
depends_on:
- install
- name: build
image: node:lts-alpine
commands:
- apk add zip
- npm run build
- zip -r bennc-m.zip dist/
depends_on:
- install
- name: publish
image: plugins/gitea-release
depends_on:
- lint
- test
- build
when:
event:
- tag
settings:
base_url: https://git.jacknet.io
api_key:
from_secret: gitea_token
files:
- bennc-m.zip
checksum:
- md5
- sha256

57
.gitea/workflows/ci..yml Normal file
View File

@@ -0,0 +1,57 @@
name: CI
on:
push:
branches: [master]
tags: ["v*"]
pull_request:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
container: node:lts-alpine
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm run test
- name: Build project
run: npm run build
publish:
needs: build
runs-on: ubuntu-latest
container: node:lts-alpine
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Build project
run: npm run build
- name: Set version from git tag
run: |
VERSION=${GITHUB_REF#refs/tags/v}
npm version $VERSION --no-git-tag-version
- name: Setup npm for publishing
run: |
npm config set //git.3t.network/api/packages/3t.network/npm/:_authToken ${{ secrets.PUBLISH_TOKEN }}
- name: Publish to Gitea npm registry
run: npm publish

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
@3t:registry=https://git.3t.network/api/packages/3t.network/npm/

View File

@@ -1,5 +1,3 @@
{
"recommendations": [
"orta.vscode-jest"
]
"recommendations": ["orta.vscode-jest"]
}

32
.vscode/launch.json vendored
View File

@@ -1,20 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/dist/index.js",
"outFiles": [
"${workspaceFolder}/**/*.js"
]
}
]
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/dist/index.js",
"outFiles": ["${workspaceFolder}/**/*.js"]
}
]
}

View File

@@ -1,3 +1,3 @@
{
"editor.tabSize": 2
"editor.tabSize": 2
}

30
.vscode/tasks.json vendored
View File

@@ -1,18 +1,16 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "typescript",
"tsconfig": "tsconfig.json",
"option": "watch",
"problemMatcher": [
"$tsc-watch"
],
"group": {
"kind": "build",
"isDefault": true
},
"label": "tsc: watch - tsconfig.json"
}
]
"version": "2.0.0",
"tasks": [
{
"type": "typescript",
"tsconfig": "tsconfig.json",
"option": "watch",
"problemMatcher": ["$tsc-watch"],
"group": {
"kind": "build",
"isDefault": true
},
"label": "tsc: watch - tsconfig.json"
}
]
}

208
BENNC_PROTOCOL_SPEC.md Normal file
View File

@@ -0,0 +1,208 @@
# BENNC Protocol Implementation Guide
**BENNCv1** (Butlersaurus Ephemeral No NONCEnse Chat) - Complete implementation documentation for a binary pub/sub messaging protocol with encryption and compression.
## Quick Implementation Checklist
1. **Connect** to server (TCP or WebSocket)
2. **Subscribe** to message types you want to receive
3. **Encrypt** data with Romulus-M (except subscribe/unsubscribe)
4. **Send keepalive** every 30 seconds when idle
5. **Handle** incoming messages and sender IDs
## Connection Endpoints
| Protocol | Endpoint |
| --------------- | --------------------------------- |
| TCP | `chat.3t.network:10009` |
| WebSocket (TLS) | `wss://chat.3t.network:443/BENNC` |
| WebSocket | `ws://chat.3t.network:80/BENNC` |
## Core Requirements
- **Encoding**: Big-endian integers, UTF-8 strings
- **Encryption**: Romulus-M with 16-byte nonce + message type as additional data (big-endian uint16)
- **Compression**: ZSTD (when specified per message type)
- **Max Data Size**: 1000 bytes per message (including nonce for encrypted messages)
- **Keepalive**: Send every 30 seconds when idle
- **Reserved IDs**: 0xFFFFFF00-0xFFFFFFFF for client internal use (256 IDs for local messages, system notifications, etc.)
## Message Structure
**Client → Server:**
```
┌─────────────┬─────────────┬─────────────────────────┐
│ Message Type│ Length │ Data │
│ (2 bytes) │ (2 bytes) │ (0-1000 bytes) │
└─────────────┴─────────────┴─────────────────────────┘
```
**Server → Client:**
```
┌─────────────┬─────────────┬─────────────┬─────────────────────┐
│ Message Type│ Sender ID │ Length │ Data │
│ (2 bytes) │ (4 bytes) │ (2 bytes) │ (0-1000 bytes) │
└─────────────┴─────────────┴─────────────┴─────────────────────┘
```
_Sender ID is randomly generated per connection to identify message source._
## Encryption Implementation
**For encrypted messages (types 0x0001, 0x0002, 0x0003, 0x0006, 0x0007):**
1. Generate random 16-byte nonce
2. Prepare additional data: message type as big-endian uint16
3. Encrypt plaintext using Romulus-M AEAD
4. Prepend nonce to ciphertext
5. Total data = nonce (16 bytes) + ciphertext ≤ 1000 bytes
## Compression Implementation
- **Algorithm**: ZSTD (Zstandard) at default compression level
- **Application**: Per message type (all messages of type or none)
- **Order**: Compression applied **before** encryption
- **Scope**: Only the data portion, headers remain uncompressed
- **Currently used by**: Advanced Text Messages (0x0006, 0x0007)
## Message Types Reference
| ID | Name | Subscribable | Encrypted | Compressed | Purpose |
| ------ | ------------------ | ------------ | --------- | ---------- | ----------------------------- |
| 0x0000 | Subscribe | ❌ | ❌ | ❌ | Subscribe to message type |
| 0x0001 | Basic Message | ✅ | ✅ | ❌ | Chat messages |
| 0x0002 | Request User Data | ✅ | ✅ | ❌ | Request user info |
| 0x0003 | User Data Response | ✅ | ✅ | ❌ | Respond with user info |
| 0x0005 | Keepalive | ❌ | ❌ | ❌ | Prevent connection timeout |
| 0x0006 | Advanced Text | ✅ | ✅ | ✅ | Long/rich text messages |
| 0x0007 | Edit Advanced Text | ✅ | ✅ | ✅ | Edit/delete advanced text |
| 0xFFFF | Unsubscribe | ❌ | ❌ | ❌ | Unsubscribe from message type |
---
## Message Type Specifications
### Subscribe (0x0000)
Subscribe to receive messages of specified type. Must resubscribe after disconnect.
**Data:** Message type to subscribe to (2 bytes, big-endian)
---
### Basic Message (0x0001)
UTF-8 chat messages. 16-byte nonce + encrypted data ≤ 1000 bytes total.
**Data:** Encrypted UTF-8 string
---
### Request User Data (0x0002)
Request user information from all users. Clients should respond with User Data Response (0x0003).
**Data Structure:**
- Username length (2 bytes, big-endian)
- Username (up to 32 bytes, UTF-8)
- Color RGB (3 bytes: R, G, B values)
- Client identifier length (2 bytes, big-endian)
- Client identifier (up to 32 bytes, UTF-8)
---
### User Data Response (0x0003)
Send user information in response to Request User Data (0x0002).
**Data Structure:** Same as Request User Data
- Username length (2 bytes, big-endian)
- Username (up to 32 bytes, UTF-8)
- Color RGB (3 bytes: R, G, B values)
- Client identifier length (2 bytes, big-endian)
- Client identifier (up to 32 bytes, UTF-8)
---
### Keepalive (0x0005)
Prevent connection timeout when idle. Send every 30 seconds when no other traffic. Not forwarded to other clients and cannot be subscribed to.
**Data:** None - empty message
---
### Unsubscribe (0xFFFF)
Stop receiving messages of specified type.
**Data:** Message type to unsubscribe from (2 bytes, big-endian)
---
### Advanced Text (0x0006)
Multi-packet messages for long text with markdown formatting and ZSTD compression.
**Implementation Notes:**
- Text is compressed **before** splitting into packets
- Header is not compressed
- Packets may arrive out of order - use packet numbers to reconstruct
- Message ID identifies the complete message, not individual packets
- Warn users before displaying very large messages
**Data Structure:**
- Message ID (4 bytes, big-endian)
- Packet number (2 bytes, big-endian, 0-indexed)
- Final packet number (2 bytes, big-endian, 0-indexed)
- Compressed markdown text (remaining bytes, ZSTD)
### Edit Advanced Text (0x0007)
Edit or delete existing Advanced Text messages. Use same Message ID to replace existing message. Empty compressed text deletes the message. Client replaces original when final packet received.
**Data Structure:** Same as Advanced Text (0x0006)
---
## Implementation Guidelines
### Validation Requirements
- All length fields must not exceed their specified maximums
- Username/Client ID lengths must match actual string lengths
- Message data must not exceed 1000 bytes (including nonce)
- Packet numbers must be sequential and within final packet range
### Connection Lifecycle
1. **Connect** to server (TCP/WebSocket)
2. **Subscribe** to required message types
3. **Send keepalive** every 30 seconds when idle
4. **Resubscribe** after any disconnection
5. **Handle** out-of-order packets for Advanced Text
### Common Issues
- **Encryption fails**: Ensure message type in additional data matches packet header
- **Messages dropped**: Check total size ≤ 1000 bytes including nonce
- **Connection timeout**: Verify keepalive frequency
- **Advanced Text garbled**: Reconstruct packets in correct order before decompression
---
## Protocol Features
- **Binary pub/sub messaging** with server-side routing
- **End-to-end encryption** using Romulus-M AEAD
- **ZSTD compression** for large text messages
- **Multi-packet support** for long messages
- **TCP and WebSocket** transport options
This is a documentation-only repository. The protocol specification is final and changes are not permitted.

60
CLAUDE.md Normal file
View File

@@ -0,0 +1,60 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is bennc-js, a TypeScript implementation of the BENNCv1 (Butlersaurus Ephemeral No NONCEnse Chat) protocol specification. It provides client functionality for a binary pub/sub messaging protocol with encryption and compression support.
## Development Commands
- `npm run build` - Compile TypeScript to JavaScript (outputs to `dist/`)
- `npm run test` - Run Jest test suite
- `npm run lint` - Check code formatting with Prettier
- `npm run format` - Format code with Prettier
- `npm install` - Install dependencies (automatically runs `tsc` via postinstall)
## Architecture
The codebase implements the BENNC protocol specification with the following structure:
### Core Files
- `src/index.ts` - Main entry point exporting all public APIs
- `src/mapping.ts` - Central registry mapping message types (0x0000-0xFFFF) to their pack/unpack functions
- `src/common.ts` - Protocol constants including `MAX_DATA_LENGTH` (1000 bytes), `DEFAULT_KEY`, and `MessageTypes` enum
- `src/messages/packet.ts` - Core packet handling with `packOutgoingPacket` and `unpackIncomingPacket` functions
### Message Types Implementation
Each BENNC message type has its own module in `src/messages/`:
- `subscribe.ts` (0x0000) - Subscribe to message types
- `basic.ts` (0x0001) - Basic encrypted chat messages
- `userDataRequest.ts` (0x0002) - Request user information
- `userDataResponse.ts` (0x0003) - Respond with user information
- `keepalive.ts` (0x0005) - Connection keepalive messages
- `history.ts` (0xFFFE) - Message history requests
- `unsubscribe.ts` (0xFFFF) - Unsubscribe from message types
### Protocol Implementation Details
- **Binary Protocol**: Big-endian integers, UTF-8 strings
- **Message Structure**: Client sends [Type(2)|Length(2)|Data(0-1000)], Server responds [Type(2)|SenderId(4)|Length(2)|Data(0-1000)]
- **Encryption**: Uses Romulus-M AEAD (via `romulus-js` dependency) with 16-byte nonces for message types 0x0001, 0x0002, 0x0003
- **Max Data Size**: 1000 bytes including nonce for encrypted messages
- **Dependencies**: Includes local `romulus-js` cryptography implementation and `color` library for user data
### Utilities
- `src/utilities/number.ts` - Big-endian number conversion functions (`numberToUint16BE`, `numberToUint32BE`)
- `src/utilities/smart-buffer.ts` - Buffer manipulation utility for packet construction/parsing
### Key Architecture Patterns
- **Mapping System**: Central `packers` and `unpackers` objects in `mapping.ts` provide type-safe message handling
- **Interface Separation**: `IncomingPacket` vs `OutgoingPacket` interfaces handle client/server message structure differences
- **Encryption Integration**: Encrypted message types automatically handle nonce generation and Romulus-M encryption
- **Protocol Compliance**: Strict adherence to BENNC specification including reserved sender IDs (0xFFFFFF00-0xFFFFFFFF)
The codebase serves as a complete client-side implementation for connecting to BENNC chat servers via TCP or WebSocket protocols.

197
README.md
View File

@@ -1,56 +1,199 @@
# BENNC-JS
[![Build Status](https://drone.jacknet.io/api/badges/TerribleCodeClub/bennc-js/status.svg)](https://drone.jacknet.io/TerribleCodeClub/bennc-js) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
An implementation of the [BENNC](https://wiki.jacknet.io/books/simontech/chapter/bennc) client specification.
[![Build Status](https://drone.jacknet.io/api/badges/TerribleCodeClub/bennc-js/status.svg)](https://drone.jacknet.io/TerribleCodeClub/bennc-js)
A TypeScript implementation of the [BENNC](https://wiki.jacknet.io/books/simontech/chapter/bennc) (Butlersaurus Ephemeral No NONCEnse Chat) protocol specification. This library provides both low-level protocol utilities and a high-level client for connecting to BENNC chat servers via WebSocket.
## Features
- **Complete BENNC Protocol Support**: All message types (0x0000-0xFFFF) including subscribe, basic chat, user data, keepalive, history, and unsubscribe
- **High-Level Client**: `BenncClient` class with automatic reconnection and event-driven architecture
- **Browser & Node.js Compatible**: Works in modern browsers and Node.js environments
- **TypeScript First**: Full type safety with comprehensive TypeScript definitions
- **Encryption Support**: Built-in Romulus-M AEAD encryption for secure messaging
- **Automatic Reconnection**: Configurable backoff strategies (constant, exponential)
- **Small Bundle Size**: Minimal dependencies and efficient implementation
## Installation
```bash
npm install @3t/bennc
```
## Quick Start
### Using the BenncClient (Recommended)
```typescript
import { BenncClient, MessageTypes } from "@3t/bennc";
const client = new BenncClient({
url: "wss://your-bennc-server.com",
autoReconnect: true,
reconnectBackoff: "exponential",
reconnectDelay: 1000,
});
// Listen for events
client.addEventListener("connected", () => {
console.log("Connected to BENNC server");
// Subscribe to basic messages
client.subscribe(MessageTypes.Basic);
});
client.addEventListener("message:1", (event) => {
const { senderId, data } = event.detail;
console.log(`Message from ${senderId}:`, new TextDecoder().decode(data));
});
client.addEventListener("disconnected", (event) => {
console.log("Disconnected:", event.detail);
});
// Connect to server
await client.connect();
// Send a message
client.sendBasicMessage("Hello, BENNC!");
```
### Using Low-Level Protocol Functions
```typescript
import { packers, unpackers, MessageTypes } from "@3t/bennc";
// Pack a basic message
const messageData = new TextEncoder().encode("Hello World");
const packet = packers[MessageTypes.Basic](messageData);
// Unpack incoming message
const incomingMessage = unpackers[MessageTypes.Basic](receivedData);
```
## API Reference
### BenncClient
#### Constructor Options
```typescript
interface BenncClientOptions {
url: string; // WebSocket server URL
protocols?: string[]; // WebSocket protocols
autoReconnect?: boolean; // Enable auto-reconnection (default: true)
reconnectBackoff?: "constant" | "exponential"; // Backoff strategy (default: 'exponential')
reconnectDelay?: number; // Reconnection delay in ms (default: 1000)
maxReconnectAttempts?: number; // Max reconnection attempts (default: 10)
}
```
#### Methods
- `connect(): Promise<void>` - Connect to the BENNC server
- `disconnect(): void` - Disconnect from the server
- `isConnected(): boolean` - Check connection status
- `subscribe(messageType: MessageTypes): void` - Subscribe to message type
- `unsubscribe(messageType: MessageTypes): void` - Unsubscribe from message type
- `sendBasicMessage(message: string, key?: Uint8Array): void` - Send encrypted chat message
- `sendUserDataRequest(username: string, colour: Color, clientId: string, key?: Uint8Array): void` - Request user data
- `sendUserDataResponse(username: string, colour: Color, clientId: string, key?: Uint8Array): void` - Respond with user data
- `sendKeepalive(): void` - Send keepalive message
- `requestHistory(): void` - Request message history
#### Events
The client extends `EventTarget` and emits the following events:
- `connected` - Successfully connected to server
- `disconnected` - Disconnected from server (detail: `{code, reason}`)
- `reconnecting` - Attempting to reconnect
- `error` - Connection or protocol error (detail: error object)
- `packet` - Raw incoming packet (detail: packet object)
- `message` - Parsed message (detail: `{messageType, senderId, data}`)
- `message:${messageType}` - Specific message type events (detail: `{senderId, data}`)
- `unknown-message` - Unknown message type received
- `parse-error` - Error parsing incoming message
### Message Types
```typescript
enum MessageTypes {
Subscribe = 0x0000,
Basic = 0x0001,
UserDataRequest = 0x0002,
UserDataResponse = 0x0003,
Keepalive = 0x0005,
GetHistory = 0xfffe,
Unsubscribe = 0xffff,
}
```
### Protocol Constants
```typescript
const MAX_DATA_LENGTH = 1000; // Maximum data payload size
const DEFAULT_KEY: Uint8Array; // Default encryption key
```
## Build
To build the BENNC-JS library, first clone this repository.
To build the BENNC-JS library from source:
Run the following commands from the root of the repository:
```bash
$ git clone <repository-url>
$ cd bennc-js
$ npm install
$ npm run build
```
The build output will be saved to the `dist` directory.
## Development instructions
## Protocol Details
Requirements:
- The latest LTS builds of Node and npm.
BENNC uses a binary protocol with the following message structure:
Follow the instructions below to lint, test and build BENNC-JS.
**Client → Server**: `[Type(2)|Length(2)|Data(0-1000)]`
**Server → Client**: `[Type(2)|SenderId(4)|Length(2)|Data(0-1000)]`
#### Lint
- All integers are big-endian
- Maximum data payload is 1000 bytes (including encryption overhead)
- Encrypted message types (0x0001, 0x0002, 0x0003) use Romulus-M AEAD with 16-byte nonces
- Reserved sender IDs: 0xFFFFFF00-0xFFFFFFFF
## Development
Requirements: Node.js LTS and npm
### Commands
```bash
$ npm install
$ npm run lint
```
# Install dependencies
npm install
#### Test
# Run tests
npm run test
```bash
$ npm install
$ npm run test
```
#### Build
# Lint code
npm run lint
```bash
$ npm install
$ npm run build
# Format code
npm run format
# Build library
npm run build
```
### Visual Studio Code
This repository contains the necessary configuration files to debug, test and build BENNC-JS using only Visual Studio Code.
This repository includes VS Code configuration for debugging and testing. Use `Ctrl+Shift+B` (or `⇧⌘B`) to run the build task.
Run the build task (`Ctrl+Shift+B` or `⇧⌘B`) to automatically compile the Typescript source files in the background.
Unit tests use [Jest](https://jestjs.io/) with VS Code support via the [Jest extension](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest).
Unit tests use the [Jest](https://jestjs.io/) library. Support for Visual Studio Code is offered through the [Jest marketplace package](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest) maintained by Orta.
## License
## Contribution guidelines
ISC License - see package.json for details.
[![JavaScript Style Guide](https://cdn.rawgit.com/standard/standard/master/badge.svg)](https://github.com/standard/standard)
## Contributing
This library uses [ts-standard](https://github.com/standard/ts-standard), based on [JavaScript Standard Style](https://standardjs.com/rules.html). Please ensure all contributions are ts-standard compliant before submitting a pull request.
This is part of the BENNC protocol ecosystem. Please refer to the [BENNC specification](https://wiki.jacknet.io/books/simontech/chapter/bennc) for protocol details.

11625
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,25 @@
{
"name": "bennc-js",
"name": "@3t/bennc",
"version": "1.0.0",
"description": "A TypeScript/Javascript BENNC implementation.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"lint": "ts-standard",
"lint": "prettier --check .",
"format": "prettier --write .",
"test": "jest",
"build": "tsc",
"postinstall": "tsc"
},
"repository": {
"type": "git",
"url": "https://git.jacknet.io/TerribleCodeClub/bennc-js"
"url": "https://git.3t.network/3t.network/bennc"
},
"publishConfig": {
"registry": "https://git.3t.network/api/packages/3t.network/npm/"
},
"author": "Butlersaurus",
"license": "ISC",
"devDependencies": {
"@types/jest": "^27.4.0",
"jest": "^27.4.7",
"ts-jest": "^27.1.3",
"ts-standard": "^11.0.0",
"typescript": "^4.5.5"
},
"jest": {
"verbose": true,
"transform": {
@@ -30,8 +27,16 @@
}
},
"dependencies": {
"color": "^4.2.0",
"@3t/romulus": "^1.0.2",
"@types/color": "^3.0.3",
"romulus-js": "git+https://git.jacknet.io/TerribleCodeClub/romulus-js.git"
"color": "^4.2.0",
"websocket-ts": "^2.2.1"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"jest": "^30.1.3",
"prettier": "^3.6.2",
"ts-jest": "^29.4.1",
"typescript": "^5.9.2"
}
}

211
src/BenncClient.ts Normal file
View File

@@ -0,0 +1,211 @@
import {
WebsocketBuilder,
ConstantBackoff,
ExponentialBackoff,
} from "websocket-ts";
import { MessageTypes } from "./common";
import { packOutgoingPacket, unpackIncomingPacket } from "./messages/packet";
import { packers, unpackers } from "./mapping";
import { numberToUint16BE } from "./utilities/number";
import Color from "color";
export interface BenncClientOptions {
url: string;
protocols?: string[];
autoReconnect?: boolean;
reconnectBackoff?: "constant" | "linear" | "exponential";
reconnectDelay?: number;
maxReconnectAttempts?: number;
}
export class BenncClient extends EventTarget {
private ws: any = null;
private options: BenncClientOptions;
constructor(options: BenncClientOptions) {
super();
this.options = {
autoReconnect: true,
reconnectBackoff: "exponential",
reconnectDelay: 1000,
maxReconnectAttempts: 10,
...options,
};
}
connect(): Promise<void> {
return new Promise((resolve, reject) => {
if (
this.ws &&
(this.ws.state === WebSocket.OPEN ||
this.ws.state === WebSocket.CONNECTING)
) {
resolve();
return;
}
const builder = new WebsocketBuilder(this.options.url);
if (this.options.protocols) {
builder.withProtocols(this.options.protocols);
}
if (this.options.autoReconnect) {
let backoff;
switch (this.options.reconnectBackoff) {
case "constant":
backoff = new ConstantBackoff(this.options.reconnectDelay!);
break;
case "exponential":
default:
backoff = new ExponentialBackoff(this.options.reconnectDelay!);
break;
}
builder.withBackoff(backoff);
}
this.ws = builder
.onOpen(() => {
this.dispatchEvent(new CustomEvent("connected"));
resolve();
})
.onClose((_: any, event: any) => {
this.dispatchEvent(
new CustomEvent("disconnected", {
detail: { code: event.code, reason: event.reason },
}),
);
})
.onError((_: any, error: any) => {
this.dispatchEvent(new CustomEvent("error", { detail: error }));
reject(error);
})
.onMessage((_: any, event: any) => {
this.handleMessage(event.data);
})
.onReconnect(() => {
this.dispatchEvent(new CustomEvent("reconnecting"));
})
.build();
});
}
disconnect(): void {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
isConnected(): boolean {
return this.ws && this.ws.state === WebSocket.OPEN;
}
getConnectionState(): { connected: boolean; connecting: boolean } {
return {
connected: this.ws?.state === WebSocket.OPEN || false,
connecting: this.ws?.state === WebSocket.CONNECTING || false,
};
}
private sendRawMessage(messageType: number, data?: Uint8Array): void {
if (!this.isConnected()) {
throw new Error("Client is not connected");
}
const packet = packOutgoingPacket({
messageType: numberToUint16BE(messageType),
data,
});
this.ws.send(packet);
}
subscribe(messageType: MessageTypes): void {
const packer = packers[MessageTypes.Subscribe];
const data = packer({ messageType });
this.sendRawMessage(MessageTypes.Subscribe, data);
}
unsubscribe(messageType: MessageTypes): void {
const packer = packers[MessageTypes.Unsubscribe];
const data = packer({ messageType });
this.sendRawMessage(MessageTypes.Unsubscribe, data);
}
sendBasicMessage(message: string, key?: Uint8Array): void {
const encoder = new TextEncoder();
const messageBytes = encoder.encode(message);
const packer = packers[MessageTypes.Basic];
const data = packer(messageBytes, key);
this.sendRawMessage(MessageTypes.Basic, data);
}
sendUserDataRequest(
username: string,
colour: Color,
clientId: string,
key?: Uint8Array,
): void {
const packer = packers[MessageTypes.UserDataRequest];
const data = packer({ username, colour, clientId }, key);
this.sendRawMessage(MessageTypes.UserDataRequest, data);
}
sendUserDataResponse(
username: string,
colour: Color,
clientId: string,
key?: Uint8Array,
): void {
const packer = packers[MessageTypes.UserDataResponse];
const data = packer({ username, colour, clientId }, key);
this.sendRawMessage(MessageTypes.UserDataResponse, data);
}
sendKeepalive(): void {
const packer = packers[MessageTypes.Keepalive];
const data = packer();
this.sendRawMessage(MessageTypes.Keepalive, data);
}
requestHistory(): void {
const packer = packers[MessageTypes.GetHistory];
const data = packer();
this.sendRawMessage(MessageTypes.GetHistory, data);
}
private handleMessage(data: ArrayBuffer): void {
try {
const packet = unpackIncomingPacket(data);
this.dispatchEvent(new CustomEvent("packet", { detail: packet }));
const unpacker = unpackers[packet.messageType as keyof typeof unpackers];
if (unpacker) {
const unpackedData = unpacker(packet.data);
const messageEvent = {
messageType: packet.messageType,
senderId: packet.senderId,
data: unpackedData,
};
this.dispatchEvent(
new CustomEvent("message", { detail: messageEvent }),
);
this.dispatchEvent(
new CustomEvent(`message:${packet.messageType}`, {
detail: {
senderId: packet.senderId,
data: unpackedData,
},
}),
);
} else {
this.dispatchEvent(
new CustomEvent("unknown-message", { detail: packet }),
);
}
} catch (error) {
this.dispatchEvent(new CustomEvent("parse-error", { detail: error }));
}
}
}

View File

@@ -1,6 +1,6 @@
export const MAX_DATA_LENGTH = 1000
export const MAX_DATA_LENGTH = 1000;
export const DEFAULT_KEY = new Uint8Array(16)
export const DEFAULT_KEY = new Uint8Array(16);
export enum MessageTypes {
Subscribe = 0x0000,
@@ -8,5 +8,6 @@ export enum MessageTypes {
UserDataRequest = 0x0002,
UserDataResponse = 0x0003,
Keepalive = 0x0005,
Unsubscribe = 0xFFFF
GetHistory = 0xfffe,
Unsubscribe = 0xffff,
}

View File

@@ -1,9 +1,11 @@
export { numberToUint16BE, numberToUint32BE } from './utilities/number'
export { unpackIncomingPacket } from './messages/packet'
export { packers, unpackers } from './mapping'
export { MessageTypes } from './common'
export { IncomingPacket, OutgoingPacket } from './messages/packet'
export { SubscribeMessage } from './messages/subscribe'
export { BasicMessage } from './messages/basic'
export { UserDataRequestMessage } from './messages/userDataRequest'
export { UserDataResponseMessage } from './messages/userDataResponse'
export { numberToUint16BE, numberToUint32BE } from "./utilities/number";
export { unpackIncomingPacket } from "./messages/packet";
export { packers, unpackers } from "./mapping";
export type { MessageTypes } from "./common";
export type { IncomingPacket, OutgoingPacket } from "./messages/packet";
export type { SubscribeMessage } from "./messages/subscribe";
export type { BasicMessage } from "./messages/basic";
export type { UserDataRequestMessage } from "./messages/userDataRequest";
export type { UserDataResponseMessage } from "./messages/userDataResponse";
export { BenncClient } from "./BenncClient";
export type { BenncClientOptions } from "./BenncClient";

View File

@@ -1,9 +1,16 @@
import { packSubscribeMessage } from './messages/subscribe'
import { packBasicMessage, unpackBasicMessage } from './messages/basic'
import { packUserDataRequestMessage, unpackUserDataRequestMessage } from './messages/userDataRequest'
import { packUserDataResponseMessage, unpackUserDataResponseMessage } from './messages/userDataResponse'
import { packKeepaliveMessage } from './messages/keepalive'
import { packUnsubscribeMessage } from './messages/unsubscribe'
import { packSubscribeMessage } from "./messages/subscribe";
import { packBasicMessage, unpackBasicMessage } from "./messages/basic";
import {
packUserDataRequestMessage,
unpackUserDataRequestMessage,
} from "./messages/userDataRequest";
import {
packUserDataResponseMessage,
unpackUserDataResponseMessage,
} from "./messages/userDataResponse";
import { packKeepaliveMessage } from "./messages/keepalive";
import { packUnsubscribeMessage } from "./messages/unsubscribe";
import { packGetHistoryMessage } from "./messages/history";
export const packers = {
0x0000: packSubscribeMessage,
@@ -11,11 +18,12 @@ export const packers = {
0x0002: packUserDataRequestMessage,
0x0003: packUserDataResponseMessage,
0x0005: packKeepaliveMessage,
0xffff: packUnsubscribeMessage
}
0xfffe: packGetHistoryMessage,
0xffff: packUnsubscribeMessage,
};
export const unpackers = {
0x0001: unpackBasicMessage,
0x0002: unpackUserDataRequestMessage,
0x0003: unpackUserDataResponseMessage
}
0x0003: unpackUserDataResponseMessage,
};

View File

@@ -1,12 +1,12 @@
import { encrypt } from 'romulus-js'
import { DEFAULT_KEY, MessageTypes } from '../common'
import { numberToUint16BE } from '../utilities/number'
import { packOutgoingPacket } from './packet'
import { encrypt, decrypt } from "@3t/romulus";
import { DEFAULT_KEY, MessageTypes } from "../common";
import { numberToUint16BE } from "../utilities/number";
import { packOutgoingPacket } from "./packet";
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.Basic)
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.Basic);
export interface BasicMessage {
data: Uint8Array
data: Uint8Array;
}
/**
@@ -15,19 +15,30 @@ export interface BasicMessage {
* @param key The key to encrypt the data with.
* @returns An encrypted outgoing basic message (0x0001) packet.
*/
export function packBasicMessage (message: Uint8Array, key: Uint8Array = DEFAULT_KEY): Uint8Array {
const data = encrypt(message, MESSAGE_TYPE, key)
export function packBasicMessage(
message: Uint8Array,
key: Uint8Array = DEFAULT_KEY,
): Uint8Array {
const data = encrypt(message, MESSAGE_TYPE, key);
return packOutgoingPacket({
messageType: MESSAGE_TYPE,
data: data
})
data: data,
});
}
/**
* Unpack the data section of an incoming basic message (0x0001) message.
* @param data The data section of an incoming basic message (0x0001) message.
* @returns An encrypted unpacked basic message (0x0001) message.
* @param data The encrypted data section of an incoming basic message (0x0001) message.
* @param key The key to decrypt the data with.
* @returns The decrypted plaintext message.
*/
export function unpackBasicMessage (data: Uint8Array): Uint8Array {
return data
export function unpackBasicMessage(
data: Uint8Array,
key: Uint8Array = DEFAULT_KEY,
): Uint8Array {
const result = decrypt(data, MESSAGE_TYPE, key);
if (result.success) {
return result.plaintext;
}
throw new Error("Failed to decrypt basic message");
}

15
src/messages/history.ts Normal file
View File

@@ -0,0 +1,15 @@
import { MessageTypes } from "../common";
import { numberToUint16BE } from "../utilities/number";
import { packOutgoingPacket } from "./packet";
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.GetHistory);
/**
* Create an outgoing get history (0xfffe) packet.
* @returns An outgoing get history (0xfffe) packet.
*/
export function packGetHistoryMessage(): Uint8Array {
return packOutgoingPacket({
messageType: MESSAGE_TYPE,
});
}

View File

@@ -1,16 +1,15 @@
import { MessageTypes } from '../common'
import { numberToUint16BE } from '../utilities/number'
import { packOutgoingPacket } from './packet'
import { MessageTypes } from "../common";
import { numberToUint16BE } from "../utilities/number";
import { packOutgoingPacket } from "./packet";
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.Keepalive)
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.Keepalive);
/**
* Create an outgoing keepalive (0x0005) packet.
* @returns An outgoing keepalive (0x0005) packet.
*/
export function packKeepaliveMessage (): Uint8Array {
export function packKeepaliveMessage(): Uint8Array {
return packOutgoingPacket({
messageType: MESSAGE_TYPE,
data: new Uint8Array(0)
})
});
}

View File

@@ -1,16 +1,16 @@
import { MAX_DATA_LENGTH } from '../common'
import { numberToUint16BE } from '../utilities/number'
import { SmartBuffer } from '../utilities/smart-buffer'
import { MAX_DATA_LENGTH } from "../common";
import { numberToUint16BE } from "../utilities/number";
import { SmartBuffer } from "../utilities/smart-buffer";
export interface IncomingPacket {
messageType: number
senderId: number
data: Uint8Array
messageType: number;
senderId: number;
data: Uint8Array;
}
export interface OutgoingPacket {
messageType: Uint8Array
data: Uint8Array
messageType: Uint8Array;
data?: Uint8Array;
}
/**
@@ -18,19 +18,24 @@ export interface OutgoingPacket {
* @param outgoingPacket The message type and data to send.
* @returns A buffer containing the ready-to-send packet.
*/
export function packOutgoingPacket (outgoingPacket: OutgoingPacket): Uint8Array {
export function packOutgoingPacket(outgoingPacket: OutgoingPacket): Uint8Array {
// Default to empty data if not provided
const data = outgoingPacket.data ?? new Uint8Array(0);
// Verify that the data does not exceed the maximum data length.
if (outgoingPacket.data.length > MAX_DATA_LENGTH) {
throw RangeError(`Specified data of length ${outgoingPacket.data.length} exceeds max data length ${MAX_DATA_LENGTH}.`)
if (data.length > MAX_DATA_LENGTH) {
throw RangeError(
`Specified data of length ${data.length} exceeds max data length ${MAX_DATA_LENGTH}.`,
);
}
// Prepare the outgoing packet.
const buffer = new SmartBuffer()
buffer.writeBytes(outgoingPacket.messageType)
buffer.writeBytes(numberToUint16BE(outgoingPacket.data.length))
buffer.writeBytes(outgoingPacket.data)
const buffer = new SmartBuffer();
buffer.writeBytes(outgoingPacket.messageType);
buffer.writeBytes(numberToUint16BE(data.length));
buffer.writeBytes(data);
return buffer.data
return buffer.data;
}
/**
@@ -38,17 +43,19 @@ export function packOutgoingPacket (outgoingPacket: OutgoingPacket): Uint8Array
* @param incomingPacket The incoming buffer from a WebSocket to unpack.
* @returns The unpacked data.
*/
export function unpackIncomingPacket (incomingPacket: ArrayBuffer): IncomingPacket {
const buffer = SmartBuffer.from(incomingPacket)
export function unpackIncomingPacket(
incomingPacket: ArrayBuffer,
): IncomingPacket {
const buffer = SmartBuffer.from(incomingPacket);
const messageType = buffer.readUInt16()
const senderId = buffer.readUInt32()
const length = buffer.readUInt16()
const data = buffer.readBytes(length)
const messageType = buffer.readUInt16();
const senderId = buffer.readUInt32();
const length = buffer.readUInt16();
const data = buffer.readBytes(length);
return {
messageType: messageType,
senderId: senderId,
data: data
}
data: data,
};
}

View File

@@ -1,11 +1,11 @@
import { MessageTypes } from '../common'
import { numberToUint16BE } from '../utilities/number'
import { packOutgoingPacket } from './packet'
import { MessageTypes } from "../common";
import { numberToUint16BE } from "../utilities/number";
import { packOutgoingPacket } from "./packet";
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.Subscribe)
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.Subscribe);
export interface SubscribeMessage {
messageType: number
messageType: number;
}
/**
@@ -13,10 +13,10 @@ export interface SubscribeMessage {
* @param properties The properties for the message.
* @returns An outgoing subscribe (0x0000) packet.
*/
export function packSubscribeMessage (properties: SubscribeMessage): Uint8Array {
const data = numberToUint16BE(properties.messageType)
export function packSubscribeMessage(properties: SubscribeMessage): Uint8Array {
const data = numberToUint16BE(properties.messageType);
return packOutgoingPacket({
messageType: MESSAGE_TYPE,
data: data
})
data: data,
});
}

View File

@@ -1,11 +1,11 @@
import { MessageTypes } from '../common'
import { numberToUint16BE } from '../utilities/number'
import { packOutgoingPacket } from './packet'
import { MessageTypes } from "../common";
import { numberToUint16BE } from "../utilities/number";
import { packOutgoingPacket } from "./packet";
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.Unsubscribe)
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.Unsubscribe);
export interface UnsubscribeMessage {
messageType: number
messageType: number;
}
/**
@@ -13,10 +13,12 @@ export interface UnsubscribeMessage {
* @param properties The properties for the message.
* @returns An outgoing unsubscribe (0xFFFF) packet.
*/
export function packUnsubscribeMessage (properties: UnsubscribeMessage): Uint8Array {
const data = numberToUint16BE(properties.messageType)
export function packUnsubscribeMessage(
properties: UnsubscribeMessage,
): Uint8Array {
const data = numberToUint16BE(properties.messageType);
return packOutgoingPacket({
messageType: MESSAGE_TYPE,
data: data
})
data: data,
});
}

View File

@@ -1,16 +1,16 @@
import Color from 'color'
import { encrypt } from 'romulus-js'
import { DEFAULT_KEY, MessageTypes } from '../common'
import { numberToUint16BE } from '../utilities/number'
import { SmartBuffer } from '../utilities/smart-buffer'
import { packOutgoingPacket } from './packet'
import Color from "color";
import { encrypt } from "@3t/romulus";
import { DEFAULT_KEY, MessageTypes } from "../common";
import { numberToUint16BE } from "../utilities/number";
import { SmartBuffer } from "../utilities/smart-buffer";
import { packOutgoingPacket } from "./packet";
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.UserDataRequest)
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.UserDataRequest);
export interface UserDataRequestMessage {
username: string
colour: Color
clientId: string
username: string;
colour: Color;
clientId: string;
}
/**
@@ -19,31 +19,36 @@ export interface UserDataRequestMessage {
* @param key The key to encrypt the data with.
* @returns An outgoing user data request (0x0002) packet.
*/
export function packUserDataRequestMessage (properties: UserDataRequestMessage, key: Uint8Array = DEFAULT_KEY): Uint8Array {
const encoder = new TextEncoder()
export function packUserDataRequestMessage(
properties: UserDataRequestMessage,
key: Uint8Array = DEFAULT_KEY,
): Uint8Array {
const encoder = new TextEncoder();
// Prepare data in correct format.
const username = encoder.encode(properties.username)
const usernameLength = numberToUint16BE(username.length)
const colour = new Uint8Array(properties.colour.array())
const clientId = encoder.encode(properties.clientId)
const clientIdLength = numberToUint16BE(clientId.length)
const username = encoder.encode(properties.username);
const usernameLength = numberToUint16BE(username.length);
const colour = new Uint8Array(properties.colour.array());
const clientId = encoder.encode(properties.clientId);
const clientIdLength = numberToUint16BE(clientId.length);
// Pack data.
const packedData = new SmartBuffer()
packedData.writeBytes(usernameLength)
packedData.writeBytes(username)
packedData.writeBytes(colour)
packedData.writeBytes(clientIdLength)
packedData.writeBytes(clientId)
const packedData = new SmartBuffer();
packedData.writeBytes(usernameLength);
packedData.writeBytes(username);
packedData.pad(32 - username.length);
packedData.writeBytes(colour);
packedData.writeBytes(clientIdLength);
packedData.writeBytes(clientId);
packedData.pad(32 - clientId.length);
// Encrypt the data.
const data = encrypt(packedData.data, MESSAGE_TYPE, key)
const data = encrypt(packedData.data, MESSAGE_TYPE, key);
return packOutgoingPacket({
messageType: MESSAGE_TYPE,
data: data
})
data: data,
});
}
/**
@@ -51,22 +56,25 @@ export function packUserDataRequestMessage (properties: UserDataRequestMessage,
* @param data The decrypted data section of an incoming user data request (0x0002) message.
* @returns An unpacked user data request (0x0002) message.
*/
export function unpackUserDataRequestMessage (data: Uint8Array): UserDataRequestMessage {
export function unpackUserDataRequestMessage(
data: Uint8Array,
): UserDataRequestMessage {
// Unpack and read data in correct format.
const packedData = SmartBuffer.from(data)
const packedData = SmartBuffer.from(Array.from(data));
const usernameLength = packedData.readUInt16()
const username = packedData.readBytes(usernameLength)
const colour = packedData.readBytes(3)
const clientIdLength = packedData.readUInt16()
const clientId = packedData.readBytes(clientIdLength)
const usernameLength = packedData.readUInt16();
const username = packedData.readBytes(usernameLength);
packedData.cursor = 34;
const colour = packedData.readBytes(3);
const clientIdLength = packedData.readUInt16();
const clientId = packedData.readBytes(clientIdLength);
const decoder = new TextDecoder()
const decoder = new TextDecoder();
// Return data in correct format.
return {
username: decoder.decode(username),
colour: Color.rgb(colour),
clientId: decoder.decode(clientId)
}
clientId: decoder.decode(clientId),
};
}

View File

@@ -1,16 +1,16 @@
import Color from 'color'
import { encrypt } from 'romulus-js'
import { DEFAULT_KEY, MessageTypes } from '../common'
import { numberToUint16BE } from '../utilities/number'
import { SmartBuffer } from '../utilities/smart-buffer'
import { packOutgoingPacket } from './packet'
import Color from "color";
import { encrypt } from "@3t/romulus";
import { DEFAULT_KEY, MessageTypes } from "../common";
import { numberToUint16BE } from "../utilities/number";
import { SmartBuffer } from "../utilities/smart-buffer";
import { packOutgoingPacket } from "./packet";
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.UserDataResponse)
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.UserDataResponse);
export interface UserDataResponseMessage {
username: string
colour: Color
clientId: string
username: string;
colour: Color;
clientId: string;
}
/**
@@ -19,30 +19,35 @@ export interface UserDataResponseMessage {
* @param key The key to encrypt the data with.
* @returns The data section of an outgoing user data response (0x0003) message.
*/
export function packUserDataResponseMessage (properties: UserDataResponseMessage, key: Uint8Array = DEFAULT_KEY): Uint8Array {
const encoder = new TextEncoder()
export function packUserDataResponseMessage(
properties: UserDataResponseMessage,
key: Uint8Array = DEFAULT_KEY,
): Uint8Array {
const encoder = new TextEncoder();
// Prepare data in correct format.
const username = encoder.encode(properties.username)
const usernameLength = numberToUint16BE(username.length)
const colour = new Uint8Array(properties.colour.array())
const clientId = encoder.encode(properties.clientId)
const clientIdLength = numberToUint16BE(clientId.length)
const username = encoder.encode(properties.username);
const usernameLength = numberToUint16BE(username.length);
const colour = new Uint8Array(properties.colour.array());
const clientId = encoder.encode(properties.clientId);
const clientIdLength = numberToUint16BE(clientId.length);
// Pack data.
const packedData = new SmartBuffer()
packedData.writeBytes(usernameLength)
packedData.writeBytes(username)
packedData.writeBytes(colour)
packedData.writeBytes(clientIdLength)
packedData.writeBytes(clientId)
const packedData = new SmartBuffer();
packedData.writeBytes(usernameLength);
packedData.writeBytes(username);
packedData.pad(32 - username.length);
packedData.writeBytes(colour);
packedData.writeBytes(clientIdLength);
packedData.writeBytes(clientId);
packedData.pad(32 - clientId.length);
// Return encrypted data.
const data = encrypt(packedData.data, MESSAGE_TYPE, key)
const data = encrypt(packedData.data, MESSAGE_TYPE, key);
return packOutgoingPacket({
messageType: MESSAGE_TYPE,
data: data
})
data: data,
});
}
/**
@@ -50,22 +55,25 @@ export function packUserDataResponseMessage (properties: UserDataResponseMessage
* @param data The decrypted data section of an incoming user data response (0x0003) message.
* @returns A unpacked user data response (0x0003) message.
*/
export function unpackUserDataResponseMessage (data: Uint8Array): UserDataResponseMessage {
export function unpackUserDataResponseMessage(
data: Uint8Array,
): UserDataResponseMessage {
// Unpack and read data in correct format.
const packedData = SmartBuffer.from(data)
const packedData = SmartBuffer.from(Array.from(data));
const usernameLength = packedData.readUInt16()
const username = packedData.readBytes(usernameLength)
const colour = packedData.readBytes(3)
const clientIdLength = packedData.readUInt16()
const clientId = packedData.readBytes(clientIdLength)
const usernameLength = packedData.readUInt16();
const username = packedData.readBytes(usernameLength);
packedData.cursor = 34;
const colour = packedData.readBytes(3);
const clientIdLength = packedData.readUInt16();
const clientId = packedData.readBytes(clientIdLength);
const decoder = new TextDecoder()
const decoder = new TextDecoder();
// Return data in correct format.
return {
username: decoder.decode(username),
colour: Color.rgb(colour),
clientId: decoder.decode(clientId)
}
clientId: decoder.decode(clientId),
};
}

View File

@@ -3,11 +3,11 @@
* @param number The number to pack.
* @returns The packed buffer.
*/
export function numberToUint16BE (number: number): Uint8Array {
const ret = new Uint8Array(2)
ret[0] = (number & 0xFF00) >> 8
ret[1] = (number & 0x00FF) >> 0
return ret
export function numberToUint16BE(number: number): Uint8Array {
const ret = new Uint8Array(2);
ret[0] = (number & 0xff00) >> 8;
ret[1] = (number & 0x00ff) >> 0;
return ret;
}
/**
@@ -15,11 +15,11 @@ export function numberToUint16BE (number: number): Uint8Array {
* @param number The number to pack.
* @returns The packed buffer.
*/
export function numberToUint32BE (number: number): Uint8Array {
const ret = new Uint8Array(4)
ret[0] = (number & 0xFF000000) >> 24
ret[1] = (number & 0x00FF0000) >> 16
ret[2] = (number & 0x0000FF00) >> 8
ret[3] = (number & 0x000000FF) >> 0
return ret
export function numberToUint32BE(number: number): Uint8Array {
const ret = new Uint8Array(4);
ret[0] = (number & 0xff000000) >> 24;
ret[1] = (number & 0x00ff0000) >> 16;
ret[2] = (number & 0x0000ff00) >> 8;
ret[3] = (number & 0x000000ff) >> 0;
return ret;
}

View File

@@ -1,62 +1,64 @@
import { numberToUint16BE, numberToUint32BE } from './number'
import { numberToUint16BE, numberToUint32BE } from "./number";
export class SmartBuffer {
private _data: number[]
private _cursor: number
private _data: number[];
private _cursor: number;
/**
* Wrap a buffer to track position and provide useful read / write functionality.
* @param data Buffer to wrap (optional).
*/
constructor (length: number = 0) {
this._data = new Array<number>(length)
this._cursor = 0
* Wrap a buffer to track position and provide useful read / write functionality.
* @param data Buffer to wrap (optional).
*/
constructor(length: number = 0) {
this._data = new Array<number>(length);
this._cursor = 0;
}
/**
* Return a regular buffer.
*/
get data (): Uint8Array {
return new Uint8Array(this._data)
* Return a regular buffer.
*/
get data(): Uint8Array {
return new Uint8Array(this._data);
}
/**
* Update the smart buffer to wrap new data.
*/
set data (data: Uint8Array) {
this._data = Array.from(data)
this.cursor = 0
* Update the smart buffer to wrap new data.
*/
set data(data: Uint8Array) {
this._data = Array.from(data);
this.cursor = 0;
}
/**
* Get the length of the smart buffer data.
*/
get length (): number {
return this._data.length
* Get the length of the smart buffer data.
*/
get length(): number {
return this._data.length;
}
/**
* Set the length of the smart buffer data.
*/
set length (length: number) {
this._data.length = length
* Set the length of the smart buffer data.
*/
set length(length: number) {
this._data.length = length;
}
/**
* Get the current cursor position of the smart buffer.
*/
get cursor (): number {
return this._cursor
* Get the current cursor position of the smart buffer.
*/
get cursor(): number {
return this._cursor;
}
/**
* Set the cursor position of the smart buffer.
*/
set cursor (position: number) {
* Set the cursor position of the smart buffer.
*/
set cursor(position: number) {
if (position < 0) {
throw RangeError(`Cannot seek to ${this.cursor} of ${this.length} bytes.`)
throw RangeError(
`Cannot seek to ${this.cursor} of ${this.length} bytes.`,
);
}
this._cursor = position
this._cursor = position;
}
/**
@@ -64,32 +66,33 @@ export class SmartBuffer {
* @param data The object to convert to a new SmartBuffer.
* @returns A new SmartBuffer.
*/
static from (data: number[] | ArrayBuffer): SmartBuffer {
const smartBuffer = new SmartBuffer()
static from(data: number[] | ArrayBuffer): SmartBuffer {
const smartBuffer = new SmartBuffer();
if (data instanceof ArrayBuffer) {
smartBuffer._data = Array.from(new Uint8Array(data))
smartBuffer._data = Array.from(new Uint8Array(data));
} else {
smartBuffer._data = Array.from(data)
smartBuffer._data = Array.from(data);
}
return smartBuffer
return smartBuffer;
}
/**
* Pads bytes to the end of the smart buffer.
* @param length The number of bytes to pad.
*/
pad (length: number): void {
this._data.push(...Array<number>(length))
* Pads bytes to the end of the smart buffer.
* @param length The number of bytes to pad.
*/
pad(length: number): void {
this._data.push(...Array<number>(length).fill(0));
this.cursor += length;
}
/**
* Return the data from the specified range.
* @param start The start position.
* @param end The end position.
* @returns A new buffer containing data from the specified range.
*/
slice (start: number, end: number): Uint8Array {
return new Uint8Array(this._data.slice(start, end))
* Return the data from the specified range.
* @param start The start position.
* @param end The end position.
* @returns A new buffer containing data from the specified range.
*/
slice(start: number, end: number): Uint8Array {
return new Uint8Array(this._data.slice(start, end));
}
/**
@@ -98,66 +101,66 @@ export class SmartBuffer {
* @param deleteCount The number of items to remove before inserting new data.
* @param items The items to insert at the specified position.
*/
splice (start: number, deleteCount: number, ...items: number[]): void {
splice(start: number, deleteCount: number, ...items: number[]): void {
if (this.length < start) {
this.pad(start)
this._data.push(...Array<number>(start));
}
this._data.splice(this.cursor, deleteCount, ...items)
this._data.splice(this.cursor, deleteCount, ...items);
}
/**
* Read a UInt16 number from the smart buffer at the current cursor position, and increment the cursor.
* @returns A number represented by the bytes at the current cursor position.
*/
readUInt16 (): number {
const num = this.slice(this.cursor, this.cursor + 2)
this.cursor += 2
return (num[0] << 8 | num[1]) >>> 0
* Read a UInt16 number from the smart buffer at the current cursor position, and increment the cursor.
* @returns A number represented by the bytes at the current cursor position.
*/
readUInt16(): number {
const num = this.slice(this.cursor, this.cursor + 2);
this.cursor += 2;
return ((num[0] << 8) | num[1]) >>> 0;
}
/**
* Read a UInt32 number from the smart buffer at the current cursor position, and increment the cursor.
* @returns A number represented by the bytes at the current cursor position.
*/
readUInt32 (): number {
const num = this.slice(this.cursor, this.cursor + 4)
this.cursor += 4
return (num[0] << 24 | num[1] << 16 | num[2] << 8 | num[3]) >>> 0
* Read a UInt32 number from the smart buffer at the current cursor position, and increment the cursor.
* @returns A number represented by the bytes at the current cursor position.
*/
readUInt32(): number {
const num = this.slice(this.cursor, this.cursor + 4);
this.cursor += 4;
return ((num[0] << 24) | (num[1] << 16) | (num[2] << 8) | num[3]) >>> 0;
}
/**
* Read the specified number of bytes from the smart buffer at the current cursor position, and increment the cursor.
* @returns A buffer containing the bytes read from the current cursor position.
*/
readBytes (length: number): Uint8Array {
this.cursor += length
return this.slice(this.cursor - length, this.cursor)
* Read the specified number of bytes from the smart buffer at the current cursor position, and increment the cursor.
* @returns A buffer containing the bytes read from the current cursor position.
*/
readBytes(length: number): Uint8Array {
this.cursor += length;
return this.slice(this.cursor - length, this.cursor);
}
/**
* Write a UInt16 number to the smart buffer at the current cursor position, and increment the cursor.
* @param number The number to write in UInt16 format at the current cursor position.
*/
writeUInt16 (number: number): void {
this.splice(this.cursor, 2, ...numberToUint16BE(number))
this.cursor += 2
* Write a UInt16 number to the smart buffer at the current cursor position, and increment the cursor.
* @param number The number to write in UInt16 format at the current cursor position.
*/
writeUInt16(number: number): void {
this.splice(this.cursor, 2, ...numberToUint16BE(number));
this.cursor += 2;
}
/**
* Write a UInt32 number to the smart buffer at the current cursor position, and increment the cursor.
* @param number The number to write in UInt32 format at the current cursor position.
*/
writeUInt32 (number: number): void {
this.splice(this.cursor, 4, ...numberToUint32BE(number))
this.cursor += 4
* Write a UInt32 number to the smart buffer at the current cursor position, and increment the cursor.
* @param number The number to write in UInt32 format at the current cursor position.
*/
writeUInt32(number: number): void {
this.splice(this.cursor, 4, ...numberToUint32BE(number));
this.cursor += 4;
}
/**
* Write the specified bytes to the smart buffer at the current cursor position, and increment the cursor.
* @param buffer The bytes to write at the current cursor position.
*/
writeBytes (buffer: number[] | Uint8Array): void {
this.splice(this.cursor, buffer.length, ...buffer)
this.cursor += buffer.length
* Write the specified bytes to the smart buffer at the current cursor position, and increment the cursor.
* @param buffer The bytes to write at the current cursor position.
*/
writeBytes(buffer: number[] | Uint8Array): void {
this.splice(this.cursor, buffer.length, ...buffer);
this.cursor += buffer.length;
}
}

View File

@@ -1,36 +1,42 @@
import { MessageTypes } from '../../src/common'
import { packers, unpackers } from '../../src/mapping'
import { MessageTypes } from "../../src/common";
import { packers, unpackers } from "../../src/mapping";
const KEY = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])
const KEY = new Uint8Array([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
]);
test('Create a basic message (0x0001) packet.', () => {
test("Create a basic message (0x0001) packet.", () => {
// Given
const encoder = new TextEncoder()
const message = encoder.encode('Hello, World!')
const encoder = new TextEncoder();
const message = encoder.encode("Hello, World!");
// When
const packedPacket = packers[MessageTypes.Basic](
message,
KEY
)
const packedPacket = packers[MessageTypes.Basic](message, KEY);
// Then
// We can't check the contents of the data as it's encrypted with a random nonce.
// Check the message type and length.
expect(packedPacket.slice(0, 4)).toMatchObject(new Uint8Array([0x00, 0x01, 0x00, 0x2D]))
expect(packedPacket.slice(0, 4)).toMatchObject(
new Uint8Array([0x00, 0x01, 0x00, 0x2d]),
);
// Check the total length is as expected.
expect(packedPacket.length).toBe(49)
})
expect(packedPacket.length).toBe(49);
});
test('Parse a basic message (0x0001).', () => {
test("Parse a basic message (0x0001).", () => {
// Given
const data = new Uint8Array([1, 2, 3, 4])
const encoder = new TextEncoder();
const originalMessage = encoder.encode("Test message");
// First encrypt the message to get valid encrypted data
const encryptedData = packers[MessageTypes.Basic](originalMessage, KEY);
// Extract just the data portion (skip the 4-byte header)
const dataOnly = encryptedData.slice(4);
// When
const unpackedPacket = unpackers[MessageTypes.Basic](data)
const unpackedMessage = unpackers[MessageTypes.Basic](dataOnly, KEY);
// Then
expect(unpackedPacket)
expect(unpackedPacket).toMatchObject(data)
})
expect(unpackedMessage).toEqual(originalMessage);
});

View File

@@ -1,11 +1,11 @@
import { MessageTypes } from '../../src/common'
import { packers } from '../../src/mapping'
import { MessageTypes } from "../../src/common";
import { packers } from "../../src/mapping";
test('Create a keepalive (0x0005) packet.', () => {
test("Create a keepalive (0x0005) packet.", () => {
// When
const packedPacket = packers[MessageTypes.Keepalive]()
const packedPacket = packers[MessageTypes.Keepalive]();
// Then
const expectedResult = new Uint8Array([0x00, 0x05, 0x00, 0x00])
expect(packedPacket).toMatchObject(expectedResult)
})
const expectedResult = new Uint8Array([0x00, 0x05, 0x00, 0x00]);
expect(packedPacket).toMatchObject(expectedResult);
});

View File

@@ -1,15 +1,18 @@
import { packOutgoingPacket, unpackIncomingPacket } from '../../src/messages/packet'
import {
packOutgoingPacket,
unpackIncomingPacket,
} from "../../src/messages/packet";
test('Pack an outgoing packet.', () => {
test("Pack an outgoing packet.", () => {
// Given
const messageType = new Uint8Array([0x12, 0x34])
const data = new Uint8Array([0x12, 0x34, 0x56, 0x78])
const messageType = new Uint8Array([0x12, 0x34]);
const data = new Uint8Array([0x12, 0x34, 0x56, 0x78]);
// When
const packedPacket = packOutgoingPacket({
messageType: messageType,
data: data
})
data: data,
});
// Then
const expectedResult = new Uint8Array([
@@ -18,12 +21,12 @@ test('Pack an outgoing packet.', () => {
// Data length
0x00, 0x04,
// Data
0x12, 0x34, 0x56, 0x78
])
expect(packedPacket).toMatchObject(expectedResult)
})
0x12, 0x34, 0x56, 0x78,
]);
expect(packedPacket).toMatchObject(expectedResult);
});
test('Unpack an incoming packet.', () => {
test("Unpack an incoming packet.", () => {
// Given
const incomingPacket = new Uint8Array([
// Message type
@@ -33,14 +36,16 @@ test('Unpack an incoming packet.', () => {
// Data length
0x00, 0x04,
// Data
0x12, 0x34, 0x56, 0x78
])
0x12, 0x34, 0x56, 0x78,
]);
// When
const unpackedResult = unpackIncomingPacket(incomingPacket)
const unpackedResult = unpackIncomingPacket(incomingPacket);
// Then
expect(unpackedResult.messageType).toBe(0x1234)
expect(unpackedResult.senderId).toBe(0xaabbccdd)
expect(unpackedResult.data).toMatchObject(new Uint8Array([0x12, 0x34, 0x56, 0x78]))
})
expect(unpackedResult.messageType).toBe(0x1234);
expect(unpackedResult.senderId).toBe(0xaabbccdd);
expect(unpackedResult.data).toMatchObject(
new Uint8Array([0x12, 0x34, 0x56, 0x78]),
);
});

View File

@@ -1,14 +1,16 @@
import { MessageTypes } from '../../src/common'
import { packers } from '../../src/mapping'
import { MessageTypes } from "../../src/common";
import { packers } from "../../src/mapping";
test('Create a subscribe (0x0000) packet.', () => {
test("Create a subscribe (0x0000) packet.", () => {
// Given
const messageType = 0xabcd
const messageType = 0xabcd;
// When
const packedPacket = packers[MessageTypes.Subscribe]({ messageType: messageType })
const packedPacket = packers[MessageTypes.Subscribe]({
messageType: messageType,
});
// Then
const expectedResult = new Uint8Array([0x00, 0x00, 0x00, 0x02, 0xab, 0xcd])
expect(packedPacket).toMatchObject(expectedResult)
})
const expectedResult = new Uint8Array([0x00, 0x00, 0x00, 0x02, 0xab, 0xcd]);
expect(packedPacket).toMatchObject(expectedResult);
});

View File

@@ -1,14 +1,16 @@
import { MessageTypes } from '../../src/common'
import { packers } from '../../src/mapping'
import { MessageTypes } from "../../src/common";
import { packers } from "../../src/mapping";
test('Create an unsubscribe (0xffff) packet.', () => {
test("Create an unsubscribe (0xffff) packet.", () => {
// Given
const messageType = 0xabcd
const messageType = 0xabcd;
// When
const packedPacket = packers[MessageTypes.Unsubscribe]({ messageType: messageType })
const packedPacket = packers[MessageTypes.Unsubscribe]({
messageType: messageType,
});
// Then
const expectedResult = new Uint8Array([0xff, 0xff, 0x00, 0x02, 0xab, 0xcd])
expect(packedPacket).toMatchObject(expectedResult)
})
const expectedResult = new Uint8Array([0xff, 0xff, 0x00, 0x02, 0xab, 0xcd]);
expect(packedPacket).toMatchObject(expectedResult);
});

View File

@@ -1,46 +1,56 @@
import Color from 'color'
import { MessageTypes } from '../../src/common'
import { packers, unpackers } from '../../src/mapping'
import Color from "color";
import { MessageTypes } from "../../src/common";
import { packers, unpackers } from "../../src/mapping";
const KEY = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F])
const KEY = new Uint8Array([
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
0x0d, 0x0e, 0x0f,
]);
test('Create a user data request (0x0002) packet.', () => {
test("Create a user data request (0x0002) packet.", () => {
// Given
const username = 'Butlersaurus'
const colour = Color('#FF4000')
const clientId = 'Mercury'
const username = "Butlersaurus";
const colour = Color("#FF4000");
const clientId = "Mercury";
// When
const packedPacket = packers[MessageTypes.UserDataRequest](
{
username: username,
colour: colour,
clientId: clientId
clientId: clientId,
},
KEY
)
KEY,
);
// Then
// We can't check the contents of the data as it's encrypted with a random nonce.
// Check the message type and length.
expect(packedPacket.slice(0, 4)).toMatchObject(new Uint8Array([0x00, 0x02, 0x00, 0x3A]))
expect(packedPacket.slice(0, 4)).toMatchObject(
new Uint8Array([0x00, 0x02, 0x00, 0x67]),
);
// Check the total length is as expected.
expect(packedPacket.length).toBe(62)
})
expect(packedPacket.length).toBe(107);
});
test('Parse a user data request (0x0002).', () => {
test("Parse a user data request (0x0002).", () => {
// Given
const data = new Uint8Array([0, 12, 66, 117, 116, 108, 101, 114, 115, 97, 117, 114, 117, 115, 255, 64, 0, 0, 7, 77, 101, 114, 99, 117, 114, 121])
const username = 'Butlersaurus'
const colour = Color('#FF4000')
const clientId = 'Mercury'
const data = new Uint8Array([
0, 12, 66, 117, 116, 108, 101, 114, 115, 97, 117, 114, 117, 115, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 64, 0, 0, 7, 77, 101,
114, 99, 117, 114, 121, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
]);
const username = "Butlersaurus";
const colour = Color("#FF4000");
const clientId = "Mercury";
// When
const unpackedPacket = unpackers[MessageTypes.UserDataRequest](data)
const unpackedPacket = unpackers[MessageTypes.UserDataRequest](data);
// Then
expect(unpackedPacket.username).toBe(username)
expect(unpackedPacket.colour).toMatchObject(colour)
expect(unpackedPacket.clientId).toBe(clientId)
})
expect(unpackedPacket.username).toBe(username);
expect(unpackedPacket.colour).toMatchObject(colour);
expect(unpackedPacket.clientId).toBe(clientId);
});

View File

@@ -1,46 +1,56 @@
import Color from 'color'
import { MessageTypes } from '../../src/common'
import { packers, unpackers } from '../../src/mapping'
import Color from "color";
import { MessageTypes } from "../../src/common";
import { packers, unpackers } from "../../src/mapping";
const KEY = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F])
const KEY = new Uint8Array([
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
0x0d, 0x0e, 0x0f,
]);
test('Create a user data response (0x0003) packet.', () => {
test("Create a user data response (0x0003) packet.", () => {
// Given
const username = 'Butlersaurus'
const colour = Color('#FF4000')
const clientId = 'Mercury'
const username = "Butlersaurus";
const colour = Color("#FF4000");
const clientId = "Mercury";
// When
const packedPacket = packers[MessageTypes.UserDataResponse](
{
username: username,
colour: colour,
clientId: clientId
clientId: clientId,
},
KEY
)
KEY,
);
// Then
// We can't check the contents of the data as it's encrypted with a random nonce.
// Check the message type and length.
expect(packedPacket.slice(0, 4)).toMatchObject(new Uint8Array([0x00, 0x03, 0x00, 0x3A]))
expect(packedPacket.slice(0, 4)).toMatchObject(
new Uint8Array([0x00, 0x03, 0x00, 0x67]),
);
// Check the total length is as expected.
expect(packedPacket.length).toBe(62)
})
expect(packedPacket.length).toBe(107);
});
test('Parse a user data response (0x0003).', () => {
test("Parse a user data response (0x0003).", () => {
// Given
const data = new Uint8Array([0, 12, 66, 117, 116, 108, 101, 114, 115, 97, 117, 114, 117, 115, 255, 64, 0, 0, 7, 77, 101, 114, 99, 117, 114, 121])
const username = 'Butlersaurus'
const colour = Color('#FF4000')
const clientId = 'Mercury'
const data = new Uint8Array([
0, 12, 66, 117, 116, 108, 101, 114, 115, 97, 117, 114, 117, 115, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 64, 0, 0, 7, 77, 101,
114, 99, 117, 114, 121, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
]);
const username = "Butlersaurus";
const colour = Color("#FF4000");
const clientId = "Mercury";
// When
const unpackedPacket = unpackers[MessageTypes.UserDataResponse](data)
const unpackedPacket = unpackers[MessageTypes.UserDataResponse](data);
// Then
expect(unpackedPacket.username).toBe(username)
expect(unpackedPacket.colour).toMatchObject(colour)
expect(unpackedPacket.clientId).toBe(clientId)
})
expect(unpackedPacket.username).toBe(username);
expect(unpackedPacket.colour).toMatchObject(colour);
expect(unpackedPacket.clientId).toBe(clientId);
});

View File

@@ -1,19 +1,19 @@
import { numberToUint16BE, numberToUint32BE } from '../../src/utilities/number'
import { numberToUint16BE, numberToUint32BE } from "../../src/utilities/number";
test('Test number conversion to Uint16 big endian buffer.', () => {
test("Test number conversion to Uint16 big endian buffer.", () => {
// When
const result = numberToUint16BE(1234)
const result = numberToUint16BE(1234);
// Then
const expectedResult = new Uint8Array([0x04, 0xd2])
expect(result).toMatchObject(expectedResult)
})
const expectedResult = new Uint8Array([0x04, 0xd2]);
expect(result).toMatchObject(expectedResult);
});
test('Test number conversion to Uint32 big endian buffer.', () => {
test("Test number conversion to Uint32 big endian buffer.", () => {
// When
const result = numberToUint32BE(123456)
const result = numberToUint32BE(123456);
// Then
const expectedResult = new Uint8Array([0x00, 0x01, 0xE2, 0x40])
expect(result).toMatchObject(expectedResult)
})
const expectedResult = new Uint8Array([0x00, 0x01, 0xe2, 0x40]);
expect(result).toMatchObject(expectedResult);
});

View File

@@ -1,230 +1,240 @@
import { SmartBuffer } from '../../src/utilities/smart-buffer'
import { SmartBuffer } from "../../src/utilities/smart-buffer";
test('Read a UInt16.', () => {
test("Read a UInt16.", () => {
// Given
const buffer = [0x30, 0x39]
const buffer = [0x30, 0x39];
// When
const smartBuffer = SmartBuffer.from(buffer)
const smartBuffer = SmartBuffer.from(buffer);
// Then
expect(smartBuffer.readUInt16()).toBe(12345)
})
expect(smartBuffer.readUInt16()).toBe(12345);
});
test('Read a UInt32.', () => {
test("Read a UInt32.", () => {
// Given
const buffer = [0x49, 0x96, 0x02, 0xD2]
const buffer = [0x49, 0x96, 0x02, 0xd2];
// When
const smartBuffer = SmartBuffer.from(buffer)
const smartBuffer = SmartBuffer.from(buffer);
// Then
expect(smartBuffer.readUInt32()).toBe(1234567890)
})
expect(smartBuffer.readUInt32()).toBe(1234567890);
});
test('Read a buffer.', () => {
test("Read a buffer.", () => {
// Given
const buffer = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
const buffer = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
// When
const smartBuffer = SmartBuffer.from(buffer)
const smartBuffer = SmartBuffer.from(buffer);
// Then
const result = smartBuffer.readBytes(4)
expect(result).toMatchObject(new Uint8Array([0, 1, 2, 3]))
})
const result = smartBuffer.readBytes(4);
expect(result).toMatchObject(new Uint8Array([0, 1, 2, 3]));
});
test('Read a UInt16 from an offset.', () => {
test("Read a UInt16 from an offset.", () => {
// Given
const buffer = [0x00, 0x00, 0x30, 0x39]
const buffer = [0x00, 0x00, 0x30, 0x39];
// When
const smartBuffer = SmartBuffer.from(buffer)
smartBuffer.cursor = 2
const smartBuffer = SmartBuffer.from(buffer);
smartBuffer.cursor = 2;
// Then
expect(smartBuffer.readUInt16()).toBe(12345)
})
expect(smartBuffer.readUInt16()).toBe(12345);
});
test('Read a UInt32 from an offset.', () => {
test("Read a UInt32 from an offset.", () => {
// Given
const buffer = [0x00, 0x00, 0x49, 0x96, 0x02, 0xD2]
const buffer = [0x00, 0x00, 0x49, 0x96, 0x02, 0xd2];
// When
const smartBuffer = SmartBuffer.from(buffer)
smartBuffer.cursor = 2
const smartBuffer = SmartBuffer.from(buffer);
smartBuffer.cursor = 2;
// Then
expect(smartBuffer.readUInt32()).toBe(1234567890)
})
expect(smartBuffer.readUInt32()).toBe(1234567890);
});
test('Read a buffer from an offset.', () => {
test("Read a buffer from an offset.", () => {
// Given
const buffer = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
const buffer = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
// When
const smartBuffer = SmartBuffer.from(buffer)
smartBuffer.cursor = 2
const smartBuffer = SmartBuffer.from(buffer);
smartBuffer.cursor = 2;
// Then
expect(smartBuffer.readBytes(4)).toMatchObject(new Uint8Array([2, 3, 4, 5]))
})
expect(smartBuffer.readBytes(4)).toMatchObject(new Uint8Array([2, 3, 4, 5]));
});
test('Write a UInt16.', () => {
test("Write a UInt16.", () => {
// Given
const smartBuffer = new SmartBuffer()
const smartBuffer = new SmartBuffer();
// When
smartBuffer.writeUInt16(12345)
smartBuffer.writeUInt16(12345);
// Then
expect(smartBuffer.data).toMatchObject(new Uint8Array([0x30, 0x39]))
})
expect(smartBuffer.data).toMatchObject(new Uint8Array([0x30, 0x39]));
});
test('Write a UInt32.', () => {
test("Write a UInt32.", () => {
// Given
const smartBuffer = new SmartBuffer()
const smartBuffer = new SmartBuffer();
// When
smartBuffer.writeUInt32(1234567890)
smartBuffer.writeUInt32(1234567890);
// Then
expect(smartBuffer.data).toMatchObject(new Uint8Array([0x49, 0x96, 0x02, 0xD2]))
})
expect(smartBuffer.data).toMatchObject(
new Uint8Array([0x49, 0x96, 0x02, 0xd2]),
);
});
test('Write a buffer.', () => {
test("Write a buffer.", () => {
// Given
const smartBuffer = new SmartBuffer()
const smartBuffer = new SmartBuffer();
// When
smartBuffer.writeBytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
smartBuffer.writeBytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
// Then
expect(smartBuffer.data).toMatchObject(new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))
})
expect(smartBuffer.data).toMatchObject(
new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
);
});
test('Write a UInt16 at an offset.', () => {
test("Write a UInt16 at an offset.", () => {
// Given
const smartBuffer = new SmartBuffer()
const smartBuffer = new SmartBuffer();
// When
smartBuffer.cursor = 2
smartBuffer.writeUInt16(12345)
smartBuffer.cursor = 2;
smartBuffer.writeUInt16(12345);
// Then
expect(smartBuffer.data).toMatchObject(new Uint8Array([0x00, 0x00, 0x30, 0x39]))
})
expect(smartBuffer.data).toMatchObject(
new Uint8Array([0x00, 0x00, 0x30, 0x39]),
);
});
test('Write a UInt32 at an offset.', () => {
test("Write a UInt32 at an offset.", () => {
// Given
const smartBuffer = new SmartBuffer()
const smartBuffer = new SmartBuffer();
// When
smartBuffer.cursor = 2
smartBuffer.writeUInt32(1234567890)
smartBuffer.cursor = 2;
smartBuffer.writeUInt32(1234567890);
// Then
expect(smartBuffer.data).toMatchObject(new Uint8Array([0x00, 0x00, 0x49, 0x96, 0x02, 0xD2]))
})
expect(smartBuffer.data).toMatchObject(
new Uint8Array([0x00, 0x00, 0x49, 0x96, 0x02, 0xd2]),
);
});
test('Write a buffer at an offset.', () => {
test("Write a buffer at an offset.", () => {
// Given
const smartBuffer = new SmartBuffer()
const smartBuffer = new SmartBuffer();
// When
smartBuffer.cursor = 2
smartBuffer.writeBytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
smartBuffer.cursor = 2;
smartBuffer.writeBytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
// Then
expect(smartBuffer.data).toMatchObject(new Uint8Array([0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))
})
expect(smartBuffer.data).toMatchObject(
new Uint8Array([0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
);
});
test('Cursor is correctly incremented after reading a UInt16.', () => {
test("Cursor is correctly incremented after reading a UInt16.", () => {
// Given
const buffer = new Uint8Array(4)
const buffer = new Uint8Array(4);
// When
const smartBuffer = SmartBuffer.from(buffer)
const smartBuffer = SmartBuffer.from(buffer);
// Then
smartBuffer.readUInt16()
expect(smartBuffer.cursor).toBe(2)
})
smartBuffer.readUInt16();
expect(smartBuffer.cursor).toBe(2);
});
test('Cursor is correctly incremented after reading a UInt32.', () => {
test("Cursor is correctly incremented after reading a UInt32.", () => {
// Given
const buffer = new Uint8Array(4)
const buffer = new Uint8Array(4);
// When
const smartBuffer = SmartBuffer.from(buffer)
const smartBuffer = SmartBuffer.from(buffer);
// Then
smartBuffer.readUInt32()
expect(smartBuffer.cursor).toBe(4)
})
smartBuffer.readUInt32();
expect(smartBuffer.cursor).toBe(4);
});
test('Cursor is correctly incremented after reading a buffer.', () => {
test("Cursor is correctly incremented after reading a buffer.", () => {
// Given
const buffer = new Uint8Array(8)
const buffer = new Uint8Array(8);
// When
const smartBuffer = SmartBuffer.from(buffer)
const smartBuffer = SmartBuffer.from(buffer);
// Then
smartBuffer.readBytes(4)
expect(smartBuffer.cursor).toBe(4)
})
smartBuffer.readBytes(4);
expect(smartBuffer.cursor).toBe(4);
});
test('Cursor is correctly incremented after writing a UInt16.', () => {
test("Cursor is correctly incremented after writing a UInt16.", () => {
// Given
const smartBuffer = new SmartBuffer()
const smartBuffer = new SmartBuffer();
// When
smartBuffer.writeUInt16(12345)
smartBuffer.writeUInt16(12345);
// Then
expect(smartBuffer.cursor).toBe(2)
})
expect(smartBuffer.cursor).toBe(2);
});
test('Cursor is correctly incremented after writing a UInt32.', () => {
test("Cursor is correctly incremented after writing a UInt32.", () => {
// Given
const smartBuffer = new SmartBuffer()
const smartBuffer = new SmartBuffer();
// When
smartBuffer.writeUInt32(1234567890)
smartBuffer.writeUInt32(1234567890);
// Then
expect(smartBuffer.cursor).toBe(4)
})
expect(smartBuffer.cursor).toBe(4);
});
test('Cursor is correctly incremented after writing a buffer.', () => {
test("Cursor is correctly incremented after writing a buffer.", () => {
// Given
const smartBuffer = new SmartBuffer()
const smartBuffer = new SmartBuffer();
// When
smartBuffer.writeBytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
smartBuffer.writeBytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
// Then
expect(smartBuffer.cursor).toBe(10)
})
expect(smartBuffer.cursor).toBe(10);
});
test('Seek to position below 0 throws range error.', () => {
test("Seek to position below 0 throws range error.", () => {
// When
const smartBuffer = new SmartBuffer()
const smartBuffer = new SmartBuffer();
// Then
expect(() => {
smartBuffer.cursor = -1
}).toThrow(RangeError)
})
smartBuffer.cursor = -1;
}).toThrow(RangeError);
});
test('Pad some data.', () => {
test("Pad some data.", () => {
// Given
const smartBuffer = new SmartBuffer()
const smartBuffer = new SmartBuffer();
// When
smartBuffer.pad(10)
smartBuffer.pad(10);
// Then
expect(smartBuffer.length).toBe(10)
})
expect(smartBuffer.length).toBe(10);
});

View File

@@ -1,19 +1,19 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"module": "commonjs", /* Specify what module code is generated. */
"rootDir": "src", /* Specify the root folder within your source files. */
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
"outDir": "dist", /* Specify an output folder for all emitted files. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"strict": true, /* Enable all strict type-checking options. */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
"declaration": true
"target": "esnext",
"module": "node20",
"moduleResolution": "node16",
"rootDir": "src",
"outDir": "dist",
"sourceMap": true,
"declaration": true,
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"isolatedModules": true,
"noEmitOnError": true
},
"exclude": [
"tests",
"dist"
]
"include": ["src/**/*"],
"exclude": ["tests", "dist"]
}