New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@votingworks/ballot-encoder

Package Overview
Dependencies
Maintainers
3
Versions
28
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@votingworks/ballot-encoder - npm Package Compare versions

Comparing version 1.0.2 to 1.1.0

src/election.test.d.ts

5

package.json
{
"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",

8

README.md

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc