Compare commits
16 Commits
be9b84de81
...
v1.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 68f31018ef | |||
| 356b70474c | |||
| ed1dc6870e | |||
| 22999c34f9 | |||
| 61b7c50868 | |||
| 15fa2b1608 | |||
| 61082d32e7 | |||
| dd8e6ee49f | |||
| 02c9cfdabc | |||
| 695964a636 | |||
| eb620087c9 | |||
|
|
8a6a73206e | ||
|
|
26af3b8b69 | ||
|
|
386e8c3ed1 | ||
|
|
3a163df991 | ||
|
|
0b19b83271 |
52
.drone.yml
52
.drone.yml
@@ -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
57
.gitea/workflows/ci..yml
Normal 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
1
.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
@3t:registry=https://git.3t.network/api/packages/3t.network/npm/
|
||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@@ -1,5 +1,3 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"orta.vscode-jest"
|
||||
]
|
||||
"recommendations": ["orta.vscode-jest"]
|
||||
}
|
||||
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@@ -8,13 +8,9 @@
|
||||
"type": "pwa-node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"program": "${workspaceFolder}/dist/index.js",
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/**/*.js"
|
||||
]
|
||||
"outFiles": ["${workspaceFolder}/**/*.js"]
|
||||
}
|
||||
]
|
||||
}
|
||||
4
.vscode/tasks.json
vendored
4
.vscode/tasks.json
vendored
@@ -5,9 +5,7 @@
|
||||
"type": "typescript",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"option": "watch",
|
||||
"problemMatcher": [
|
||||
"$tsc-watch"
|
||||
],
|
||||
"problemMatcher": ["$tsc-watch"],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
|
||||
208
BENNC_PROTOCOL_SPEC.md
Normal file
208
BENNC_PROTOCOL_SPEC.md
Normal 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
60
CLAUDE.md
Normal 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
197
README.md
@@ -1,56 +1,199 @@
|
||||
# BENNC-JS
|
||||
[](https://drone.jacknet.io/TerribleCodeClub/bennc-js) [](https://standardjs.com)
|
||||
|
||||
An implementation of the [BENNC](https://wiki.jacknet.io/books/simontech/chapter/bennc) client specification.
|
||||
[](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.
|
||||
|
||||
[](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
11625
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@@ -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
211
src/BenncClient.ts
Normal 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 }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
20
src/index.ts
20
src/index.ts
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
15
src/messages/history.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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)
|
||||
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
|
||||
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 length(): number {
|
||||
return this._data.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the length of the smart buffer data.
|
||||
*/
|
||||
set length (length: number) {
|
||||
this._data.length = length
|
||||
set length(length: number) {
|
||||
this._data.length = length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current cursor position of the smart buffer.
|
||||
*/
|
||||
get cursor (): number {
|
||||
return this._cursor
|
||||
get cursor(): number {
|
||||
return this._cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cursor position of the smart buffer.
|
||||
*/
|
||||
set cursor (position: number) {
|
||||
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,22 +66,23 @@ 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))
|
||||
pad(length: number): void {
|
||||
this._data.push(...Array<number>(length).fill(0));
|
||||
this.cursor += length;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,8 +91,8 @@ export class SmartBuffer {
|
||||
* @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))
|
||||
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
|
||||
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
|
||||
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)
|
||||
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
|
||||
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
|
||||
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
|
||||
writeBytes(buffer: number[] | Uint8Array): void {
|
||||
this.splice(this.cursor, buffer.length, ...buffer);
|
||||
this.cursor += buffer.length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user