Split node & browser Uint8Array usage (#1811)

Split node & browser Uint8Array usage
This commit is contained in:
Rafael Weinstein
2016-06-14 12:28:28 -07:00
committed by GitHub
parent 040397ca92
commit 62dcc6f377
27 changed files with 568 additions and 364 deletions

View File

@@ -49,12 +49,10 @@
"copy-flow-files-commonjs": "node build/copy-flow-files.js -d dist/commonjs/ src/"
},
"browser": {
"./src/bytes.js": "./src/browser/bytes.js",
"./dist/commonjs/bytes.js": "./dist/commonjs/browser/bytes.js",
"./src/fetch.js": "./src/browser/fetch.js",
"./dist/commonjs/fetch.js": "./dist/commonjs/browser/fetch.js",
"./src/sha1.js": "./src/browser/sha1.js",
"./dist/commonjs/sha1.js": "./dist/commonjs/browser/sha1.js",
"./src/utf8.js": "./src/browser/utf8.js",
"./dist/commonjs/utf8.js": "./dist/commonjs/browser/utf8.js",
"./src/put-cache.js": "./src/browser/put-cache.js",
"./dist/commonjs/put-cache.js": "./dist/commonjs/browser/put-cache.js"
}

View File

@@ -18,6 +18,7 @@ import SequenceChunker from './sequence-chunker.js';
import type {BoundaryChecker, makeChunkFn} from './sequence-chunker.js';
import {Kind} from './noms-kind.js';
import type {EqualsFn} from './edit-distance.js';
import Bytes from './bytes.js';
export default class Blob extends Collection<IndexedSequence> {
constructor(bytes: Uint8Array) {
@@ -76,7 +77,7 @@ export class BlobReader {
const idx = cur.indexInChunk;
if (idx > 0) {
invariant(idx < arr.byteLength);
arr = new Uint8Array(arr.buffer, arr.byteOffset + idx, arr.byteLength - idx);
arr = Bytes.subarray(arr, idx, arr.byteLength);
}
return cur.advanceChunk().then(() => {
@@ -147,7 +148,7 @@ const blobPattern = ((1 << 11) | 0) - 1; // Avg Chunk Size: 2k
function newBlobLeafChunkFn(vr: ?ValueReader, vw: ?ValueWriter): makeChunkFn {
return (items: Array<number>) => {
const blobLeaf = new BlobLeafSequence(vr, new Uint8Array(items));
const blobLeaf = new BlobLeafSequence(vr, Bytes.fromValues(items));
const blob = Blob.fromSequence(blobLeaf);
let mt;
if (vw) {

125
js/src/browser/bytes.js Normal file
View File

@@ -0,0 +1,125 @@
// @flow
// Copyright 2016 The Noms Authors. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
import {TextEncoder, TextDecoder} from './text-encoding.js';
import Rusha from 'rusha';
const decoder = new TextDecoder();
const encoder = new TextEncoder();
const r = new Rusha();
const littleEndian = true;
export default class Bytes {
static alloc(size: number): Uint8Array {
return new Uint8Array(size);
}
static fromValues(values: number[]): Uint8Array {
return new Uint8Array(values);
}
static fromString(s: string): Uint8Array {
return encoder.encode(s);
}
static toString(buff: Uint8Array): string {
return decoder.decode(buff);
}
static fromHexString(s: string): Uint8Array {
const length = s.length / 2;
const buff = new Uint8Array(length);
for (let i = 0; i < length; i++) {
const hc = asciiToBinary(s.charCodeAt(2 * i));
const lc = asciiToBinary(s.charCodeAt(2 * i + 1));
buff[i] = hc << 4 | lc;
}
return buff;
}
static toHexString(buff: Uint8Array): string {
const hex = new Array(buff.byteLength * 2);
for (let i = 0; i < buff.length; i++) {
hex[i] = byteToAscii[buff[i]];
}
return hex.join('');
}
static grow(buff: Uint8Array, size: number): Uint8Array {
const b = new Uint8Array(size);
b.set(buff);
return b;
}
static copy(source: Uint8Array, target: Uint8Array, targetStart: number = 0) {
target.set(source, targetStart);
}
static slice(buff: Uint8Array, start: number, end: number): Uint8Array {
return buff.slice(start, end);
}
static subarray(buff: Uint8Array, start: number, end: number): Uint8Array {
return buff.subarray(start, end);
}
static readUtf8(buff: Uint8Array, start: number, end: number): string {
return Bytes.toString(buff.subarray(start, end));
}
static encodeUtf8(str: string, buff: Uint8Array, dv: DataView, offset: number): number {
const strBuff = Bytes.fromString(str);
const size = strBuff.byteLength;
dv.setUint32(offset, size, littleEndian);
offset += 4;
buff.set(strBuff, offset);
offset += size;
return offset;
}
static compare(b1: Uint8Array, b2: Uint8Array): number {
const b1Len = b1.byteLength;
const b2Len = b2.byteLength;
for (let i = 0; i < b1Len && i < b2Len; i++) {
if (b1[i] < b2[i]) {
return -1;
}
if (b1[i] > b2[i]) {
return 1;
}
}
if (b1Len < b2Len) {
return -1;
}
if (b1Len > b2Len) {
return 1;
}
return 0;
}
static sha1(data: Uint8Array): Uint8Array {
const ta = r.rawDigest(data);
return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
}
}
function asciiToBinary(cc: number): number {
// This only accepts the char code for '0' - '9', 'a' - 'f'
return cc - (cc <= 57 ? 48 : 87); // '9', '0', 'a' - 10
}
// Precompute '00' to 'ff'.
const byteToAscii = new Array(256);
for (let i = 0; i < 256; i++) {
byteToAscii[i] = (i < 0x10 ? '0' : '') + i.toString(16);
}

View File

@@ -45,9 +45,9 @@ export function fetchText(url: string, options: FetchOptions = {}): Promise<stri
return fetch(url, 'text', options);
}
export function fetchArrayBuffer(url: string, options: FetchOptions = {}): Promise<ArrayBuffer> {
export function fetchUint8Array(url: string, options: FetchOptions = {}): Promise<Uint8Array> {
if (self.fetch) {
return self.fetch(url, options).then(resp => resp.arrayBuffer());
return self.fetch(url, options).then(resp => resp.arrayBuffer()).then(ar => new Uint8Array(ar));
}
return fetch(url, 'arraybuffer', options);

View File

@@ -1,16 +0,0 @@
// @flow
// Copyright 2016 The Noms Authors. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
// This is the browser version. The Node.js version is in ../sha1.js.
import Rusha from 'rusha';
const r = new Rusha();
export default function sha1(data: Uint8Array): Uint8Array {
const ta = r.rawDigest(data);
return new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength);
}

View File

@@ -1,20 +0,0 @@
// @flow
// Copyright 2016 The Noms Authors. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
// This is the browser version. The Node.js version is in ../utf8.js.
import {TextEncoder, TextDecoder} from './text-encoding.js';
const decoder = new TextDecoder();
const encoder = new TextEncoder();
export function encode(s: string): Uint8Array {
return encoder.encode(s);
}
export function decode(data: Uint8Array): string {
return decoder.decode(data);
}

View File

@@ -7,11 +7,7 @@
import {suite, test} from 'mocha';
import {assert} from 'chai';
import BuzHash from './buzhash.js';
import {encode} from './utf8.js';
function bytes(s: string): Uint8Array {
return encode(s);
}
import Bytes from './bytes.js';
const loremipsum1 = `Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et
@@ -78,10 +74,12 @@ suite('BuzHash', () => {
assert.isTrue(found);
}
test(32, bytes('Aenean massa. Cum sociis natoque'), bytes(loremipsum1), 91);
test(32,
Bytes.fromString('Aenean massa. Cum sociis natoque'),
Bytes.fromString(loremipsum1), 91);
test(64,
bytes('Phasellus leo dolor, tempus non, auctor et, hendrerit quis, nisi'),
bytes(loremipsum2),
Bytes.fromString('Phasellus leo dolor, tempus non, auctor et, hendrerit quis, nisi'),
Bytes.fromString(loremipsum2),
592);
});
});

View File

@@ -6,6 +6,8 @@
// This a javascript port of github.com/kch42/buzhash.
import Bytes from './bytes.js';
const bytehash: Array<number> = [
0x12bd9527, 0xf4140cea, 0x987bd6e1, 0x79079850, 0xafbfd539, 0xd350ce0a,
0x82973931, 0x9fc32b9c, 0x28003b88, 0xc30c13aa, 0x6b678c34, 0x5844ef1d,
@@ -64,7 +66,7 @@ export default class BuzHash {
this._n = n;
this._bshiftn = n % 32;
this._bshiftm = 32 - this._bshiftn;
this._buf = new Uint8Array(n);
this._buf = Bytes.alloc(n);
this.reset();
}

184
js/src/bytes-test.js Normal file
View File

@@ -0,0 +1,184 @@
// @flow
// Copyright 2016 The Noms Authors. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
import {assert} from 'chai';
import {suite, test} from 'mocha';
import {default as NodeBytes} from './bytes.js';
import {default as BrowserBytes} from './browser/bytes.js';
function assertUint8Equal(ar1: Uint8Array, ar2: Uint8Array) {
assert.equal(ar1.length, ar2.length);
for (let i = 0; i < ar1.length; i++) {
assert.equal(ar1[i], ar2[i]);
}
}
function assertBytes(expect: number[], buff: Uint8Array) {
const buffValues = [];
for (let i = 0; i < buff.length; i++) {
buffValues.push(buff[i]);
}
assert.deepEqual(expect, buffValues);
}
suite('Bytes', () => {
test('alloc', () => {
function test(size: number) {
const n = NodeBytes.alloc(size);
const b = BrowserBytes.alloc(size);
assert.strictEqual(size, n.length);
assert.strictEqual(size, b.length);
}
test(0);
test(10);
test(2048);
});
test('string', () => {
function test(expect: number[], str: string) {
assertBytes(expect, NodeBytes.fromString(str));
assertBytes(expect, BrowserBytes.fromString(str));
assert.strictEqual(str, NodeBytes.toString(NodeBytes.fromValues(expect)));
assert.strictEqual(str, BrowserBytes.toString(BrowserBytes.fromValues(expect)));
}
test([], '');
test([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100], 'hello world');
test([207, 128], '\u03c0');
});
test('hex', () => {
function test(expect: number[], hexString: string) {
assertBytes(expect, NodeBytes.fromHexString(hexString));
assertBytes(expect, BrowserBytes.fromHexString(hexString));
assert.strictEqual(hexString, NodeBytes.toHexString(NodeBytes.fromValues(expect)));
assert.strictEqual(hexString, BrowserBytes.toHexString(BrowserBytes.fromValues(expect)));
}
test([], '');
test([0x3e, 0x9a, 0x25, 0x9b], '3e9a259b');
test([0x04, 0x03, 0x97, 0xca, 0x92, 0x1d, 0x02, 0x7d, 0xae, 0x6d,
0xfc, 0x01, 0x02, 0xf2, 0x04, 0x46, 0x6f, 0xea, 0xd5, 0xc9],
'040397ca921d027dae6dfc0102f204466fead5c9');
});
test('grow', () => {
function test(bytes: any) {
const buff = bytes.alloc(4);
buff[0] = 1;
buff[1] = 2;
buff[2] = 3;
const b2 = bytes.grow(buff, 8);
assert.strictEqual(1, b2[0]);
assert.strictEqual(2, b2[1]);
assert.strictEqual(3, b2[2]);
assert.strictEqual(8, b2.length);
}
test(NodeBytes);
test(BrowserBytes);
});
test('copy', () => {
function test(bytes: any) {
const buff = bytes.alloc(3);
buff[0] = 1;
buff[1] = 2;
buff[2] = 3;
const b2 = bytes.alloc(6);
b2[0] = 10;
b2[1] = 11;
b2[2] = 12;
bytes.copy(buff, b2, 3);
assert.strictEqual(10, b2[0]);
assert.strictEqual(11, b2[1]);
assert.strictEqual(12, b2[2]);
assert.strictEqual(1, b2[3]);
assert.strictEqual(2, b2[4]);
assert.strictEqual(3, b2[5]);
}
test(NodeBytes);
test(BrowserBytes);
});
test('slice', () => {
function test(bytes: any) {
const buff = bytes.alloc(3);
buff[0] = 1;
buff[1] = 2;
buff[2] = 3;
const b2 = bytes.slice(buff, 1, 3);
buff[2] = 4;
assert.strictEqual(2, b2[0]);
assert.strictEqual(3, b2[1]);
}
test(NodeBytes);
test(BrowserBytes);
});
test('subarray', () => {
function test(bytes: any) {
const buff = bytes.alloc(3);
buff[0] = 1;
buff[1] = 2;
buff[2] = 3;
const b2 = bytes.subarray(buff, 1, 3);
buff[2] = 4;
assert.strictEqual(2, b2[0]);
assert.strictEqual(4, b2[1]);
}
test(NodeBytes);
test(BrowserBytes);
});
test('compare', () => {
function test(bytes: any, expect: number, a1: number[], a2: number[]) {
assert.strictEqual(expect, bytes.compare(bytes.fromValues(a1), bytes.fromValues(a2)));
}
test(NodeBytes, 0, [], []);
test(BrowserBytes, 0, [], []);
test(NodeBytes, 0, [1, 2, 3], [1, 2, 3]);
test(BrowserBytes, 0, [1, 2, 3], [1, 2, 3]);
test(NodeBytes, -1, [1, 2], [1, 2, 3]);
test(BrowserBytes, -1, [1, 2], [1, 2, 3]);
test(NodeBytes, 1, [1, 2, 3, 4], [1, 2, 3]);
test(BrowserBytes, 1, [1, 2, 3, 4], [1, 2, 3]);
test(NodeBytes, 1, [2, 2, 3], [1, 2, 3]);
test(BrowserBytes, 1, [2, 2, 3], [1, 2, 3]);
test(NodeBytes, -1, [0, 2, 3], [1, 2, 3]);
test(BrowserBytes, -1, [0, 2, 3], [1, 2, 3]);
});
test('sha1', () => {
function test(arr: number[]) {
// Node uses a Buffer, browser uses a Uint8Array
const n = NodeBytes.sha1(NodeBytes.fromValues(arr));
const b = BrowserBytes.sha1(BrowserBytes.fromValues(arr));
assertUint8Equal(n, b);
}
test([]);
test([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
test([1, 2, 3, 4, 5]);
test([2, 3, 4, 5, 10]);
});
});

98
js/src/bytes.js Normal file
View File

@@ -0,0 +1,98 @@
// @flow
// Copyright 2016 The Noms Authors. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
import crypto from 'crypto';
// Note: Flow doesn't know that Buffer extends Uint8Array, thus all of the FlowIssues.
export default class Bytes {
static alloc(size: number): Uint8Array {
// $FlowIssue
return Buffer.alloc(size);
}
static fromValues(values: number[]): Uint8Array {
// $FlowIssue
return Buffer.from(values);
}
static fromString(s: string): Uint8Array {
// $FlowIssue
return Buffer.from(s);
}
static toString(buff: Uint8Array): string {
return buff.toString();
}
static fromHexString(str: string): Uint8Array {
// $FlowIssue
return Buffer.from(str, 'hex');
}
static toHexString(buff: Uint8Array): string {
return buff.toString('hex');
}
static grow(buff: Uint8Array, size: number): Uint8Array {
const b = Bytes.alloc(size);
// $FlowIssue
buff.copy(b);
return b;
}
static copy(source: Uint8Array, target: Uint8Array, targetStart: number = 0) {
// $FlowIssue
if (source instanceof Buffer) {
// $FlowIssue
source.copy(target, targetStart);
return;
}
for (let i = 0; i < source.length; i++) {
target[targetStart++] = source[i];
}
}
static slice(buff: Uint8Array, start: number, end: number): Uint8Array {
const v = Bytes.alloc(end - start);
// $FlowIssue
buff.copy(v, 0, start, end);
return v;
}
static subarray(buff: Uint8Array, start: number, end: number): Uint8Array {
// $FlowIssue
return Buffer.from(buff.buffer, buff.byteOffset + start, end - start);
}
static readUtf8(buff: Uint8Array, start: number, end: number): string {
return buff.toString('utf8', start, end);
}
static encodeUtf8(str: string, buff: Uint8Array, dv: DataView, offset: number): number {
const size = Buffer.byteLength(str);
// $FlowIssue
buff.writeUInt32LE(size, offset);
offset += 4;
// $FlowIssue
buff.write(str, offset);
offset += size;
return offset;
}
static compare(b1: Uint8Array, b2: Uint8Array): number {
// $FlowIssue
return b1.compare(b2);
}
static sha1(data: Uint8Array): Uint8Array {
const hash = crypto.createHash('sha1');
hash.update(data);
return hash.digest();
}
}

View File

@@ -10,6 +10,7 @@ import Chunk from './chunk.js';
import type Hash from './hash.js';
import {deserialize, serialize} from './chunk-serializer.js';
import type {ChunkStream} from './chunk-serializer.js';
import Bytes from './bytes.js';
suite('ChunkSerializer', () => {
@@ -81,10 +82,10 @@ suite('ChunkSerializer', () => {
test('large chunk', async () => {
const expHints = [];
const expChunks = [
new Chunk(new Uint8Array(1024)),
new Chunk(Bytes.alloc(1024)),
Chunk.fromString('abc'),
Chunk.fromString('def'),
new Chunk(new Uint8Array(2048))];
new Chunk(Bytes.alloc(2048))];
const pSerialized = serialize(new Set(expHints), createChunkStream(expChunks));
const {hints, chunks} = deserialize(await pSerialized);

View File

@@ -7,6 +7,7 @@
import Chunk from './chunk.js';
import Hash from './hash.js';
import {invariant} from './assert.js';
import Bytes from './bytes.js';
const headerSize = 4; // uint32
const bigEndian = false; // Passing false to DataView methods makes them use big-endian byte order.
@@ -16,60 +17,57 @@ const chunkHeaderSize = sha1Size + chunkLengthSize;
export type ChunkStream = (cb: (chunk: Chunk) => void) => Promise<void>
export function serialize(hints: Set<Hash>, stream: ChunkStream): Promise<ArrayBuffer> {
let buf = new ArrayBuffer(1024);
const ensureCapacity = (needed: number) => {
if (buf.byteLength >= needed) {
export function serialize(hints: Set<Hash>, stream: ChunkStream): Promise<Uint8Array> {
const hintsLength = serializedHintsLength(hints);
let buf = Bytes.alloc(Math.max(hintsLength * 2, 2048));
let dv = new DataView(buf.buffer);
let offset = 0;
const ensureCapacity = (n: number) => {
let length = buf.byteLength;
if (offset + n <= length) {
return;
}
let newLen = buf.byteLength;
for (; newLen < needed; newLen *= 2)
;
const newBuf = new ArrayBuffer(newLen);
new Uint8Array(newBuf).set(new Uint8Array(buf));
buf = newBuf;
while (offset + n > length) {
length *= 2;
}
buf = Bytes.grow(buf, length);
dv = new DataView(buf.buffer);
};
const hintsLength = serializedHintsLength(hints);
if (buf.byteLength < hintsLength) {
buf = new ArrayBuffer(hintsLength * 2); // Leave space for some chunks.
}
let offset = serializeHints(hints, buf);
offset = serializeHints(hints, buf, dv);
return stream(chunk => {
const chunkLength = serializedChunkLength(chunk);
ensureCapacity(offset + chunkLength);
offset = serializeChunk(chunk, buf, offset);
}).then(() => buf.slice(0, offset));
ensureCapacity(chunkLength);
offset = serializeChunk(chunk, buf, dv, offset);
}).then(() => Bytes.subarray(buf, 0, offset));
}
function serializeChunk(chunk: Chunk, buffer: ArrayBuffer, offset: number): number {
function serializeChunk(chunk: Chunk, buffer: Uint8Array, dv: DataView, offset: number): number {
invariant(buffer.byteLength - offset >= serializedChunkLength(chunk),
'Invalid chunk buffer');
const hashArray = new Uint8Array(buffer, offset, sha1Size);
hashArray.set(chunk.hash.digest);
Bytes.copy(chunk.hash.digest, buffer, offset);
offset += sha1Size;
const chunkLength = chunk.data.length;
const view = new DataView(buffer, offset, chunkLengthSize);
view.setUint32(0, chunkLength, bigEndian); // Coerce number to uint32
dv.setUint32(offset, chunkLength, bigEndian);
offset += chunkLengthSize;
const dataArray = new Uint8Array(buffer, offset, chunkLength);
dataArray.set(chunk.data);
Bytes.copy(chunk.data, buffer, offset);
offset += chunkLength;
return offset;
}
function serializeHints(hints: Set<Hash>, buffer: ArrayBuffer): number {
function serializeHints(hints: Set<Hash>, buff: Uint8Array, dv: DataView): number {
let offset = 0;
const view = new DataView(buffer, offset, headerSize);
view.setUint32(0, hints.size | 0, bigEndian); // Coerce number to uint32
dv.setUint32(offset, hints.size, bigEndian);
offset += headerSize;
hints.forEach(hash => {
const hashArray = new Uint8Array(buffer, offset, sha1Size);
hashArray.set(hash.digest);
Bytes.copy(hash.digest, buff, offset);
offset += sha1Size;
});
@@ -84,54 +82,46 @@ function serializedChunkLength(chunk: Chunk): number {
return chunkHeaderSize + chunk.data.length;
}
export function deserialize(buffer: ArrayBuffer): {hints: Array<Hash>, chunks: Array<Chunk>} {
const {hints, offset} = deserializeHints(buffer);
return {hints: hints, chunks: deserializeChunks(buffer, offset)};
export function deserialize(buff: Uint8Array): {hints: Array<Hash>, chunks: Array<Chunk>} {
const dv = new DataView(buff.buffer, buff.byteOffset, buff.byteLength);
const {hints, offset} = deserializeHints(buff, dv);
return {hints: hints, chunks: deserializeChunks(buff, dv, offset)};
}
function deserializeHints(buffer: ArrayBuffer): {hints: Array<Hash>, offset: number} {
function deserializeHints(buff: Uint8Array, dv: DataView): {hints: Array<Hash>, offset: number} {
const hints:Array<Hash> = [];
let offset = 0;
const view = new DataView(buffer, offset, headerSize);
const numHints = view.getUint32(0, bigEndian);
const numHints = dv.getUint32(offset, bigEndian);
offset += headerSize;
const totalLength = headerSize + (numHints * sha1Size);
for (; offset < totalLength;) {
invariant(buffer.byteLength - offset >= sha1Size, 'Invalid hint buffer');
// Make a slice/copy of the ArrayBuffer to prevent `buffer` from being gc'ed.
const hashArray = new Uint8Array(buffer.slice(offset, offset + sha1Size));
const hash = new Hash(hashArray);
invariant(buff.byteLength - offset >= sha1Size * numHints, 'Invalid hint buffer');
for (let i = 0; i < numHints; i++) {
const hash = new Hash(Bytes.slice(buff, offset, offset + sha1Size)); // copy
offset += sha1Size;
hints.push(hash);
}
return {hints: hints, offset: offset};
}
export function deserializeChunks(buffer: ArrayBuffer, offset: number = 0): Array<Chunk> {
export function deserializeChunks(buff: Uint8Array, dv: DataView, offset: number = 0):
Array<Chunk> {
const chunks:Array<Chunk> = [];
const totalLenth = buffer.byteLength;
for (; offset < totalLenth;) {
invariant(buffer.byteLength - offset >= chunkHeaderSize, 'Invalid chunk buffer');
const totalLength = buff.byteLength;
for (; offset < totalLength;) {
invariant(buff.byteLength - offset >= chunkHeaderSize, 'Invalid chunk buffer');
// No need to copy the data out since we are not holding on to the hash object.
const hashArray = new Uint8Array(buffer, offset, sha1Size);
const hash = new Hash(hashArray);
const hash = new Hash(Bytes.subarray(buff, offset, offset + sha1Size));
offset += sha1Size;
const view = new DataView(buffer, offset, chunkLengthSize);
const chunkLength = view.getUint32(0, bigEndian);
const chunkLength = dv.getUint32(offset, bigEndian);
offset += chunkLengthSize;
invariant(offset + chunkLength <= totalLenth, 'Invalid chunk buffer');
// Make a copy of the data so that buffer can be collected.
const chunk = new Chunk(new Uint8Array(buffer.slice(offset, offset + chunkLength)));
invariant(offset + chunkLength <= totalLength, 'Invalid chunk buffer');
const chunk = new Chunk(Bytes.slice(buff, offset, offset + chunkLength)); // copy
invariant(chunk.hash.equals(hash), 'Serialized hash !== computed hash');

View File

@@ -9,11 +9,11 @@ import {assert} from 'chai';
import Chunk from './chunk.js';
import Hash from './hash.js';
import {notNull} from './assert.js';
import Bytes from './bytes.js';
suite('Chunk', () => {
test('construct', () => {
const c = Chunk.fromString('abc');
assert.strictEqual(c.toString(), 'abc');
assert.isTrue(c.hash.equals(
notNull(Hash.parse('sha1-a9993e364706816aba3e25717850c26c9cd0d89d'))));
assert.isFalse(c.isEmpty());
@@ -22,7 +22,6 @@ suite('Chunk', () => {
test('construct with hash', () => {
const hash = notNull(Hash.parse('sha1-0000000000000000000000000000000000000001'));
const c = Chunk.fromString('abc', hash);
assert.strictEqual(c.toString(), 'abc');
assert.isTrue(c.hash.equals(
notNull(Hash.parse('sha1-0000000000000000000000000000000000000001'))));
assert.isFalse(c.isEmpty());
@@ -34,7 +33,7 @@ suite('Chunk', () => {
assert.isTrue(c.isEmpty());
}
assertChunkIsEmpty(new Chunk());
assertChunkIsEmpty(new Chunk(Bytes.alloc(0)));
assertChunkIsEmpty(Chunk.fromString(''));
});
});

View File

@@ -5,13 +5,13 @@
// http://www.apache.org/licenses/LICENSE-2.0
import Hash from './hash.js';
import {encode, decode} from './utf8.js';
import Bytes from './bytes.js';
export default class Chunk {
data: Uint8Array;
_hash: ?Hash;
constructor(data: Uint8Array = new Uint8Array(0), hash: ?Hash) {
constructor(data: Uint8Array, hash: ?Hash) {
this.data = data;
this._hash = hash;
}
@@ -25,14 +25,14 @@ export default class Chunk {
}
toString(): string {
return decode(this.data);
return Bytes.toString(this.data);
}
static fromString(s: string, hash: ?Hash): Chunk {
return new Chunk(Bytes.fromString(s), hash);
}
static emptyChunk: Chunk;
static fromString(s: string, hash: ?Hash): Chunk {
return new Chunk(encode(s), hash);
}
}
export const emptyChunk = new Chunk();
export const emptyChunk = new Chunk(Bytes.alloc(0));

View File

@@ -8,12 +8,12 @@ import Chunk from './chunk.js';
import Hash, {sha1Size} from './hash.js';
import ValueDecoder from './value-decoder.js';
import ValueEncoder from './value-encoder.js';
import {encode, decode} from './utf8.js';
import {invariant} from './assert.js';
import {setEncodeValue} from './get-hash.js';
import {setHash, ValueBase} from './value.js';
import type Value from './value.js';
import type {ValueReader, ValueWriter} from './value-store.js';
import Bytes from './bytes.js';
export function encodeValue(v: Value, vw: ?ValueWriter): Chunk {
const w = new BinaryNomsWriter();
@@ -68,22 +68,20 @@ export interface NomsWriter {
}
export class BinaryNomsReader {
buff: ArrayBuffer;
buff: Uint8Array;
dv: DataView;
offset: number;
length: number;
constructor(data: Uint8Array) {
this.buff = data.buffer;
this.offset = data.byteOffset;
this.length = data.byteLength;
this.dv = new DataView(this.buff, this.offset, this.length);
constructor(buff: Uint8Array) {
this.buff = buff;
this.dv = new DataView(buff.buffer, buff.byteOffset, buff.byteLength);
this.offset = 0;
}
readBytes(): Uint8Array {
const size = this.readUint32();
// Make a copy of the buffer to return
const v = new Uint8Array(new Uint8Array(this.buff, this.offset, size));
const v = Bytes.slice(this.buff, this.offset, this.offset + size);
this.offset += size;
return v;
}
@@ -122,54 +120,49 @@ export class BinaryNomsReader {
readString(): string {
const size = this.readUint32();
const v = new Uint8Array(this.buff, this.offset, size);
const str = Bytes.readUtf8(this.buff, this.offset, this.offset + size);
this.offset += size;
return decode(v);
return str;
}
readHash(): Hash {
// Make a copy of the data.
const digest = new Uint8Array(this.buff.slice(this.offset, this.offset + sha1Size));
const digest = Bytes.slice(this.buff, this.offset, this.offset + sha1Size);
this.offset += sha1Size;
return new Hash(digest);
}
}
const initialBufferSize = 2048;
const initialBufferSize = 16;
export class BinaryNomsWriter {
buff: ArrayBuffer;
buff: Uint8Array;
dv: DataView;
offset: number;
length: number;
constructor() {
this.buff = new ArrayBuffer(initialBufferSize);
this.dv = new DataView(this.buff, 0);
this.buff = Bytes.alloc(initialBufferSize);
this.dv = new DataView(this.buff.buffer, 0);
this.offset = 0;
this.length = this.buff.byteLength;
}
get data(): Uint8Array {
// Callers now owns the copied data.
return new Uint8Array(new Uint8Array(this.buff, 0, this.offset));
return Bytes.slice(this.buff, 0, this.offset);
}
ensureCapacity(n: number): void {
if (this.offset + n <= this.length) {
let length = this.buff.byteLength;
if (this.offset + n <= length) {
return;
}
const oldData = new Uint8Array(this.buff);
while (this.offset + n > this.length) {
this.length *= 2;
while (this.offset + n > length) {
length *= 2;
}
this.buff = new ArrayBuffer(this.length);
this.dv = new DataView(this.buff, 0);
const a = new Uint8Array(this.buff);
a.set(oldData);
this.buff = Bytes.grow(this.buff, length);
this.dv = new DataView(this.buff.buffer);
}
writeBytes(v: Uint8Array): void {
@@ -177,8 +170,7 @@ export class BinaryNomsWriter {
this.writeUint32(size);
this.ensureCapacity(size);
const a = new Uint8Array(this.buff, this.offset, size);
a.set(v);
Bytes.copy(v, this.buff, this.offset);
this.offset += size;
}
@@ -213,13 +205,14 @@ export class BinaryNomsWriter {
}
writeString(v: string): void {
this.writeBytes(encode(v));
// TODO: This is a bummer. Ensure even the largest UTF8 string will fit.
this.ensureCapacity(4 + v.length * 4);
this.offset = Bytes.encodeUtf8(v, this.buff, this.dv, this.offset);
}
writeHash(h: Hash): void {
this.ensureCapacity(sha1Size);
const a = new Uint8Array(this.buff, this.offset, sha1Size);
a.set(h.digest);
Bytes.copy(h.digest, this.buff, this.offset);
this.offset += sha1Size;
}
}

View File

@@ -9,6 +9,7 @@ import {suite, test} from 'mocha';
import {assert} from 'chai';
import Blob from './blob.js';
import Bytes from './bytes.js';
import Hash from './hash.js';
import List, {newListLeafSequence} from './list.js';
import Map from './map.js';
@@ -266,8 +267,8 @@ suite('Encoding', () => {
test('simple blob', () => {
assertEncoding([
uint8(BlobKind), false, new Uint8Array([0, 1]),
], new Blob(new Uint8Array([0, 1])));
uint8(BlobKind), false, Bytes.fromValues([0, 1]),
], new Blob(Bytes.fromValues([0, 1])));
});
test('list', () => {
@@ -386,9 +387,9 @@ suite('Encoding', () => {
test('struct with blob', () => {
assertEncoding([
uint8(StructKind), 'S', uint32(1) /* len */, 'b', uint8(BlobKind), uint8(BlobKind), false, new Uint8Array([0, 1]),
uint8(StructKind), 'S', uint32(1) /* len */, 'b', uint8(BlobKind), uint8(BlobKind), false, Bytes.fromValues([0, 1]),
],
newStruct('S', {b: new Blob(new Uint8Array([0, 1]))}));
newStruct('S', {b: new Blob(Bytes.fromValues([0, 1]))}));
});
test('compound list', () => {

View File

@@ -6,6 +6,7 @@
import {request} from 'http';
import {parse} from 'url';
import Bytes from './bytes.js';
export type FetchOptions = {
method?: string,
@@ -14,7 +15,7 @@ export type FetchOptions = {
withCredentials? : boolean,
};
function fetch<T>(url: string, f: (buf: Buffer) => T, options: FetchOptions = {}): Promise<T> {
function fetch(url: string, options: FetchOptions = {}): Promise<Uint8Array> {
const opts: any = parse(url);
opts.method = options.method || 'GET';
if (options.headers) {
@@ -26,12 +27,30 @@ function fetch<T>(url: string, f: (buf: Buffer) => T, options: FetchOptions = {}
reject(res.statusCode);
return;
}
let buf = new Buffer(0);
res.on('data', (chunk: Buffer) => {
buf = Buffer.concat([buf, chunk]);
let buf = Bytes.alloc(2048);
let offset = 0;
const ensureCapacity = (n: number) => {
let length = buf.byteLength;
if (offset + n <= length) {
return;
}
while (offset + n > length) {
length *= 2;
}
buf = Bytes.grow(buf, length);
};
res.on('data', (chunk: Uint8Array) => {
const size = chunk.byteLength;
ensureCapacity(size);
Bytes.copy(chunk, buf, offset);
offset += size;
});
res.on('end', () => {
resolve(f(buf));
resolve(Bytes.subarray(buf, 0, offset));
});
});
req.on('error', err => {
@@ -51,22 +70,13 @@ function fetch<T>(url: string, f: (buf: Buffer) => T, options: FetchOptions = {}
});
}
function bufferToArrayBuffer(buf: Buffer): ArrayBuffer {
const ab = new ArrayBuffer(buf.length);
const view = new Uint8Array(ab);
for (let i = 0; i < buf.length; i++) {
view[i] = buf[i];
}
return ab;
}
function arrayBufferToBuffer(ab: ArrayBuffer): Buffer {
// $FlowIssue: Node type declaration doesn't include ArrayBuffer.
return new Buffer(ab);
}
function bufferToString(buf: Buffer): string {
return buf.toString();
function bufferToString(buf: Uint8Array): string {
return Bytes.readUtf8(buf, 0, buf.byteLength);
}
function normalizeBody(opts: FetchOptions): FetchOptions {
@@ -77,9 +87,9 @@ function normalizeBody(opts: FetchOptions): FetchOptions {
}
export function fetchText(url: string, options: FetchOptions = {}): Promise<string> {
return fetch(url, bufferToString, normalizeBody(options));
return fetch(url, normalizeBody(options)).then(ar => bufferToString(ar));
}
export function fetchArrayBuffer(url: string, options: FetchOptions = {}): Promise<ArrayBuffer> {
return fetch(url, bufferToArrayBuffer, options);
export function fetchUint8Array(url: string, options: FetchOptions = {}): Promise<Uint8Array> {
return fetch(url, options);
}

View File

@@ -4,11 +4,11 @@
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
import {assert} from 'chai';
import {suite, test} from 'mocha';
import Hash, {emptyHash} from './hash.js';
import {encode} from './utf8.js';
import {assert} from 'chai';
import Bytes from './bytes.js';
import {notNull} from './assert.js';
import {suite, test} from 'mocha';
suite('Hash', () => {
test('parse', () => {
@@ -51,13 +51,13 @@ suite('Hash', () => {
});
test('fromData', () => {
const r = Hash.fromData(encode('abc'));
const r = Hash.fromData(Bytes.fromString('abc'));
assert.strictEqual('sha1-a9993e364706816aba3e25717850c26c9cd0d89d', r.toString());
});
test('isEmpty', () => {
const digest = new Uint8Array(20);
const digest = Bytes.alloc(20);
let r = new Hash(digest);
assert.isTrue(r.isEmpty());

View File

@@ -4,33 +4,11 @@
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
import sha1 from './sha1.js';
import Bytes from './bytes.js';
export const sha1Size = 20;
const pattern = /^sha1-[0-9a-f]{40}$/;
const sha1Prefix = 'sha1-';
const sha1PrefixLength = sha1Prefix.length;
function uint8ArrayToSha1(a: Uint8Array): string {
const sha1 = new Array(1 + sha1Size * 2);
sha1[0] = [sha1Prefix];
for (let i = 0; i < a.length; i++) {
sha1[i + 1] = byteToAscii[a[i]];
}
return sha1.join('');
}
function sha1ToUint8Array(s: string): Uint8Array {
const digest = new Uint8Array(sha1Size);
for (let i = 0; i < sha1Size; i++) {
const hc = asciiToBinary(s.charCodeAt(sha1PrefixLength + 2 * i));
const lc = asciiToBinary(s.charCodeAt(sha1PrefixLength + 2 * i + 1));
digest[i] = hc << 4 | lc;
}
return digest;
}
export default class Hash {
_digest: Uint8Array;
@@ -47,58 +25,31 @@ export default class Hash {
}
isEmpty(): boolean {
for (let i = 0; i < sha1Size; i++) {
if (this._digest[i]) {
return false;
}
}
return true;
return this.equals(emptyHash);
}
equals(other: Hash): boolean {
for (let i = 0; i < sha1Size; i++) {
if (this._digest[i] !== other._digest[i]) {
return false;
}
}
return true;
return this.compare(other) === 0;
}
compare(other: Hash): number {
for (let i = 0; i < sha1Size; i++) {
const d = this._digest[i] - other._digest[i];
if (d) {
return d;
}
}
return 0;
return Bytes.compare(this._digest, other._digest);
}
toString(): string {
return uint8ArrayToSha1(this._digest);
return 'sha1-' + Bytes.toHexString(this._digest);
}
static parse(s: string): ?Hash {
if (pattern.test(s)) {
return new Hash(sha1ToUint8Array(s));
return new Hash(Bytes.fromHexString(s.substring(5)));
}
return null;
}
static fromData(data: Uint8Array): Hash {
return new Hash(sha1(data));
return new Hash(Bytes.sha1(data));
}
}
export const emptyHash = new Hash(new Uint8Array(sha1Size));
function asciiToBinary(cc: number): number {
// This only accepts the char code for '0' - '9', 'a' - 'f'
return cc - (cc <= 57 ? 48 : 87); // '9', '0', 'a' - 10
}
// Precompute '00' to 'ff'.
const byteToAscii = new Array(256);
for (let i = 0; i < 256; i++) {
byteToAscii[i] = (i < 0x10 ? '0' : '') + i.toString(16);
}
export const emptyHash = new Hash(Bytes.alloc(sha1Size));

View File

@@ -11,7 +11,7 @@ import type {FetchOptions} from './fetch.js';
import type {ChunkStream} from './chunk-serializer.js';
import {serialize, deserializeChunks} from './chunk-serializer.js';
import {emptyChunk} from './chunk.js';
import {fetchArrayBuffer, fetchText} from './fetch.js';
import {fetchUint8Array, fetchText} from './fetch.js';
import {notNull} from './assert.js';
const HTTP_STATUS_CONFLICT = 409;
@@ -81,9 +81,9 @@ export class Delegate {
const hashStrs = Object.keys(reqs);
const body = hashStrs.map(r => 'ref=' + r).join('&');
const opts = Object.assign(this._readBatchOptions, {body: body});
const buf = await fetchArrayBuffer(this._rpc.getRefs, opts);
const buf = await fetchUint8Array(this._rpc.getRefs, opts);
const chunks = deserializeChunks(buf);
const chunks = deserializeChunks(buf, new DataView(buf.buffer, buf.byteOffset, buf.byteLength));
// Return success
chunks.forEach(chunk => {

View File

@@ -9,6 +9,7 @@ import type {tcoll as Collection} from 'tingodb';
import fs from 'fs';
import Chunk, {emptyChunk} from './chunk.js';
import {invariant} from './assert.js';
import Bytes from './bytes.js';
const __tingodb = tingodb();
@@ -57,8 +58,10 @@ export default class OrderedPutCache {
return false;
}
this._chunkIndex.set(hash, -1);
// TODO: Bug #1814.
const data = Bytes.slice(c.data, 0, c.data.byteLength);
const p = this._coll
.then(coll => coll.insert({hash: hash, data: c.data}))
.then(coll => coll.insert({hash: hash, data: data}))
.then(itemId => this._chunkIndex.set(hash, itemId))
.then(() => { this._appends.delete(p); });
this._appends.add(p);
@@ -206,7 +209,7 @@ class DbCollection {
}
function recordToItem(record: DbRecord): ChunkItem {
return {hash: record.hash, data: new Uint8Array(record.data.buffer)};
return {hash: record.hash, data: record.data.buffer};
}
function makeTempDir(): Promise<string> {

View File

@@ -1,36 +0,0 @@
// @flow
// Copyright 2016 The Noms Authors. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
import {assert} from 'chai';
import {suite, test} from 'mocha';
import sha1Node from './sha1.js';
import sha1Browser from './browser/sha1.js';
suite('Sha1', () => {
test('hex', () => {
function assertSame(arr: Uint8Array) {
// Node uses a Buffer, browser uses a Uint8Array
const n = sha1Node(arr);
const b = sha1Browser(arr);
assert.equal(n.byteLength, b.byteLength);
const n2 = new Uint8Array(n.buffer, n.byteOffset, n.byteLength);
const b2 = new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
for (let i = 0; i < n2.length; i++) {
assert.equal(n2[i], b2[i]);
}
}
assertSame(new Uint8Array(0));
assertSame(new Uint8Array(42));
const arr = new Uint8Array([1, 2, 3, 4, 5]);
assertSame(arr);
assertSame(new Uint8Array(arr));
assertSame(new Uint8Array(arr.buffer));
assertSame(new Uint8Array(arr.buffer, 1, 2));
});
});

View File

@@ -1,15 +0,0 @@
// @flow
// Copyright 2016 The Noms Authors. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
// This is the Node.js version. The browser version is in ./browser/sha1.js.
import crypto from 'crypto';
export default function sha1(data: Uint8Array): Uint8Array {
const hash = crypto.createHash('sha1');
hash.update(data);
return hash.digest();
}

View File

@@ -14,7 +14,7 @@ import {equals} from './compare.js';
import {getTypeOfValue, makeStructType, findFieldIndex} from './type.js';
import {invariant} from './assert.js';
import {isPrimitive} from './primitives.js';
import {encode as utf8Encode} from './utf8.js';
import Bytes from './bytes.js';
type StructData = {[key: string]: Value};
@@ -271,7 +271,7 @@ export function escapeStructField(input: string): string {
}
let out = escapeChar;
utf8Encode(c).forEach(b => {
Bytes.fromString(c).forEach(b => {
const hex = b.toString(16).toUpperCase();
if (hex.length === 1) {
out += '0';

View File

@@ -1,33 +0,0 @@
// @flow
// Copyright 2016 The Noms Authors. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
import {suite, test} from 'mocha';
import {assert} from 'chai';
import {encode as encodeNative, decode as decodeNative} from './utf8.js';
import {encode as encodeBrowser, decode as decodeBrowser} from './browser/utf8.js';
suite('Utf8', () => {
test('encode', () => {
function assertSame(s: string) {
assert.deepEqual(encodeNative(s), encodeBrowser(s));
}
assertSame('');
assertSame('hello world');
assertSame('\u03c0');
});
test('decode', () => {
function assertSame(data: Uint8Array) {
assert.strictEqual(decodeNative(data), decodeBrowser(data));
}
assertSame(new Uint8Array(0));
assertSame(new Uint8Array([100, 101, 102]));
assertSame(new Uint8Array([0x3c0]));
});
});

View File

@@ -1,29 +0,0 @@
// @flow
// Copyright 2016 The Noms Authors. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
// This is the Node.js version. The browser version is in ./browser/utf8.js.
// TODO: Use opaque type for the data. https://github.com/attic-labs/noms/issues/1082
export function encode(s: string): Uint8Array {
const buf = new Buffer(s, 'utf8');
// Note: it would be better not to copy the memory, and instead do:
//
// return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)
//
// but Node5 seems to get confused converting between Buffer and Uint8Array while trying to use
// the same underlying buffer. Node6 works fine, but it's not the LTS release yet.
return new Uint8Array(buf);
}
export function decode(data: Uint8Array): string {
// Note: see comment above. For a similar reason, it would be better, but impossible, to do:
//
// return new Buffer(data.buffer, data.byteOffset, data.byteLength).toString('utf8');
//
// $FlowIssue: flow doesn't know this is a legit constructor.
return new Buffer(data).toString('utf8');
}

View File

@@ -4,6 +4,9 @@
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
import {suite, suiteSetup, suiteTeardown, test} from 'mocha';
import {assert} from 'chai';
import {BatchStoreAdaptor} from './batch-store.js';
import {createStructClass} from './struct.js';
import Database from './database.js';
@@ -19,10 +22,6 @@ import List from './list.js';
import Map from './map.js';
import NomsSet from './set.js'; // namespace collision with JS Set
import walk from './walk.js';
import {suite, suiteSetup, suiteTeardown, test} from 'mocha';
import {assert} from 'chai';
import type Value from './value.js';
suite('walk', () => {