Skip to content

Commit 79673be

Browse files
committed
Remove ASCII restriction from sublevel names
Closes: #70 Category: change
1 parent f195d99 commit 79673be

File tree

4 files changed

+41
-34
lines changed

4 files changed

+41
-34
lines changed

README.md

+2-6
Original file line numberDiff line numberDiff line change
@@ -382,11 +382,11 @@ for await (const [key, value] of db.iterator()) {
382382

383383
> :pushpin: The key structure is equal to that of [`subleveldown`](https://github.com/Level/subleveldown) which offered sublevels before they were built-in to `abstract-level`. This means that an `abstract-level` sublevel can read sublevels previously created with (and populated by) `subleveldown`.
384384
385-
Internally, sublevels operate on keys that are either a string, Buffer or Uint8Array, depending on parent database and choice of encoding. Which is to say: binary keys are fully supported. The `name` must however always be a string and can only contain ASCII characters.
385+
Internally, sublevels operate on keys that are either a string, Buffer or Uint8Array, depending on parent database and choice of encoding. Which is to say: binary keys are fully supported. The `name` must however always be a string.
386386

387387
The optional `options` object may contain:
388388

389-
- `separator` (string, default: `'!'`): Character for separating sublevel names from user keys and each other. Must sort before characters used in `name`. An error will be thrown if that's not the case.
389+
- `separator` (string, default: `'!'`): Character for separating sublevel names from user keys and each other.
390390
- `keyEncoding` (string or object, default `'utf8'`): encoding to use for keys
391391
- `valueEncoding` (string or object, default `'utf8'`): encoding to use for values.
392392

@@ -1313,10 +1313,6 @@ Data could not be read (from an underlying store) due to a corruption.
13131313

13141314
Data could not be read (from an underlying store) due to an input/output error, for example from the filesystem.
13151315

1316-
#### `LEVEL_INVALID_PREFIX`
1317-
1318-
When a sublevel prefix contains characters outside of the supported byte range.
1319-
13201316
#### `LEVEL_NOT_SUPPORTED`
13211317

13221318
When a module needs a certain feature, typically as indicated by `db.supports`, but that feature is not available on a database argument or other. For example, some kind of plugin may depend on snapshots:

lib/abstract-sublevel.js

+23-19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use strict'
22

3-
const ModuleError = require('module-error')
43
const { Buffer } = require('buffer') || {}
54
const {
65
AbstractSublevelIterator,
@@ -38,18 +37,11 @@ module.exports = function ({ AbstractLevel }) {
3837
const { separator, manifest, ...forward } = AbstractSublevel.defaults(options)
3938
const names = [].concat(name).map(name => trim(name, separator))
4039

41-
// Reserve one character between separator and name to give us an upper bound
40+
// Reserve one character between separator and name to give us an upper bound, by
41+
// default '"'. Keys should sort like ['!a!', '!a!!a!', '!a"', '!aa!', '!b!']
4242
const reserved = separator.charCodeAt(0) + 1
4343
const root = db[kRoot] || db
4444

45-
// Keys should sort like ['!a!', '!a!!a!', '!a"', '!aa!', '!b!'].
46-
// Use ASCII for consistent length between string, Buffer and Uint8Array
47-
if (!names.every(name => textEncoder.encode(name).every(x => x > reserved && x < 127))) {
48-
throw new ModuleError(`Sublevel name must use bytes > ${reserved} < ${127}`, {
49-
code: 'LEVEL_INVALID_PREFIX'
50-
})
51-
}
52-
5345
super(mergeManifests(db, manifest), forward)
5446

5547
const localPrefix = names.map(name => separator + name + separator).join('')
@@ -179,14 +171,22 @@ module.exports = function ({ AbstractLevel }) {
179171
// TODO (refactor): move to AbstractLevel
180172
this.#prefixRange(options, options.keyEncoding)
181173
const iterator = this[kRoot].iterator(options)
182-
const unfix = this.#unfix.get(this.#globalPrefix.utf8.length, options.keyEncoding)
174+
const unfix = this.#unfix.get(
175+
this.#globalPrefix.utf8.length,
176+
this.#globalPrefix.view.byteLength,
177+
options.keyEncoding
178+
)
183179
return new AbstractSublevelIterator(this, options, iterator, unfix)
184180
}
185181

186182
_keys (options) {
187183
this.#prefixRange(options, options.keyEncoding)
188184
const iterator = this[kRoot].keys(options)
189-
const unfix = this.#unfix.get(this.#globalPrefix.utf8.length, options.keyEncoding)
185+
const unfix = this.#unfix.get(
186+
this.#globalPrefix.utf8.length,
187+
this.#globalPrefix.view.byteLength,
188+
options.keyEncoding
189+
)
190190
return new AbstractSublevelKeyIterator(this, options, iterator, unfix)
191191
}
192192

@@ -252,20 +252,24 @@ class Unfixer {
252252
this.cache = new Map()
253253
}
254254

255-
get (prefixLength, keyFormat) {
255+
get (stringLength, byteLength, keyFormat) {
256256
let unfix = this.cache.get(keyFormat)
257257

258258
if (unfix === undefined) {
259259
if (keyFormat === 'view') {
260-
unfix = function (prefixLength, key) {
260+
unfix = function (byteLength, key) {
261261
// Avoid Uint8Array#slice() because it copies
262-
return key.subarray(prefixLength)
263-
}.bind(null, prefixLength)
262+
return key.subarray(byteLength)
263+
}.bind(null, byteLength)
264+
} else if (keyFormat === 'utf8') {
265+
unfix = function (stringLength, key) {
266+
return key.slice(stringLength)
267+
}.bind(null, stringLength)
264268
} else {
265-
unfix = function (prefixLength, key) {
269+
unfix = function (byteLength, key) {
266270
// Avoid Buffer#subarray() because it's slow
267-
return key.slice(prefixLength)
268-
}.bind(null, prefixLength)
271+
return key.slice(byteLength)
272+
}.bind(null, byteLength)
269273
}
270274

271275
this.cache.set(keyFormat, unfix)

test/self/sublevel-test.js

-9
Original file line numberDiff line numberDiff line change
@@ -175,15 +175,6 @@ test('sublevel name and options', function (t) {
175175
t.end()
176176
})
177177

178-
t.test('invalid sublevel prefix', function (t) {
179-
t.throws(() => new NoopLevel().sublevel('foo\x05'), (err) => err.code === 'LEVEL_INVALID_PREFIX')
180-
t.throws(() => new NoopLevel().sublevel('foo\xff'), (err) => err.code === 'LEVEL_INVALID_PREFIX')
181-
t.throws(() => new NoopLevel().sublevel(['ok', 'foo\xff']), (err) => err.code === 'LEVEL_INVALID_PREFIX')
182-
t.throws(() => new NoopLevel().sublevel('foo!', { separator: '@' }), (err) => err.code === 'LEVEL_INVALID_PREFIX')
183-
t.throws(() => new NoopLevel().sublevel(['ok', 'foo!'], { separator: '@' }), (err) => err.code === 'LEVEL_INVALID_PREFIX')
184-
t.end()
185-
})
186-
187178
// See https://github.com/Level/subleveldown/issues/78
188179
t.test('doubly nested sublevel has correct prefix', async function (t) {
189180
t.plan(1)

test/sublevel-test.js

+16
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,20 @@ exports.all = function (test, testCommon) {
206206
})
207207
}
208208
}
209+
210+
test('sublevel name with unicode', async function (t) {
211+
const db = testCommon.factory()
212+
const name = '🐄'
213+
const sub = db.sublevel(name)
214+
215+
// To illustrate why this test matters. We would remove too many
216+
// characters from the prefixed key if we use the wrong length.
217+
t.is(name.length, 2)
218+
t.is(new TextEncoder().encode(name).byteLength, 4)
219+
220+
await sub.put('a', 'a')
221+
t.same(await sub.keys().all(), ['a'], 'correctly removed prefix')
222+
223+
return db.close()
224+
})
209225
}

0 commit comments

Comments
 (0)