UNPKG

@web4/bitdrive

Version:

Bitdrive is a secure, real time distributed file system

527 lines (448 loc) 15.1 kB
const tape = require('tape') const collect = require('stream-collector') const FuzzBuzz = require('fuzzbuzz') const create = require('./helpers/create') const MAX_PATH_DEPTH = 30 const MAX_FILE_LENGTH = 1e3 const CHARACTERS = 1e3 const APPROX_READS_PER_FD = 5 const APPROX_WRITES_PER_FD = 5 const INVALID_CHARS = new Set(['/', '\\', '?', '%', '*', ':', '|', '"', '<', '>', '.', ' ', '\n', '\t', '\r']) class BitdriveFuzzer extends FuzzBuzz { constructor (opts) { super(opts) this.add(10, this.writeFile) this.add(5, this.deleteFile) this.add(5, this.existingFileOverwrite) this.add(5, this.randomStatefulFileDescriptorRead) this.add(5, this.randomStatefulFileDescriptorWrite) this.add(3, this.statFile) // this.add(3, this.statDirectory) this.add(2, this.deleteInvalidFile) this.add(2, this.randomReadStream) this.add(2, this.randomStatelessFileDescriptorRead) this.add(1, this.createReadableFileDescriptor) // this.add(1, this.writeAndMkdir) } // START Helper functions. _select (map) { let idx = this.randomInt(map.size - 1) if (idx < 0) return null let ite = map.entries() while (idx--) ite.next() return ite.next().value } _selectFile () { return this._select(this.files) } _selectDirectory () { return this._select(this.directories) } _selectReadableFileDescriptor () { return this._select(this.readable_fds) } _validChar () { do { var char = String.fromCharCode(this.randomInt(CHARACTERS)) } while (INVALID_CHARS.has(char)) return char } _fileName () { do { let depth = Math.max(this.randomInt(MAX_PATH_DEPTH), 1) var name = (new Array(depth)).fill(0).map(() => this._validChar()).join('/') } while (this.files.get(name) || this.directories.get(name)) return name } _content () { return Buffer.allocUnsafe(this.randomInt(MAX_FILE_LENGTH)).fill(0).map(() => this.randomInt(10)) } _createFile () { let name = this._fileName() let content = this._content() return { name, content } } _deleteFile (name) { return new Promise((resolve, reject) => { this.drive.unlink(name, err => { if (err) return reject(err) this.files.delete(name) return resolve({ type: 'delete', name }) }) }) } // START FuzzBuzz interface _setup () { this.drive = create() this.files = new Map() this.directories = new Map() this.streams = new Map() this.readable_fds = new Map() this.log = [] return new Promise((resolve, reject) => { this.drive.ready(err => { if (err) return reject(err) return resolve() }) }) } _validationDrive () { return this.drive } _validateFile (name, content) { let drive = this._validationDrive() return new Promise((resolve, reject) => { drive.readFile(name, (err, data) => { if (err) return reject(err) if (!data.equals(content)) return reject(new Error(`Read data for ${name} does not match written content.`)) return resolve() }) }) } _validateDirectory (name, list) { /* let drive = this._validationDrive() return new Promise((resolve, reject) => { drive.readdir(name, (err, list) => { if (err) return reject(err) let fileSet = new Set(list) for (const file of list) { if (!fileSet.has(file)) return reject(new Error(`Directory does not contain expected file: ${file}`)) fileSet.delete(file) } if (fileSet.size) return reject(new Error(`Directory contains unexpected files: ${fileSet}`)) return resolve() }) }) */ } async _validate () { for (const [fileName, content] of this.files) { await this._validateFile(fileName, content) } for (const [dirName, list] of this.directories) { await this._validateDirectory(dirName, list) } } async call (ops) { let res = await super.call(ops) this.log.push(res) } // START Fuzzing operations writeFile () { let { name, content } = this._createFile() return new Promise((resolve, reject) => { this.debug(`Writing file ${name} with content ${content.length}`) this.drive.writeFile(name, content, err => { if (err) return reject(err) this.files.set(name, content) return resolve({ type: 'write', name, content }) }) }) } deleteFile () { let selected = this._selectFile() if (!selected) return let fileName = selected[0] this.debug(`Deleting valid file: ${fileName}`) return this._deleteFile(fileName) } async deleteInvalidFile () { let name = this._fileName() while (this.files.get(name)) name = this._fileName() try { this.debug(`Deleting invalid file: ${name}`) await this._deleteFile(name) } catch (err) { if (err && err.code !== 'ENOENT') throw err } } statFile () { let selected = this._selectFile() if (!selected) return let [fileName, content] = selected return new Promise((resolve, reject) => { this.debug(`Statting file: ${fileName}`) this.drive.stat(fileName, (err, st) => { if (err) return reject(err) if (!st) return reject(new Error(`File ${fileName} should exist but does not exist.`)) if (st.size !== content.length) return reject(new Error(`Incorrect content length for file ${fileName}.`)) return resolve({ type: 'stat', fileName, stat: st }) }) }) } statDirectory () { let selected = this._selectDirectory() if (!selected) return let [dirName, { offset, byteOffset }] = selected this.debug(`Statting directory ${dirName}.`) return new Promise((resolve, reject) => { this.drive.stat(dirName, (err, st) => { if (err) return reject(err) if (!st) return reject(new Error(`Directory ${dirName} should exist but does not exist.`)) if (!st.isDirectory()) return reject(new Error(`Stat for directory ${dirName} does not have directory mode`)) console.log('st:', st, 'offset:', offset, 'byteOffset:', byteOffset) if (st.offset !== offset || st.byteOffset !== byteOffset) return reject(new Error(`Invalid offsets for ${dirName}`)) this.debug(` Successfully statted directory.`) return resolve({ type: 'stat', dirName }) }) }) } existingFileOverwrite () { let selected = this._selectFile() if (!selected) return let [fileName] = selected let { content: newContent } = this._createFile() return new Promise((resolve, reject) => { this.debug(`Overwriting existing file: ${fileName}`) let writeStream = this.drive.createWriteStream(fileName) writeStream.on('error', err => reject(err)) writeStream.on('finish', () => { this.files.set(fileName, newContent) resolve() }) writeStream.end(newContent) }) } randomReadStream () { let selected = this._selectFile() if (!selected) return let [fileName, content] = selected return new Promise((resolve, reject) => { let drive = this._validationDrive() let start = this.randomInt(content.length) let length = this.randomInt(content.length - start) this.debug(`Creating random read stream for ${fileName} at start ${start} with length ${length}`) let stream = drive.createReadStream(fileName, { start, length }) collect(stream, (err, bufs) => { if (err) return reject(err) let buf = bufs.length === 1 ? bufs[0] : Buffer.concat(bufs) if (!buf.equals(content.slice(start, start + length))) { console.log('buf:', buf, 'content slice:', content.slice(start, start + length)) return reject(new Error('Read stream does not match content slice.')) } this.debug(`Random read stream for ${fileName} succeeded.`) return resolve() }) }) } randomStatelessFileDescriptorRead () { let selected = this._selectFile() if (!selected) return let [fileName, content] = selected let length = this.randomInt(content.length) let start = this.randomInt(content.length) let actualLength = Math.min(length, content.length) let buf = Buffer.alloc(actualLength) return new Promise((resolve, reject) => { let drive = this._validationDrive() this.debug(`Random stateless file descriptor read for ${fileName}, ${length} starting at ${start}`) drive.open(fileName, 'r', (err, fd) => { if (err) return reject(err) drive.read(fd, buf, 0, length, start, (err, bytesRead) => { if (err) return reject(err) buf = buf.slice(0, bytesRead) let expected = content.slice(start, start + bytesRead) if (!buf.equals(expected)) return reject(new Error('File descriptor read does not match slice.')) drive.close(fd, err => { if (err) return reject(err) this.debug(`Random file descriptor read for ${fileName} succeeded`) return resolve() }) }) }) }) } createReadableFileDescriptor () { let selected = this._selectFile() if (!selected) return let [fileName, content] = selected let start = this.randomInt(content.length / 5) let drive = this._validationDrive() return new Promise((resolve, reject) => { this.debug(`Creating readable FD for file ${fileName} and start: ${start}`) drive.open(fileName, 'r', (err, fd) => { if (err) return reject(err) this.readable_fds.set(fd, { pos: start, started: false, content }) return resolve() }) }) } randomStatefulFileDescriptorRead () { let selected = this._selectReadableFileDescriptor() if (!selected) return let [fd, fdInfo] = selected let { content, pos, started } = fdInfo // Try to get multiple reads of of each fd. let length = this.randomInt(content.length / APPROX_READS_PER_FD) let actualLength = Math.min(length, content.length) let buf = Buffer.alloc(actualLength) this.debug(`Reading from random stateful FD ${fd}`) let self = this return new Promise((resolve, reject) => { let drive = this._validationDrive() let start = null if (!started) { fdInfo.started = true start = fdInfo.pos } drive.read(fd, buf, 0, length, start, (err, bytesRead) => { if (err) return reject(err) if (!bytesRead && length) { return close() } buf = buf.slice(0, bytesRead) let expected = content.slice(pos, pos + bytesRead) if (!buf.equals(expected)) return reject(new Error('File descriptor read does not match slice.')) fdInfo.pos += bytesRead return resolve() }) function close () { drive.close(fd, err => { if (err) return reject(err) self.readable_fds.delete(fd) return resolve() }) } }) } randomStatefulFileDescriptorWrite () { let append = !!this.randomInt(1) let flags = append ? 'a' : 'w+' if (append) { let selected = this._selectFile() if (!selected) return var [fileName, content] = selected var pos = content.length } else { fileName = this._fileName() content = Buffer.alloc(0) pos = 0 } const bufs = new Array(this.randomInt(APPROX_WRITES_PER_FD - 1)).fill(0).map(() => this._content()) const self = this let count = 0 return new Promise((resolve, reject) => { this.debug(`Writing stateful file descriptor for fileName ${fileName} with flags ${flags} and buffers ${bufs.length}`) this.drive.open(fileName, flags, (err, fd) => { if (err) return reject(err) if (!bufs.length) return close(fd) return writeNext(fd) }) function writeNext (fd) { let next = bufs[count] self.debug(` Writing content with length ${next.length} to FD ${fd} at pos: ${pos}`) self.drive.write(fd, next, 0, next.length, pos, (err, bytesWritten) => { if (err) return reject(err) pos += bytesWritten bufs[count] = next.slice(0, bytesWritten) if (++count === bufs.length) return close(fd) return writeNext(fd) }) } function close (fd) { self.drive.close(fd, err => { if (err) return reject(err) self.files.set(fileName, Buffer.concat([content, ...bufs])) return resolve() }) } }) } writeAndMkdir () { const self = this let { name: fileName, content } = this._createFile() let dirName = this._fileName() return new Promise((resolve, reject) => { this.debug(`Writing ${fileName} and making dir ${dirName} simultaneously`) let pending = 2 let offset = this.drive._contentFeedLength let byteOffset = this.drive._contentFeedByteLength let writeStream = this.drive.createWriteStream(fileName) writeStream.on('finish', done) this.drive.mkdir(dirName, done) writeStream.end(content) function done (err) { if (err) return reject(err) if (!--pending) { self.files.set(fileName, content) self.debug(`Created directory ${dirName}`) self.directories.set(dirName, { offset, byteOffset }) return resolve() } } }) } } class SparseBitdriveFuzzer extends BitdriveFuzzer { async _setup () { await super._setup() this.remoteDrive = create(this.drive.key, { sparse: true }) return new Promise((resolve, reject) => { this.remoteDrive.ready(err => { if (err) throw err let s1 = this.remoteDrive.replicate(true, { live: true, timeout: 0 }) s1.pipe(this.drive.replicate(false, { live: true, timeout: 0 })).pipe(s1) this.remoteDrive.ready(err => { if (err) return reject(err) return resolve() }) }) }) } _validationDrive () { return this.remoteDrive } } module.exports = BitdriveFuzzer tape('20000 mixed operations, single drive', async t => { t.plan(1) const fuzz = new BitdriveFuzzer({ seed: 'bitdrive', debugging: false }) try { await fuzz.run(20000) t.pass('fuzzing succeeded') } catch (err) { t.error(err, 'no error') } }) tape('20000 mixed operations, replicating drives', async t => { t.plan(1) const fuzz = new SparseBitdriveFuzzer({ seed: 'bitdrive2', debugging: false }) try { await fuzz.run(20000) t.pass('fuzzing succeeded') } catch (err) { t.error(err, 'no error') } }) tape('100 quick validations (initialization timing)', async t => { t.plan(1) try { for (let i = 0; i < 100; i++) { const fuzz = new BitdriveFuzzer({ seed: 'iteration #' + i, debugging: false }) await fuzz.run(100) } t.pass('fuzzing suceeded') } catch (err) { t.error(err, 'no error') } })