Skip to content

Commit b59855e

Browse files
PetarMaxseebees
authored andcommitted
fix: updating readElement to match underlying data structure (aws#215)
The readElement function was previously not taking advantage of the underlying data structure, which is a sequence of elements, each of which is a sequence of fields. This resulted in several superfluous calls to built-in functions (e.g. Array.prototype.splice). This commit adds an additional parameter to readElement, which is the number of fields per element, and adapts the code accordingly. The return of readElement is now an array of arrays, which simplifies its manipulation by the two caller functions.
1 parent 03061d1 commit b59855e

File tree

3 files changed

+118
-131
lines changed

3 files changed

+118
-131
lines changed

modules/serialize/src/deserialize_factory.ts

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,7 @@ export function deserializeFactory<Suite extends AlgorithmSuite> (
8888
*/
8989
if (22 + contextLength > dataView.byteLength) return false // not enough data
9090

91-
const encryptionContext = contextLength > 0
92-
? decodeEncryptionContext(messageBuffer.slice(22, 22 + contextLength))
93-
: {}
91+
const encryptionContext = decodeEncryptionContext(messageBuffer.slice(22, 22 + contextLength))
9492
const dataKeyInfo = deserializeEncryptedDataKeys(messageBuffer, 22 + contextLength)
9593

9694
/* Check for early return (Postcondition): Not Enough Data. deserializeEncryptedDataKeys will return false if it does not have enough data.
@@ -176,23 +174,20 @@ export function deserializeFactory<Suite extends AlgorithmSuite> (
176174
/* Precondition: There must be at least 1 EncryptedDataKey element. */
177175
needs(encryptedDataKeysCount, 'No EncryptedDataKey found.')
178176

179-
const elementInfo = readElements(encryptedDataKeysCount * 3, buffer, startPos + 2)
177+
const elementInfo = readElements(encryptedDataKeysCount, 3, buffer, startPos + 2)
180178
/* Check for early return (Postcondition): readElement will return false if there is not enough data.
181179
* I can only continue if I have at least the entire EDK section.
182180
*/
183181
if (!elementInfo) return false
184182
const { elements, readPos } = elementInfo
185183

186-
let remainingKeyCount = encryptedDataKeysCount
187-
const encryptedDataKeys = []
188-
while (remainingKeyCount--) {
189-
const [rawId, rawInfo] = elements.splice(0, 2)
190-
const [providerId, providerInfo] = [rawId, rawInfo].map(toUtf8)
191-
const [encryptedDataKey] = elements.splice(0, 1)
192-
// The providerInfo is technically a binary field, so I must pass rawInfo
193-
const edk = new EncryptedDataKey({ providerInfo, providerId, encryptedDataKey, rawInfo })
194-
encryptedDataKeys.push(edk)
195-
}
184+
const encryptedDataKeys = elements.map(
185+
([rawId, rawInfo, encryptedDataKey], _) => {
186+
const providerId = toUtf8(rawId)
187+
const providerInfo = toUtf8(rawInfo)
188+
return new EncryptedDataKey({ providerInfo, providerId, encryptedDataKey, rawInfo })
189+
}
190+
)
196191
Object.freeze(encryptedDataKeys)
197192
return { encryptedDataKeys, readPos }
198193
}
@@ -219,7 +214,7 @@ export function deserializeFactory<Suite extends AlgorithmSuite> (
219214
encodedEncryptionContext.byteLength
220215
)
221216
const pairsCount = dataView.getUint16(0, false) // big endian
222-
const elementInfo = readElements(pairsCount * 2, encodedEncryptionContext, 2)
217+
const elementInfo = readElements(pairsCount, 2, encodedEncryptionContext, 2)
223218
/* Postcondition: Since the encryption context has a length, it must have pairs.
224219
* Unlike the encrypted data key section, the encryption context has a length
225220
* element. This means I should always pass the entire section.
@@ -230,9 +225,8 @@ export function deserializeFactory<Suite extends AlgorithmSuite> (
230225
/* Postcondition: The byte length of the encodedEncryptionContext must match the readPos. */
231226
needs(encodedEncryptionContext.byteLength === readPos, 'Overflow, too much data.')
232227

233-
let count = pairsCount
234-
while (count--) {
235-
const [key, value] = elements.splice(0, 2).map(toUtf8)
228+
for (let count = 0; count < pairsCount; count++) {
229+
const [key, value] = elements[count].map(toUtf8)
236230
/* Postcondition: The number of keys in the encryptionContext must match the pairsCount.
237231
* If the same Key value is serialized...
238232
*/

modules/serialize/src/read_element.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { needs } from '@aws-crypto/material-management'
4343
*/
4444
export function readElements (
4545
elementCount: number,
46+
fieldsPerElement: number,
4647
buffer: Uint8Array,
4748
readPos: number = 0
4849
) {
@@ -59,22 +60,27 @@ export function readElements (
5960
)
6061
const elements = []
6162

62-
/* Precondition: readElements readPos must be within the byte length of the buffer given. */
63+
/* Precondition: readPos must be non-negative and within the byte length of the buffer given. */
6364
needs(readPos >= 0 && dataView.byteLength >= readPos, 'readPos out of bounds.')
6465

65-
/* Precondition: elementCount must not be negative. */
66-
needs(elementCount >= 0, 'elementCount must be positive.')
66+
/* Precondition: elementCount and fieldsPerElement must be non-negative. */
67+
needs(elementCount >= 0 && fieldsPerElement >= 0, 'elementCount and fieldsPerElement must be positive.')
6768

6869
while (elementCount--) {
69-
/* Check for early return (Postcondition): Enough data must exist to read the Uint16 length value. */
70-
if (readPos + 2 > dataView.byteLength) return false
71-
const length = dataView.getUint16(readPos, false) // big endian
72-
readPos += 2
73-
/* Check for early return (Postcondition): Enough data must exist length of the value. */
74-
if (readPos + length > dataView.byteLength) return false
75-
const elementBinary = buffer.slice(readPos, readPos + length)
76-
elements.push(elementBinary)
77-
readPos += length
70+
let element = []
71+
let fieldCount = fieldsPerElement
72+
while (fieldCount--) {
73+
/* Check for early return (Postcondition): Enough data must exist to read the Uint16 length value. */
74+
if (readPos + 2 > dataView.byteLength) return false
75+
const length = dataView.getUint16(readPos, false) // big endian
76+
readPos += 2
77+
/* Check for early return (Postcondition): Enough data must exist length of the value. */
78+
if (readPos + length > dataView.byteLength) return false
79+
const fieldBinary = buffer.slice(readPos, readPos + length)
80+
element.push(fieldBinary)
81+
readPos += length
82+
}
83+
elements.push(element)
7884
}
7985
return { elements, readPos }
8086
}

modules/serialize/test/read_elements.test.ts

Lines changed: 88 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,14 @@ import { concatBuffers } from '../src/concat_buffers'
2222
import { Buffer } from 'buffer'
2323
import * as fixtures from './fixtures'
2424

25-
describe('readElements', () => {
26-
it('should be able to find one element', () => {
27-
const len = Buffer.alloc(2)
28-
len.writeUInt16BE(4, 0)
29-
const data = new Uint8Array([...Buffer.from('data')])
30-
const buff = concatBuffers(len, data)
31-
const info = readElements(1, buff, 0)
32-
if (info === false) throw new Error('Fail')
33-
expect(info.elements).to.be.instanceof(Array)
34-
expect(info.elements.length).to.eql(1)
35-
expect(info.elements[0]).to.deep.equal(data)
36-
expect(info.readPos).to.eql(6)
37-
})
25+
function randomNat (limit : number) : number {
26+
return Math.floor(Math.random() * limit)
27+
}
3828

39-
it('should be able to handle multiple elements', () => {
29+
describe('readElements', () => {
30+
it('should be able to handle multiple elements containing multiple fields without padding', () => {
4031
const utf8DataStrings = [
41-
'some utf8 information', '\u00bd + \u00bc = \u00be', 'to encode'
32+
'here', 'is', 'some', 'utf8', 'information', '\u00bd + \u00bc = \u00be'
4233
]
4334
const buffData = utf8DataStrings.map(str => new Uint8Array([...Buffer.from(str)]))
4435
const buff = concatBuffers(...buffData.map(bufStr => {
@@ -47,75 +38,93 @@ describe('readElements', () => {
4738
return concatBuffers(len, bufStr)
4839
}))
4940

50-
const info = readElements(3, buff, 0)
51-
52-
if (info === false) throw new Error('Fail')
53-
expect(info.elements).to.be.instanceof(Array)
54-
expect(info.elements.length).to.eql(3)
55-
expect(info.elements[0]).to.deep.equal(buffData[0])
56-
expect(info.elements[1]).to.deep.equal(buffData[1])
57-
expect(info.elements[2]).to.deep.equal(buffData[2])
58-
expect(Buffer.from(info.elements[0]).toString()).to.deep.equal(utf8DataStrings[0])
59-
expect(Buffer.from(info.elements[1]).toString()).to.deep.equal(utf8DataStrings[1])
60-
expect(Buffer.from(info.elements[2]).toString()).to.deep.equal(utf8DataStrings[2])
61-
expect(info.readPos).to.eql(48)
41+
/* The elements in the buffer can be arranged in several ways.
42+
* For example, we can think of them as six elements with one
43+
* field each, or as three elements with two fields each.
44+
*/
45+
const dimensions = [[1, 6], [2, 3], [3, 2], [6, 1]]
46+
47+
dimensions.map(([elementCount, fieldsPerElement]) => {
48+
const info = readElements(elementCount, fieldsPerElement, buff)
49+
50+
if (info === false) throw new Error('Fail')
51+
let elements = info.elements
52+
expect(elements).to.be.instanceof(Array)
53+
expect(elements.length).to.eql(elementCount)
54+
55+
for (let eCount = 0; eCount < elementCount; eCount++) {
56+
let element = info.elements[eCount]
57+
expect(element).to.be.instanceof(Array)
58+
expect(element.length).to.eql(fieldsPerElement)
59+
for (let fCount = 0; fCount < fieldsPerElement; fCount++) {
60+
let field = element[fCount]
61+
expect(field).to.deep.equal(buffData[eCount * fieldsPerElement + fCount])
62+
expect(Buffer.from(field).toString()).to.deep.equal(utf8DataStrings[eCount * fieldsPerElement + fCount])
63+
}
64+
}
65+
66+
expect(info.readPos).to.eql(buff.byteLength)
67+
})
6268
})
6369

64-
it('should be able to handle multiple elements when not reading from the beginning', () => {
65-
const utf8DataStrings = [
66-
'some utf8 information', '\u00bd + \u00bc = \u00be', 'to encode'
67-
]
70+
it('should be able to handle multiple elements containing multiple fields with various padding', () => {
71+
let numberOfRuns = 16
72+
const maxPaddingLength = 1024
73+
const utf8DataStrings = [ 'here', 'is', 'some', 'utf8', 'information', '\u00bd + \u00bc = \u00be' ]
6874
const buffData = utf8DataStrings.map(str => new Uint8Array([...Buffer.from(str)]))
69-
const padding = Buffer.alloc(10)
70-
const buff = concatBuffers(padding, ...buffData.map(bufStr => {
75+
const mainBuffer = concatBuffers(...buffData.map(bufStr => {
7176
const len = Buffer.alloc(2)
7277
len.writeUInt16BE(bufStr.byteLength, 0)
7378
return concatBuffers(len, bufStr)
7479
}))
7580

76-
const info = readElements(3, buff, padding.length)
77-
78-
if (info === false) throw new Error('Fail')
79-
expect(info.elements).to.be.instanceof(Array)
80-
expect(info.elements.length).to.eql(3)
81-
expect(info.elements[0]).to.deep.equal(buffData[0])
82-
expect(info.elements[1]).to.deep.equal(buffData[1])
83-
expect(info.elements[2]).to.deep.equal(buffData[2])
84-
expect(Buffer.from(info.elements[0]).toString()).to.deep.equal(utf8DataStrings[0])
85-
expect(Buffer.from(info.elements[1]).toString()).to.deep.equal(utf8DataStrings[1])
86-
expect(Buffer.from(info.elements[2]).toString()).to.deep.equal(utf8DataStrings[2])
87-
expect(info.readPos).to.eql(58)
81+
const dimensions = [[1, 6], [2, 3], [3, 2], [6, 1]]
82+
83+
while (numberOfRuns--) {
84+
let leftPadding = Buffer.alloc(randomNat(maxPaddingLength))
85+
let rightPadding = Buffer.alloc(randomNat(maxPaddingLength))
86+
let buff = concatBuffers(leftPadding, mainBuffer, rightPadding)
87+
88+
dimensions.map(([elementCount, fieldsPerElement]) => {
89+
const info = readElements(elementCount, fieldsPerElement, buff, leftPadding.byteLength)
90+
91+
if (info === false) throw new Error('Fail')
92+
let elements = info.elements
93+
expect(elements).to.be.instanceof(Array)
94+
expect(elements.length).to.eql(elementCount)
95+
96+
for (let eCount = 0; eCount < elementCount; eCount++) {
97+
let element = info.elements[eCount]
98+
expect(element).to.be.instanceof(Array)
99+
expect(element.length).to.eql(fieldsPerElement)
100+
for (let fCount = 0; fCount < fieldsPerElement; fCount++) {
101+
let field = element[fCount]
102+
expect(field).to.deep.equal(buffData[eCount * fieldsPerElement + fCount])
103+
expect(Buffer.from(field).toString()).to.deep.equal(utf8DataStrings[eCount * fieldsPerElement + fCount])
104+
}
105+
}
106+
107+
expect(info.readPos).to.eql(leftPadding.byteLength + mainBuffer.byteLength)
108+
})
109+
}
88110
})
89111

90-
it('should be able to handle multiple elements with padding on both sides', () => {
91-
const utf8DataStrings = [
92-
'some utf8 information', '\u00bd + \u00bc = \u00be', 'to encode'
93-
]
94-
const buffData = utf8DataStrings.map(str => new Uint8Array([...Buffer.from(str)]))
95-
const padding = Buffer.alloc(10)
96-
const buff = concatBuffers(padding, ...buffData.map(bufStr => {
97-
const len = Buffer.alloc(2)
98-
len.writeUInt16BE(bufStr.byteLength, 0)
99-
return concatBuffers(len, bufStr)
100-
}), padding)
101-
102-
const info = readElements(3, buff, padding.length)
103-
104-
if (info === false) throw new Error('Fail')
105-
expect(info.elements).to.be.instanceof(Array)
106-
expect(info.elements.length).to.eql(3)
107-
expect(info.elements[0]).to.deep.equal(buffData[0])
108-
expect(info.elements[1]).to.deep.equal(buffData[1])
109-
expect(info.elements[2]).to.deep.equal(buffData[2])
110-
expect(Buffer.from(info.elements[0]).toString()).to.deep.equal(utf8DataStrings[0])
111-
expect(Buffer.from(info.elements[1]).toString()).to.deep.equal(utf8DataStrings[1])
112-
expect(Buffer.from(info.elements[2]).toString()).to.deep.equal(utf8DataStrings[2])
113-
expect(info.readPos).to.eql(58)
112+
it('Precondition: readPos must be non-negative and within the byte length of the buffer given.', () => {
113+
const buff = new Uint8Array(32)
114+
const readPosBeyondBuff = buff.byteLength + 1
115+
expect(() => readElements(1, 1, buff, readPosBeyondBuff)).to.throw()
116+
})
117+
118+
it('Precondition: elementCount and fieldsPerElement must be non-negative.', () => {
119+
const buff = new Uint8Array(32)
120+
expect(() => readElements(-1, 1, buff)).to.throw()
121+
expect(() => readElements(1, -1, buff)).to.throw()
122+
expect(() => readElements(-1, -1, buff)).to.throw()
114123
})
115124

116125
it('Check for early return (Postcondition): Enough data must exist to read the Uint16 length value.; Check for early return (Postcondition): Enough data must exist length of the value.', () => {
117126
const utf8DataStrings = [
118-
'some utf8 information', '\u00bd + \u00bc = \u00be', 'to encode'
127+
'here', 'is', 'some', 'utf8', 'information', '\u00bd + \u00bc = \u00be'
119128
]
120129
const buffData = utf8DataStrings.map(str => new Uint8Array([...Buffer.from(str)]))
121130
const buff = concatBuffers(...buffData.map(bufStr => {
@@ -124,35 +133,13 @@ describe('readElements', () => {
124133
return concatBuffers(len, bufStr)
125134
}))
126135

127-
// By testing every combination of byte length possible
128-
// Both loop invariants are covered.
129-
for (let i = 0; buff.byteLength > i; i++) {
130-
const info = readElements(3, buff.slice(0, i), 0)
131-
expect(info).to.eql(false)
132-
}
133-
// Engage pedantry...
134-
const info = readElements(3, buff.slice(0, buff.byteLength), 0)
135-
if (info === false) throw new Error('Fail')
136-
expect(info.elements).to.be.instanceof(Array)
137-
expect(info.elements.length).to.eql(3)
138-
expect(info.elements[0]).to.deep.equal(buffData[0])
139-
expect(info.elements[1]).to.deep.equal(buffData[1])
140-
expect(info.elements[2]).to.deep.equal(buffData[2])
141-
expect(Buffer.from(info.elements[0]).toString()).to.deep.equal(utf8DataStrings[0])
142-
expect(Buffer.from(info.elements[1]).toString()).to.deep.equal(utf8DataStrings[1])
143-
expect(Buffer.from(info.elements[2]).toString()).to.deep.equal(utf8DataStrings[2])
144-
expect(info.readPos).to.eql(48)
145-
})
146-
147-
it('Precondition: readElements readPos must be within the byte length of the buffer given.', () => {
148-
const buff = new Uint8Array(32)
149-
const readPosBeyondBuff = buff.byteLength + 1
150-
expect(() => readElements(1, buff, readPosBeyondBuff)).to.throw()
151-
})
136+
/* Will return false when trying to read the length of the seventh element */
137+
const infoFalse1 = readElements(1, 7, buff)
138+
expect(infoFalse1).to.equal(false)
152139

153-
it('Precondition: elementCount must not be negative.', () => {
154-
const buff = new Uint8Array(32)
155-
expect(() => readElements(-1, buff)).to.throw()
140+
/* Will return false when trying to read the sixth element */
141+
const infoFalse2 = readElements(1, 6, buff.slice(0, buff.byteLength - 1))
142+
expect(infoFalse2).to.equal(false)
156143
})
157144

158145
it('ArrayBuffer for a Uint8Array or Buffer may be larger than the Uint8Array or Buffer that it is a view over is.', () => {
@@ -162,12 +149,12 @@ describe('readElements', () => {
162149
* read position past the length and count should succeed.
163150
*/
164151
const buff = fixtures.basicEncryptionContext()
165-
expect(readElements(4, buff, 0)).to.equal(false)
166-
expect(readElements(4, buff, 4)).to.not.equal(false)
152+
expect(readElements(4, 1, buff, 0)).to.equal(false)
153+
expect(readElements(4, 1, buff, 4)).to.not.equal(false)
167154
/* Given this I can use this to construct a new view of part of the
168155
* ArrayBuffer to simulate a large ArrayBuffer that is sliced
169156
* into parts for efficiency. */
170157
const sharingArrayBuffer = new Uint8Array(buff.buffer, 4, buff.byteLength - 4)
171-
expect(readElements(4, sharingArrayBuffer)).to.not.equal(false)
158+
expect(readElements(4, 1, sharingArrayBuffer)).to.not.equal(false)
172159
})
173160
})

0 commit comments

Comments
 (0)