1
1
mirror of https://github.com/ariya/phantomjs.git synced 2024-09-17 15:47:09 +03:00

Filesystem module should allow reading and writing binary files.

CommonJS proposal: http://wiki.commonjs.org/wiki/Filesystem/A.
It's called "raw".

http://code.google.com/p/phantomjs/issues/detail?id=400

Squashed commit of the following:

commit dd5fab4778
Author: Milian Wolff <milian.wolff@kdab.com>
Date:   Thu Feb 23 16:19:21 2012 +0100

    the "mode" string is now properly parsed, and not only the first
    char evaluated. This allows us to do fancy things like

    fs.open(file, "rw+"); // read/write/append

    Furthermore .read() is adapted such that it will always return the
    full file contents, no matter where we have seeked to before (i.e.
    by passing + we seek to the end, hence read() would always return
    an empty string).

    To open a binary file, pass "b" in the mode string to fs.open, e.g.:

    fs.open(file, "rb"); // read binary
    fs.open(file, "wb"); // write binary
    fs.open(file, "rwb+"); // read/write binary, append

    alternatively, one can use these shortcuts:

    fs.write(file, contents, "b"); // write binary
    fs.read(file, "b"); // read binary

    Unit tests are extended and the echoToFile.js example fixed (it did not
    close the file, which lead to the contents never getting written
    on-disk since flush() is never called).

    Also note that the FileSystem::open method was cleaned up and at least
    one memory leak (if QFile* could not open) was fixed. The code should
    now also be more C++-like.

commit 4113995113
Author: Milian Wolff <milian.wolff@kdab.com>
Date:   Wed Feb 15 16:39:23 2012 +0100

    use QString instead of QByteArray for raw binary data

    QByteArray is simply unusable in JavaScript, since functions like
    e.g. window.btoa expect a string. Also there is no sane way to
    create a byte array in javascript, as ArrayBuffer e.g. is not
    supported by QScript (at least there is no conversion in place).

    If we use QString and some custom read/write code this all works
    as expected though, we can use window.btoa to base64 encode binary
    data and we can create random binary data using String.fromCharCode

    also adds a unit test

commit e45673486e
Author: Milian Wolff <milian.wolff@kdab.com>
Date:   Wed Feb 15 14:39:15 2012 +0100

    make it possible to read/write raw/binary files

    this adds File::readRaw and File::writeRaw functions,
    as well as 'shimmed' versions FS::readRaw and FS::writeRaw

    these functions directly use QFile and QByteArray instead of
    QTextStream and QString, making it possible to read and write
    binary data, e.g. images and such.
This commit is contained in:
Milian Wolff 2012-02-29 07:53:12 -08:00 committed by Ariya Hidayat
parent e88e008ecf
commit 61eb86782b
5 changed files with 236 additions and 111 deletions

View File

@ -14,6 +14,7 @@ if (phantom.args.length < 2) {
try {
f = fs.open(phantom.args[0], "w");
f.writeLine(content);
f.close();
} catch (e) {
console.log(e);
}

View File

@ -39,13 +39,12 @@
// public:
File::File(QFile *openfile, QTextCodec *codec, QObject *parent) :
QObject(parent),
m_file(openfile)
m_file(openfile),
m_fileStream(0)
{
m_fileStream.setDevice(m_file);
if ((QTextCodec *)NULL == codec) {
m_fileStream.setCodec(QTextCodec::codecForName("UTF-8"));
} else {
m_fileStream.setCodec(codec);
if ( codec ) {
m_fileStream = new QTextStream(m_file);
m_fileStream->setCodec(codec);
}
}
@ -54,33 +53,80 @@ File::~File()
this->close();
}
//NOTE: for binary files we want to use QString instead of QByteArray as the
// latter is not really useable in javascript and e.g. window.btoa expects a string
// and we need special code required since fromAsci() would stop as soon as it
// encounters \0 or similar
// public slots:
QString File::read()
{
if ( m_file->isReadable() ) {
return m_fileStream.readAll();
if ( !m_file->isReadable() ) {
qDebug() << "File::read - " << "Couldn't read:" << m_file->fileName();
return QString();
}
if ( m_file->isWritable() ) {
// make sure we write everything to disk before reading
flush();
}
if ( m_fileStream ) {
// text file
const qint64 pos = m_fileStream->pos();
m_fileStream->seek(0);
const QString ret = m_fileStream->readAll();
m_fileStream->seek(pos);
return ret;
} else {
// binary file
const qint64 pos = m_file->pos();
m_file->seek(0);
const QByteArray data = m_file->readAll();
m_file->seek(pos);
QString ret(data.size());
for(int i = 0; i < data.size(); ++i) {
ret[i] = data.at(i);
}
return ret;
}
qDebug() << "File::read - " << "Couldn't read:" << m_file->fileName();
return QString();
}
bool File::write(const QString &data)
{
if ( m_file->isWritable() ) {
m_fileStream << data;
if ( !m_file->isWritable() ) {
qDebug() << "File::write - " << "Couldn't write:" << m_file->fileName();
return true;
}
qDebug() << "File::write - " << "Couldn't write:" << m_file->fileName();
return false;
if ( m_fileStream ) {
// text file
(*m_fileStream) << data;
return true;
} else {
// binary file
QByteArray bytes(data.size(), Qt::Uninitialized);
for(int i = 0; i < data.size(); ++i) {
bytes[i] = data.at(i).toAscii();
}
return m_file->write(bytes);
}
}
QString File::readLine()
{
if ( m_file->isReadable() ) {
return m_fileStream.readLine();
if ( !m_file->isReadable() ) {
qDebug() << "File::readLine - " << "Couldn't read:" << m_file->fileName();
return QString();
}
if ( m_file->isWritable() ) {
// make sure we write everything to disk before reading
flush();
}
if ( m_fileStream ) {
// text file
return m_fileStream->readLine();
} else {
// binary file - doesn't make much sense but well...
return QString::fromAscii(m_file->readLine());
}
qDebug() << "File::readLine - " << "Couldn't read:" << m_file->fileName();
return QString();
}
bool File::writeLine(const QString &data)
@ -95,7 +141,13 @@ bool File::writeLine(const QString &data)
bool File::atEnd() const
{
if ( m_file->isReadable() ) {
return m_fileStream.atEnd();
if (m_fileStream) {
// text file
return m_fileStream->atEnd();
} else {
// binary file
return m_file->atEnd();
}
}
qDebug() << "File::atEnd - " << "Couldn't read:" << m_file->fileName();
return false;
@ -104,13 +156,22 @@ bool File::atEnd() const
void File::flush()
{
if ( m_file ) {
m_fileStream.flush();
if ( m_fileStream ) {
// text file
m_fileStream->flush();
}
// binary or text file
m_file->flush();
}
}
void File::close()
{
flush();
if ( m_fileStream ) {
delete m_fileStream;
m_fileStream = 0;
}
if ( m_file ) {
m_file->close();
delete m_file;
@ -278,95 +339,81 @@ QString FileSystem::absolute(const QString &relativePath) const
return QFileInfo(relativePath).absoluteFilePath();
}
static inline QString getCharset(const QVariant &val) {
QVariant::Type type = val.type();
// val must be either a string or null/undefined.
if (QVariant::String != type && QVariant::Invalid != type) {
qDebug() << "FileSystem::open - " << "Charset must be a string!";
return QString();
}
QString charset = val.toString();
// Default to UTF-8
if (charset.isEmpty()) {
charset = "UTF-8";
}
return charset;
}
// Files
QObject *FileSystem::_open(const QString &path, const QVariantMap &opts) const
{
File *f = NULL;
QFile *_f = new QFile(path);
QFile::OpenMode modeCode = QFile::NotOpen;
QVariant modeVar = opts["mode"];
const QVariant modeVar = opts["mode"];
// Ensure only strings
if (modeVar.type() != QVariant::String) {
qDebug() << "FileSystem::open - " << "Mode must be a string!" << modeVar;
return NULL;
return 0;
}
QString mode = modeVar.toString();
// Ensure only one "mode character" has been selected
if ( mode.length() != 1) {
qDebug() << "FileSystem::open - " << "Wrong Mode string length:" << mode;
return NULL;
}
bool isBinary = false;
QFile::OpenMode modeCode = QFile::NotOpen;
// Determine the OpenMode
switch(mode[0].toAscii()) {
case 'r': case 'R': {
modeCode |= QFile::ReadOnly;
// Make sure there is something to read
if ( !_f->exists() ) {
qDebug() << "FileSystem::open - " << "Trying to read a file that doesn't exist:" << path;
return NULL;
foreach(const QChar &c, modeVar.toString()) {
switch(c.toAscii()) {
case 'r': case 'R': {
modeCode |= QFile::ReadOnly;
break;
}
case 'a': case 'A': case '+': {
modeCode |= QFile::Append;
modeCode |= QFile::WriteOnly;
break;
}
case 'w': case 'W': {
modeCode |= QFile::WriteOnly;
break;
}
case 'b': case 'B': {
isBinary = true;
break;
}
default: {
qDebug() << "FileSystem::open - " << "Wrong Mode:" << c;
return 0;
}
break;
}
case 'a': case 'A': case '+': {
modeCode |= QFile::Append;
// NOTE: no "break" here! This case will also execute the code for case 'w'.
}
case 'w': case 'W': {
modeCode |= QFile::WriteOnly;
// Make sure the file exists OR it can be created at the required path
if ( !_f->exists() && !makeTree(QFileInfo(path).dir().absolutePath()) ) {
qDebug() << "FileSystem::open - " << "Full path coulnd't be created:" << path;
return NULL;
}
break;
}
default: {
qDebug() << "FileSystem::open - " << "Wrong Mode:" << mode;
return NULL;
}
}
QString charset = getCharset(opts["charset"]);
QTextCodec *codec = QTextCodec::codecForName(charset.toAscii());
if ((QTextCodec *)NULL == codec) {
qDebug() << "FileSystem::open - " << "Unknown charset:" << charset;
return NULL;
// Make sure the file exists OR it can be created at the required path
if ( !QFile::exists(path) && modeCode & QFile::WriteOnly ) {
if ( !makeTree(QFileInfo(path).dir().absolutePath()) ) {
qDebug() << "FileSystem::open - " << "Full path coulnd't be created:" << path;
return 0;
}
}
// Make sure there is something to read
if ( !QFile::exists(path) && modeCode & QFile::ReadOnly ) {
qDebug() << "FileSystem::open - " << "Trying to read a file that doesn't exist:" << path;
return 0;
}
QTextCodec *codec = 0;
if (!isBinary) {
// default to UTF-8 encoded files
const QString charset = opts.value("charset", "UTF-8").toString();
codec = QTextCodec::codecForName(charset.toAscii());
if (!codec) {
qDebug() << "FileSystem::open - " << "Unknown charset:" << charset;
return 0;
}
}
// Try to Open
if ( _f->open(modeCode) ) {
f = new File(_f, codec);
if ( f ) {
return f;
}
QFile* file = new QFile(path);
if ( !file->open(modeCode) ) {
// Return "NULL" if the file couldn't be opened as requested
delete file;
qDebug() << "FileSystem::open - " << "Couldn't be opened:" << path;
return 0;
}
// Return "NULL" if the file couldn't be opened as requested
qDebug() << "FileSystem::open - " << "Couldn't be opened:" << path;
return NULL;
return new File(file, codec);
}
bool FileSystem::_remove(const QString &path) const

View File

@ -42,6 +42,8 @@ class File : public QObject
Q_OBJECT
public:
// handle a textfile with given codec
// if @p codec is null, the file is considered to be binary
File(QFile *openfile, QTextCodec *codec, QObject *parent = 0);
virtual ~File();
@ -58,7 +60,7 @@ public slots:
private:
QFile *m_file;
QTextStream m_fileStream;
QTextStream *m_fileStream;
};
@ -91,7 +93,9 @@ public slots:
// 'open(path, mode|options)' implemented in "filesystem-shim.js" using '_open(path, opts)'
QObject *_open(const QString &path, const QVariantMap &opts) const;
// 'read(path, options)' implemented in "filesystem-shim.js"
// 'readRaw(path, options)' implemented in "filesystem-shim.js"
// 'write(path, mode|options)' implemented in the "filesystem-shim.js"
// 'writeRaw(path, mode|options)' implemented in the "filesystem-shim.js"
// 'remove(path)' implemented in "filesystem-shim.js" using '_remove(path)'
bool _remove(const QString &path) const;
// 'copy(source, destination)' implemented in "filesystem-shim.js" using '_copy(source, destination)'

View File

@ -32,19 +32,16 @@
// JavaScript "shim" to throw exceptions in case a critical operation fails.
/** Open and return a "file" object.
* It will throw exception if it fails.
/** Convert a modeOrOpts to a map
*
* @param path Path of the file to open
* @param modeOrOpts
* mode: Open Mode. A string made of 'r', 'w', 'a/+' characters.
* mode: Open Mode. A string made of 'r', 'w', 'a/+', 'b' characters.
* opts: Options.
* - mode (see Open Mode above)
* - charset An IANA, case insensitive, charset name.
* @return "file" object
*/
exports.open = function (path, modeOrOpts) {
var file, opts;
function modeOrOptsToOpts(modeOrOpts) {
var opts;
// Extract charset from opts
if (modeOrOpts == null) {
@ -58,27 +55,57 @@ exports.open = function (path, modeOrOpts) {
opts = modeOrOpts;
}
return opts;
}
/** Open and return a "file" object.
* It will throw exception if it fails.
*
* @param path Path of the file to open
* @param modeOrOpts
* mode: Open Mode. A string made of 'r', 'w', 'a/+', 'b' characters.
* opts: Options.
* - mode (see Open Mode above)
* - charset An IANA, case insensitive, charset name.
* @return "file" object
*/
exports.open = function (path, modeOrOpts) {
// Open file
file = exports._open(path, opts);
var file = exports._open(path, modeOrOptsToOpts(modeOrOpts));
if (file) {
return file;
}
throw "Unable to open file '" + path + "'";
};
/** Open, read and return content of a file.
/** Open, read and return text content of a file.
* It will throw an exception if it fails.
*
* @param path Path of the file to read from
* @param opts Options.
* - charset An IANA, case insensitive, charset name.
* @param modeOrOpts
* mode: Open Mode. 'b' to open a raw binary file
* opts: Options.
* - mode (see Open Mode above)
* - charset An IANA, case insensitive, charset name.
* @return file content
*/
exports.read = function (path, opts) {
if (opts == null || typeof opts !== 'object') {
opts = {};
exports.read = function (path, modeOrOpts) {
if (typeof modeOrOpts == 'string') {
if (modeOrOpts.toLowerCase() == 'b') {
// open binary
modeOrOpts = {mode: modeOrOpts};
} else {
// asume charset is given
modeOrOpts = {charset: modeOrOpts};
}
}
var opts = modeOrOptsToOpts(modeOrOpts);
// ensure we open for reading
if ( typeof opts.mode !== 'string' ) {
opts.mode = 'r';
} else if ( opts.mode.indexOf('r') == -1 ) {
opts.mode += 'r';
}
opts.mode = 'r';
var f = exports.open(path, opts),
content = f.read();
@ -86,22 +113,26 @@ exports.read = function (path, opts) {
return content;
};
/** Open and write content to a file
/** Open and write text content to a file
* It will throw an exception if it fails.
*
* @param path Path of the file to read from
* @param content Content to write to the file
* @param modeOrOpts
* mode: Open Mode. A string made of 'r', 'w', 'a/+' characters.
* mode: Open Mode. A string made of 'r', 'w', 'a/+', 'b' characters.
* opts: Options.
* - mode (see Open Mode above)
* - charset An IANA, case insensitive, charset name.
*/
exports.write = function (path, content, modeOrOpts) {
if (modeOrOpts == null) {
modeOrOpts = {};
var opts = modeOrOptsToOpts(modeOrOpts);
// ensure we open for writing
if ( typeof opts.mode !== 'string' ) {
opts.mode = 'w';
} else if ( opts.mode.indexOf('w') == -1 ) {
opts.mode += 'w';
}
var f = exports.open(path, modeOrOpts);
var f = exports.open(path, opts);
f.write(content);
f.close();

View File

@ -4,6 +4,7 @@ describe("Basic Files API (read, write, remove, ...)", function() {
FILENAME_MOVED = FILENAME + ".moved",
FILENAME_EMPTY = FILENAME + ".empty",
FILENAME_ENC = FILENAME + ".enc",
FILENAME_BIN = FILENAME + ".bin",
ABSENT = "absent-01.test";
it("should be able to create and write a file", function() {
@ -36,6 +37,19 @@ describe("Basic Files API (read, write, remove, ...)", function() {
expect(content).toEqual("hello\nworld\n");
});
it("should be able to read/write/append content from a file", function() {
var content = "";
try{
var f = fs.open(FILENAME, "rw+");
console.log(f.read().length);
f.writeLine("asdf");
content = f.read();
console.log(content.length);
f.close();
} catch (e) { }
expect(content).toEqual("hello\nworld\nasdf\n");
});
it("should be able to copy a file", function() {
expect(fs.exists(FILENAME_COPY)).toBeFalsy();
fs.copy(FILENAME, FILENAME_COPY);
@ -97,4 +111,32 @@ describe("Basic Files API (read, write, remove, ...)", function() {
} catch (e) { }
expect(content).toEqual(output);
});
it("should be read/write binary data", function() {
var content, output = String.fromCharCode(0, 1, 2, 3, 4, 5);
try {
var f = fs.open(FILENAME_BIN, "wb");
f.write(output);
f.close();
f = fs.open(FILENAME_BIN, "rb");
content = f.read();
f.close();
fs.remove(FILENAME_BIN);
} catch (e) { }
expect(content).toEqual(output);
});
it("should be read/write binary data (shortcuts)", function() {
var content, output = String.fromCharCode(0, 1, 2, 3, 4, 5);
try {
fs.write(FILENAME_BIN, output, "b");
content = fs.read(FILENAME_BIN, "b");
fs.remove(FILENAME_BIN);
} catch (e) { }
expect(content).toEqual(output);
});
});