This commit is contained in:
2025-09-03 19:18:44 +01:00
parent eb620087c9
commit 695964a636
29 changed files with 631 additions and 571 deletions

View File

@@ -39,7 +39,7 @@ steps:
- test
- build
when:
event:
event:
- tag
settings:
base_url: https://git.jacknet.io
@@ -49,4 +49,4 @@ steps:
- bennc-m.zip
checksum:
- md5
- sha256
- sha256

View File

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

34
.vscode/launch.json vendored
View File

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

View File

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

32
.vscode/tasks.json vendored
View File

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

View File

@@ -12,11 +12,11 @@
## Connection Endpoints
| Protocol | Endpoint |
|----------|----------|
| TCP | `chat.3t.network:10009` |
| Protocol | Endpoint |
| --------------- | --------------------------------- |
| TCP | `chat.3t.network:10009` |
| WebSocket (TLS) | `wss://chat.3t.network:443/BENNC` |
| WebSocket | `ws://chat.3t.network:80/BENNC` |
| WebSocket | `ws://chat.3t.network:80/BENNC` |
## Core Requirements
@@ -30,6 +30,7 @@
## Message Structure
**Client → Server:**
```
┌─────────────┬─────────────┬─────────────────────────┐
│ Message Type│ Length │ Data │
@@ -38,6 +39,7 @@
```
**Server → Client:**
```
┌─────────────┬─────────────┬─────────────┬─────────────────────┐
│ Message Type│ Sender ID │ Length │ Data │
@@ -45,7 +47,7 @@
└─────────────┴─────────────┴─────────────┴─────────────────────┘
```
*Sender ID is randomly generated per connection to identify message source.*
_Sender ID is randomly generated per connection to identify message source._
## Encryption Implementation
@@ -67,22 +69,23 @@
## 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 |
| 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)
@@ -90,6 +93,7 @@ Subscribe to receive messages of specified type. Must resubscribe after disconne
---
### Basic Message (0x0001)
UTF-8 chat messages. 16-byte nonce + encrypted data ≤ 1000 bytes total.
**Data:** Encrypted UTF-8 string
@@ -97,9 +101,11 @@ UTF-8 chat messages. 16-byte nonce + encrypted data ≤ 1000 bytes total.
---
### 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)
@@ -109,9 +115,11 @@ Request user information from all users. Clients should respond with User Data R
---
### 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)
@@ -121,6 +129,7 @@ Send user information in response to Request User Data (0x0002).
---
### 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
@@ -128,6 +137,7 @@ Prevent connection timeout when idle. Send every 30 seconds when no other traffi
---
### Unsubscribe (0xFFFF)
Stop receiving messages of specified type.
**Data:** Message type to unsubscribe from (2 bytes, big-endian)
@@ -135,9 +145,11 @@ Stop receiving messages of specified type.
---
### 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
@@ -145,12 +157,14 @@ Multi-packet messages for long text with markdown formatting and ZSTD compressio
- 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)
@@ -160,12 +174,14 @@ Edit or delete existing Advanced Text messages. Use same Message ID to replace e
## 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
@@ -173,6 +189,7 @@ Edit or delete existing Advanced Text messages. Use same Message ID to replace e
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
@@ -188,4 +205,4 @@ Edit or delete existing Advanced Text messages. Use same Message ID to replace e
- **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.
This is a documentation-only repository. The protocol specification is final and changes are not permitted.

View File

@@ -30,7 +30,7 @@ The codebase implements the BENNC protocol specification with the following stru
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
- `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
@@ -47,7 +47,7 @@ Each BENNC message type has its own module in `src/messages/`:
### Utilities
- `src/utilities/number.ts` - Big-endian number conversion functions (`numberToUint16BE`, `numberToUint32BE`)
- `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
@@ -57,4 +57,4 @@ Each BENNC message type has its own module in `src/messages/`:
- **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.
The codebase serves as a complete client-side implementation for connecting to BENNC chat servers via TCP or WebSocket protocols.

View File

@@ -1,4 +1,5 @@
# BENNC-JS
[![Build Status](https://drone.jacknet.io/api/badges/TerribleCodeClub/bennc-js/status.svg)](https://drone.jacknet.io/TerribleCodeClub/bennc-js)
An implementation of the [BENNC](https://wiki.jacknet.io/books/simontech/chapter/bennc) client specification.
@@ -8,15 +9,18 @@ An implementation of the [BENNC](https://wiki.jacknet.io/books/simontech/chapter
To build the BENNC-JS library, first clone this repository.
Run the following commands from the root of the repository:
```bash
$ npm install
$ npm run build
```
The build output will be saved to the `dist` directory.
## Development instructions
Requirements:
- The latest LTS builds of Node and npm.
Follow the instructions below to lint, test and build BENNC-JS.
@@ -34,6 +38,7 @@ $ npm run lint
$ npm install
$ npm run test
```
#### Build
```bash
@@ -43,7 +48,7 @@ $ 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 contains the necessary configuration files to debug, test and build BENNC-JS using only Visual Studio Code.
Run the build task (`Ctrl+Shift+B` or `⇧⌘B`) to automatically compile the Typescript source files in the background.

View File

@@ -1,9 +1,9 @@
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 { 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";

View File

@@ -1,12 +1,12 @@
import { encrypt } from 'romulus-js'
import { DEFAULT_KEY, MessageTypes } from '../common'
import { numberToUint16BE } from '../utilities/number'
import { packOutgoingPacket } from './packet'
import { encrypt } from "romulus-js";
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,12 +15,15 @@ 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,
});
}
/**
@@ -28,6 +31,6 @@ export function packBasicMessage (message: Uint8Array, key: Uint8Array = DEFAULT
* @param data The data section of an incoming basic message (0x0001) message.
* @returns An encrypted unpacked basic message (0x0001) message.
*/
export function unpackBasicMessage (data: Uint8Array): Uint8Array {
return data
export function unpackBasicMessage(data: Uint8Array): Uint8Array {
return data;
}

View File

@@ -1,16 +1,16 @@
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)
})
data: new Uint8Array(0),
});
}

View File

@@ -1,16 +1,16 @@
import { MAX_DATA_LENGTH } from '../common'
import { numberToUint16BE } from '../utilities/number'
import { SmartBuffer } from '../utilities/smart-buffer'
import { MAX_DATA_LENGTH } from "../common";
import { numberToUint16BE } from "../utilities/number";
import { SmartBuffer } from "../utilities/smart-buffer";
export interface IncomingPacket {
messageType: number
senderId: number
data: Uint8Array
messageType: number;
senderId: number;
data: Uint8Array;
}
export interface OutgoingPacket {
messageType: Uint8Array
data: Uint8Array
messageType: Uint8Array;
data: Uint8Array;
}
/**
@@ -18,19 +18,21 @@ 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 {
// 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}.`)
throw RangeError(
`Specified data of length ${outgoingPacket.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(outgoingPacket.data.length));
buffer.writeBytes(outgoingPacket.data);
return buffer.data
return buffer.data;
}
/**
@@ -38,17 +40,19 @@ export function packOutgoingPacket (outgoingPacket: OutgoingPacket): Uint8Array
* @param incomingPacket The incoming buffer from a WebSocket to unpack.
* @returns The unpacked data.
*/
export function unpackIncomingPacket (incomingPacket: ArrayBuffer): IncomingPacket {
const buffer = SmartBuffer.from(incomingPacket)
export function unpackIncomingPacket(
incomingPacket: ArrayBuffer,
): IncomingPacket {
const buffer = SmartBuffer.from(incomingPacket);
const messageType = buffer.readUInt16()
const senderId = buffer.readUInt32()
const length = buffer.readUInt16()
const data = buffer.readBytes(length)
const messageType = buffer.readUInt16();
const senderId = buffer.readUInt32();
const length = buffer.readUInt16();
const data = buffer.readBytes(length);
return {
messageType: messageType,
senderId: senderId,
data: data
}
data: data,
};
}

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
import Color from 'color'
import { encrypt } from 'romulus-js'
import { DEFAULT_KEY, MessageTypes } from '../common'
import { numberToUint16BE } from '../utilities/number'
import { SmartBuffer } from '../utilities/smart-buffer'
import { packOutgoingPacket } from './packet'
import Color from "color";
import { encrypt } from "romulus-js";
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,33 +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.pad(32 - username.length)
packedData.writeBytes(colour)
packedData.writeBytes(clientIdLength)
packedData.writeBytes(clientId)
packedData.pad(32 - clientId.length)
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,
});
}
/**
@@ -53,23 +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(data);
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 usernameLength = packedData.readUInt16();
const username = packedData.readBytes(usernameLength);
packedData.cursor = 34;
const colour = packedData.readBytes(3);
const clientIdLength = packedData.readUInt16();
const clientId = packedData.readBytes(clientIdLength);
const decoder = new TextDecoder()
const decoder = new TextDecoder();
// Return data in correct format.
return {
username: decoder.decode(username),
colour: Color.rgb(colour),
clientId: decoder.decode(clientId)
}
clientId: decoder.decode(clientId),
};
}

View File

@@ -1,16 +1,16 @@
import Color from 'color'
import { encrypt } from 'romulus-js'
import { DEFAULT_KEY, MessageTypes } from '../common'
import { numberToUint16BE } from '../utilities/number'
import { SmartBuffer } from '../utilities/smart-buffer'
import { packOutgoingPacket } from './packet'
import Color from "color";
import { encrypt } from "romulus-js";
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,32 +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.pad(32 - username.length)
packedData.writeBytes(colour)
packedData.writeBytes(clientIdLength)
packedData.writeBytes(clientId)
packedData.pad(32 - clientId.length)
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,
});
}
/**
@@ -52,23 +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(data);
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 usernameLength = packedData.readUInt16();
const username = packedData.readBytes(usernameLength);
packedData.cursor = 34;
const colour = packedData.readBytes(3);
const clientIdLength = packedData.readUInt16();
const clientId = packedData.readBytes(clientIdLength);
const decoder = new TextDecoder()
const decoder = new TextDecoder();
// Return data in correct format.
return {
username: decoder.decode(username),
colour: Color.rgb(colour),
clientId: decoder.decode(clientId)
}
clientId: decoder.decode(clientId),
};
}

View File

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

View File

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

View File

@@ -1,36 +1,37 @@
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 data = new Uint8Array([1, 2, 3, 4]);
// When
const unpackedPacket = unpackers[MessageTypes.Basic](data)
const unpackedPacket = unpackers[MessageTypes.Basic](data);
// Then
expect(unpackedPacket)
expect(unpackedPacket).toMatchObject(data)
})
expect(unpackedPacket);
expect(unpackedPacket).toMatchObject(data);
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,52 +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, 0x67]))
expect(packedPacket.slice(0, 4)).toMatchObject(
new Uint8Array([0x00, 0x02, 0x00, 0x67]),
);
// Check the total length is as expected.
expect(packedPacket.length).toBe(107)
})
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, 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'
0, 12, 66, 117, 116, 108, 101, 114, 115, 97, 117, 114, 117, 115, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 64, 0, 0, 7, 77, 101,
114, 99, 117, 114, 121, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
]);
const username = "Butlersaurus";
const colour = Color("#FF4000");
const clientId = "Mercury";
// When
const unpackedPacket = unpackers[MessageTypes.UserDataRequest](data)
const unpackedPacket = unpackers[MessageTypes.UserDataRequest](data);
// Then
expect(unpackedPacket.username).toBe(username)
expect(unpackedPacket.colour).toMatchObject(colour)
expect(unpackedPacket.clientId).toBe(clientId)
})
expect(unpackedPacket.username).toBe(username);
expect(unpackedPacket.colour).toMatchObject(colour);
expect(unpackedPacket.clientId).toBe(clientId);
});

View File

@@ -1,52 +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, 0x67]))
expect(packedPacket.slice(0, 4)).toMatchObject(
new Uint8Array([0x00, 0x03, 0x00, 0x67]),
);
// Check the total length is as expected.
expect(packedPacket.length).toBe(107)
})
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, 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'
0, 12, 66, 117, 116, 108, 101, 114, 115, 97, 117, 114, 117, 115, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 64, 0, 0, 7, 77, 101,
114, 99, 117, 114, 121, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
]);
const username = "Butlersaurus";
const colour = Color("#FF4000");
const clientId = "Mercury";
// When
const unpackedPacket = unpackers[MessageTypes.UserDataResponse](data)
const unpackedPacket = unpackers[MessageTypes.UserDataResponse](data);
// Then
expect(unpackedPacket.username).toBe(username)
expect(unpackedPacket.colour).toMatchObject(colour)
expect(unpackedPacket.clientId).toBe(clientId)
})
expect(unpackedPacket.username).toBe(username);
expect(unpackedPacket.colour).toMatchObject(colour);
expect(unpackedPacket.clientId).toBe(clientId);
});

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": []
}
}

View File

@@ -1,19 +1,16 @@
{
"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. */
"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
},
"exclude": [
"tests",
"dist"
]
"exclude": ["tests", "dist"]
}