Skip to content

Commit

Permalink
Add options.wrap to encode(), make native binding optional
Browse files Browse the repository at this point in the history
  • Loading branch information
jorangreef committed Apr 18, 2017
1 parent ad4bcee commit cddb9f3
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 58 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ console.log(bufferEncoded.toString('ascii'));
// "RWNjbGVzaWFzdGVzIDk6MTEtMTg="
```

#### Encoding 76 characters per line
```javascript
var bufferEncoded = Base64.encode(buffer, { wrap: true });
```

#### Decoding
```javascript
var Base64 = require('@ronomon/base64');
Expand All @@ -114,7 +119,7 @@ console.log(buffer.toString('utf-8'));
```

#### Decoding corrupt or truncated data
Base64 will raise an exception for corrupt or truncated data by default as a defensive measure to prevent data loss and security vulnerabilities. To silence these exceptions and continue decoding in the face of bad data (**not recommended**), use `options.silent`:
Base64 will raise an exception for corrupt or truncated data by default as a defensive measure to prevent data loss and security vulnerabilities. To silence these exceptions and continue decoding in the face of bad data (not recommended), use `options.silent`:
```javascript
var Base64 = require('@ronomon/base64');
var bufferEncoded = Buffer.from('...RWNjbGVzaWFzdGVzIDk6MTEtMTg=', 'ascii');
Expand Down
68 changes: 52 additions & 16 deletions binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ uint32_t ceil_div(uint32_t x, uint32_t y) {
}

const uint8_t SILENT = 1;
const uint8_t WRAP = 2;

const uint32_t SPECIAL = 1 << 24;
const uint32_t ILLEGAL = 1 << 25;
const uint8_t PADDING = 61; // "=";
Expand Down Expand Up @@ -93,6 +95,17 @@ int decode_step(
return 1;
}

uint32_t encodeTargetLength(const uint32_t sourceLength, const uint32_t flags) {
uint32_t symbols = ceil_div(sourceLength, 3) * 4;
if (flags & WRAP) {
uint32_t lines = ceil_div(symbols, 76);
uint32_t breaks = lines > 0 ? lines - 1 : 0;
return symbols + (breaks * 2);
} else {
return symbols;
}
}

NAN_METHOD(decode) {
if (info.Length() != 3) {
return Nan::ThrowError("bad number of arguments");
Expand All @@ -104,14 +117,11 @@ NAN_METHOD(decode) {
return Nan::ThrowError("target must be a buffer");
}
if (!info[2]->IsUint32()) {
return Nan::ThrowError("flags must be an 8-bit integer");
return Nan::ThrowError("flags must be an integer");
}
v8::Local<v8::Object> sourceHandle = info[0].As<v8::Object>();
v8::Local<v8::Object> targetHandle = info[1].As<v8::Object>();
const uint32_t flags = info[2]->Uint32Value();
if (flags != 0 && flags != 1) {
return Nan::ThrowError("flags must be an 8-bit integer");
}
const uint32_t sourceLength = node::Buffer::Length(sourceHandle);
const uint32_t targetLength = node::Buffer::Length(targetHandle);
if (targetLength < (ceil_div(sourceLength, 4) * 3)) {
Expand Down Expand Up @@ -207,7 +217,7 @@ NAN_METHOD(decode) {
}

NAN_METHOD(encode) {
if (info.Length() != 2) {
if (info.Length() != 3) {
return Nan::ThrowError("bad number of arguments");
}
if (!node::Buffer::HasInstance(info[0])) {
Expand All @@ -216,11 +226,15 @@ NAN_METHOD(encode) {
if (!node::Buffer::HasInstance(info[1])) {
return Nan::ThrowError("target must be a buffer");
}
if (!info[2]->IsUint32()) {
return Nan::ThrowError("flags must be an integer");
}
v8::Local<v8::Object> sourceHandle = info[0].As<v8::Object>();
v8::Local<v8::Object> targetHandle = info[1].As<v8::Object>();
const uint32_t flags = info[2]->Uint32Value();
const uint32_t sourceLength = node::Buffer::Length(sourceHandle);
const uint32_t targetLength = node::Buffer::Length(targetHandle);
if (targetLength < (ceil_div(sourceLength, 3) * 4)) {
if (targetLength < encodeTargetLength(sourceLength, flags)) {
return Nan::ThrowError("target too small");
}
const uint8_t* source = reinterpret_cast<const uint8_t*>(
Expand All @@ -232,19 +246,41 @@ NAN_METHOD(encode) {
uint8_t a;
uint8_t b;
uint8_t c;
uint8_t line = 0;
uint32_t sourceIndex = 0;
uint32_t targetIndex = 0;
uint32_t sourceSubset = (sourceLength / 3) * 3;
while (sourceIndex < sourceSubset) {
a = source[sourceIndex + 0];
b = source[sourceIndex + 1];
c = source[sourceIndex + 2];
target[targetIndex + 0] = encode_table_0[a];
target[targetIndex + 1] = encode_table_1[(a << 4) | (b >> 4)];
target[targetIndex + 2] = encode_table_1[(b << 2) | (c >> 6)];
target[targetIndex + 3] = encode_table_1[c];
sourceIndex += 3;
targetIndex += 4;
if (flags & WRAP) {
while (sourceIndex < sourceSubset) {
a = source[sourceIndex + 0];
b = source[sourceIndex + 1];
c = source[sourceIndex + 2];
target[targetIndex + 0] = encode_table_0[a];
target[targetIndex + 1] = encode_table_1[(a << 4) | (b >> 4)];
target[targetIndex + 2] = encode_table_1[(b << 2) | (c >> 6)];
target[targetIndex + 3] = encode_table_1[c];
sourceIndex += 3;
targetIndex += 4;
line += 4;
if (line == 76 && sourceIndex < sourceLength) {
target[targetIndex + 0] = 13;
target[targetIndex + 1] = 10;
targetIndex += 2;
line = 0;
}
}
} else {
while (sourceIndex < sourceSubset) {
a = source[sourceIndex + 0];
b = source[sourceIndex + 1];
c = source[sourceIndex + 2];
target[targetIndex + 0] = encode_table_0[a];
target[targetIndex + 1] = encode_table_1[(a << 4) | (b >> 4)];
target[targetIndex + 2] = encode_table_1[(b << 2) | (c >> 6)];
target[targetIndex + 3] = encode_table_1[c];
sourceIndex += 3;
targetIndex += 4;
}
}
switch (sourceLength - sourceSubset) {
case 1:
Expand Down
105 changes: 77 additions & 28 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

var Base64 = {};

Base64.SILENT = 1;
Base64.WRAP = 2;

Base64.assertBinding = function(binding) {
var self = this;
if (!binding) throw new Error('binding must be defined');
if (!binding.decode) throw new Error('binding.decode must be defined');
if (!binding.encode) throw new Error('binding.encode must be defined');
Expand All @@ -15,11 +17,25 @@ Base64.assertBinding = function(binding) {
}
};

Base64.assertBoolean = function(key, value) {
if (value !== true && value !== false) {
throw new Error(key + ' must be a boolean');
}
};

Base64.assertInteger = function(key, value) {
if (typeof value !== 'number' || Math.floor(value) !== value || value < 0) {
throw new Error(key + ' must be an integer');
}
};

Base64.binding = {};

Base64.binding.javascript = (function() {

var SILENT = 1;
var SILENT = Base64.SILENT;
var WRAP = Base64.WRAP;

var SPECIAL = 1 << 24;
var ILLEGAL = 1 << 25;
var PADDING = '='.charCodeAt(0);
Expand Down Expand Up @@ -91,14 +107,7 @@ Base64.binding.javascript = (function() {
if (!Buffer.isBuffer(target)) {
throw new Error('target must be a buffer');
}
if (
typeof flags !== 'number' ||
Math.floor(flags) !== flags ||
flags < 0 ||
flags > 255
) {
throw new Error('flags must be an 8-bit integer');
}
Base64.assertInteger('flags', flags);
var targetLength = target.length;
var sourceLength = source.length;
if (targetLength < (Math.ceil(sourceLength / 4) * 3)) {
Expand Down Expand Up @@ -193,34 +202,57 @@ Base64.binding.javascript = (function() {
return targetIndex;
}

function encode(source, target) {
function encode(source, target, flags) {
if (!Buffer.isBuffer(source)) {
throw new Error('source must be a buffer');
}
if (!Buffer.isBuffer(target)) {
throw new Error('target must be a buffer');
}
Base64.assertInteger('flags', flags);
var sourceLength = source.length;
var targetLength = target.length;
if (targetLength < (Math.ceil(sourceLength / 3) * 4)) {
if (targetLength < Base64.encodeTargetLength(sourceLength, flags)) {
throw new Error('target too small');
}
var a;
var b;
var c;
var line = 0;
var targetIndex = 0;
var sourceIndex = 0;
var sourceSubset = Math.floor(sourceLength / 3) * 3;
while (sourceIndex < sourceSubset) {
a = source[sourceIndex + 0];
b = source[sourceIndex + 1];
c = source[sourceIndex + 2];
target[targetIndex + 0] = encode_table_0[a];
target[targetIndex + 1] = encode_table_1[(a << 4) | (b >> 4)];
target[targetIndex + 2] = encode_table_1[(b << 2) | (c >> 6)];
target[targetIndex + 3] = encode_table_1[c];
sourceIndex += 3;
targetIndex += 4;
if (flags & WRAP) {
while (sourceIndex < sourceSubset) {
a = source[sourceIndex + 0];
b = source[sourceIndex + 1];
c = source[sourceIndex + 2];
target[targetIndex + 0] = encode_table_0[a];
target[targetIndex + 1] = encode_table_1[(a << 4) | (b >> 4)];
target[targetIndex + 2] = encode_table_1[(b << 2) | (c >> 6)];
target[targetIndex + 3] = encode_table_1[c];
sourceIndex += 3;
targetIndex += 4;
line += 4;
if (line === 76 && sourceIndex < sourceLength) {
target[targetIndex + 0] = 13;
target[targetIndex + 1] = 10;
targetIndex += 2;
line = 0;
}
}
} else {
while (sourceIndex < sourceSubset) {
a = source[sourceIndex + 0];
b = source[sourceIndex + 1];
c = source[sourceIndex + 2];
target[targetIndex + 0] = encode_table_0[a];
target[targetIndex + 1] = encode_table_1[(a << 4) | (b >> 4)];
target[targetIndex + 2] = encode_table_1[(b << 2) | (c >> 6)];
target[targetIndex + 3] = encode_table_1[c];
sourceIndex += 3;
targetIndex += 4;
}
}
switch (sourceLength - sourceSubset) {
case 1:
Expand Down Expand Up @@ -275,10 +307,8 @@ Base64.decode = function(source, options) {
binding = options.binding;
}
if (options.hasOwnProperty('silent')) {
if (options.silent !== true && options.silent !== false) {
throw new Error('options.silent must be a boolean');
}
if (options.silent) flags |= 1;
self.assertBoolean('silent', options.silent);
if (options.silent) flags |= self.SILENT;
}
}
var target = Buffer.alloc(Math.ceil(source.length / 4) * 3);
Expand All @@ -290,18 +320,37 @@ Base64.decode = function(source, options) {
Base64.encode = function(source, options) {
var self = this;
var binding = self.binding.active;
var flags = 0;
if (options) {
if (options.hasOwnProperty('binding')) {
self.assertBinding(options.binding);
binding = options.binding;
}
if (options.hasOwnProperty('wrap')) {
self.assertBoolean('wrap', options.wrap);
if (options.wrap) flags |= self.WRAP;
}
}
var target = Buffer.alloc(Math.ceil(source.length / 3) * 4);
var targetSize = binding.encode(source, target);
var target = Buffer.alloc(self.encodeTargetLength(source.length, flags));
var targetSize = binding.encode(source, target, flags);
if (targetSize > target.length) throw new Error('target overflow');
return target.slice(0, targetSize);
};

Base64.encodeTargetLength = function(sourceLength, flags) {
var self = this;
self.assertInteger('sourceLength', sourceLength);
self.assertInteger('flags', flags);
var symbols = Math.ceil(sourceLength / 3) * 4;
if (flags & self.WRAP) {
var lines = Math.ceil(symbols / 76);
var breaks = lines > 0 ? lines - 1 : 0;
return symbols + (breaks * 2);
} else {
return symbols;
}
};

module.exports = Base64;

// S.D.G.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"homepage": "https://github.com/ronomon/base64#readme",
"scripts": {
"install": "node-gyp rebuild || exit 0",
"test": "node test.js"
},
"dependencies": {
Expand Down
Loading

0 comments on commit cddb9f3

Please sign in to comment.