Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit fb8cb4d

Browse files
committedMar 4, 2017
fix(index): add length header before JSON for verification
1 parent 655799e commit fb8cb4d

File tree

4 files changed

+45
-16
lines changed

4 files changed

+45
-16
lines changed
 

Diff for: ‎lib/entry-index.js

+22-3
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,16 @@ function insert (cache, key, digest, opts) {
2929
time: +(new Date()),
3030
metadata: opts.metadata
3131
}
32+
const stringified = JSON.stringify(entry)
33+
// NOTE - Cleverness ahoy!
34+
//
35+
// This works because it's tremendously unlikely for an entry to corrupt
36+
// another while still preserving the string length of the JSON in
37+
// question. So, we just slap the length in there and verify it on read.
38+
//
39+
// Thanks to @isaacs for the whiteboarding session that ended up with this.
3240
return appendFileAsync(
33-
bucket, '\n' + JSON.stringify(entry)
41+
bucket, `\n${stringified.length}\t${stringified}`
3442
).then(() => entry)
3543
}).then(entry => (
3644
fixOwner.chownr(bucket, opts.uid, opts.gid).then(() => (
@@ -46,10 +54,16 @@ function find (cache, key) {
4654
let ret
4755
return Promise.fromNode(cb => {
4856
pipe(stream, split('\n', null, {trailing: true}).on('data', function (l) {
57+
const pieces = l.split('\t')
58+
if (!pieces[1] || pieces[1].length !== parseInt(pieces[0], 10)) {
59+
// Length is no good! Corruption ahoy!
60+
return
61+
}
4962
let obj
5063
try {
51-
obj = JSON.parse(l)
64+
obj = JSON.parse(pieces[1])
5265
} catch (e) {
66+
// Entry is corrupted!
5367
return
5468
}
5569
if (obj && (obj.key === key)) {
@@ -92,9 +106,14 @@ function lsStream (cache) {
92106
if (err) { return cb(err) }
93107
const entries = {}
94108
data.split('\n').slice(1).forEach(function (entry) {
109+
const pieces = entry.split('\t')
110+
if (pieces[1].length !== parseInt(pieces[0], 10)) {
111+
// Length is no good! Corruption ahoy!
112+
return
113+
}
95114
let parsed
96115
try {
97-
parsed = JSON.parse(entry)
116+
parsed = JSON.parse(pieces[1])
98117
} catch (e) {
99118
}
100119
// NOTE - it's possible for an entry to be

Diff for: ‎test/index.find.js

+8-5
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,15 @@ test('index.find garbled data in index file', function (t) {
171171
// to the process crashing). In this case, the corrupt entry
172172
// will simply be skipped.
173173
const key = 'whatever'
174+
const stringified = JSON.stringify({
175+
key: key,
176+
digest: 'deadbeef',
177+
time: 54321
178+
})
174179
const fixture = new Tacks(CacheIndex({
175-
'whatever': '\n' + JSON.stringify({
176-
key: key,
177-
digest: 'deadbeef',
178-
time: 54321
179-
}) + '\n{"key": "' + key + '"\noway'
180+
'whatever': '\n' +
181+
`${stringified.length}\t${stringified}` +
182+
'\n{"key": "' + key + '"\noway'
180183
}))
181184
fixture.create(CACHE)
182185
return index.find(CACHE, key).then(info => {

Diff for: ‎test/index.insert.js

+11-7
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ const testDir = require('./util/test-dir')(__filename)
1212
Promise.promisifyAll(fs)
1313

1414
const CACHE = path.join(testDir, 'cache')
15-
const Dir = Tacks.Dir
1615
const index = require('../lib/entry-index')
1716

1817
const KEY = 'foo'
@@ -35,7 +34,9 @@ test('basic insertion', function (t) {
3534
return fs.readFileAsync(BUCKET, 'utf8')
3635
}).then(data => {
3736
t.equal(data[0], '\n', 'first entry starts with a \\n')
38-
const entry = JSON.parse(data)
37+
const split = data.split('\t')
38+
t.equal(parseInt(split[0], 10), split[1].length, 'length header correct')
39+
const entry = JSON.parse(split[1])
3940
t.ok(entry.time, 'entry has a timestamp')
4041
t.deepEqual(entry, {
4142
key: KEY,
@@ -55,7 +56,9 @@ test('inserts additional entries into existing key', function (t) {
5556
)).then(() => {
5657
return fs.readFileAsync(BUCKET, 'utf8')
5758
}).then(data => {
58-
const entries = data.split('\n').slice(1).map(JSON.parse)
59+
const entries = data.split('\n').slice(1).map(line => {
60+
return JSON.parse(line.split('\t')[1])
61+
})
5962
entries.forEach(function (e) { delete e.time })
6063
t.deepEqual(entries, [{
6164
key: KEY,
@@ -70,6 +73,7 @@ test('inserts additional entries into existing key', function (t) {
7073
})
7174

7275
test('separates entries even if one is corrupted', function (t) {
76+
// TODO - check that middle-of-string corrupted writes won't hurt.
7377
const fixture = new Tacks(CacheIndex({
7478
'foo': '\n' + JSON.stringify({
7579
key: KEY,
@@ -83,7 +87,7 @@ test('separates entries even if one is corrupted', function (t) {
8387
).then(() => {
8488
return fs.readFileAsync(BUCKET, 'utf8')
8589
}).then(data => {
86-
const entry = JSON.parse(data.split('\n')[4])
90+
const entry = JSON.parse(data.split('\n')[4].split('\t')[1])
8791
delete entry.time
8892
t.deepEqual(entry, {
8993
key: KEY,
@@ -99,7 +103,7 @@ test('optional arbitrary metadata', function (t) {
99103
).then(() => {
100104
return fs.readFileAsync(BUCKET, 'utf8')
101105
}).then(data => {
102-
const entry = JSON.parse(data)
106+
const entry = JSON.parse(data.split('\t')[1])
103107
delete entry.time
104108
t.deepEqual(entry, {
105109
key: KEY,
@@ -147,7 +151,7 @@ test('path-breaking characters', function (t) {
147151
const bucket = index._bucketPath(CACHE, newKey)
148152
return fs.readFileAsync(bucket, 'utf8')
149153
}).then(data => {
150-
const entry = JSON.parse(data)
154+
const entry = JSON.parse(data.split('\t')[1])
151155
delete entry.time
152156
t.deepEqual(entry, {
153157
key: newKey,
@@ -167,7 +171,7 @@ test('extremely long keys', function (t) {
167171
const bucket = index._bucketPath(CACHE, newKey)
168172
return fs.readFileAsync(bucket, 'utf8')
169173
}).then(data => {
170-
const entry = JSON.parse(data)
174+
const entry = JSON.parse(data.split('\t')[1])
171175
delete entry.time
172176
t.deepEqual(entry, {
173177
key: newKey,

Diff for: ‎test/util/cache-index.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ function CacheIndex (entries, hashAlgorithm) {
2626
if (typeof lines.length !== 'number') {
2727
lines = [lines]
2828
}
29-
serialised = '\n' + lines.map(JSON.stringify).join('\n')
29+
serialised = '\n' + lines.map(line => {
30+
const stringified = JSON.stringify(line)
31+
return `${stringified.length}\t${stringified}`
32+
}).join('\n')
3033
}
3134
insertContent(tree, parts, serialised)
3235
})

0 commit comments

Comments
 (0)
Failed to load comments.