From bf7cdbf5b446aae512d3f7aaefb34569ec1ac0be Mon Sep 17 00:00:00 2001 From: Rafael Weinstein Date: Fri, 23 Oct 2015 12:55:03 -0700 Subject: [PATCH] Beginnings of new js sdk --- js2/.flowconfig | 8 ++++ js2/.gitignore | 2 + js2/package.json | 17 ++++++++ js2/src/chunk.js | 25 +++++++++++ js2/src/chunk_test.js | 35 ++++++++++++++++ js2/src/memory_store.js | 53 +++++++++++++++++++++++ js2/src/memory_store_test.js | 60 ++++++++++++++++++++++++++ js2/src/ref.js | 81 ++++++++++++++++++++++++++++++++++++ js2/src/ref_test.js | 70 +++++++++++++++++++++++++++++++ 9 files changed, 351 insertions(+) create mode 100644 js2/.flowconfig create mode 100644 js2/.gitignore create mode 100644 js2/package.json create mode 100644 js2/src/chunk.js create mode 100644 js2/src/chunk_test.js create mode 100644 js2/src/memory_store.js create mode 100644 js2/src/memory_store_test.js create mode 100644 js2/src/ref.js create mode 100644 js2/src/ref_test.js diff --git a/js2/.flowconfig b/js2/.flowconfig new file mode 100644 index 0000000000..c5d1a06f83 --- /dev/null +++ b/js2/.flowconfig @@ -0,0 +1,8 @@ +[ignore] + +[include] + +[libs] + +[options] +unsafe.enable_getters_and_setters=true diff --git a/js2/.gitignore b/js2/.gitignore new file mode 100644 index 0000000000..f06235c460 --- /dev/null +++ b/js2/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/js2/package.json b/js2/package.json new file mode 100644 index 0000000000..2641eac573 --- /dev/null +++ b/js2/package.json @@ -0,0 +1,17 @@ +{ + "name": "newjs", + "main": "dist/noms.js", + "dependencies": { + "rusha": "^0.8.3" + }, + "devDependencies": { + "babel": "^5.6.23", + "chai": "^3.2.0", + "mocha": "^2.3.0" + }, + "scripts": { + "start": "babel -w src/ -d dist/", + "build": "babel src/ -d dist/", + "test": "babel src/ -d dist/; flow; mocha --ui tdd dist/" + } +} diff --git a/js2/src/chunk.js b/js2/src/chunk.js new file mode 100644 index 0000000000..be1ee92691 --- /dev/null +++ b/js2/src/chunk.js @@ -0,0 +1,25 @@ +/* @flow */ + +'use strict'; + +const Ref = require('./ref.js'); + +class Chunk { + ref: Ref; + data: string; + + constructor(data: string = '', ref: ?Ref) { + this.data = data; + this.ref = ref ? ref : Ref.fromData(data); + } + + isEmpty(): boolean { + return this.data.length === 0; + } + + static emptyChunk: Chunk; +} + +Chunk.emptyChunk = new Chunk(); + +module.exports = Chunk; diff --git a/js2/src/chunk_test.js b/js2/src/chunk_test.js new file mode 100644 index 0000000000..f6de70036f --- /dev/null +++ b/js2/src/chunk_test.js @@ -0,0 +1,35 @@ +/* @flow */ + +'use strict'; + +const {suite, test} = require('mocha'); +const {assert} = require('chai'); +const Chunk = require('./chunk.js'); +const Ref = require('./ref.js'); + +suite('Chunk', () => { + test('construct', () => { + let c = new Chunk('abc'); + assert.strictEqual(c.data, 'abc'); + assert.isTrue(c.ref.equals(Ref.parse('sha1-a9993e364706816aba3e25717850c26c9cd0d89d'))); + assert.isFalse(c.isEmpty()); + }); + + test('construct with ref', () => { + let ref = Ref.parse('sha1-0000000000000000000000000000000000000001'); + let c = new Chunk('abc', ref); + assert.strictEqual(c.data, 'abc'); + assert.isTrue(c.ref.equals(Ref.parse('sha1-0000000000000000000000000000000000000001'))) + assert.isFalse(c.isEmpty()); + }); + + test('isEmpty', () => { + function assertChunkIsEmpty(c: Chunk) { + assert.strictEqual(c.data.length, 0); + assert.isTrue(c.isEmpty()); + } + + assertChunkIsEmpty(new Chunk()); + assertChunkIsEmpty(new Chunk('')); + }); +}); diff --git a/js2/src/memory_store.js b/js2/src/memory_store.js new file mode 100644 index 0000000000..1fb06dce05 --- /dev/null +++ b/js2/src/memory_store.js @@ -0,0 +1,53 @@ +/* @flow */ + +'use strict'; + +const Ref = require('./ref.js'); +const Chunk = require('./chunk.js'); + +class MemoryStore { + _data: { [key: string]: Chunk }; + _root: Ref; + + constructor() { + this._data = Object.create(null); + this._root = new Ref(); + } + + get root(): Ref { + return this._root; + } + + updateRoot(current: Ref, last: Ref): boolean { + if (!this._root.equals(last)) { + return false + } + + this._root = current; + return true; + } + + get(ref: Ref): Chunk { + var c = this._data[ref.toString()]; + if (c == null) { + c = Chunk.emptyChunk; + } + return c; + } + + has(ref: Ref): boolean { + return this._data[ref.toString()] == null; + } + + put(c: Chunk) { + this._data[c.ref.toString()] = c; + } + + get size(): number { + return Object.keys(this._data).length; + } + + close() {} +} + +module.exports = MemoryStore; diff --git a/js2/src/memory_store_test.js b/js2/src/memory_store_test.js new file mode 100644 index 0000000000..ecb46a85b6 --- /dev/null +++ b/js2/src/memory_store_test.js @@ -0,0 +1,60 @@ +/* @flow */ + +'use strict'; + +const {suite, test} = require('mocha'); +const {assert} = require('chai'); +const Chunk = require('./chunk.js'); +const Ref = require('./ref.js'); +const MemoryStore = require('./memory_store.js'); + +suite('MemoryStore', () => { + function assertInputInStore(input: string, ref: Ref, ms: MemoryStore) { + let chunk = ms.get(ref); + assert.isFalse(chunk.isEmpty()); + assert.strictEqual(input, chunk.data); + } + + test('put', () => { + let ms = new MemoryStore(); + let input = 'abc'; + let c = new Chunk(input); + ms.put(c); + + // See http://www.di-mgt.com.au/sha_testvectors.html + assert.strictEqual('sha1-a9993e364706816aba3e25717850c26c9cd0d89d', c.ref.toString()); + + ms.updateRoot(c.ref, ms.root); + + assertInputInStore(input, c.ref, ms); + + // Re-writing the same data should be idempotent and should not result in a second put + c = new Chunk(input); + ms.put(c); + assertInputInStore(input, c.ref, ms); + }); + + test('updateRoot', () => { + let ms = new MemoryStore(); + let oldRoot = ms.root; + assert.isTrue(oldRoot.isEmpty()); + + let bogusRoot = Ref.parse('sha1-81c870618113ba29b6f2b396ea3a69c6f1d626c5'); // sha1("Bogus, Dude") + let newRoot = Ref.parse('sha1-907d14fb3af2b0d4f18c2d46abe8aedce17367bd'); // sha1("Hello, World") + + // Try to update root with bogus oldRoot + let result = ms.updateRoot(newRoot, bogusRoot); + assert.isFalse(result); + + // Now do a valid root update + result = ms.updateRoot(newRoot, oldRoot); + assert.isTrue(result); + }); + + test('get non-existing', () => { + let ms = new MemoryStore(); + let ref = Ref.parse('sha1-1111111111111111111111111111111111111111'); + let c = ms.get(ref); + assert.isTrue(c.isEmpty()); + }); +}); diff --git a/js2/src/ref.js b/js2/src/ref.js new file mode 100644 index 0000000000..39b71a1e5a --- /dev/null +++ b/js2/src/ref.js @@ -0,0 +1,81 @@ +/* @flow */ + +'use strict'; + +const Rusha = require('rusha'); + +const r = new Rusha(); +const sha1Size = 20; +const pattern = /^sha1-([0-9a-f]{40})$/; + +function uint8ArrayToHex(a: Uint8Array): string { + let hex = ''; + for (let i = 0; i < a.length; i++) { + let v = a[i].toString(16); + if (v.length == 1) { + hex += '0' + v; + } else { + hex += v; + } + } + + return hex; +} + +function hexToUint8(s: string): Uint8Array { + let digest = new Uint8Array(sha1Size); + for (let i = 0; i < sha1Size; i++) { + let ch = s.substring(i*2, i*2 + 2); + digest[i] = parseInt(ch, 16) + } + + return digest; +} + +class Ref { + digest: Uint8Array; + + constructor(digest: Uint8Array = new Uint8Array(sha1Size)) { + this.digest = digest; + } + + isEmpty(): boolean { + for (let i = 0; i < sha1Size; i++) { + if (this.digest[i] != 0) { + return false; + } + } + + return true; + } + + equals(other: Ref): boolean { + for (let i = 0; i < sha1Size; i++) { + if (this.digest[i] != other.digest[i]) { + return false; + } + } + + return true; + } + + toString(): string { + return 'sha1-' + uint8ArrayToHex(this.digest); + } + + static parse(s: string): Ref { + let m = s.match(pattern); + if (m === null) { + throw Error('Could not parse ref: ' + s); + } + + return new Ref(hexToUint8(m[1])); + } + + static fromData(data: string): Ref { + let digest = r.rawDigest(data); + return new Ref(new Uint8Array(digest.buffer)); + } +} + +module.exports = Ref; diff --git a/js2/src/ref_test.js b/js2/src/ref_test.js new file mode 100644 index 0000000000..069e40d619 --- /dev/null +++ b/js2/src/ref_test.js @@ -0,0 +1,70 @@ +/* @flow */ + +'use strict'; + +const {suite, test} = require('mocha'); +const {assert} = require('chai'); +const Ref = require('./ref.js'); + +suite('Ref', () => { + test('parse', () => { + function assertParseError(s) { + assert.throws(() => { + Ref.parse(s); + }); + } + + assertParseError('foo'); + assertParseError('sha1'); + assertParseError('sha1-0'); + + // too many digits + assertParseError('sha1-00000000000000000000000000000000000000000'); + + // 'g' not valid hex + assertParseError('sha1- 000000000000000000000000000000000000000g'); + + // sha2 not supported + assertParseError('sha2-0000000000000000000000000000000000000000'); + + let r = Ref.parse('sha1-0000000000000000000000000000000000000000'); + assert.isNotNull(r); + }); + + test('equals', () => { + let r0 = Ref.parse('sha1-0000000000000000000000000000000000000000'); + let r01 = Ref.parse('sha1-0000000000000000000000000000000000000000'); + let r1 = Ref.parse('sha1-0000000000000000000000000000000000000001'); + + assert.isTrue(r0.equals(r01)); + assert.isTrue(r01.equals(r0)); + assert.isFalse(r0.equals(r1)); + assert.isFalse(r1.equals(r0)); + }); + + test('toString', () => { + let s = 'sha1-0123456789abcdef0123456789abcdef01234567'; + let r = Ref.parse(s); + assert.strictEqual(s, r.toString()); + }); + + test('fromData', () => { + let r = Ref.fromData('abc'); + + assert.strictEqual('sha1-a9993e364706816aba3e25717850c26c9cd0d89d', r.toString()); + }); + + test('isEmpty', () => { + let digest = new Uint8Array(20); + let r = new Ref(digest); + assert.isTrue(r.isEmpty()); + + digest[0] = 10; + r = new Ref(digest); + assert.isFalse(r.isEmpty()); + + r = new Ref(); + assert.isTrue(r.isEmpty()); + }); + +});