mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-05-25 16:24:37 +00:00
encoding/decoding of LGET/LNK
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
// SMP protocol commands and transmission format.
|
||||
// Mirrors: Simplex.Messaging.Protocol
|
||||
|
||||
import {
|
||||
Decoder, concatBytes,
|
||||
encodeBytes, decodeBytes,
|
||||
decodeLarge
|
||||
} from "@simplex-chat/xftp-web/dist/protocol/encoding.js"
|
||||
import {readTag, readSpace} from "@simplex-chat/xftp-web/dist/protocol/commands.js"
|
||||
|
||||
// -- Transmission encoding (Protocol.hs:2201-2203)
|
||||
// encodeTransmission_ v (CorrId corrId, queueId, command) =
|
||||
// smpEncode (corrId, queueId) <> encodeProtocol v command
|
||||
|
||||
export function encodeTransmission(corrId: Uint8Array, entityId: Uint8Array, command: Uint8Array): Uint8Array {
|
||||
return concatBytes(
|
||||
encodeBytes(new Uint8Array(0)), // empty auth
|
||||
encodeBytes(corrId),
|
||||
encodeBytes(entityId),
|
||||
command
|
||||
)
|
||||
}
|
||||
|
||||
// -- Transmission parsing (Protocol.hs:1629-1642)
|
||||
// For implySessId = True (v7+): no sessId on wire
|
||||
|
||||
export interface RawTransmission {
|
||||
corrId: Uint8Array
|
||||
entityId: Uint8Array
|
||||
command: Uint8Array
|
||||
}
|
||||
|
||||
export function decodeTransmission(d: Decoder): RawTransmission {
|
||||
const _auth = decodeBytes(d) // authenticator (empty for unsigned)
|
||||
const corrId = decodeBytes(d)
|
||||
const entityId = decodeBytes(d)
|
||||
const command = d.takeAll()
|
||||
return {corrId, entityId, command}
|
||||
}
|
||||
|
||||
// -- SMP command tags
|
||||
|
||||
const SPACE = 0x20
|
||||
|
||||
function ascii(s: string): Uint8Array {
|
||||
const buf = new Uint8Array(s.length)
|
||||
for (let i = 0; i < s.length; i++) buf[i] = s.charCodeAt(i)
|
||||
return buf
|
||||
}
|
||||
|
||||
// -- LGET command (Protocol.hs:1709)
|
||||
// No parameters. EntityId carries LinkId in transmission.
|
||||
|
||||
export function encodeLGET(): Uint8Array {
|
||||
return ascii("LGET")
|
||||
}
|
||||
|
||||
// -- LNK response (Protocol.hs:1834)
|
||||
// LNK sId d -> e (LNK_, ' ', sId, d)
|
||||
// where d = (EncFixedDataBytes, EncUserDataBytes), both Large-encoded
|
||||
|
||||
export interface LNKResponse {
|
||||
senderId: Uint8Array
|
||||
encFixedData: Uint8Array
|
||||
encUserData: Uint8Array
|
||||
}
|
||||
|
||||
export function decodeLNK(d: Decoder): LNKResponse {
|
||||
const senderId = decodeBytes(d)
|
||||
const encFixedData = decodeLarge(d)
|
||||
const encUserData = decodeLarge(d)
|
||||
return {senderId, encFixedData, encUserData}
|
||||
}
|
||||
|
||||
// -- Response dispatch (same pattern as xftp-web decodeResponse)
|
||||
|
||||
export type SMPResponse =
|
||||
| {type: "LNK", response: LNKResponse}
|
||||
| {type: "OK"}
|
||||
| {type: "ERR", message: string}
|
||||
|
||||
export function decodeResponse(d: Decoder): SMPResponse {
|
||||
const tag = readTag(d)
|
||||
switch (tag) {
|
||||
case "LNK": {
|
||||
readSpace(d)
|
||||
return {type: "LNK", response: decodeLNK(d)}
|
||||
}
|
||||
case "OK": return {type: "OK"}
|
||||
case "ERR": {
|
||||
readSpace(d)
|
||||
return {type: "ERR", message: readTag(d)}
|
||||
}
|
||||
default: throw new Error("unknown SMP response: " + tag)
|
||||
}
|
||||
}
|
||||
+67
-12
@@ -9,7 +9,6 @@
|
||||
module SMPWebTests (smpWebTests) where
|
||||
|
||||
import qualified Data.ByteString as B
|
||||
import Data.Word (Word16)
|
||||
import Simplex.Messaging.Encoding
|
||||
import Test.Hspec hiding (it)
|
||||
import Util
|
||||
@@ -21,17 +20,73 @@ smpWebDir = "smp-web"
|
||||
callNode :: String -> IO B.ByteString
|
||||
callNode = callNode_ smpWebDir
|
||||
|
||||
impEnc :: String
|
||||
impEnc = "import { encodeBytes, encodeWord16 } from '@simplex-chat/xftp-web/dist/protocol/encoding.js';"
|
||||
impProto :: String
|
||||
impProto = "import { encodeTransmission, decodeTransmission, encodeLGET, decodeLNK, decodeResponse } from './dist/protocol.js';"
|
||||
<> "import { Decoder } from '@simplex-chat/xftp-web/dist/protocol/encoding.js';"
|
||||
|
||||
smpWebTests :: SpecWith ()
|
||||
smpWebTests = describe "SMP Web Client" $ do
|
||||
describe "xftp-web imports" $ do
|
||||
it "encodeBytes via xftp-web" $ do
|
||||
let val = "hello" :: B.ByteString
|
||||
actual <- callNode $ impEnc <> jsOut ("encodeBytes(" <> jsUint8 val <> ")")
|
||||
actual `shouldBe` smpEncode val
|
||||
it "encodeWord16 via xftp-web" $ do
|
||||
let val = 12345 :: Word16
|
||||
actual <- callNode $ impEnc <> jsOut ("encodeWord16(" <> show val <> ")")
|
||||
actual `shouldBe` smpEncode val
|
||||
describe "protocol" $ do
|
||||
describe "transmission" $ do
|
||||
it "encodeTransmission matches Haskell" $ do
|
||||
let corrId = "1"
|
||||
entityId = B.pack [1..24]
|
||||
command = "LGET"
|
||||
hsEncoded = smpEncode (corrId :: B.ByteString, entityId :: B.ByteString) <> command
|
||||
tsEncoded <- callNode $ impProto
|
||||
<> jsOut ("encodeTransmission("
|
||||
<> jsUint8 corrId <> ","
|
||||
<> jsUint8 entityId <> ","
|
||||
<> "new Uint8Array([0x4C,0x47,0x45,0x54])" -- "LGET"
|
||||
<> ")")
|
||||
-- TS encodes with empty auth prefix, HS encodeTransmission_ doesn't include auth
|
||||
-- So TS output = [0x00] ++ hsEncoded
|
||||
tsEncoded `shouldBe` (B.singleton 0 <> hsEncoded)
|
||||
|
||||
it "decodeTransmission parses Haskell-encoded" $ do
|
||||
let corrId = "abc"
|
||||
entityId = B.pack [10..33]
|
||||
command = "TEST"
|
||||
encoded = smpEncode (B.empty :: B.ByteString) -- empty auth
|
||||
<> smpEncode corrId
|
||||
<> smpEncode entityId
|
||||
<> command
|
||||
-- TS decodes and returns corrId ++ entityId ++ command concatenated with length prefixes
|
||||
tsResult <- callNode $ impProto
|
||||
<> "const t = decodeTransmission(new Decoder(" <> jsUint8 encoded <> "));"
|
||||
<> jsOut ("new Uint8Array([...t.corrId, ...t.entityId, ...t.command])")
|
||||
tsResult `shouldBe` (corrId <> entityId <> command)
|
||||
|
||||
describe "LGET" $ do
|
||||
it "encodeLGET produces correct bytes" $ do
|
||||
tsResult <- callNode $ impProto <> jsOut "encodeLGET()"
|
||||
tsResult `shouldBe` "LGET"
|
||||
|
||||
describe "LNK" $ do
|
||||
it "decodeLNK parses correctly" $ do
|
||||
let senderId = B.pack [1..24]
|
||||
fixedData = B.pack [100..110]
|
||||
userData = B.pack [200..220]
|
||||
encoded = smpEncode senderId <> smpEncode (Large fixedData) <> smpEncode (Large userData)
|
||||
tsResult <- callNode $ impProto
|
||||
<> "const r = decodeLNK(new Decoder(" <> jsUint8 encoded <> "));"
|
||||
<> jsOut ("new Uint8Array([...r.senderId, ...r.encFixedData, ...r.encUserData])")
|
||||
tsResult `shouldBe` (senderId <> fixedData <> userData)
|
||||
|
||||
describe "decodeResponse" $ do
|
||||
it "decodes LNK response" $ do
|
||||
let senderId = B.pack [1..24]
|
||||
fixedData = B.pack [100..110]
|
||||
userData = B.pack [200..220]
|
||||
commandBytes = "LNK " <> smpEncode senderId <> smpEncode (Large fixedData) <> smpEncode (Large userData)
|
||||
tsResult <- callNode $ impProto
|
||||
<> "const r = decodeResponse(new Decoder(" <> jsUint8 commandBytes <> "));"
|
||||
<> "if (r.type !== 'LNK') throw new Error('expected LNK, got ' + r.type);"
|
||||
<> jsOut ("new Uint8Array([...r.response.senderId])")
|
||||
tsResult `shouldBe` senderId
|
||||
|
||||
it "decodes OK response" $ do
|
||||
tsResult <- callNode $ impProto
|
||||
<> "const r = decodeResponse(new Decoder(new Uint8Array([0x4F, 0x4B])));" -- "OK"
|
||||
<> jsOut ("new Uint8Array([r.type === 'OK' ? 1 : 0])")
|
||||
tsResult `shouldBe` B.singleton 1
|
||||
|
||||
@@ -81,7 +81,7 @@ export function encodePING(): Uint8Array { return ascii("PING") }
|
||||
|
||||
// -- Response decoding
|
||||
|
||||
function readTag(d: Decoder): string {
|
||||
export function readTag(d: Decoder): string {
|
||||
const start = d.offset()
|
||||
while (d.remaining() > 0) {
|
||||
if (d.buf[d.offset()] === 0x20 || d.buf[d.offset()] === 0x0a) break
|
||||
@@ -92,7 +92,7 @@ function readTag(d: Decoder): string {
|
||||
return s
|
||||
}
|
||||
|
||||
function readSpace(d: Decoder): void {
|
||||
export function readSpace(d: Decoder): void {
|
||||
if (d.anyByte() !== 0x20) throw new Error("expected space")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user