version bump 0.13.2

- file writing includes `fs` as appropriate
- CLI modify and write options
- infrastructure update
This commit is contained in:
SheetJS 2017-10-20 16:36:54 -04:00
parent e6d1c8b06e
commit 9e22a4425e
25 changed files with 306 additions and 144 deletions

@ -3,7 +3,6 @@
.*/dist/.*
.*/test_files/.*
.*/test_files_pres/.*
.*/test.js
.*/bits/.*
.*/ctest/.*
@ -23,6 +22,7 @@
cfb.flow.js
xlscfb.flow.js
.*/bin/.*.njs
test.js
[libs]
bits/10_types.js

16
.spelling Normal file

@ -0,0 +1,16 @@
# cfb.js (C) 2013-present SheetJS -- http://sheetjs.com
SheetJS
js-xlsx
# CFB-related terms
CFB
storages
# Third-party
NPM
nodejs
npm
# Other terms
Base64
metadata

@ -66,6 +66,11 @@ XLSDEPS=misc/suppress_export.js $(filter-out $(XLSSKIP),$(DEPS))
xlscfb.flow.js: $(XLSDEPS) ## Build support library
cat $^ | tr -d '\15\32' > $@
BYTEFILE=dist/cfb.min.js dist/xlscfb.js
.PHONY: bytes
bytes: ## Display minified and gzipped file sizes
for i in $(BYTEFILE); do printj "%-30s %7d %10d" $$i $$(wc -c < $$i) $$(gzip --best --stdout $$i | wc -c); done
## Testing
@ -74,6 +79,7 @@ test mocha: test.js $(TARGET) ## Run test suite
mocha -R spec -t 20000
#* To run tests for one format, make test_<fmt>
#* To run the core test suite, make test_misc
TESTFMT=$(patsubst %,test_%,$(FMT))
.PHONY: $(TESTFMT)
$(TESTFMT): test_%:
@ -82,6 +88,9 @@ $(TESTFMT): test_%:
## Code Checking
.PHONY: fullint
fullint: lint old-lint tslint flow mdlint ## Run all checks
.PHONY: lint
lint: $(TARGET) $(AUXTARGETS) ## Run eslint checks
@eslint --ext .js,.njs,.json,.html,.htm $(TARGET) $(AUXTARGETS) $(CMDS) $(HTMLLINT) package.json
@ -91,9 +100,9 @@ lint: $(TARGET) $(AUXTARGETS) ## Run eslint checks
old-lint: $(TARGET) $(AUXTARGETS) ## Run jshint and jscs checks
@jshint --show-non-errors $(TARGET) $(AUXTARGETS)
@jshint --show-non-errors $(CMDS)
@jshint --show-non-errors package.json
@jshint --show-non-errors package.json test.js
@jshint --show-non-errors --extract=always $(HTMLLINT)
@jscs $(TARGET) $(AUXTARGETS)
@jscs $(TARGET) $(AUXTARGETS) test.js
if [ -e $(CLOSURE) ]; then java -jar $(CLOSURE) $(REQS) $(FLOWTARGET) --jscomp_warning=reportUnknownTypes >/dev/null; fi
.PHONY: tslint
@ -115,6 +124,11 @@ misc/coverage.html: $(TARGET) test.js
coveralls: ## Coverage Test + Send to coveralls.io
mocha --require blanket --reporter mocha-lcov-reporter -t 20000 | node ./node_modules/coveralls/bin/coveralls.js
MDLINT=README.md
.PHONY: mdlint
mdlint: $(MDLINT) ## Check markdown documents
alex $^
mdspell -a -n -x -r --en-us $^
.PHONY: help
help:

@ -1,6 +1,6 @@
# Compound File Binary Format
Pure-JS implementation of MS-CFB: Compound File Binary File Format, a container
Pure JS implementation of MS-CFB: Compound File Binary File Format, a container
format used in many Microsoft file types (XLS, DOC, VBA blobs in XLSX and XLSB)
[![Build Status](https://travis-ci.org/SheetJS/js-cfb.svg?branch=master)](https://travis-ci.org/SheetJS/js-cfb)
@ -35,7 +35,7 @@ In node:
var CFB = require('cfb');
```
For example, to get the Workbook content from an XLS file:
For example, to get the Workbook content from an Excel 2003 XLS file:
```js
var cfb = CFB.read(filename, {type: 'file'});
@ -52,15 +52,23 @@ It is preferable to install the library globally with npm:
$ npm install -g cfb
```
The global installation adds a command `cfb` which can work with existing files:
The global installation adds a command `cfb` which can work with files:
- `cfb file` will extract the contents of the file to the current directory.
It will make the corresponding subdirectories.
- `cfb --list-files file` will show a listing of the contained files.
The format follows the `unzip -l` "short format".
- `cfb --repair file` will attempt to repair by reading and re-writing the file.
- `cfb file [names...]` extracts the contents of the file. If additional names
are supplied, only the listed files will be extracted.
- `cfb -l file` lists the contained files (following `unzip -l` "short format")
- `cfb -r file` attempts to repair by reading and re-writing the file.
This fixes some issues with files generated by non-standard tools.
- `cfb -c file [files...]` creates a new file containing the listed files.
The default root entry name is `Root Entry`.
- `cfb -a file [files...]` adds the listed files to the original file.
- `cfb -d file [files...]` deletes the listed files from the original file.
## JS API
@ -73,10 +81,12 @@ parsed representation of the data.
`CFB.read(blob, opts)` wraps `parse`. `opts.type` controls the behavior:
- `file`: `blob` is interpreted as a file name that will be read
- `base64`: `blob` is interpreted as base64 string
- `binary`: `blob` is interpreted as binary string
- default: `blob` is interpreted as nodejs buffer or array of bytes
| `type` | expected input |
|------------|-----------------------------------------------------------------|
| `"base64"` | string: Base64 encoding of the file |
| `"binary"` | string: binary string (byte `n` is `data.charCodeAt(n)`) |
| `"file"` | string: path of file that will be read (nodejs only) |
| (default) | buffer or array of 8-bit unsigned int (byte `n` is `data[n]`) |
`CFB.find(cfb, path)` performs a case-insensitive match for the path (or file
name, if there are no slashes) and returns an entry object or null if not found.
@ -84,9 +94,11 @@ name, if there are no slashes) and returns an entry object or null if not found.
`CFB.write(cfb, opts)` generates a file based on the container. `opts.type`
controls the behavior:
- `base64`: returns a base64 string
- `binary`: returns a binary string
- default: returns a nodejs buffer or array of bytes
| `type` | output |
|------------|-----------------------------------------------------------------|
| `"base64"` | string: Base64 encoding of the file |
| `"binary"` | string: binary string (byte `n` is `data.charCodeAt(n)`) |
| (default) | buffer if available, array of 8-bit unsigned int otherwise |
`CFB.writeFile(cfb, filename, opts)` creates a file with the specified name.
@ -110,19 +122,8 @@ The objects returned by `parse` and `read` have the following properties:
storages (directories) in the container. The paths are properly prefixed from
the root entry (so the entries are unique)
- `.FullPathDir` is an object whose keys are entries in `.FullPaths` and whose
values are objects with metadata and content (described below)
- `.FileIndex` is an array of the objects from `.FullPathDir`, in the same order
as `.FullPaths`.
- `.raw` contains the raw header and sectors
## Entry Object Description
The entry objects are available from `FullPathDir` and `FileIndex` elements of
the container object:
- `.FileIndex` is an array, in the same order as `.FullPaths`, whose values are
objects following the schema:
```typescript
interface CFBEntry {
@ -151,4 +152,3 @@ granted by the Apache 2.0 License are reserved by the Original Author.
</details>

@ -2,57 +2,67 @@
/* cfb.js (C) 2013-present SheetJS -- http://sheetjs.com */
/* eslint-env node */
/* vim: set ts=2 ft=javascript: */
var n = "cfb";
var X = require('../');
var fs = require('fs');
var program = require('commander');
var PRINTJ = require("printj");
var sprintf = PRINTJ.sprintf;
program
.version(X.version)
.usage('[options] <file>')
.option('-q, --quiet', 'process but do not report')
.usage('[options] <file> [subfiles...]')
.option('-l, --list-files', 'list files')
.option('-d, --dump', 'dump internal representation but do not extract')
.option('-r, --repair', 'attempt to repair and garbage-collect archive')
.option('-c, --create', 'create file')
.option('-a, --append', 'add files to CFB (overwrite existing data)')
.option('-d, --delete', 'delete files from CFB')
.option('-z, --dump', 'dump internal representation but do not extract')
.option('-q, --quiet', 'process but do not report')
.option('--dev', 'development mode')
.option('--read', 'read but do not print out contents');
program.parse(process.argv);
if(program.args.length === 0 || !fs.existsSync(program.args[0])) {
console.error("Usage: " + process.argv[1] + " [-q] <cfb_file>");
process.exit(1);
var exit = process.exit;
var die = function(errno/*:number*/, msg/*:string*/) { console.error(n + ": " + msg); exit(errno); };
var logit = function(cmd/*:string*/, f/*:string*/) { console.error(sprintf("%-6s %s", cmd, f)); };
if(program.args.length === 0) die(1, "must specify a filename");
if(program.create) {
logit("create", program.args[0]);
var newcfb = X.utils.cfb_new();
X.writeFile(newcfb, program.args[0]);
}
if(!fs.existsSync(program.args[0])) die(1, "must specify a filename");
var opts = ({type:'file'}/*:any*/);
if(program.dev) opts.WTF = true;
var cfb = X.read(program.args[0], opts);
if(program.quiet) process.exit(0);
if(program.quiet) exit(0);
if(program.dump) {
console.log("Full Paths:");
console.log(cfb.FullPaths.map(function(x) { return " " + x; }).join("\n"));
console.log("Full Path Directory:");
console.log(cfb.FullPathDir);
process.exit(0);
}
if(program.repair) {
X.writeFile(cfb, program.args[0]);
process.exit(0);
console.log(cfb.FullPaths.map(function(x/*:string*/) { return " " + x; }).join("\n"));
console.log("File Index:");
console.log(cfb.FileIndex);
exit(0);
}
if(program.repair) { X.writeFile(cfb, program.args[0]); exit(0); }
var sprintf = PRINTJ.sprintf;
function fix_string(x/*:string*/)/*:string*/ { return x.replace(/[\u0000-\u001f]/, function($$) { return sprintf("\\u%04X", $$.charCodeAt(0)); }); }
if(program.listFiles) {
var format_date = function(date/*:Date*/)/*:string*/ {
return sprintf("%02u-%02u-%02u %02u:%02u", date.getUTCMonth()+1, date.getUTCDate(), date.getUTCFullYear()%100, date.getUTCHours(), date.getUTCMinutes());
};
var format_date = function(date/*:Date*/)/*:string*/ {
return sprintf("%02u-%02u-%02u %02u:%02u", date.getUTCMonth()+1, date.getUTCDate(), date.getUTCFullYear()%100, date.getUTCHours(), date.getUTCMinutes());
};
if(program.listFiles) {
var basetime = new Date(1980,0,1);
var cnt = 0, rootsize = 0, filesize = 0;
console.log(" Length Date Time Name");
console.log(" -------- ---- ---- ----");
cfb.FileIndex.forEach(function(file, i/*:number*/) {
cfb.FileIndex.forEach(function(file/*:CFBEntry*/, i/*:number*/) {
switch(file.type) {
case 5:
basetime = file.ct || file.mt || basetime;
@ -67,14 +77,52 @@ if(program.listFiles) {
console.log(" -------- -------");
console.log(sprintf("%9lu %lu file%s", rootsize || filesize, cnt, (cnt !== 1 ? "s" : "")));
process.exit(0);
exit(0);
}
function mkdirp(path/*:string*/) { path.split("/").reduce(function(acc/*:string*/, p/*:string*/) {
acc += p + "/";
if(!fs.existsSync(acc)) { logit("mkdir", acc); fs.mkdirSync(acc); }
return acc;
}, ""); }
function write(path/*:string*/, data/*:CFBEntry*/) {
logit("write", fix_string(path));
fs.writeFileSync(path, /*::new Buffer((*/data.content/*:: :any))*/);
}
if(program.create || program.append) {
program.args.slice(1).forEach(function(x/*:string*/) {
logit("append", x);
X.utils.cfb_add(cfb, "/" + x, fs.readFileSync(x));
});
X.writeFile(cfb, program.args[0]);
exit(0);
}
if(program.delete) {
program.args.slice(1).forEach(function(x/*:string*/) {
logit("delete", x);
X.utils.cfb_del(cfb, "/" + x);
});
X.writeFile(cfb, program.args[0]);
exit(0);
}
if(program.args.length > 1) {
program.args.slice(1).forEach(function(x/*:string*/) {
var data/*:?CFBEntry*/ = X.find(cfb, x);
if(!data) { console.error(x + ": file not found"); return; }
if(data.type !== 2) { console.error(x + ": not a file"); return; }
var idx = cfb.FileIndex.indexOf(data), path = cfb.FullPaths[idx];
mkdirp(path.slice(0, path.lastIndexOf("/")));
write(path, data);
});
exit(0);
}
for(var i=0; i!==cfb.FullPaths.length; ++i) {
if(cfb.FullPaths[i].slice(-1) === "/") {
console.error("mkdir " + fix_string(cfb.FullPaths[i]));
fs.mkdirSync(cfb.FullPaths[i]);
} else {
console.error("write " + fix_string(cfb.FullPaths[i]));
fs.writeFileSync(cfb.FullPaths[i], /*::new Buffer((*/cfb.FileIndex[i].content/*:: :any))*/);
}
if(!cfb.FileIndex[i].name) continue;
if(cfb.FullPaths[i].slice(-1) === "/") mkdirp(cfb.FullPaths[i]);
else write(cfb.FullPaths[i], cfb.FileIndex[i]);
}

@ -1 +1 @@
exports.version = '0.13.1';
exports.version = '0.13.2';

2
bits/39_fs.js Normal file

@ -0,0 +1,2 @@
var fs/*:: = require('fs'); */;
function get_fs() { return fs || (fs = require('fs')); }

@ -1,6 +1,5 @@
var fs/*:: = require('fs'); */;
function read_file(filename/*:string*/, options/*:CFBReadOpts*/) {
if(fs == null) fs = require('fs');
get_fs();
return parse(fs.readFileSync(filename), options);
}

@ -1,4 +1,5 @@
function write_file(cfb/*:CFBContainer*/, filename/*:string*/, options/*:CFBWriteOpts*/)/*:void*/ {
get_fs();
var o = _write(cfb, options);
/*:: if(typeof Buffer == 'undefined' || !Buffer.isBuffer(o) || !(o instanceof Buffer)) throw new Error("unreachable"); */
fs.writeFileSync(filename, o);
@ -13,7 +14,7 @@ function a2s(o/*:RawBytes*/)/*:string*/ {
function write(cfb/*:CFBContainer*/, options/*:CFBWriteOpts*/)/*:RawBytes|string*/ {
var o = _write(cfb, options);
switch(options && options.type) {
case "file": fs.writeFileSync(options.filename, (o/*:any*/)); return o;
case "file": get_fs(); fs.writeFileSync(options.filename, (o/*:any*/)); return o;
case "binary": return a2s(o);
case "base64": return Base64.encode(a2s(o));
}

@ -8,13 +8,13 @@ function cfb_add(cfb/*:CFBContainer*/, name/*:string*/, content/*:?RawBytes*/, o
init_cfb(cfb);
var file = CFB.find(cfb, name);
if(!file) {
var fpath = cfb.FullPaths[0];
var fpath/*:string*/ = cfb.FullPaths[0];
if(name.slice(0, fpath.length) == fpath) fpath = name;
else {
if(fpath.slice(-1) != "/") fpath += "/";
fpath = (fpath + name).replace("//","/");
}
file = ({name: filename(name)}/*:any*/);
file = ({name: filename(name), type: 2}/*:any*/);
cfb.FileIndex.push(file);
cfb.FullPaths.push(fpath);
CFB.utils.cfb_gc(cfb);

@ -179,7 +179,7 @@ type CFBFiles = {[n:string]:CFBEntry};
/* [MS-CFB] v20130118 */
var CFB = (function _CFB(){
var exports/*:CFBModule*/ = /*::(*/{}/*:: :any)*/;
exports.version = '0.13.1';
exports.version = '0.13.2';
/* [MS-CFB] 2.6.4 */
function namecmp(l/*:string*/, r/*:string*/)/*:number*/ {
var L = l.split("/"), R = r.split("/");
@ -200,6 +200,8 @@ function filename(p/*:string*/)/*:string*/ {
var c = p.lastIndexOf("/");
return (c === -1) ? p : p.slice(c+1);
}
var fs/*:: = require('fs'); */;
function get_fs() { return fs || (fs = require('fs')); }
function parse(file/*:RawBytes*/, options/*:CFBReadOpts*/)/*:CFBContainer*/ {
var mver = 3;
var ssz = 512;
@ -497,9 +499,8 @@ function read_date(blob/*:RawBytes|CFBlob*/, offset/*:number*/)/*:Date*/ {
return new Date(( ( (__readUInt32LE(blob,offset+4)/1e7)*Math.pow(2,32)+__readUInt32LE(blob,offset)/1e7 ) - 11644473600)*1000);
}
var fs/*:: = require('fs'); */;
function read_file(filename/*:string*/, options/*:CFBReadOpts*/) {
if(fs == null) fs = require('fs');
get_fs();
return parse(fs.readFileSync(filename), options);
}
@ -779,6 +780,7 @@ var consts = {
};
function write_file(cfb/*:CFBContainer*/, filename/*:string*/, options/*:CFBWriteOpts*/)/*:void*/ {
get_fs();
var o = _write(cfb, options);
/*:: if(typeof Buffer == 'undefined' || !Buffer.isBuffer(o) || !(o instanceof Buffer)) throw new Error("unreachable"); */
fs.writeFileSync(filename, o);
@ -793,7 +795,7 @@ function a2s(o/*:RawBytes*/)/*:string*/ {
function write(cfb/*:CFBContainer*/, options/*:CFBWriteOpts*/)/*:RawBytes|string*/ {
var o = _write(cfb, options);
switch(options && options.type) {
case "file": fs.writeFileSync(options.filename, (o/*:any*/)); return o;
case "file": get_fs(); fs.writeFileSync(options.filename, (o/*:any*/)); return o;
case "binary": return a2s(o);
case "base64": return Base64.encode(a2s(o));
}
@ -809,13 +811,13 @@ function cfb_add(cfb/*:CFBContainer*/, name/*:string*/, content/*:?RawBytes*/, o
init_cfb(cfb);
var file = CFB.find(cfb, name);
if(!file) {
var fpath = cfb.FullPaths[0];
var fpath/*:string*/ = cfb.FullPaths[0];
if(name.slice(0, fpath.length) == fpath) fpath = name;
else {
if(fpath.slice(-1) != "/") fpath += "/";
fpath = (fpath + name).replace("//","/");
}
file = ({name: filename(name)}/*:any*/);
file = ({name: filename(name), type: 2}/*:any*/);
cfb.FileIndex.push(file);
cfb.FullPaths.push(fpath);
CFB.utils.cfb_gc(cfb);

12
cfb.js

@ -161,7 +161,7 @@ function new_buf(sz) {
/* [MS-CFB] v20130118 */
var CFB = (function _CFB(){
var exports = {};
exports.version = '0.13.1';
exports.version = '0.13.2';
/* [MS-CFB] 2.6.4 */
function namecmp(l, r) {
var L = l.split("/"), R = r.split("/");
@ -182,6 +182,8 @@ function filename(p) {
var c = p.lastIndexOf("/");
return (c === -1) ? p : p.slice(c+1);
}
var fs;
function get_fs() { return fs || (fs = require('fs')); }
function parse(file, options) {
var mver = 3;
var ssz = 512;
@ -479,9 +481,8 @@ function read_date(blob, offset) {
return new Date(( ( (__readUInt32LE(blob,offset+4)/1e7)*Math.pow(2,32)+__readUInt32LE(blob,offset)/1e7 ) - 11644473600)*1000);
}
var fs;
function read_file(filename, options) {
if(fs == null) fs = require('fs');
get_fs();
return parse(fs.readFileSync(filename), options);
}
@ -756,6 +757,7 @@ var consts = {
};
function write_file(cfb, filename, options) {
get_fs();
var o = _write(cfb, options);
fs.writeFileSync(filename, o);
}
@ -769,7 +771,7 @@ function a2s(o) {
function write(cfb, options) {
var o = _write(cfb, options);
switch(options && options.type) {
case "file": fs.writeFileSync(options.filename, (o)); return o;
case "file": get_fs(); fs.writeFileSync(options.filename, (o)); return o;
case "binary": return a2s(o);
case "base64": return Base64.encode(a2s(o));
}
@ -791,7 +793,7 @@ function cfb_add(cfb, name, content, opts) {
if(fpath.slice(-1) != "/") fpath += "/";
fpath = (fpath + name).replace("//","/");
}
file = ({name: filename(name)});
file = ({name: filename(name), type: 2});
cfb.FileIndex.push(file);
cfb.FullPaths.push(fpath);
CFB.utils.cfb_gc(cfb);

12
dist/cfb.js vendored

@ -161,7 +161,7 @@ function new_buf(sz) {
/* [MS-CFB] v20130118 */
var CFB = (function _CFB(){
var exports = {};
exports.version = '0.13.1';
exports.version = '0.13.2';
/* [MS-CFB] 2.6.4 */
function namecmp(l, r) {
var L = l.split("/"), R = r.split("/");
@ -182,6 +182,8 @@ function filename(p) {
var c = p.lastIndexOf("/");
return (c === -1) ? p : p.slice(c+1);
}
var fs;
function get_fs() { return fs || (fs = require('fs')); }
function parse(file, options) {
var mver = 3;
var ssz = 512;
@ -479,9 +481,8 @@ function read_date(blob, offset) {
return new Date(( ( (__readUInt32LE(blob,offset+4)/1e7)*Math.pow(2,32)+__readUInt32LE(blob,offset)/1e7 ) - 11644473600)*1000);
}
var fs;
function read_file(filename, options) {
if(fs == null) fs = require('fs');
get_fs();
return parse(fs.readFileSync(filename), options);
}
@ -756,6 +757,7 @@ var consts = {
};
function write_file(cfb, filename, options) {
get_fs();
var o = _write(cfb, options);
fs.writeFileSync(filename, o);
}
@ -769,7 +771,7 @@ function a2s(o) {
function write(cfb, options) {
var o = _write(cfb, options);
switch(options && options.type) {
case "file": fs.writeFileSync(options.filename, (o)); return o;
case "file": get_fs(); fs.writeFileSync(options.filename, (o)); return o;
case "binary": return a2s(o);
case "base64": return Base64.encode(a2s(o));
}
@ -791,7 +793,7 @@ function cfb_add(cfb, name, content, opts) {
if(fpath.slice(-1) != "/") fpath += "/";
fpath = (fpath + name).replace("//","/");
}
file = ({name: filename(name)});
file = ({name: filename(name), type: 2});
cfb.FileIndex.push(file);
cfb.FullPaths.push(fpath);
CFB.utils.cfb_gc(cfb);

2
dist/cfb.min.js vendored

File diff suppressed because one or more lines are too long

2
dist/cfb.min.map vendored

File diff suppressed because one or more lines are too long

14
dist/xlscfb.js vendored

@ -38,7 +38,7 @@ type CFBFiles = {[n:string]:CFBEntry};
/* [MS-CFB] v20130118 */
var CFB = (function _CFB(){
var exports/*:CFBModule*/ = /*::(*/{}/*:: :any)*/;
exports.version = '0.13.1';
exports.version = '0.13.2';
/* [MS-CFB] 2.6.4 */
function namecmp(l/*:string*/, r/*:string*/)/*:number*/ {
var L = l.split("/"), R = r.split("/");
@ -59,6 +59,8 @@ function filename(p/*:string*/)/*:string*/ {
var c = p.lastIndexOf("/");
return (c === -1) ? p : p.slice(c+1);
}
var fs/*:: = require('fs'); */;
function get_fs() { return fs || (fs = require('fs')); }
function parse(file/*:RawBytes*/, options/*:CFBReadOpts*/)/*:CFBContainer*/ {
var mver = 3;
var ssz = 512;
@ -356,9 +358,8 @@ function read_date(blob/*:RawBytes|CFBlob*/, offset/*:number*/)/*:Date*/ {
return new Date(( ( (__readUInt32LE(blob,offset+4)/1e7)*Math.pow(2,32)+__readUInt32LE(blob,offset)/1e7 ) - 11644473600)*1000);
}
var fs/*:: = require('fs'); */;
function read_file(filename/*:string*/, options/*:CFBReadOpts*/) {
if(fs == null) fs = require('fs');
get_fs();
return parse(fs.readFileSync(filename), options);
}
@ -638,6 +639,7 @@ var consts = {
};
function write_file(cfb/*:CFBContainer*/, filename/*:string*/, options/*:CFBWriteOpts*/)/*:void*/ {
get_fs();
var o = _write(cfb, options);
/*:: if(typeof Buffer == 'undefined' || !Buffer.isBuffer(o) || !(o instanceof Buffer)) throw new Error("unreachable"); */
fs.writeFileSync(filename, o);
@ -652,7 +654,7 @@ function a2s(o/*:RawBytes*/)/*:string*/ {
function write(cfb/*:CFBContainer*/, options/*:CFBWriteOpts*/)/*:RawBytes|string*/ {
var o = _write(cfb, options);
switch(options && options.type) {
case "file": fs.writeFileSync(options.filename, (o/*:any*/)); return o;
case "file": get_fs(); fs.writeFileSync(options.filename, (o/*:any*/)); return o;
case "binary": return a2s(o);
case "base64": return Base64.encode(a2s(o));
}
@ -668,13 +670,13 @@ function cfb_add(cfb/*:CFBContainer*/, name/*:string*/, content/*:?RawBytes*/, o
init_cfb(cfb);
var file = CFB.find(cfb, name);
if(!file) {
var fpath = cfb.FullPaths[0];
var fpath/*:string*/ = cfb.FullPaths[0];
if(name.slice(0, fpath.length) == fpath) fpath = name;
else {
if(fpath.slice(-1) != "/") fpath += "/";
fpath = (fpath + name).replace("//","/");
}
file = ({name: filename(name)}/*:any*/);
file = ({name: filename(name), type: 2}/*:any*/);
cfb.FileIndex.push(file);
cfb.FullPaths.push(fpath);
CFB.utils.cfb_gc(cfb);

@ -32,7 +32,7 @@ a { text-decoration: none }
<b>Advanced Demo Options:</b>
Use readAsBinaryString: (when available) <input type="checkbox" name="userabs" checked>
<a id="saveit" onclick="savefile();" href="#">Export loaded data</a>
<a id="saveit" onclick="savefile();" href="#">Export data</a>
</pre>
<pre id="out"></pre>
<br />
@ -61,14 +61,14 @@ var get_manifest = (function() {
var cnt = 0, rootsize = 0, filesize = 0;
out.push(" Length Date Time Name");
out.push(" -------- ---- ---- ----");
cfb.FileIndex.forEach(function(file, i/*:number*/) {
cfb.FileIndex.forEach(function(file/*:CFBEntry*/, i/*:number*/) {
switch(file.type) {
case 5:
basetime = file.ct || file.mt || basetime;
rootsize = file.size;
break;
case 2:
out.push(sprintf("%9lu %s %s", file.size, format_date(basetime), fix_string(cfb.FullPaths[i])));
out.push(sprintf("%9lu %s <a href=\"#\" onclick=\"download_file(%d);\">%s</a>", file.size, format_date(basetime), i, fix_string(cfb.FullPaths[i])));
filesize += file.size;
++cnt;
}
@ -82,8 +82,7 @@ var get_manifest = (function() {
function process_data(cfb) {
global_cfb = cfb;
var output = get_manifest(cfb);
if(out.innerText === undefined) out.textContent = output;
else out.innerText = output;
out.innerHTML = output;
}
var do_file = (function() {
@ -156,6 +155,22 @@ var savefile = (function() {
saveAs(new Blob([s2ab(data)],{type:"application/octet-stream"}), "sheetjs.xls");
};
})();
var download_file = (function() {
var a2ab = function a2ab(a) {
var o = new ArrayBuffer(a.length);
var view = new Uint8Array(o);
for (var i = 0; i!=a.length; ++i) view[i] = a[i];
return o;
};
return function download_file(i) {
if(!global_cfb) return alert("Must load a file first!");
console.log(global_cfb);
var file = global_cfb.FileIndex[i], data = file.content;
saveAs(new Blob([a2ab(data)],{type:"application/octet-stream"}), file.name);
};
})();
</script>
<script type="text/javascript">
/* eslint no-use-before-define:0 */

@ -67,7 +67,7 @@ type CFBlob = CFBlobArray | CFBlobBuffer | CFBlobUint8;
type CFBWriteOpts = any;
interface CFBReadOpts {
type:?string;
type?:string;
};
type CFBFileIndex = Array<CFBEntry>;

@ -6,4 +6,5 @@ declare module './' { declare var exports:CFBModule; };
declare module 'commander' { declare var exports:any; };
declare module 'printj' { declare var exports:any; };
declare module 'crc-32' { declare var exports: any; };
*/

@ -1,5 +0,0 @@
var describe = function(m,cb){if(cb) cb();};
describe.skip = function(m,cb){};
var it = function(m,cb){if(cb) cb();};
it.skip = function(m,cb){};
var before = function(cb){if(cb) cb();};

@ -1,6 +1,6 @@
{
"name": "cfb",
"version": "0.13.1",
"version": "0.13.2",
"author": "sheetjs",
"description": "Compound File Binary File Format extractor",
"keywords": [ "cfb", "compression", "office" ],

15
test.js

@ -1,8 +1,18 @@
/* cfb.js (C) 2013-present SheetJS -- http://sheetjs.com */
/* vim: set ts=2: */
/*jshint mocha:true */
/*global process, require */
/*::
declare type EmptyFunc = (() => void) | null;
declare type DescribeIt = { (desc:string, test:EmptyFunc):void; skip(desc:string, test:EmptyFunc):void; };
declare var describe : DescribeIt;
declare var it: DescribeIt;
declare var before:(test:EmptyFunc)=>void;
*/
var CFB;
var fs = require('fs');
describe('source', function() { it('should load', function() { CFB = require('./'); }); });
if(typeof CRC32 === 'undefined') CRC32 = require('crc-32');
var CRC32 = require('crc-32');
var ex = [".xls",".doc",".ppt"];
if(process.env.FMTS) ex=process.env.FMTS.split(":").map(function(x){return x[0]==="."?x:"."+x;});
@ -71,6 +81,7 @@ function parsetest(x, cfb) {
_new = CFB.find(newcfb, '/WordDocument') || CFB.find(newcfb, '/Word Document');
break;
}
/*:: if(!_old || !_new) throw "unreachable"; */
if(CRC32.buf(_old.content) != CRC32.buf(_new.content)) throw new Error(x + " failed roundtrip test");
});
it('should be idempotent', function() {
@ -107,7 +118,7 @@ describe('should parse test files', function() {
});
});
var cp = 'custom_properties.xls'
var cp = 'custom_properties.xls';
describe('input formats', function() {
it('should read binary strings', function() {

@ -1,57 +1,67 @@
/* cfb.js (C) 2013-present SheetJS -- http://sheetjs.com */
/* eslint-env node */
/* vim: set ts=2 ft=javascript: */
const n = "cfb";
import * as X from 'cfb';
import fs = require('fs');
import program = require('commander');
import PRINTJ = require("printj");
const sprintf = PRINTJ.sprintf;
program
.version(X.version)
.usage('[options] <file>')
.usage('[options] <file> [subfiles...]')
.option('-q, --quiet', 'process but do not report')
.option('-l, --list-files', 'list files')
.option('-d, --dump', 'dump internal representation but do not extract')
.option('-z, --dump', 'dump internal representation but do not extract')
.option('-r, --repair', 'attempt to repair and garbage-collect archive')
.option('-c, --create', 'create file')
.option('-a, --append', 'add files to CFB (overwrite existing data)')
.option('-d, --delete', 'delete files from CFB')
.option('--dev', 'development mode')
.option('--read', 'read but do not print out contents');
program.parse(process.argv);
if(program.args.length === 0 || !fs.existsSync(program.args[0])) {
console.error("Usage: " + process.argv[1] + " [-q] <cfb_file>");
process.exit(1);
const exit = process.exit;
const die = (errno: number, msg: string) => { console.error(n + ": " + msg); exit(errno); };
const logit = (cmd: string, f: string) => { console.error(sprintf("%-6s %s", cmd, f)); };
if(program.args.length === 0) die(1, "must specify a filename");
if(program.create) {
logit("create", program.args[0]);
const newcfb = X.utils.cfb_new();
X.writeFile(newcfb, program.args[0]);
}
if(!fs.existsSync(program.args[0])) die(1, "must specify a filename");
const opts: X.CFBParsingOptions = {type:'file'};
if(program.dev) opts.WTF = true;
const cfb: X.CFBContainer = X.read(program.args[0], opts);
if(program.quiet) process.exit(0);
if(program.quiet) exit(0);
if(program.dump) {
console.log("Full Paths:");
console.log(cfb.FullPaths.map((x) => " " + x).join("\n"));
console.log("Full Path Directory:");
console.log(cfb.FullPathDir);
process.exit(0);
}
if(program.repair) {
X.writeFile(cfb, program.args[0]);
process.exit(0);
console.log("File Index:");
console.log(cfb.FileIndex);
exit(0);
}
if(program.repair) { X.writeFile(cfb, program.args[0]); exit(0); }
const fix_string = (x: string): string => x.replace(/[\u0000-\u001f]/, ($$) => sprintf("\\u%04X", $$.charCodeAt(0)));
const format_date = (date: Date): string => {
return sprintf("%02u-%02u-%02u %02u:%02u", date.getUTCMonth()+1, date.getUTCDate(), date.getUTCFullYear()%100, date.getUTCHours(), date.getUTCMinutes());
};
const sprintf = PRINTJ.sprintf;
function fix_string(x: string): string { return x.replace(/[\u0000-\u001f]/, function($$) { return sprintf("\\u%04X", $$.charCodeAt(0)); }); }
if(program.listFiles) {
const format_date = function(date: Date): string {
return sprintf("%02u-%02u-%02u %02u:%02u", date.getUTCMonth()+1, date.getUTCDate(), date.getUTCFullYear()%100, date.getUTCHours(), date.getUTCMinutes());
};
let basetime = new Date(1980,0,1);
let cnt = 0, rootsize = 0, filesize = 0;
console.log(" Length Date Time Name");
console.log(" -------- ---- ---- ----");
cfb.FileIndex.forEach(function(file: X.CFBEntry, i: number) {
cfb.FileIndex.forEach((file: X.CFBEntry, i: number) => {
switch(file.type) {
case 5:
basetime = file.ct || file.mt || basetime;
@ -66,14 +76,52 @@ if(program.listFiles) {
console.log(" -------- -------");
console.log(sprintf("%9lu %lu file%s", rootsize || filesize, cnt, (cnt !== 1 ? "s" : "")));
process.exit(0);
exit(0);
}
const mkdirp = (path: string) => { path.split("/").reduce((acc: string, p: string): string => {
acc += p + "/";
if(!fs.existsSync(acc)) { logit("mkdir", acc); fs.mkdirSync(acc); }
return acc;
}, ""); };
const write = (path: string, data: X.CFBEntry) => {
logit("write", fix_string(path));
fs.writeFileSync(path, /*::new Buffer((*/data.content/*:: :any))*/);
};
if(program.create || program.append) {
program.args.slice(1).forEach((x: string) => {
logit("append", x);
X.utils.cfb_add(cfb, "/" + x, fs.readFileSync(x));
});
X.writeFile(cfb, program.args[0]);
exit(0);
}
if(program.delete) {
program.args.slice(1).forEach((x: string) => {
logit("delete", x);
X.utils.cfb_del(cfb, "/" + x);
});
X.writeFile(cfb, program.args[0]);
exit(0);
}
if(program.args.length > 1) {
program.args.slice(1).forEach((x: string) => {
const data/*:?CFBEntry*/ = X.find(cfb, x);
if(!data) { console.error(x + ": file not found"); return; }
if(data.type !== 2) { console.error(x + ": not a file"); return; }
const idx = cfb.FileIndex.indexOf(data), path = cfb.FullPaths[idx];
mkdirp(path.slice(0, path.lastIndexOf("/")));
write(path, data);
});
exit(0);
}
for(let i=0; i!==cfb.FullPaths.length; ++i) {
if(cfb.FullPaths[i].slice(-1) === "/") {
console.error("mkdir " + fix_string(cfb.FullPaths[i]));
fs.mkdirSync(cfb.FullPaths[i]);
} else {
console.error("write " + fix_string(cfb.FullPaths[i]));
fs.writeFileSync(cfb.FullPaths[i], cfb.FileIndex[i].content);
}
if(!cfb.FileIndex[i].name) continue;
if(cfb.FullPaths[i].slice(-1) === "/") mkdirp(cfb.FullPaths[i]);
else write(cfb.FullPaths[i], cfb.FileIndex[i]);
}

@ -38,7 +38,7 @@ type CFBFiles = {[n:string]:CFBEntry};
/* [MS-CFB] v20130118 */
var CFB = (function _CFB(){
var exports/*:CFBModule*/ = /*::(*/{}/*:: :any)*/;
exports.version = '0.13.1';
exports.version = '0.13.2';
/* [MS-CFB] 2.6.4 */
function namecmp(l/*:string*/, r/*:string*/)/*:number*/ {
var L = l.split("/"), R = r.split("/");
@ -59,6 +59,8 @@ function filename(p/*:string*/)/*:string*/ {
var c = p.lastIndexOf("/");
return (c === -1) ? p : p.slice(c+1);
}
var fs/*:: = require('fs'); */;
function get_fs() { return fs || (fs = require('fs')); }
function parse(file/*:RawBytes*/, options/*:CFBReadOpts*/)/*:CFBContainer*/ {
var mver = 3;
var ssz = 512;
@ -356,9 +358,8 @@ function read_date(blob/*:RawBytes|CFBlob*/, offset/*:number*/)/*:Date*/ {
return new Date(( ( (__readUInt32LE(blob,offset+4)/1e7)*Math.pow(2,32)+__readUInt32LE(blob,offset)/1e7 ) - 11644473600)*1000);
}
var fs/*:: = require('fs'); */;
function read_file(filename/*:string*/, options/*:CFBReadOpts*/) {
if(fs == null) fs = require('fs');
get_fs();
return parse(fs.readFileSync(filename), options);
}
@ -638,6 +639,7 @@ var consts = {
};
function write_file(cfb/*:CFBContainer*/, filename/*:string*/, options/*:CFBWriteOpts*/)/*:void*/ {
get_fs();
var o = _write(cfb, options);
/*:: if(typeof Buffer == 'undefined' || !Buffer.isBuffer(o) || !(o instanceof Buffer)) throw new Error("unreachable"); */
fs.writeFileSync(filename, o);
@ -652,7 +654,7 @@ function a2s(o/*:RawBytes*/)/*:string*/ {
function write(cfb/*:CFBContainer*/, options/*:CFBWriteOpts*/)/*:RawBytes|string*/ {
var o = _write(cfb, options);
switch(options && options.type) {
case "file": fs.writeFileSync(options.filename, (o/*:any*/)); return o;
case "file": get_fs(); fs.writeFileSync(options.filename, (o/*:any*/)); return o;
case "binary": return a2s(o);
case "base64": return Base64.encode(a2s(o));
}
@ -668,13 +670,13 @@ function cfb_add(cfb/*:CFBContainer*/, name/*:string*/, content/*:?RawBytes*/, o
init_cfb(cfb);
var file = CFB.find(cfb, name);
if(!file) {
var fpath = cfb.FullPaths[0];
var fpath/*:string*/ = cfb.FullPaths[0];
if(name.slice(0, fpath.length) == fpath) fpath = name;
else {
if(fpath.slice(-1) != "/") fpath += "/";
fpath = (fpath + name).replace("//","/");
}
file = ({name: filename(name)}/*:any*/);
file = ({name: filename(name), type: 2}/*:any*/);
cfb.FileIndex.push(file);
cfb.FullPaths.push(fpath);
CFB.utils.cfb_gc(cfb);

@ -8,7 +8,7 @@ var DO_NOT_EXPORT_CFB = true;
/* [MS-CFB] v20130118 */
var CFB = (function _CFB(){
var exports = {};
exports.version = '0.13.1';
exports.version = '0.13.2';
/* [MS-CFB] 2.6.4 */
function namecmp(l, r) {
var L = l.split("/"), R = r.split("/");
@ -29,6 +29,8 @@ function filename(p) {
var c = p.lastIndexOf("/");
return (c === -1) ? p : p.slice(c+1);
}
var fs;
function get_fs() { return fs || (fs = require('fs')); }
function parse(file, options) {
var mver = 3;
var ssz = 512;
@ -326,9 +328,8 @@ function read_date(blob, offset) {
return new Date(( ( (__readUInt32LE(blob,offset+4)/1e7)*Math.pow(2,32)+__readUInt32LE(blob,offset)/1e7 ) - 11644473600)*1000);
}
var fs;
function read_file(filename, options) {
if(fs == null) fs = require('fs');
get_fs();
return parse(fs.readFileSync(filename), options);
}
@ -603,6 +604,7 @@ var consts = {
};
function write_file(cfb, filename, options) {
get_fs();
var o = _write(cfb, options);
fs.writeFileSync(filename, o);
}
@ -616,7 +618,7 @@ function a2s(o) {
function write(cfb, options) {
var o = _write(cfb, options);
switch(options && options.type) {
case "file": fs.writeFileSync(options.filename, (o)); return o;
case "file": get_fs(); fs.writeFileSync(options.filename, (o)); return o;
case "binary": return a2s(o);
case "base64": return Base64.encode(a2s(o));
}
@ -638,7 +640,7 @@ function cfb_add(cfb, name, content, opts) {
if(fpath.slice(-1) != "/") fpath += "/";
fpath = (fpath + name).replace("//","/");
}
file = ({name: filename(name)});
file = ({name: filename(name), type: 2});
cfb.FileIndex.push(file);
cfb.FullPaths.push(fpath);
CFB.utils.cfb_gc(cfb);