Skip to content

Commit

Permalink
Hpack context (#4)
Browse files Browse the repository at this point in the history
* not even work in progress

* wip

* wip

* easy way first

* wip

* wip

* wip

* wip

* wip
  • Loading branch information
dtudury committed Mar 6, 2019
1 parent c6f2a79 commit e920b55
Show file tree
Hide file tree
Showing 10 changed files with 246 additions and 7 deletions.
File renamed without changes.
2 changes: 1 addition & 1 deletion lib/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
exports.huffmanCodes = require('./huffmanCodes')

// --- HPack predefined static list of header fields ---
exports.hPackStaticTable = require('./hPackStaticTable')
exports.hpackStaticTable = require('./hpackStaticTable')

// --- Http2Lite events
exports.FRAME = 'frame'
Expand Down
40 changes: 40 additions & 0 deletions lib/utils/HpackContexts/HpackContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @typedef {Object} HeaderField
* @property {string} name
* @property {string} value
*/

module.exports = class HpackContext {
constructor (maximumTableSize = 4096) {
this._maximumTableSize = maximumTableSize
this._tableSize = 0
this._dynamicTable = []
}

/**
* @type {Number}
*/
get maximumTableSize () {
return this._maximumTableSize
}

set maximumTableSize (maximumTableSize) {
this._maximumTableSize = maximumTableSize
this._evict()
}

_addEntry (name, value) {
const entrySize = name.length + value.length + 32
this._evict(entrySize)
this._dynamicTable.unshift([name, value])
this._tableSize += entrySize
}

_evict (extraSpace = 0) {
const targetSize = Math.max(0, this._maximumTableSize - extraSpace)
while (this._tableSize > targetSize) {
let evictedEntry = this._dynamicTable.pop()
this._tableSize -= (evictedEntry[0].length + evictedEntry[1].length + 32)
}
}
}
65 changes: 65 additions & 0 deletions lib/utils/HpackContexts/HpackDecodingContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const { hpackStaticTable } = require('../../constants')
const { stringFromUi8a } = require('../ui8aHelpers')
const { decodeHeader, decodeHuffman } = require('../codecs')
const { INDEXED, INCREMENTAL, SIZE_UPDATE } = require('../../constants')
const HpackContext = require('./HpackContext')

/**
* @typedef {Object} HeaderField
* @property {string} name
* @property {string} value
*/

module.exports = class HpackDecodingContext extends HpackContext {
_combinedTable (index) {
if (index <= hpackStaticTable.length) {
return hpackStaticTable[index - 1]
} else {
return this._dynamicTable[index - hpackStaticTable.length - 1]
}
}

/**
* @param {Uint8Array} headerBlock - An ordered list of header field representations, which, when decoded, yields a complete header list.
* @returns {Array.<HeaderField>}
*/
decode (headerBlock) {
const headerFields = []
let byteOffset = 0
while (byteOffset < headerBlock.length) {
let name, value
let headerField
let header = decodeHeader(headerBlock, byteOffset)
let kind = header.kind
byteOffset = header.byteOffset
if (kind === INDEXED) {
[name, value] = this._combinedTable(header.index)
headerField = { name, value, kind }
} else if (kind === SIZE_UPDATE) {
this.maximumTableSize = header.maxSize
headerField = { maxSize: header.maxSize, kind }
} else {
if (header.index) {
[name] = this._combinedTable(header.index)
} else {
name = decodeString(header.name)
}
value = decodeString(header.value)
if (kind === INCREMENTAL) {
this._addEntry(name, value)
}
headerField = { name, value, kind }
}
headerFields.push(headerField)
}
return headerFields
}
}

function decodeString (encodedString) {
if (encodedString.isHuffmanEncoded) {
return stringFromUi8a(decodeHuffman(encodedString.stringLiteral))
} else {
return stringFromUi8a(encodedString.stringLiteral)
}
}
89 changes: 89 additions & 0 deletions lib/utils/HpackContexts/HpackEncodingContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
const { hpackStaticTable } = require('../../constants')
const { concat, ui8aFromString } = require('../ui8aHelpers')
const { encodeHeader, encodeStringLiteral, encodeHuffman } = require('../codecs')
const { INDEXED, INCREMENTAL, SIZE_UPDATE } = require('../../constants')
const HpackContext = require('./HpackContext')

/**
* @typedef {Object} HeaderField
* @property {number} kind
* @property {string} name
* @property {string} value
* @property {number} maxSize
*/

module.exports = class HpackEncodingContext extends HpackContext {
/**
* @private
* @param {HeaderField} headerField
* @returns {Uint8Array}
*/
_encodeHeaderField (headerField) {
let kind = headerField.kind
if (kind == null) {
kind = INCREMENTAL
}
let header
if (kind === SIZE_UPDATE) {
this.maximumTableSize = headerField.maxSize
return encodeHeader(headerField.kind, headerField.maxSize)
}
let index = _findInTables(headerField.name, headerField.value, [hpackStaticTable, this._dynamicTable])
if (index.indexed === 'both') {
header = encodeHeader(INDEXED, index.index)
} else {
if (kind === INCREMENTAL) {
this._addEntry(headerField.name, headerField.value)
}
if (index.indexed === 'name') {
header = encodeHeader(kind, index.index, encodeString(headerField.value))
} else {
header = encodeHeader(kind, 0, encodeString(headerField.name), encodeString(headerField.value))
}
}
return header
}

/**
* @param {Array.<HeaderField>} headerFieldList
* @returns {Uint8Array}
*/
encode (headerFieldList) {
return concat(headerFieldList.map(this._encodeHeaderField.bind(this)))
}
}

function _findInTables (name, value, tables) {
let tableOffset = 1
let nameMatch
for (let i = 0; i < tables.length; i++) {
let table = tables[i]
for (let j = 0; j < table.length; j++) {
let row = table[j]
if (row[0] === name) {
if (row[1] === value) {
return { indexed: 'both', index: j + tableOffset }
}
if (nameMatch == null) {
nameMatch = j + tableOffset
}
}
}
tableOffset += table.length
}
if (nameMatch != null) {
return { indexed: 'name', index: nameMatch }
} else {
return { indexed: 'none' }
}
}

function encodeString (string) {
let ui8a = ui8aFromString(string)
let huffmanEncoded = encodeHuffman(ui8a)
if (huffmanEncoded.length < ui8a.length) {
return encodeStringLiteral(true, huffmanEncoded)
} else {
return encodeStringLiteral(false, ui8a)
}
}
2 changes: 2 additions & 0 deletions lib/utils/HpackContexts/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
exports.HpackEncodingContext = require('./HpackEncodingContext')
exports.HpackDecodingContext = require('./HpackDecodingContext')
12 changes: 6 additions & 6 deletions lib/utils/codecs/hpack/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ function _decodeStringLiteralIntoHeader (ui8a, header, name) {
function _readKind (ui8a, byteOffset) {
const firstByte = ui8aHelpers.readUInt8(ui8a, byteOffset)
let kind
if (firstByte & NEVER_INDEXED) {
kind = NEVER_INDEXED
} else if (firstByte & SIZE_UPDATE) {
kind = SIZE_UPDATE
if (firstByte & INDEXED) {
kind = INDEXED
} else if (firstByte & INCREMENTAL) {
kind = INCREMENTAL
} else if (firstByte & INDEXED) {
kind = INDEXED
} else if (firstByte & SIZE_UPDATE) {
kind = SIZE_UPDATE
} else if (firstByte & NEVER_INDEXED) {
kind = NEVER_INDEXED
} else {
kind = NOT_INDEXED
}
Expand Down
10 changes: 10 additions & 0 deletions lib/utils/ui8aHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ function concat (ui8as) {
return ui8a
}

function ui8aFromString (string) {
return new Uint8Array(string.split('').map(char => char.charCodeAt(0)))
}

function stringFromUi8a (ui8a) {
return String.fromCharCode.apply(null, new Uint8Array(ui8a))
}

exports.alloc = alloc
exports.allocUnsafe = allocUnsafe
exports.readUInt8 = readUInt8
Expand All @@ -97,3 +105,5 @@ exports.writeUInt8 = writeUInt8
exports.writeUInt24BE = writeUInt24BE
exports.writeUInt32BE = writeUInt32BE
exports.concat = concat
exports.ui8aFromString = ui8aFromString
exports.stringFromUi8a = stringFromUi8a
33 changes: 33 additions & 0 deletions test/HpackContexts.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* globals describe it */
const { expect } = require('chai')
const { HpackEncodingContext, HpackDecodingContext } = require('../lib/utils/HpackContexts')
const { SIZE_UPDATE, NOT_INDEXED, NEVER_INDEXED } = require('../lib/constants')

function _toNameValue (headers) {
return headers.map(header => ({ name: headers.name, value: headers.value }))
}
describe('HpackContext', () => {
describe('encode/decode', () => {
it('encodes and decodes headers', () => {
let headers = [
{ name: ':path', value: '/index.html' },
{ name: ':path', value: '/index.htm' },
{ name: ':path', value: '/' },
{ name: ':pat', value: '/', kind: NOT_INDEXED },
{ name: ':pat', value: '/', kind: NEVER_INDEXED },
{ name: ':pat', value: '/' },
{ name: ':pat', value: '/' },
{ maxSize: 0, kind: SIZE_UPDATE },
{ name: ':pat', value: '/' },
{ name: ':pat', value: '/' }
]
let hec = new HpackEncodingContext(100)
let encoding = hec.encode(headers)
let hdc = new HpackDecodingContext()
hdc.maximumTableSize = 100
let decoding = hdc.decode(encoding)
expect(_toNameValue(decoding)).to.deep.equal(_toNameValue(headers))
expect(hdc.maximumTableSize).to.equal(0)
})
})
})
File renamed without changes.

0 comments on commit e920b55

Please sign in to comment.