OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / forward-relay-encoding.test.js

150 lines · javascript ref:
import test from 'brittle'
import b4a from 'b4a'
import c from 'compact-encoding'
import {
  MAX_FORWARD_DATA_MSG_BYTES,
  MAX_FORWARD_STATUS_MESSAGE_BYTES,
  forwardOpenEncoding,
  forwardDataEncoding,
  forwardStatusEncoding,
  forwardCloseEncoding
} from 'p2p-hiverelay/core/protocol/forward-relay.js'

function encodeFrame (encoding, msg) {
  const state = { start: 0, end: 0, buffer: null }
  encoding.preencode(state, msg)
  state.buffer = b4a.alloc(state.end)
  encoding.encode(state, msg)
  return state.buffer
}

function declaredBytesFrame (len, payload = null) {
  const state = { start: 0, end: 0, buffer: null }
  c.uint.preencode(state, len)
  state.end += payload ? payload.length : 0
  state.buffer = b4a.alloc(state.end)
  c.uint.encode(state, len)
  if (payload) payload.copy(state.buffer, state.start)
  return state.buffer
}

function declaredStatusFrame (code, len, payload = null) {
  const state = { start: 0, end: 0, buffer: null }
  c.uint.preencode(state, code)
  c.uint.preencode(state, len)
  state.end += payload ? payload.length : 0
  state.buffer = b4a.alloc(state.end)
  c.uint.encode(state, code)
  c.uint.encode(state, len)
  if (payload) payload.copy(state.buffer, state.start)
  return state.buffer
}

test('forward-relay encodings: round-trip valid frames', (t) => {
  const target = b4a.alloc(32, 0x11)
  const data = b4a.from('hello')

  t.alike(forwardOpenEncoding.decode({ buffer: encodeFrame(forwardOpenEncoding, { target }), start: 0, end: 32 }), { target })

  const dataFrame = encodeFrame(forwardDataEncoding, { data })
  const dataOut = forwardDataEncoding.decode({ buffer: dataFrame, start: 0, end: dataFrame.length })
  t.alike(dataOut, { data })

  const statusFrame = encodeFrame(forwardStatusEncoding, { code: 0, message: 'ok' })
  t.alike(forwardStatusEncoding.decode({ buffer: statusFrame, start: 0, end: statusFrame.length }), { code: 0, message: 'ok' })

  const closeFrame = encodeFrame(forwardCloseEncoding, { reason: 2 })
  t.alike(forwardCloseEncoding.decode({ buffer: closeFrame, start: 0, end: closeFrame.length }), { reason: 2 })
})

test('forward-relay encodings: reject oversized outbound frames before allocation growth', (t) => {
  const dataState = { start: 0, end: 0, buffer: null }
  t.exception(() => {
    forwardDataEncoding.preencode(dataState, { data: b4a.alloc(MAX_FORWARD_DATA_MSG_BYTES + 1) })
  }, /frame too large/, 'data preencode rejects oversize')
  t.is(dataState.end, 0, 'data preencode does not grow state.end')

  const statusState = { start: 0, end: 0, buffer: null }
  t.exception(() => {
    forwardStatusEncoding.preencode(statusState, { code: 0, message: 'x'.repeat(MAX_FORWARD_STATUS_MESSAGE_BYTES + 1) })
  }, /status message too large/, 'status preencode rejects oversize')
  t.is(statusState.end, 0, 'status preencode does not grow state.end')

  const badCodeState = { start: 0, end: 0, buffer: null }
  t.exception(() => {
    forwardStatusEncoding.preencode(badCodeState, { code: -1, message: 'bad' })
  }, /status code/, 'status preencode rejects invalid code')
  t.is(badCodeState.end, 0, 'invalid status code does not grow state.end')

  const badCloseState = { start: 0, end: 0, buffer: null }
  t.exception(() => {
    forwardCloseEncoding.preencode(badCloseState, { reason: -1 })
  }, /close reason/, 'close preencode rejects invalid reason')
  t.is(badCloseState.end, 0, 'invalid close reason does not grow state.end')
})

test('forward-relay encodings: reject oversized declared inbound frames before decode materializes them', (t) => {
  const dataFrame = declaredBytesFrame(MAX_FORWARD_DATA_MSG_BYTES + 1)
  let dataOut = null
  t.execution(() => {
    dataOut = forwardDataEncoding.decode({ buffer: dataFrame, start: 0, end: dataFrame.length })
  }, 'oversized declared data frame does not throw')
  t.is(dataOut.data, null)
  t.is(dataOut.error, 'frame too large')

  const statusFrame = declaredStatusFrame(0, MAX_FORWARD_STATUS_MESSAGE_BYTES + 1)
  let statusOut = null
  t.execution(() => {
    statusOut = forwardStatusEncoding.decode({ buffer: statusFrame, start: 0, end: statusFrame.length })
  }, 'oversized declared status frame does not throw')
  t.is(statusOut.code, 4)
  t.is(statusOut.error, 'status message too large')
})

test('forward-relay encodings: reject malformed and truncated frames without throwing', (t) => {
  const malformedOpen = b4a.alloc(31)
  let openOut = null
  t.execution(() => {
    openOut = forwardOpenEncoding.decode({ buffer: malformedOpen, start: 0, end: malformedOpen.length })
  }, 'truncated open frame does not throw')
  t.is(openOut.target, null)
  t.is(openOut.error, 'malformed target')

  const truncatedData = declaredBytesFrame(32)
  let dataOut = null
  t.execution(() => {
    dataOut = forwardDataEncoding.decode({ buffer: truncatedData, start: 0, end: truncatedData.length })
  }, 'truncated data frame does not throw')
  t.is(dataOut.data, null)
  t.is(dataOut.error, 'malformed data')

  const truncatedStatus = declaredStatusFrame(0, 32)
  let statusOut = null
  t.execution(() => {
    statusOut = forwardStatusEncoding.decode({ buffer: truncatedStatus, start: 0, end: truncatedStatus.length })
  }, 'truncated status frame does not throw')
  t.is(statusOut.code, 4)
  t.is(statusOut.error, 'malformed status')
})

test('forward-relay encodings: reject invalid decode state without throwing', (t) => {
  const frame = encodeFrame(forwardDataEncoding, { data: b4a.from('ok') })
  const cases = [
    { name: 'missing state', state: null },
    { name: 'missing buffer', state: { start: 0, end: 0 } },
    { name: 'negative start', state: { buffer: frame, start: -1, end: frame.length } },
    { name: 'end before start', state: { buffer: frame, start: 4, end: 2 } },
    { name: 'end past buffer', state: { buffer: frame, start: 0, end: frame.length + 1 } },
    { name: 'unsafe start', state: { buffer: frame, start: Number.MAX_SAFE_INTEGER + 1, end: frame.length } }
  ]

  for (const { name, state } of cases) {
    let result = null
    t.execution(() => {
      result = forwardDataEncoding.decode(state)
    }, `${name} does not throw`)
    t.is(result.data, null, `${name} data is null`)
    t.is(result.error, 'malformed data', `${name} error`)
  }
})