@votingworks/ballot-encoder
Advanced tools
Comparing version 1.0.2 to 1.1.0
{ | ||
"name": "@votingworks/ballot-encoder", | ||
"version": "1.0.2", | ||
"version": "1.1.0", | ||
"files": [ | ||
@@ -11,3 +11,3 @@ "src/**/*.{js,d.ts,d.ts.map,json}" | ||
"lint": "eslint 'src/**/*.{ts,js}' --report-unused-disable-directives", | ||
"prepare": "tsc", | ||
"prepare": "in-publish && tsc || not-in-publish", | ||
"test": "jest", | ||
@@ -51,2 +51,3 @@ "test:ci": "jest --ci --collectCoverage", | ||
"husky": "^3.0.5", | ||
"in-publish": "^2.0.0", | ||
"jest": "^24.9.0", | ||
@@ -53,0 +54,0 @@ "lint-staged": "^9.3.0", |
@@ -14,3 +14,3 @@ # Ballot Encoder | ||
vote, | ||
} from 'ballot-encoder' | ||
} from '@votingworks/ballot-encoder' | ||
@@ -75,3 +75,3 @@ const ballotStyle = election.ballotStyles[0] | ||
```ts | ||
import { encodeBallot, EncoderVersion } from 'ballot-encoder' | ||
import { encodeBallot, EncoderVersion } from '@votingworks/ballot-encoder' | ||
@@ -85,3 +85,3 @@ const encodedBallot = encodeBallot(ballot, EncoderVersion.v1) | ||
```ts | ||
import { decodeBallot, EncoderVersion } from 'ballot-encoder' | ||
import { decodeBallot, EncoderVersion } from '@votingworks/ballot-encoder' | ||
@@ -102,3 +102,3 @@ // automatically detect version | ||
```ts | ||
import { detect } from 'ballot-encoder' | ||
import { detect } from '@votingworks/ballot-encoder' | ||
@@ -105,0 +105,0 @@ const version = detect(encodedBallot) |
@@ -92,2 +92,3 @@ import { Uint1, Uint8 } from './types'; | ||
}): boolean; | ||
private sizeofUint; | ||
/** | ||
@@ -109,5 +110,5 @@ * Skips N bits if they match the next N bits that would be read. | ||
*/ | ||
canRead(): boolean; | ||
canRead(size?: number): boolean; | ||
private getCurrentByte; | ||
} | ||
//# sourceMappingURL=BitReader.d.ts.map |
@@ -44,13 +44,5 @@ "use strict"; | ||
readUint({ max, size }) { | ||
if (typeof max !== 'undefined' && typeof size !== 'undefined') { | ||
throw new Error("cannot specify both 'max' and 'size' options"); | ||
} | ||
if (typeof max !== 'undefined') { | ||
return this.readUint({ size: utils_1.sizeof(max) }); | ||
} | ||
if (typeof size === 'undefined') { | ||
throw new Error('size cannot be undefined'); | ||
} | ||
const sizeofUint = this.sizeofUint({ max, size }); | ||
// Optimize for the case of reading a byte straight from the underlying buffer. | ||
if (size === types_1.Uint8Size && this.cursor.isByteStart) { | ||
if (sizeofUint === types_1.Uint8Size && this.cursor.isByteStart) { | ||
const result = this.getCurrentByte(); | ||
@@ -62,3 +54,3 @@ this.cursor.advance(types_1.Uint8Size); | ||
let result = 0; | ||
for (const mask of utils_1.makeMasks(size)) { | ||
for (const mask of utils_1.makeMasks(sizeofUint)) { | ||
const bit = this.readUint1(); | ||
@@ -106,4 +98,5 @@ if (bit) { | ||
const options = { max, size }; | ||
const sizeofUint = this.sizeofUint(options); | ||
for (const uint of uints) { | ||
if (!this.canRead() || uint !== this.readUint(options)) { | ||
if (!this.canRead(sizeofUint) || uint !== this.readUint(options)) { | ||
this.cursor = originalCursor; | ||
@@ -115,2 +108,14 @@ return false; | ||
} | ||
sizeofUint({ max, size }) { | ||
if (typeof max !== 'undefined' && typeof size !== 'undefined') { | ||
throw new Error("cannot specify both 'max' and 'size' options"); | ||
} | ||
if (typeof max !== 'undefined') { | ||
return utils_1.sizeof(max); | ||
} | ||
if (typeof size === 'undefined') { | ||
throw new Error('size cannot be undefined'); | ||
} | ||
return size; | ||
} | ||
/** | ||
@@ -136,4 +141,6 @@ * Skips N bits if they match the next N bits that would be read. | ||
*/ | ||
canRead() { | ||
return this.cursor.byteOffset < this.data.length; | ||
canRead(size = 1) { | ||
const totalBits = this.data.length * types_1.Uint8Size; | ||
const readBits = this.cursor.combinedBitOffset; | ||
return readBits + size <= totalBits; | ||
} | ||
@@ -140,0 +147,0 @@ getCurrentByte() { |
@@ -70,1 +70,9 @@ "use strict"; | ||
}); | ||
test('can skip uints of arbitrary size', () => { | ||
const bits = new BitReader_1.default(Uint8Array.of(0b11001100)); | ||
expect(bits.skipUint(0b11, { size: 2 })).toBe(true); | ||
expect(bits.skipUint(0b11, { size: 2 })).toBe(false); | ||
expect(bits.skipUint(0b0011, { size: 4 })).toBe(true); | ||
expect(bits.skipUint(0b001, { size: 3 })).toBe(false); | ||
expect(bits.skipUint([0b0, 0b0], { size: 1 })).toBe(true); | ||
}); |
@@ -128,6 +128,8 @@ "use strict"; | ||
} | ||
debug(label = 'bits') { | ||
debug(label) { | ||
if (label) { | ||
// eslint-disable-next-line no-console | ||
console.log(label); | ||
} | ||
// eslint-disable-next-line no-console | ||
console.log(label); | ||
// eslint-disable-next-line no-console | ||
console.log(inGroupsOf(8, inGroupsOf(types_1.Uint8Size, Array.from(this.toUint8Array()) | ||
@@ -134,0 +136,0 @@ .map(n => n.toString(2).padStart(types_1.Uint8Size, '0')) |
@@ -83,1 +83,16 @@ "use strict"; | ||
}); | ||
test('has a debug method to help understanding the contents', () => { | ||
jest.spyOn(console, 'log').mockImplementation(); | ||
new BitWriter_1.default() | ||
.writeBoolean(true) | ||
.debug('wrote true') | ||
.writeBoolean(false) | ||
.debug('wrote false') | ||
.writeUint8(1, 2, 3) | ||
.debug(); | ||
expect(console.log).toHaveBeenNthCalledWith(1, 'wrote true'); | ||
expect(console.log).toHaveBeenNthCalledWith(2, '1'); | ||
expect(console.log).toHaveBeenNthCalledWith(3, 'wrote false'); | ||
expect(console.log).toHaveBeenNthCalledWith(4, '10'); | ||
expect(console.log).toHaveBeenNthCalledWith(5, '10000000 01000000 10000000 11'); | ||
}); |
@@ -82,4 +82,5 @@ export declare type VoidFunction = () => void; | ||
precinct: Precinct; | ||
ballotId: string; | ||
votes: VotesDict; | ||
ballotId: string; | ||
isTestBallot: boolean; | ||
} | ||
@@ -86,0 +87,0 @@ export declare type CardDataTypes = 'voter' | 'pollworker' | 'clerk'; |
@@ -15,2 +15,18 @@ "use strict"; | ||
}); | ||
test('encodes with v1 by default', () => { | ||
const ballotStyle = election_1.electionSample.ballotStyles[0]; | ||
const precinct = election_1.electionSample.precincts[0]; | ||
const contests = election_1.getContests({ election: election_1.electionSample, ballotStyle }); | ||
const votes = election_1.vote(contests, {}); | ||
const ballotId = 'abcde'; | ||
const ballot = { | ||
election: election_1.electionSample, | ||
ballotId, | ||
ballotStyle, | ||
precinct, | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
expect(_1.detect(_1.encodeBallot(ballot))).toEqual(_1.EncoderVersion.v1); | ||
}); | ||
test('can encode by version number', () => { | ||
@@ -28,2 +44,3 @@ const ballotStyle = election_1.electionSample.ballotStyles[0]; | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
@@ -48,4 +65,8 @@ expect(_1.encodeBallot(ballot, _1.EncoderVersion.v0)).toEqual(_1.v0.encodeBallot(ballot)); | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
expect(_1.decodeBallot(election_1.electionSample, _1.v0.encodeBallot(ballot), _1.EncoderVersion.v0)).toEqual({ version: _1.EncoderVersion.v0, ballot }); | ||
expect(_1.decodeBallot(election_1.electionSample, _1.v0.encodeBallot(ballot), _1.EncoderVersion.v0)).toEqual({ | ||
version: _1.EncoderVersion.v0, | ||
ballot, | ||
}); | ||
}); | ||
@@ -64,2 +85,3 @@ test('can decode specifying v1', () => { | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
@@ -80,2 +102,3 @@ expect(_1.decodeBallot(election_1.electionSample, _1.v1.encodeBallot(ballot), _1.EncoderVersion.v1)).toEqual({ version: _1.EncoderVersion.v1, ballot }); | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
@@ -106,4 +129,5 @@ expect(_1.decodeBallot(election_1.electionSample, _1.v0.encodeBallot(ballot))).toEqual({ | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
expect(_1.detect(_1.v0.encodeBallot(ballot))).toEqual(_1.EncoderVersion.v0); | ||
}); |
@@ -116,2 +116,3 @@ "use strict"; | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
@@ -118,0 +119,0 @@ } |
@@ -33,5 +33,19 @@ "use strict"; | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
expect(index_1.detect(v1.encodeBallot(ballot))).toBe(false); | ||
}); | ||
test('does not detect if the check throws', () => { | ||
const decode = jest | ||
.spyOn(TextDecoder.prototype, 'decode') | ||
.mockImplementation(() => { | ||
throw new Error(); | ||
}); | ||
try { | ||
expect(index_1.detect(Uint8Array.of())).toBe(false); | ||
} | ||
finally { | ||
decode.mockRestore(); | ||
} | ||
}); | ||
test('encodes & decodes with Uint8Array as the standard encoding interface', () => { | ||
@@ -49,2 +63,3 @@ const ballotStyle = election_1.electionSample.ballotStyles[0]; | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
@@ -65,2 +80,3 @@ expect(index_1.decodeBallot(election_1.electionSample, index_1.encodeBallot(ballot))).toEqual(ballot); | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
@@ -87,2 +103,3 @@ const encodedBallot = '12.23.|||||||||||||||||||.abcde'; | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
@@ -108,2 +125,3 @@ const encodedBallot = '12.23.||||||||||||1|0||||||.abcde'; | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
@@ -131,7 +149,9 @@ const encodedBallot = '12.23.0,1|||||||||||||||||||.abcde'; | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
const encodedBallot = '12.23.W|||||||||||||||||||.abcde'; | ||
expect(index_1.encodeBallotAsString(ballot)).toEqual(encodedBallot); | ||
// v0 can't fully round-trip write-ins: we lose the actual name | ||
expect(index_1.decodeBallotFromString(election_1.electionSample, encodedBallot)).toEqual(Object.assign(Object.assign({}, ballot), { votes: Object.assign(Object.assign({}, votes), { [contest.id]: [ | ||
expect(index_1.decodeBallotFromString(election_1.electionSample, encodedBallot)).toEqual(Object.assign(Object.assign({}, ballot), { | ||
// v0 can't fully round-trip write-ins: we lose the actual name | ||
votes: Object.assign(Object.assign({}, votes), { [contest.id]: [ | ||
{ | ||
@@ -142,4 +162,25 @@ name: '', | ||
}, | ||
] }) })); | ||
] }), | ||
// v0 does not encode whether a ballot is a test ballot | ||
isTestBallot: false })); | ||
}); | ||
test('cannot encode a yesno contest with an invalid value', () => { | ||
const ballotStyle = election_1.electionSample.ballotStyles[0]; | ||
const contests = election_1.getContests({ ballotStyle, election: election_1.electionSample }); | ||
const precinct = election_1.electionSample.precincts[0]; | ||
const yesnos = contests.filter(contest => contest.type === 'yesno'); | ||
const votes = election_1.vote(contests, { | ||
[yesnos[0].id]: 'YEP', | ||
}); | ||
const ballotId = 'abcde'; | ||
const ballot = { | ||
election: election_1.electionSample, | ||
ballotId, | ||
ballotStyle, | ||
precinct, | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
expect(() => index_1.encodeBallot(ballot)).toThrowError('cannot encode yesno vote, expected "no" or "yes" but got "YEP"'); | ||
}); | ||
test('cannot decode a ballot with a ballot style id that does not exist', () => { | ||
@@ -146,0 +187,0 @@ expect(() => { |
@@ -120,3 +120,3 @@ # Ballot Encoder v0 | ||
### A non-trivial number of contests with votes for all of them | ||
### A non-trivial number of contests, skipping one vote | ||
@@ -123,0 +123,0 @@ ``` |
@@ -11,5 +11,5 @@ import { CompletedBallot, Election } from '../election'; | ||
export declare function encodeBallot(ballot: CompletedBallot): Uint8Array; | ||
export declare function encodeBallotInto({ election, ballotStyle, precinct, votes, ballotId }: CompletedBallot, bits: BitWriter): void; | ||
export declare function encodeBallotInto({ election, ballotStyle, precinct, votes, ballotId, isTestBallot, }: CompletedBallot, bits: BitWriter): BitWriter; | ||
export declare function decodeBallot(election: Election, data: Uint8Array): CompletedBallot; | ||
export declare function decodeBallotFromReader(election: Election, bits: BitReader): CompletedBallot; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -28,3 +28,3 @@ "use strict"; | ||
exports.encodeBallot = encodeBallot; | ||
function encodeBallotInto({ election, ballotStyle, precinct, votes, ballotId }, bits) { | ||
function encodeBallotInto({ election, ballotStyle, precinct, votes, ballotId, isTestBallot, }, bits) { | ||
election_1.validateVotes({ election, ballotStyle, votes }); | ||
@@ -35,5 +35,5 @@ const contests = election_1.getContests({ ballotStyle, election }); | ||
.writeString(ballotStyle.id) | ||
.writeString(precinct.id); | ||
encodeBallotVotesInto(contests, votes, bits); | ||
bits.writeString(ballotId); | ||
.writeString(precinct.id) | ||
.writeString(ballotId); | ||
return encodeBallotVotesInto(contests, votes, bits).writeBoolean(isTestBallot); | ||
} | ||
@@ -81,2 +81,3 @@ exports.encodeBallotInto = encodeBallotInto; | ||
} | ||
return bits; | ||
} | ||
@@ -105,5 +106,6 @@ function decodeBallot(election, data) { | ||
} | ||
const ballotId = bits.readString(); | ||
const contests = election_1.getContests({ ballotStyle, election }); | ||
const votes = decodeBallotVotes(contests, bits); | ||
const ballotId = bits.readString(); | ||
const isTestBallot = bits.readBoolean(); | ||
readPaddingToEnd(bits); | ||
@@ -116,2 +118,3 @@ return { | ||
votes, | ||
isTestBallot, | ||
}; | ||
@@ -118,0 +121,0 @@ } |
@@ -12,4 +12,4 @@ "use strict"; | ||
const election_1 = require("../election"); | ||
const v0 = __importStar(require("../v0")); | ||
const index_1 = require("./index"); | ||
const v0 = __importStar(require("../v0")); | ||
function falses(count) { | ||
@@ -36,2 +36,3 @@ return new Array(count).fill(false); | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
@@ -52,2 +53,3 @@ expect(index_1.detect(v0.encodeBallot(ballot))).toBe(false); | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
@@ -68,2 +70,3 @@ expect(index_1.decodeBallot(election_1.electionSample, index_1.encodeBallot(ballot))).toEqual(ballot); | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
@@ -78,6 +81,38 @@ const encodedBallot = new bits_1.BitWriter() | ||
.writeString('23') | ||
// ballot Id | ||
.writeString('abcde') | ||
// vote roll call only, no vote data | ||
.writeBoolean(...contests.map(() => false)) | ||
.toUint8Array(); | ||
expect(index_1.encodeBallot(ballot)).toEqualBits(encodedBallot); | ||
expect(index_1.decodeBallot(election_1.electionSample, encodedBallot)).toEqual(ballot); | ||
}); | ||
it('encodes & decodes whether it is a test ballot', () => { | ||
const ballotStyle = election_1.electionSample.ballotStyles[0]; | ||
const precinct = election_1.electionSample.precincts[0]; | ||
const contests = election_1.getContests({ ballotStyle, election: election_1.electionSample }); | ||
const votes = election_1.vote(contests, {}); | ||
const ballotId = 'abcde'; | ||
const ballot = { | ||
election: election_1.electionSample, | ||
ballotId, | ||
ballotStyle, | ||
precinct, | ||
votes, | ||
isTestBallot: true, | ||
}; | ||
const encodedBallot = new bits_1.BitWriter() | ||
// prelude + version number | ||
.writeString('VX', { includeLength: false }) | ||
.writeUint8(1) | ||
// ballot style id | ||
.writeString('12') | ||
// precinct id | ||
.writeString('23') | ||
// ballot Id | ||
.writeString('abcde') | ||
// vote roll call only, no vote data | ||
.writeBoolean(...contests.map(() => false)) | ||
// test ballot? | ||
.writeBoolean(true) | ||
.toUint8Array(); | ||
@@ -108,2 +143,3 @@ expect(index_1.encodeBallot(ballot)).toEqualBits(encodedBallot); | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
@@ -118,2 +154,4 @@ const encodedBallot = new bits_1.BitWriter() | ||
.writeString('23') | ||
// ballot Id | ||
.writeString('abcde') | ||
// vote roll call | ||
@@ -130,4 +168,2 @@ .writeBoolean(...contests.map(contest => contest.id in votes)) | ||
.writeBoolean(true) | ||
// ballot Id | ||
.writeString('abcde') | ||
.toUint8Array(); | ||
@@ -162,2 +198,3 @@ expect(index_1.encodeBallot(ballot)).toEqualBits(encodedBallot); | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
@@ -172,2 +209,4 @@ const encodedBallot = new bits_1.BitWriter() | ||
.writeString('23') | ||
// ballot Id | ||
.writeString('abcde') | ||
// vote roll call | ||
@@ -204,4 +243,2 @@ .writeBoolean(...contests.map(contest => contest.id in votes)) | ||
.writeUint(0, { max: 2 }) // 3 seats - 1 selection = 2 write-ins max | ||
// ballot Id | ||
.writeString('abcde') | ||
.toUint8Array(); | ||
@@ -227,2 +264,3 @@ expect(index_1.encodeBallot(ballot)).toEqualBits(encodedBallot); | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
@@ -237,2 +275,4 @@ const encodedBallot = new bits_1.BitWriter() | ||
.writeString('23') | ||
// ballot Id | ||
.writeString('abcde') | ||
// vote roll call | ||
@@ -249,4 +289,4 @@ .writeBoolean(...contests.map(contest => contest.id in votes)) | ||
}) | ||
// ballot Id | ||
.writeString('abcde') | ||
// test ballot? | ||
.writeBoolean(false) | ||
.toUint8Array(); | ||
@@ -256,1 +296,75 @@ expect(index_1.encodeBallot(ballot)).toEqualBits(encodedBallot); | ||
}); | ||
test('cannot decode a ballot without the prelude', () => { | ||
const encodedBallot = new bits_1.BitWriter() | ||
// prelude + version number | ||
.writeString('XV', { includeLength: false }) | ||
.writeUint8(1) | ||
.toUint8Array(); | ||
expect(() => index_1.decodeBallot(election_1.electionSample, encodedBallot)).toThrowError("expected leading prelude 'V' 'X' 0b00000001 but it was not found"); | ||
}); | ||
test('cannot decode a ballot with a ballot style ID not in the election', () => { | ||
const encodedBallot = new bits_1.BitWriter() | ||
// prelude + version number | ||
.writeString('VX', { includeLength: false }) | ||
.writeUint8(1) | ||
// ballot style id | ||
.writeString('ZZZ') | ||
// precinct id | ||
.writeString('23') | ||
// ballot Id | ||
.writeString('abcde') | ||
.toUint8Array(); | ||
expect(() => index_1.decodeBallot(election_1.electionSample, encodedBallot)).toThrowError('ballot style with id "ZZZ" could not be found, expected one of: 12, 5, 7C'); | ||
}); | ||
test('cannot decode a ballot with a precinct ID not in the election', () => { | ||
const encodedBallot = new bits_1.BitWriter() | ||
// prelude + version number | ||
.writeString('VX', { includeLength: false }) | ||
.writeUint8(1) | ||
// ballot style id | ||
.writeString('12') | ||
// precinct id | ||
.writeString('ZZZ') | ||
// ballot Id | ||
.writeString('abcde') | ||
.toUint8Array(); | ||
expect(() => index_1.decodeBallot(election_1.electionSample, encodedBallot)).toThrowError('precinct with id "ZZZ" could not be found, expected one of: 23, 21, 20'); | ||
}); | ||
test('cannot decode a ballot that includes extra data at the end', () => { | ||
const ballotStyle = election_1.electionSample.ballotStyles[0]; | ||
const precinct = election_1.electionSample.precincts[0]; | ||
const contests = election_1.getContests({ election: election_1.electionSample, ballotStyle }); | ||
const votes = election_1.vote(contests, {}); | ||
const ballotId = 'abcde'; | ||
const ballot = { | ||
election: election_1.electionSample, | ||
ballotId, | ||
ballotStyle, | ||
precinct, | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
const writer = new bits_1.BitWriter(); | ||
index_1.encodeBallotInto(ballot, writer); | ||
const corruptedBallot = writer.writeBoolean(true).toUint8Array(); | ||
expect(() => index_1.decodeBallot(election_1.electionSample, corruptedBallot)).toThrowError('unexpected data found while reading padding, expected EOF'); | ||
}); | ||
test('cannot decode a ballot that includes too much padding at the end', () => { | ||
const ballotStyle = election_1.electionSample.ballotStyles[0]; | ||
const precinct = election_1.electionSample.precincts[0]; | ||
const contests = election_1.getContests({ election: election_1.electionSample, ballotStyle }); | ||
const votes = election_1.vote(contests, {}); | ||
const ballotId = 'abcde'; | ||
const ballot = { | ||
election: election_1.electionSample, | ||
ballotId, | ||
ballotStyle, | ||
precinct, | ||
votes, | ||
isTestBallot: false, | ||
}; | ||
const writer = new bits_1.BitWriter(); | ||
index_1.encodeBallotInto(ballot, writer); | ||
const corruptedBallot = writer.writeUint8(0).toUint8Array(); | ||
expect(() => index_1.decodeBallot(election_1.electionSample, corruptedBallot)).toThrowError('unexpected data found while reading padding, expected EOF'); | ||
}); |
@@ -24,3 +24,3 @@ # Ballot Encoder v1 | ||
`ABCDEFGHIJKLMNOPQRSTUVWXYZ '"-.,`. | ||
- **dynamic-width string**: a UTF-8 string with maximum length `M`, prefixed | ||
- **dynamic-length string**: a UTF-8 string with maximum length `M`, prefixed | ||
with a _dynamic-width number_ (max `M`) which is the length of the string in | ||
@@ -35,2 +35,16 @@ bytes. | ||
- **Prelude:** This is the literal string `VX` encoded as UTF-8 bytes, followed | ||
by the integer 1 encoded as a byte. In binary, this is | ||
`01010110 01011000 00000001`. This must be at the start of the encoded data, | ||
or the data does not represent a valid v1-encoded ballot. | ||
- Size: 24 bits. | ||
- **Ballot Style ID:** This is a dynamic-length string whose maximum length is | ||
255 bytes. | ||
- Size: `(1 + bytes(ballotStyleID)) * 8` bits. | ||
- **Precinct ID:** This is a dynamic-length string whose maximum length is 255 | ||
bytes. | ||
- Size: `(1 + bytes(precinctID)) * 8` bits. | ||
- **Ballot ID:** This is a dynamic-length string whose maximum length is 255 | ||
bytes. | ||
- Size: `(1 + bytes(ballotId)) * 8` bits. | ||
- **Roll Call**: Encodes which contests have votes using one bit per contest, | ||
@@ -62,4 +76,7 @@ where a bit at offset `i` from the start of this section is set if and only if | ||
bits. | ||
- **Test Ballot Flag:** This is a single bit that is set if the ballot is a test | ||
ballot, unset otherwise. | ||
- Size: 1 bit. | ||
- **Padding**: To ensure the encoded data is composed of whole bytes, 0 bits | ||
will be added to the end if necessary. | ||
will be added to the end until the number of bits is a multiple of 8. | ||
@@ -157,4 +174,2 @@ ## Examples | ||
| | | ||
| | | ||
| | | ||
| Set bit for the 3rd candidate in second contest | ||
@@ -183,3 +198,3 @@ | | ||
### A non-trivial number of contests with votes for all of them | ||
### A non-trivial number of contests, skipping one vote | ||
@@ -186,0 +201,0 @@ ``` |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
229197
69
3530
19