diff --git a/ide/app/lib/wam/wamfs.dart b/ide/app/lib/wam/wamfs.dart new file mode 100644 index 000000000..2b604a942 --- /dev/null +++ b/ide/app/lib/wam/wamfs.dart @@ -0,0 +1,97 @@ +// Copyright (c) 2014, Google Inc. Please see the AUTHORS file for details. +// All rights reserved. Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +library spark.wamfs; + +import 'dart:async'; +import 'dart:js' as js; +import 'dart:typed_data'; + +class WAMFS { + + // Javascript object to wrap. + js.JsObject _jsWAMFS; + + WAMFS() { + _jsWAMFS = new js.JsObject(js.context['WAMFS'], []); + } + + Future connect(String extensionID, String mountPath) { + Completer completer = new Completer(); + Function callback = () { + completer.complete(); + }; + Function errorHandler = (error) { + completer.completeError(error); + }; + _jsWAMFS.callMethod('connect', [extensionID, mountPath, callback, errorHandler]); + return completer.future; + } + + Future copyFile(String source, String destination) { + Completer completer = new Completer(); + Function callback = () { + completer.complete(); + }; + Function errorHandler = (error) { + completer.completeError(error); + }; + _jsWAMFS.callMethod('copyFile', [source, destination, callback, + errorHandler]); + return completer.future; + } + + Future readFile(String filename) { + Completer completer = new Completer(); + Function callback = (data) { + completer.complete(data); + }; + Function errorHandler = (error) { + completer.completeError(error); + }; + _jsWAMFS.callMethod('readFile', [callback, errorHandler]); + return completer.future; + } + + Future writeDataToFile(String filename, Uint8List content) { + Completer completer = new Completer(); + Function callback = () { + completer.complete(); + }; + Function errorHandler = (error) { + completer.completeError(error); + }; + _jsWAMFS.callMethod('writeDataToFile', [filename, content, callback, + errorHandler]); + return completer.future; + } + + Future writeStringToFile(String filename, String content) { + Completer completer = new Completer(); + Function callback = () { + completer.complete(); + }; + Function errorHandler = (error) { + completer.completeError(error); + }; + _jsWAMFS.callMethod('writeStringToFile', [filename, content, callback, + errorHandler]); + return completer.future; + } + + Future executeCommand(String executablePath, List parameters, + void printStdout(String string), void printStderr(String string)) { + Completer completer = new Completer(); + Function callback = () { + completer.complete(); + }; + Function errorHandler = (error) { + completer.completeError(error); + }; + _jsWAMFS.callMethod('executeCommand', [executablePath, + new js.JsObject.jsify(parameters), printStdout, printStderr, callback, + errorHandler]); + return completer.future; + } +} diff --git a/ide/app/lib/wam/wamfs.js b/ide/app/lib/wam/wamfs.js new file mode 100644 index 000000000..607dad570 --- /dev/null +++ b/ide/app/lib/wam/wamfs.js @@ -0,0 +1,141 @@ +'use strict'; + +var WAMFS = function() { + // Our in-memory wam-ready file system. + this.jsfs = new wam.jsfs.FileSystem(); +} + +WAMFS.prototype.connect = function(extensionID, mountPath, onSuccess, onError) { + // Reflect our DOM file system into the wam file system. + this.jsfs.makeEntry('/domfs', new wam.jsfs.dom.FileSystem(), + function() { + this._mount(extensionID, mountPath, onSuccess, onError) + }.bind(this), + function(e) { + onError(e); + }); +}; + +WAMFS.prototype._mount = function(id, path, onSuccess, onError) { + // wam remote file system object. + var rfs = null; + + // Called when the remote file system becomes ready. + var onFileSystemReady = function() { + // Now that it's ready we can remove our initial listeners. These + // were only intended to cover the initial ready or close-with-error + // conditions. A longer term onClose listener should be installed to + // check for unexpected loss of the file system. + rfs.onReady.removeListener(onFileSystemReady); + rfs.onClose.removeListener(onFileSystemClose); + + // Link the remote file system into our root file system under the given + // path. + this.jsfs.makeEntry(path, rfs, function() { + onSuccess(); + }, function(value) { + transport.disconnect(); + onError(value); + }); + }.bind(this); + + // Called when the remote file system gets closed. + var onFileSystemClose = function(value) { + rfs.onReady.removeListener(onFileSystemReady); + rfs.onClose.removeListener(onFileSystemClose); + + onError(value); + }; + + // Connection failed at the transport level. The target app probably isn't + // installed or isn't willing to acknowledge us. + var onTransportClose = function(reason, value) { + transport.readyBinding.onClose.removeListener(onTransportClose); + transport.readyBinding.onReady.removeListener(onTransportReady); + + onError(wam.mkerr('wam.FileSystem.Error.RuntimeError', + ['Transport connection failed.'])); + return; + }; + + // The target app accepted our connection request, now we've got to negotiate + // a wam channel. + var onTransportReady = function(reason, value) { + transport.readyBinding.onClose.removeListener(onTransportClose); + transport.readyBinding.onReady.removeListener(onTransportReady); + + var channel = new wam.Channel(transport, 'crx:' + id); + // Uncomment the next line for verbose logging. + // channel.verbose = wam.Channel.verbosity.ALL; + rfs = new wam.jsfs.RemoteFileSystem(channel); + rfs.onReady.addListener(onFileSystemReady); + rfs.onClose.addListener(onFileSystemClose); + }; + + // Create a new chrome.runtime.connect based transport. + var transport = new wam.transport.ChromePort(); + + // Register our close and ready handlers. + transport.readyBinding.onClose.addListener(onTransportClose); + transport.readyBinding.onReady.addListener(onTransportReady); + + // Connect to the target extension/app id. + transport.connect(id); +}; + +WAMFS.prototype.copyFile = function(source, destination, onSuccess, onError) { + var executeContext = this.jsfs.defaultBinding.createExecuteContext(); + executeContext.copyFile(source, destination, + function() { + onSuccess(); + }, function(e) { + onError(e); + }); +}; + +WAMFS.prototype.readFile = function(filename, onSuccess, onError) { + var executeContext = this.jsfs.defaultBinding.createExecuteContext(); + executeContext.readFile(filename, + function() { + onSuccess(); + }, function(e) { + onError(e); + }); +}; + +WAMFS.prototype.writeDataToFile = function(filename, content, onSuccess, onError) { + this.jsfs.defaultBinding.writeFile(filename, + {mode: {create: true}}, + {dataType: 'arraybuffer', data: content}, + function() { + onSuccess(); + }, function(e) { + onError(); + }); +}; + +WAMFS.prototype.writeStringToFile = function(filename, stringContent, onSuccess, onError) { + this.jsfs.defaultBinding.writeFile(filename, + {mode: {create: true}}, + {dataType: 'utf8-string', data: stringContent}, + function() { + onSuccess(); + }, function(e) { + onError(e); + }); +}; + +WAMFS.prototype.executeCommand = function(executablePath, parameters, + printStdout, printStderr, onSuccess, onError) { + var executeContext = this.jsfs.defaultBinding.createExecuteContext(); + executeContext.onStdOut.addListener(function(l) { + printStdout(l); + }); + executeContext.onStdErr.addListener(function(l) { + printStderr(l); + }); + executeContext.onClose.addListener(function() { + onSuccess(); + }); + executeContext.execute(executablePath, parameters); +} diff --git a/ide/app/spark.dart b/ide/app/spark.dart index 080c1df6e..761904386 100644 --- a/ide/app/spark.dart +++ b/ide/app/spark.dart @@ -53,6 +53,7 @@ import 'lib/webstore_client.dart'; import 'lib/workspace.dart' as ws; import 'lib/workspace_utils.dart' as ws_utils; import 'test/all.dart' as all_tests; +import 'lib/wam/wamfs.dart'; import 'spark_flags.dart'; import 'spark_model.dart'; @@ -527,6 +528,7 @@ abstract class Spark actionManager.registerAction(new SendFeedbackAction(this)); actionManager.registerAction(new ShowSearchView(this)); actionManager.registerAction(new ShowFilesView(this)); + actionManager.registerAction(new RunPythonAction(this)); actionManager.registerKeyListener(); @@ -3948,6 +3950,41 @@ class ShowFilesView extends SparkAction { } } +class RunPythonAction extends SparkAction { + WAMFS _fs; + bool _connected; + + RunPythonAction(Spark spark) : super(spark, 'run-python', 'Run Python') { + _fs = new WAMFS(); + } + + Future _connect() { + if (_connected) return new Future.value(); + return _fs.connect('bjffolomlcjmflonfneaijabbpnflija', '/saltpig').then((_) { + _connected = true; + }); + } + + void _invoke([context]) { + List selection = spark._getSelection(); + if (selection.length == 0) return; + ws.File file = selection.first; + file.getContents().then((content) { + return _connect().then((_) { + return _fs.writeStringToFile('/saltpig/domfs/foo.py', content).then((_) { + _fs.executeCommand('/saltpig/exe/python', ['/mnt/html5/foo.py'], + (String string) { + print('stdout: ${string}'); + }, (String string) { + print('stderr: ${string}'); + }); + }); + }); + }); + } + +} + // Analytics code. void _handleUncaughtException(error, [StackTrace stackTrace]) { diff --git a/ide/app/spark_polymer.html b/ide/app/spark_polymer.html index e74ed66e4..c7284e414 100644 --- a/ide/app/spark_polymer.html +++ b/ide/app/spark_polymer.html @@ -146,6 +146,8 @@ + + diff --git a/ide/app/spark_polymer_ui.html b/ide/app/spark_polymer_ui.html index 899744fb4..5fab05b63 100644 --- a/ide/app/spark_polymer_ui.html +++ b/ide/app/spark_polymer_ui.html @@ -108,6 +108,8 @@ + + diff --git a/ide/app/third_party/wam/wam_fs.concat.js b/ide/app/third_party/wam/wam_fs.concat.js new file mode 100644 index 000000000..c42602c62 --- /dev/null +++ b/ide/app/third_party/wam/wam_fs.concat.js @@ -0,0 +1,4720 @@ +// This file was generated by libdot/bin/concat.sh. +// It has been marked read-only for your safety. Rather +// than edit it directly, please modify one of these source +// files... +// +// wam/js/wam.js +// wam/js/wam_channel.js +// wam/js/wam_error_manager.js +// wam/js/wam_errors.js +// wam/js/wam_event.js +// wam/js/wam_transport_chrome_port.js +// wam/js/wam_transport_direct.js +// wam/js/wam_in_message.js +// wam/js/wam_out_message.js +// wam/js/wam_binding_ready.js +// wam/js/wam_binding_fs.js +// wam/js/wam_binding_fs_file_system.js +// wam/js/wam_binding_fs_execute_context.js +// wam/js/wam_binding_fs_open_context.js +// wam/js/wam_remote_ready.js +// wam/js/wam_remote_fs.js +// wam/js/wam_remote_fs_handshake.js +// wam/js/wam_remote_fs_execute.js +// wam/js/wam_remote_fs_open.js +// wam/js/wam_jsfs.js +// wam/js/wam_jsfs_file_system.js +// wam/js/wam_jsfs_entry.js +// wam/js/wam_jsfs_remote_file_system.js +// wam/js/wam_jsfs_directory.js +// wam/js/wam_jsfs_execute_context.js +// wam/js/wam_jsfs_open_context.js +// wam/js/wam_jsfs_executable.js +// wam/js/wam_jsfs_dom.js +// wam/js/wam_jsfs_dom_file_system.js +// wam/js/wam_jsfs_dom_open_context.js +// + +// SOURCE FILE: wam/js/wam.js +// Copyright (c) 2013 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +var wam = {}; + +/** + * This is filled out during the concat process, either from + * concat/wam_test_deps.concat or concat/wam_fs.concat + */ +wam.changelogVersion = null; + +/** + * Namespace for transport classes. + */ +wam.transport = {}; + +/** + * Namespace for the binding classes that sit between concrete implementations. + */ +wam.binding = {}; + +/** + * Namespace for the Request/Response classes that marshal bindings over a + * wam connection. + */ +wam.remote = {}; + +wam.remote.closeTimeoutMs = 5 * 1000; + +/** + * Shortcut to wam.errorManager.createValue. + */ +wam.mkerr = function(name, argList) { + return wam.errorManager.createValue(name, argList); +}; + +/** + * Promise based setImmediate polyfill. + */ +wam.setImmediate = function(f) { + var p = new Promise(function(resolve) { resolve() }); + p.then(f) + .catch(function(ex) { + if ('message' in ex && 'stack' in ex) { + console.warn(ex.message, ex.stack); + } else { + if (lib && lib.TestManager && + ex instanceof lib.TestManager.Result.TestComplete) { + // Tests throw this non-error when they complete, we don't want + // to log it. + return; + } + + console.warn(ex); + } + }); +}; + +/** + * Shortcut for setImmediate of a function bound to static args. + */ +wam.async = function(f, args) { + wam.setImmediate(f.bind.apply(f, args)); +}; + + +/** + * Make a globally unique id. + * + * TODO(rginda) We probably don't need to use crypto entropy for this. + */ +wam.guid = function() { + var ary = new Uint8Array(16) + window.crypto.getRandomValues(ary); + + var rv = ''; + for (var i = 0; i < ary.length; i++) { + var byte = ary[i].toString(16); + if (byte.length == 2) { + rv += byte; + } else { + rv += '0' + byte; + } + } + + return rv; +}; +// Inserted by libdot/concat/wam_fs.concat +wam.changelogVersion = '1.1'; +// SOURCE FILE: wam/js/wam_channel.js +// Copyright (c) 2013 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * wam.Channel directs messages to a companion instance of wam.Channel + * on the other side of an abstract "transport". + * + * @param {Object} A transport object. See wam.DirectTransport and + * wam.ChromePortTransport for the de-facto interface. + */ +wam.Channel = function(transport, opt_name) { + transport.readyBinding.assertReady(); + + this.transport_ = transport; + this.transport_.onMessage.addListener(this.onTransportMessage_, this); + this.transport_.readyBinding.onClose.addListener( + this.onTransportClose_, this); + this.transport_.readyBinding.onReady.addListener( + this.onTransportReady_, this); + + this.readyBinding = new wam.binding.Ready(); + this.readyBinding.onClose.addListener(this.onReadyBindingClose_, this); + + if (this.transport_.readyBinding.isReadyState('READY')) + this.readyBinding.ready(); + + /** + * Called when the remote end requests a handshake. + * + * The event handler will be passed a wam.ReadyResponse, and should + * call its replyReady method to accept the handshake. Use the closeError + * method to reject the handshake with a specific reason, or do nothing and + * the handshake will be fail by default with a wam.Error.HandshakeDeclined + * error. + * + * By the time this event is invoked the channel has already checked the + * channelProtocol name and version. + */ + this.onHandshakeOffered = new wam.Event(); + + /** + * Messages we sent that are expecting replies, keyed by message subject. + */ + this.openOutMessages_ = {}; + + /** + * Bitfield of verbosity. + */ + this.verbose = wam.Channel.verbosity.NONE; + + /** + * Name to include as a prefix in verbose logs. + */ + this.name = opt_name || 'ch-' + (wam.Channel.nameSequence_++).toString(16); +}; + +wam.Channel.verbosity = { + /** + * Log nothing. (Assign to wam.Channel..verbose, rather than bitwise OR.) + */ + 'NONE': 0x00, + + /** + * Log messages sent from this channel. + */ + 'OUT': 0x01, + + /** + * Log messages received by this channel. + */ + 'IN': 0x02, + + /** + * Log synthetic messages that appear on this channel. + */ + 'SYNTHETIC': 0x04, + + /** + * Log all of the above. + */ + 'ALL': 0x0f +}; + +wam.Channel.nameSequence_ = 0; + +/** + * Shared during the handshake message. + */ +wam.Channel.protocolName = 'x.wam.Channel'; + +/** + * Shared during the handshake message. + */ +wam.Channel.protocolVersion = '1.0'; + +/** + * Return a summary of the given message for logging purposes. + */ +wam.Channel.prototype.summarize_ = function(message) { + var rv = message.name; + + if (message.subject) + rv += '@' + message.subject.substr(0, 5); + + if (message.regardingSubject) { + rv += ', re:'; + if (message.regardingMessage) { + rv += message.regardingMessage.name + '@'; + } else { + rv += '???@'; + } + rv += message.regardingSubject.substr(0, 5); + } + + + return rv; +}; + +wam.Channel.prototype.reconnect = function() { + if (this.transport_.readyBinding.isReadyState('READY')) + this.transport_.readyBinding.closeOk(null); + + this.readyBinding.reset(); + this.transport_.reconnect(); +}; + +wam.Channel.prototype.disconnect = function(diagnostic) { + var outMessage = new wam.OutMessage( + this, 'disconnect', {diagnostic: diagnostic}); + + outMessage.onSend.addListener(function() { + if (this.readyBinding.isOpen) + this.readyBinding.closeOk(null); + }.bind(this)); + + outMessage.send(); +}; + +/** + * Send a message across the channel. + * + * This method should only be called by wam.OutMessage..send(). Don't call + * it directly or you'll miss out on the bookkeeping from OutMessage..send(). + * + * @param {wam.OutMessage} outMessage The message to send. + * @param {function()} opt_onSend Optional callback to invoke after the message + * is actually sent. + */ +wam.Channel.prototype.sendMessage = function(outMessage, opt_onSend) { + if (this.verbose & wam.Channel.verbosity.OUT) { + console.log(this.name + '/OUT: ' + this.summarize_(outMessage) + + ',', outMessage.arg); + } + + this.transport_.send(outMessage.toValue(), opt_onSend); +}; + +/** + * Send a value to this channel as if it came from the remote. + * + * This is used to cleanly close out open messages in the event that we lose + * contact with the remote, or they neglect to send a required final reply. + */ +wam.Channel.prototype.injectMessage = function( + name, arg, opt_regardingSubject) { + + var inMessage = new wam.InMessage( + this, {name: name, arg: arg, regarding: opt_regardingSubject}); + inMessage.isSynthetic = true; + + wam.setImmediate(this.routeMessage_.bind(this, inMessage)); + + return inMessage; +}; + +/** + * Create the argument for a handshake message. + * + * @param {*} payload Any "transportable" value. This is sent to the remote end + * to help it decide whether or not to accept the handshake, and how to + * deal with subsequent messages. The current implementation expects a + * `null` payload, and assumes that means you want to talk directly to + * a wam.fs.Directory. + * @return {wam.ReadyRequest} + */ +wam.Channel.prototype.createHandshakeMessage = function(payload) { + return new wam.OutMessage + (this, 'handshake', + { channelProtocol: { + name: wam.Channel.protocolName, + version: wam.Channel.protocolVersion + }, + payload: payload + }); +}; + +wam.Channel.prototype.cleanup = function() { + // Construct synthetic 'error' messages to close out any orphans. + for (var subject in this.openOutMessages_) { + this.injectMessage('error', + wam.mkerr('wam.Error.ChannelDisconnect', + ['Channel cleanup']), + subject); + } +}; + +/** + * Register an OutMessage that is expecting replies. + * + * Any incoming messages that are 'regarding' the outMessage.subject will + * be routed to the onReply event of outMessage. + * + * When the message is closed it will automatically be unregistered. + * + * @param {wam.OutMessage} The message to mark as open. + */ +wam.Channel.prototype.registerOpenOutMessage = function(outMessage) { + var subject = outMessage.subject; + + if (!outMessage.isOpen || !subject) + throw new Error('Message has no subject.'); + + if (subject in this.openOutMessages_) + throw new Error('Subject already open: ' + subject); + + outMessage.onClose.addListener(function() { + if (!(subject in this.openOutMessages_)) + throw new Error('OutMessage not found.'); + + delete this.openOutMessages_[subject]; + }.bind(this)); + + this.openOutMessages_[subject] = outMessage; +}; + +/** + * Return the opened message associated with the given subject. + * + * @return {wam.OutMessage} The open message. + */ +wam.Channel.prototype.getOpenOutMessage = function(subject) { + if (!(subject in this.openOutMessages_)) + return null; + + return this.openOutMessages_[subject]; +}; + +/** + * Route an incoming message to the correct onReply or channel message handler. + */ +wam.Channel.prototype.routeMessage_ = function(inMessage) { + if ((this.verbose & wam.Channel.verbosity.IN) || + (inMessage.isSynthetic && + this.verbose & wam.Channel.verbosity.SYNTHETIC)) { + console.log(this.name + '/' + (inMessage.isSynthetic ? 'SYN: ' : 'IN: ') + + this.summarize_(inMessage) + + ',', inMessage.arg); + } + + if (inMessage.regardingSubject) { + if (!inMessage.regardingMessage) { + // The message has a regardingSubject, but no corresponding + // regardingMessage was found. That's a problem. + console.warn(this.name + ': Got message for unknown subject: ' + + inMessage.regardingSubject); + console.log(inMessage); + + if (inMessage.isOpen) + inMessage.replyError('wam.Error.UnknownSubject', [inMessage.subject]); + return; + } + + try { + inMessage.regardingMessage.onReply(inMessage); + } catch(ex) { + console.error('onReply raised exception: ' + ex, ex.stack); + } + + if (inMessage.isFinalReply) { + if (!inMessage.regardingMessage.isOpen) { + console.warn(this.name + ': Outbound closed a regardingMessage that ' + + 'isn\'t open: outbound: ' + + inMessage.regardingMessage.name + '/' + + inMessage.regardingSubject + ', ' + + 'final reply: ' + inMessage.name + '/' + inMessage.subject); + } + + inMessage.regardingMessage.onClose(); + } + + } else { + console.log + inMessage.dispatch(this, wam.Channel.on); + } +}; + +wam.Channel.prototype.onReadyBindingClose_ = function(reason, value) { + this.cleanup(); +}; + +/** + * Handle a raw message from the transport object. + */ +wam.Channel.prototype.onTransportMessage_ = function(value) { + this.routeMessage_(new wam.InMessage(this, value)); +}; + +wam.Channel.prototype.onTransportReady_ = function() { + this.readyBinding.ready(); +}; + +/** + * Handler for transport disconnects. + */ +wam.Channel.prototype.onTransportClose_ = function() { + if (!this.readyBinding.isOpen) + return; + + this.readyBinding.closeError('wam.Error.TransportDisconnect', + ['Unexpected transport disconnect.']); +}; + +/** + * Message handlers, bound to a wam.Channel instance in the constructor. + * + * These functions are invoked with an instance of wam.fs.Channel + * as `this`, in response to some inbound wam.Message. + */ +wam.Channel.on = {}; + +/** + * Remote end initiated a disconnect. + */ +wam.Channel.on['disconnect'] = function(inMessage) { + this.readyBinding.closeError('wam.Error.ChannelDisconnect', + [inMessage.arg.diagnostic]); +}; + +/** + * Remote end is offering a handshake. + */ +wam.Channel.on['handshake'] = function(inMessage) { + if (inMessage.arg.channelProtocol.name != wam.Channel.protocolName) { + inMessage.replyError('wam.Error.InvalidChannelProtocol', + [inMessage.arg.channelProtocol.name]); + return; + } + + if (inMessage.arg.channelProtocol.version != + wam.Channel.protocolVersion) { + inMessage.replyError('wam.Error.InvalidChannelVersion', + [inMessage.arg.channelProtocol.version]); + return; + } + + var offerEvent = { + inMessage: inMessage, + response: null + }; + + this.onHandshakeOffered(offerEvent); + + if (!offerEvent.response) { + inMessage.replyError('wam.Error.HandshakeDeclined', + ['Declined by default.']); + } +}; +// SOURCE FILE: wam/js/wam_error_manager.js +// Copyright (c) 2013 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +wam.errorManager = {}; +wam.errorManager.errorDefs_ = {}; + +wam.errorManager.defineError = function(errorName, argNames) { + wam.errorManager.errorDefs_[errorName] = { + 'errorName': errorName, 'argNames': argNames + }; +}; + +wam.errorManager.defineErrors = function(/* ... */) { + for (var i = 0; i < arguments.length; i++) { + this.defineError(arguments[i][0], arguments[i][1]); + } +}; + +wam.errorManager.normalize = function(value) { + var errorName = value.errorName; + var errorArg = value.errorArg; + + if (!name) { + errorName = 'wam.Error.InvalidError'; + errorArg = {value: value}; + } + + if (!this.errorDefs_.hasOwnProperty(errorName)) { + errorName = 'wam.Error.UnknownError'; + errorArg = {errorName: errorName, errorArg: arg}; + } + + var errorDef = this.errorDefs_[name]; + for (var argName in errorDef.argNames) { + if (!argMap.hasOwnProperty(argName)) + argMap[argName] = null; + } + + return {errorName: errorName, errorArg: errorArg}; +}; + +wam.errorManager.createValue = function(name, argList) { + var errorDef = this.errorDefs_[name]; + if (!errorDef) + throw new Error('Unknown error name: ' + name); + + if (argList.length != errorDef.argNames.length) { + throw new Error('Argument list length mismatch, expected ' + + errorDef.argNames.length + ', got ' + argList.length); + } + + var value = { + 'errorName': errorDef.errorName, + 'errorArg': {} + }; + + for (var i = 0; i < argList.length; i++) { + value['errorArg'][errorDef.argNames[i]] = argList[i]; + } + + return value; +}; +// SOURCE FILE: wam/js/wam_errors.js +// Copyright (c) 2013 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +wam.errorManager.defineErrors +( + ['wam.Error.ChannelDisconnect', ['diagnostic']], + ['wam.Error.CloseTimeout', []], + ['wam.Error.HandshakeDeclined', ['diagnostic']], + ['wam.Error.InvalidChannelProtocol', ['channelProtocol']], + ['wam.Error.InvalidChannelVersion', ['channelVersion']], + ['wam.Error.ReadyAbort', ['abortErrorArg']], + ['wam.Error.ParentClosed', ['name', 'arg']], + ['wam.Error.TransportDisconnect', ['diagnostic']], + ['wam.Error.UnknownMessage', ['name']], + ['wam.Error.UnexpectedMessage', ['name', 'arg']], + ['wam.Error.UnknownPayload', []], + ['wam.Error.UnknownSubject', ['subject']] +); +// SOURCE FILE: wam/js/wam_event.js +// Copyright (c) 2013 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * An event is a JavaScript function with addListener and removeListener + * properties. + * + * When the endpoint function is called, the firstCallback will be invoked, + * followed by all of the listeners in the order they were attached, then + * the finalCallback. + * + * The returned function will have the list of callbacks, excluding + * opt_firstCallback and opt_lastCallback, as its 'observers' property. + * + * @param {function(...)} opt_firstCallback The optional function to call + * before the observers. + * @param {function(...)} opt_finalCallback The optional function to call + * after the observers. + * + * @return {function(...)} A function that, when called, invokes all callbacks + * with whatever arguments it was passed. + */ +wam.Event = function(opt_firstCallback, opt_finalCallback) { + var ep = function() { + var args = Array.prototype.slice.call(arguments); + + var rv; + if (opt_firstCallback) + rv = opt_firstCallback.apply(null, args); + + if (rv === false) + return; + + for (var i = ep.observers.length - 1; i >= 0; i--) { + var observer = ep.observers[i]; + observer[0].apply(observer[1], args); + } + + if (opt_finalCallback) + opt_finalCallback.apply(null, args); + } + + /** + * Add a callback function. + * + * @param {function(...)} callback The function to call back. + * @param {Object} opt_obj The optional |this| object to apply the function + * to. Use this rather than bind when you plan on removing the listener + * later, so that you don't have to save the bound-function somewhere. + */ + ep.addListener = function(callback, opt_obj) { + if (!callback) + throw new Error('Missing param: callback'); + + ep.observers.unshift([callback, opt_obj]); + }; + + /** + * Remove a callback function. + */ + ep.removeListener = function(callback, opt_obj) { + for (var i = 0; i < ep.observers.length; i++) { + if (ep.observers[i][0] == callback && ep.observers[i][1] == opt_obj) { + ep.observers.splice(i, 1); + break; + } + } + }; + + ep.observers = []; + + + return ep; +}; +// SOURCE FILE: wam/js/wam_transport_chrome_port.js +// Copyright (c) 2013 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * This is the chrome message port based transport. + */ +wam.transport.ChromePort = function() { + // The underlying Chrome "platform app" port. + this.port_ = null; + + this.extensionId_ = null; + + this.readyBinding = new wam.binding.Ready(); + this.readyBinding.onClose.addListener(this.onReadyBindingClose_.bind(this)); + + this.onMessage = new wam.Event(); +}; + +wam.transport.ChromePort.prototype.setPort_ = function(port) { + if (this.port_) + throw new Error('Port already set'); + + this.port_ = port; + var id = port.sender ? port.sender.id : 'anonymous'; + + var thisOnMessage = function(msg) { + wam.async(this.onMessage, [this, msg]); + }.bind(this); + + var thisOnDisconnect = function() { + console.log('wam.transport.ChromePort: disconnect: ' + id); + + this.port_.onMessage.removeListener(thisOnMessage); + this.port_.onMessage.removeListener(thisOnDisconnect); + this.port_ = null; + if (this.readyBinding.isOpen) { + this.readyBinding.closeError('wam.Error.TransportDisconnect', + ['Transport disconnect.']); + } + }.bind(this); + + this.port_.onMessage.addListener(thisOnMessage); + this.port_.onDisconnect.addListener(thisOnDisconnect); +}; + +wam.transport.ChromePort.prototype.send = function(value, opt_onSend) { + this.port_.postMessage(value); + if (opt_onSend) + wam.async(opt_onSend); +}; + +wam.transport.ChromePort.prototype.accept = function(port) { + this.readyBinding.assertReadyState('WAIT'); + this.setPort_(port); + this.send('accepted'); + this.readyBinding.ready(); + console.log('wam.transport.ChromePort: accept: ' + port.sender.id); +}; + +wam.transport.ChromePort.prototype.reconnect = function() { + if (!this.extensionId_) + throw new Error('Cannot reconnect.'); + + this.readyBinding.reset(); + this.connect(this.extensionId_); +}; + +wam.transport.ChromePort.prototype.connect = function(extensionId) { + this.readyBinding.assertReadyState('WAIT'); + this.extensionId_ = extensionId; + + var port = chrome.runtime.connect( + extensionId, {name: 'x.wam.transport.ChromePort/1.0'}); + + if (!port) { + this.readyBinding.closeError('wam.Error.TransportDisconnect', + ['Transport creation failed.']); + return; + } + + var onDisconnect = function(e) { + console.log('wam.transport.ChromePort.connect: disconnect'); + port.onMessage.removeListener(onMessage); + port.onDisconnect.removeListener(onDisconnect); + this.readyBinding.closeError('wam.Error.TransportDisconnect', + ['Transport disconnected before accept.']); + }.bind(this); + + var onMessage = function(msg) { + port.onMessage.removeListener(onMessage); + port.onDisconnect.removeListener(onDisconnect); + + if (msg != 'accepted') { + port.disconnect(); + this.readyBinding.closeError('wam.Error.TransportDisconnect', + ['Bad transport handshake.']); + return; + } + + this.setPort_(port); + this.readyBinding.ready(); + }.bind(this); + + port.onDisconnect.addListener(onDisconnect); + port.onMessage.addListener(onMessage); +}; + +/** + * The 'onConnect' function passed to listen. + * + * We invoke this whenever we hear a connection from port with the proper name. + */ +wam.transport.ChromePort.onConnectCallback_ = null; + +wam.transport.ChromePort.prototype.onReadyBindingClose_ = function() { + if (this.port_) + this.port_.disconnect(); +}; + +/** + * Invoked when an foreign extension attempts to connect while we're listening. + */ +wam.transport.ChromePort.onConnectExternal_ = function(port) { + var whitelist = wam.transport.ChromePort.connectWhitelist_ + if (whitelist && whitelist.indexOf(port.sender.id) == -1) { + console.log('wam.transport.ChromePort: reject: ' + + 'Sender is not on the whitelist: ' + port.sender.id); + port.disconnect(); + return; + } + + if (port.name != 'x.wam.transport.ChromePort/1.0') { + console.log('wam.transport.ChromePort: ' + + 'reject: Ignoring unknown connection: ' + port.name); + port.disconnect(); + return; + } + + var transport = new wam.transport.ChromePort(); + transport.accept(port); + wam.transport.ChromePort.onListenCallback_(transport); +}; + +/** + * Start listening for connections from foreign extensions. + * + * @param {Array} whitelist A whitelist of extension ids that may + * connect. Pass null to disable the whitelist and allow all connections. + * @param {function(wam.transport.ChromePort)} onConnect A callback to invoke + * when a new connection is made. + */ +wam.transport.ChromePort.listen = function(whitelist, onConnect) { + if (onConnect == null) { + if (!wam.transport.ChromePort.onConnectCallback_) + throw new Error('wam.transport.ChromePort is not listening.'); + + console.log('wam.transport.ChromePort.connect: listen cancelled'); + + wam.transport.ChromePort.onListenCallback_ = null; + wam.transport.ChromePort.connectWhitelist_ = null; + chrome.runtime.onConnectExternal.removeListener( + wam.transport.ChromePort.onConnectExternal_); + + } else { + if (wam.transport.ChromePort.onConnectCallback_) + throw new Error('wam.transport.ChromePort is already listening.'); + + console.log('wam.transport.ChromePort.connect: listen'); + + wam.transport.ChromePort.onListenCallback_ = onConnect; + wam.transport.ChromePort.connectWhitelist_ = whitelist; + chrome.runtime.onConnectExternal.addListener( + wam.transport.ChromePort.onConnectExternal_); + } +}; +// SOURCE FILE: wam/js/wam_transport_direct.js +// Copyright (c) 2012 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +wam.transport.Direct = function(name) { + /** + * An arbitrary name for this transport used for debugging. + */ + this.name = name; + + this.readyBinding = new wam.binding.Ready(); + this.readyBinding.onClose.addListener(this.onReadyBindingClose_, this); + + /** + * Subscribe to this event to peek at inbound messages. + */ + this.onMessage = new wam.Event(function(msg) { + if (this.verbose) + console.log(this.name + ' got: ' + JSON.stringify(msg)); + }.bind(this)); + + this.isConnected_ = false; + this.remoteEnd_ = null; + + this.queue_ = []; + this.boundServiceMethod_ = this.service_.bind(this); + this.servicePromise_ = new Promise(function(resolve) { resolve() }); +}; + +wam.transport.Direct.createPair = function(opt_namePrefix) { + var prefix = opt_namePrefix ? (opt_namePrefix + '-') : ''; + var a = new wam.transport.Direct(prefix + 'a'); + var b = new wam.transport.Direct(prefix + 'b'); + + a.remoteEnd_= b; + b.remoteEnd_ = a; + + a.readyBinding.onClose.addListener(function() { + setTimeout(function() { + if (b.readyBinding.isOpen) + b.readyBinding.closeErrorValue(null); + }, 0); + }); + + b.readyBinding.onClose.addListener(function() { + setTimeout(function() { + if (a.readyBinding.isOpen) + a.readyBinding.closeErrorValue(null); + }, 0); + }); + + a.reconnect(); + + return [a, b]; +}; + +wam.transport.Direct.prototype.service_ = function() { + for (var i = 0; i < this.queue_.length; i++) { + var ary = this.queue_[i]; + var method = ary[0]; + this[method].call(this, ary[1]); + if (ary[2]) + wam.async(ary[2]); + } + + this.queue_.length = 0; +}; + +wam.transport.Direct.prototype.push_ = function(name, args, opt_onSend) { + if (!this.queue_.length) { + this.servicePromise_ + .then(this.boundServiceMethod_) + .catch(function(ex) { + if ('message' in ex && 'stack' in ex) { + console.warn(ex.message, ex.stack); + } else { + if (lib && lib.TestManager && + ex instanceof lib.TestManager.Result.TestComplete) { + // Tests throw this non-error when they complete, we don't want + // to log it. + return; + } + + console.warn(ex); + } + }); + } + + this.queue_.push([name, args, opt_onSend]); +}; + +wam.transport.Direct.prototype.reconnect = function() { + this.readyBinding.reset(); + this.isConnected_ = true; + this.readyBinding.ready(); + + this.remoteEnd_.readyBinding.reset(); + this.remoteEnd_.isConnected_ = true; + this.remoteEnd_.readyBinding.ready(); +}; + +wam.transport.Direct.prototype.disconnect = function() { + this.readyBinding.closeOk(null); +}; + +wam.transport.Direct.prototype.send = function(msg, opt_onSend) { + if (!this.isConnected_) + throw new Error('Not connected.'); + + this.remoteEnd_.push_('onMessage', msg, opt_onSend); +}; + +wam.transport.Direct.prototype.onReadyBindingClose_ = function(reason, value) { + if (!this.isConnected_) + return; + + this.isConnected_ = false; +}; +// SOURCE FILE: wam/js/wam_in_message.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * Create a new inbound message for the given channel from the given JSON value. + * + * @param {wam.Channel} channel The channel that received the value. + * @param {Object} value The value received on the channel. + */ +wam.InMessage = function(channel, value) { + this.channel = channel; + this.name = value.name; + this.arg = value.arg; + + if (value.subject) { + this.subject = value.subject; + this.isOpen = true; + } + + if (value.regarding) { + this.regardingSubject = value.regarding || null; + this.regardingMessage = channel.getOpenOutMessage(value.regarding); + } + + this.isFinalReply = !!(value.name == 'ok' || value.name == 'error'); + + /** + * True if this message did not actually arrive on the transport. Indicates + * it was created locally because we can't on the remote end to send some + * required final reply. + */ + this.isSynthetic = false; + + /** + * Invoked when we send any reply to this message. + */ + this.onReply = new wam.Event(); + + /** + * Invoked when we send our final reply to this message. + */ + this.onClose = new wam.Event(this.onClose_.bind(this)); +}; + +/** + * Create a wam.OutMessage which is a reply to this message. + * + * @param {string} name The name of the message to reply with. + * @param {*} arg The message arg for the reply. + * @param {function(wam.InMessage)} opt_onReply The callback to invoke + * with message replies. + */ +wam.InMessage.prototype.createReply = function(name, arg) { + if (!this.isOpen) + throw new Error('Attempt to reply to closed message.'); + + return new wam.OutMessage(this.channel, name, arg, this); +}; + +/** + * Send a reply to this message. + * + * If you're expecting a reply to this message you *must* provide a callback + * function to opt_onReply, otherwise the reply will not get a 'subject' and + * will not be eligible for replies. + * + * After replying you may attach *additional* reply handlers to the onReply + * event of the returned wam.OutMessage. + * + * @param {string} name The name of the message to reply with. + * @param {*} arg The message arg for the reply. + * @param {function(wam.InMessage)} opt_onReply The callback to invoke + * with message replies. + */ +wam.InMessage.prototype.reply = function(name, arg, opt_onReply) { + var outMessage = this.createReply(name, arg); + if (opt_onReply) + outMessage.onReply.addListener(opt_onReply); + + outMessage.send(); + return outMessage; +}; + +/** + * Reply with a final 'ok' message. + */ +wam.InMessage.prototype.replyOk = function(arg, opt_onReply) { + return this.reply('ok', arg, opt_onReply); +}; + +/** + * Reply with a final 'error' message. + */ +wam.InMessage.prototype.replyError = function( + errorName, argList, opt_onReply) { + var errorValue = wam.errorManager.createValue(errorName, argList); + return this.reply('error', errorValue, opt_onReply); +}; + +/** + * Reply with a final 'error' message. + */ +wam.InMessage.prototype.replyErrorValue = function( + errorValue, opt_onReply) { + return this.reply('error', errorValue, opt_onReply); +}; + +/** + * Internal bookeeping needed when the message is closed. + */ +wam.InMessage.prototype.onClose_ = function() { + if (!this.subject) + console.warn('Closed inbound message without a subject.'); + + if (this.isOpen) { + this.isOpen = false; + } else { + console.warn('Inbound message closed more than once.'); + } +}; + +/** + * Try to route a message to one of the provided event handlers. + * + * The handlers object should be keyed by message name. + * + * If the message name is not handled and the message requires a reply we + * close the reply with an error and return false. If the message does not + * require a reply, we just return false. + * + * If you want to handle your own unknown messages, include a handler for + * '__unknown__'. + * + * @param {Object} obj The `this` object to use when calling the message + * handlers. + * @param {Object} handlers A map of message-name -> handler function. + */ +wam.InMessage.prototype.dispatch = function(obj, handlers, opt_args) { + var name = this.name; + + if (!handlers.hasOwnProperty(this.name)) { + if (this.name == 'ok' || this.name == 'error') + return true; + + if (handlers.hasOwnProperty('__unknown__')) { + name = '__unknown__'; + } else { + console.log('Unknown Message: ' + name); + if (this.isOpen) + this.replyError('wam.Error.UnknownMessage', [name]); + + return false; + } + } + + if (opt_args) { + opt_args.push(this) + handlers[name].apply(obj, opt_args); + } else { + handlers[name].call(obj, this); + } + return true; +}; +// SOURCE FILE: wam/js/wam_out_message.js +// Copyright (c) 2013 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * Create a new outbound message for the given channel. + * + * @param {wam.Channel} channel The channel that received the value. + * @param {Object} value The value received on the channel. + */ +wam.OutMessage = function(channel, name, arg, opt_regardingMessage) { + if (!(channel instanceof wam.Channel)) + throw new Error('Invalid channel'); + + this.channel = channel; + this.name = name; + this.arg = arg; + this.subject = null; + + if (opt_regardingMessage) { + this.regardingMessage = opt_regardingMessage; + this.regardingSubject = opt_regardingMessage.subject; + } + + /** + * True if this is the final reply we're going to send. + */ + this.isFinalReply = (name == 'ok' || name == 'error'); + + /** + * True if we're expecting replies. + */ + this.isOpen = false; + + /** + * Invoked when we receive a reply to this message. + */ + this.onReply = new wam.Event(); + + /** + * Invoked when this message is actually sent over the wire. + */ + this.onSend = new wam.Event(); + + /** + * Invoked when the message has received its last reply. + */ + this.onClose = new wam.Event(this.onClose_.bind(this)); +}; + +/** + * Convert this object into a plain JS Object ready to send over a transport. + */ +wam.OutMessage.prototype.toValue = function() { + var value = { + 'name': this.name, + 'arg': this.arg, + }; + + if (this.subject) + value['subject'] = this.subject; + + if (this.regardingSubject) + value['regarding'] = this.regardingSubject; + + return value; +}; + +/** + * Prepare this message for sending, then send it. + * + * This is the correct way to cause a message to be sent. Do not directly + * call wam.Channel..sendMessage, as you'll end up skipping the bookeeping + * done by this method. + */ +wam.OutMessage.prototype.send = function() { + if (this.onReply.observers.length && !this.subject) { + this.subject = wam.guid(); + this.isOpen = true; + this.channel.registerOpenOutMessage(this); + } + + if (this.regardingMessage && !this.regardingMessage.isOpen) + throw new Error('Reply to a closed message.'); + + if (this.isFinalReply) + this.regardingMessage.onClose(); + + if (this.regardingMessage) + this.regardingMessage.onReply(this); + + var onSend = this.onSend.observers.length > 0 ? this.onSend : null; + this.channel.sendMessage(this, onSend); +}; + +/** + * Internal bookeeping needed when the message is closed. + */ +wam.OutMessage.prototype.onClose_ = function() { + this.isOpen = false; +}; +// SOURCE FILE: wam/js/wam_binding_ready.js +// Copyright (c) 2013 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +wam.binding.Ready = function() { + this.readyState = wam.binding.Ready.state.WAIT; + + this.isOpen = false; + + this.readyValue = null; + this.closeReason = null; + this.closeValue = null; + + this.onReady = new wam.Event(function(value) { + this.readyValue = value; + this.readyState = wam.binding.Ready.state.READY; + this.isOpen = true; + }.bind(this)); + + this.onClose = new wam.Event(function(reason, value) { + this.closeReason = (reason == 'ok' ? 'ok' : 'error'); + this.closeValue = value; + this.isOpen = false; + + if (reason == 'ok') { + this.readyState = wam.binding.Ready.state.CLOSED; + } else { + this.readyState = wam.binding.Ready.state.ERROR; + } + }.bind(this)); +}; + +wam.binding.Ready.state = { + WAIT: 'WAIT', + READY: 'READY', + ERROR: 'ERROR', + CLOSED: 'CLOSED' +}; + +wam.binding.Ready.prototype.isReadyState = function(/* stateName , ... */) { + for (var i = 0; i < arguments.length; i++) { + var stateName = arguments[i]; + if (!wam.binding.Ready.state.hasOwnProperty(stateName)) + throw new Error('Unknown state: ' + stateName); + + if (this.readyState == wam.binding.Ready.state[stateName]) + return true; + } + + return false; +}; + +wam.binding.Ready.prototype.assertReady = function() { + if (this.readyState != wam.binding.Ready.state.READY) + throw new Error('Invalid ready call: ' + this.readyState); +}; + +wam.binding.Ready.prototype.assertReadyState = function(/* stateName , ... */) { + if (!this.isReadyState.apply(this, arguments)) + throw new Error('Invalid ready call: ' + this.readyState); +}; + +wam.binding.Ready.prototype.dependsOn = function(otherReady) { + otherReady.onClose.addListener(function() { + if (this.isReadyState('CLOSED', 'ERROR')) + return; + + this.closeError('wam.Error.ParentClosed', + [otherReady.closeReason, otherReady.closeValue]); + }.bind(this)); +}; + +wam.binding.Ready.prototype.reset = function() { + this.assertReadyState('WAIT', 'CLOSED', 'ERROR'); + this.readyState = wam.binding.Ready.state['WAIT']; +}; + +wam.binding.Ready.prototype.ready = function(value) { + this.assertReadyState('WAIT'); + this.onReady(value); +}; + +wam.binding.Ready.prototype.closeOk = function(value) { + this.assertReadyState('READY'); + this.onClose('ok', value); +}; + +wam.binding.Ready.prototype.closeErrorValue = function(value) { + this.assertReadyState('READY', 'WAIT'); + this.onClose('error', value); +}; + +wam.binding.Ready.prototype.closeError = function(name, arg) { + this.closeErrorValue(wam.mkerr(name, arg)); +}; +// SOURCE FILE: wam/js/wam_binding_fs.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * Namespace for bindings related to wam.FileSystem. + */ +wam.binding.fs = {}; + +wam.errorManager.defineErrors +( + ['wam.FileSystem.Error.BadOrMissingArgument', ['name', 'expected']], + ['wam.FileSystem.Error.BeginningOfFile', []], + ['wam.FileSystem.Error.EndOfFile', []], + ['wam.FileSystem.Error.UnexpectedArgvType', ['expected']], + ['wam.FileSystem.Error.Interrupt', []], + ['wam.FileSystem.Error.InvalidPath', ['path']], + ['wam.FileSystem.Error.NotFound', ['path']], + ['wam.FileSystem.Error.NotExecutable', ['path']], + ['wam.FileSystem.Error.NotListable', ['path']], + ['wam.FileSystem.Error.NotOpenable', ['path']], + ['wam.FileSystem.Error.OperationTimedOut', []], + ['wam.FileSystem.Error.OperationNotSupported', []], + ['wam.FileSystem.Error.PathExists', ['path']], + ['wam.FileSystem.Error.PermissionDenied', []], + ['wam.FileSystem.Error.ReadError', ['diagnostic']], + ['wam.FileSystem.Error.ReadyTimeout', []], + ['wam.FileSystem.Error.ResultTooLarge', ['maxSize', 'resultSize']], + ['wam.FileSystem.Error.RuntimeError', ['diagnostic']] +); + +wam.binding.fs.baseName = function(path) { + var lastSlash = path.lastIndexOf('/'); + return path.substr(lastSlash + 1); +}; + +wam.binding.fs.dirName = function(path) { + var lastSlash = path.lastIndexOf('/'); + return path.substr(0, lastSlash); +}; + +wam.binding.fs.absPath = function(pwd, path) { + if (path.substr(0, 1) != '/') + path = pwd + path; + + return '/' + wam.binding.fs.normalizePath(path); +}; + +wam.binding.fs.normalizePath = function(path) { + return wam.binding.fs.splitPath(path).join('/'); +}; + +wam.binding.fs.splitPath = function(path) { + var rv = []; + var ary = path.split(/\//g); + for (var i = 0; i < ary.length; i++) { + if (!ary[i] || ary[i] == '.') + continue; + + if (ary[i] == '..') { + rv.pop(); + } else { + rv.push(ary[i]); + } + } + + return rv; +}; +// SOURCE FILE: wam/js/wam_binding_fs_file_system.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * A binding that represents a wam file system. + * + * This is the idealized interface to all wam file systems. Specific + * implementations bind with this by subscribing to events. + */ +wam.binding.fs.FileSystem = function() { + // We're a subclass of a ready binding. This file system is not usable + // until the readyStatus becomes 'ready'. If the readStatus is closed, + // the file system is no longer valid. + wam.binding.Ready.call(this); + + /** + * A client is trying to stat a file. + */ + this.onStat = new wam.Event(); + + /** + * A client is trying to unlink a file. + */ + this.onUnlink = new wam.Event(); + + /** + * A client is trying to list the contents of a directory. + */ + this.onList = new wam.Event(); + + /** + * A client is trying create an execute context. + */ + this.onExecuteContextCreated = new wam.Event(); + + /** + * A client is trying to create an open context. + */ + this.onOpenContextCreated = new wam.Event(); +}; + +wam.binding.fs.FileSystem.prototype = Object.create( + wam.binding.Ready.prototype); + +/** + * Stat a file. + */ +wam.binding.fs.FileSystem.prototype.stat = function(arg, onSuccess, onError) { + this.assertReady(); + this.onStat({path: arg.path}, onSuccess, onError); +}; + +/** + * Unlink a file. + */ +wam.binding.fs.FileSystem.prototype.unlink = function(arg, onSuccess, onError) { + this.assertReady(); + this.onUnlink({path: arg.path}, onSuccess, onError); +}; + +/** + * List the contents of a directory. + */ +wam.binding.fs.FileSystem.prototype.list = function(arg, onSuccess, onError) { + this.assertReady(); + this.onList({path: arg.path}, onSuccess, onError); +}; + +/** + * Create an execute context associated with this file system. + */ +wam.binding.fs.FileSystem.prototype.createExecuteContext = function() { + this.assertReady(); + var executeContext = new wam.binding.fs.ExecuteContext(this); + executeContext.dependsOn(this); + this.onExecuteContextCreated(executeContext); + return executeContext; +}; + +/** + * Create an open context associated with this file system. + */ +wam.binding.fs.FileSystem.prototype.createOpenContext = function() { + this.assertReady(); + var openContext = new wam.binding.fs.OpenContext(this); + openContext.dependsOn(this); + this.onOpenContextCreated(openContext); + return openContext; +}; + +/** + * Copy a single file using the readFile/writeFile methods of this class. + * + * + */ +wam.binding.fs.FileSystem.prototype.copyFile = function( + sourcePath, targetPath, onSuccess, onError) { + this.readFile( + sourcePath, {}, {}, + function(result) { + this.writeFile( + targetPath, + {mode: {create: true, truncate: true}}, + {dataType: result.dataType, data: result.data}, + onSuccess, + onError); + }.bind(this), + onError); +}; + +/** + * Read the entire contents of a file. + * + * This is a utility method that creates an OpenContext, uses the read + * method to read in the entire file (by default) and then discards the + * open context. + * + * By default this will return the data in the dataType preferred by the + * file. You can request a specific dataType by including it in readArg. + * + * @param {string} path The path to read. + * @param {Object} openArg Additional arguments to pass to the + * OpenContext..open() call. + * @param {Object} readArg Additional arguments to pass to the + * OpenContext..read() call. + * @param {function(Object)} onSuccess The function to invoke with the read + * results. Object will have dataType and data properties as specified + * by OpenContext..read(). + * @param {function(Object)} onError The function to invoke if the open + * or read fail. Object will be a wam error value. + * + * @return {wam.binding.fs.OpenContext} The new OpenContext instance. You + * can attach your own listeners to this if you need to. + */ +wam.binding.fs.FileSystem.prototype.readFile = function( + path, openArg, readArg, onSuccess, onError) { + var ocx = this.createOpenContext(); + + ocx.onClose.addListener(function(value) { + if (!ocx.readyValue) + onError(ocx.closeValue); + }); + + ocx.onReady.addListener(function() { + ocx.read( + readArg, + function(result) { + ocx.closeOk(null); + onSuccess(result); + }, + function(value) { + ocx.closeOk(null); + onError(value); + }); + }); + + ocx.open(path, openArg); + return ocx; +}; + +/** + * Write the entire contents of a file. + * + * This is a utility method that creates an OpenContext, uses the write + * method to write the entire file (by default) and then discards the + * open context. + * + * @param {string} path The path to read. + * @param {Object} openArg Additional arguments to pass to the + * OpenContext..open() call. + * @param {Object} writeArg Additional arguments to pass to the + * OpenContext..write() call. + * @param {function(Object)} onSuccess The function to invoke if the write + * succeeds. Object will have dataType and data properties as specified + * by OpenContext..read(). + * @param {function(Object)} onError The function to invoke if the open + * or read fail. Object will be a wam error value. + * + * @return {wam.binding.fs.OpenContext} The new OpenContext instance. You + * can attach your own listeners to this if you need to. + */ +wam.binding.fs.FileSystem.prototype.writeFile = function( + path, openArg, writeArg, onSuccess, onError) { + var ocx = this.createOpenContext(); + + ocx.onClose.addListener(function(value) { + if (!ocx.readyValue) + onError(ocx.closeValue); + }); + + ocx.onReady.addListener(function() { + ocx.write( + writeArg, + function(result) { + ocx.closeOk(null); + onSuccess(result); + }, + function(value) { + ocx.closeOk(null); + onError(value); + }); + }); + + if (!openArg) + openArg = {}; + + if (!openArg.mode) + openArg.mode = {}; + + openArg.mode.write = true; + + ocx.open(path, openArg); + return ocx; +}; +// SOURCE FILE: wam/js/wam_binding_fs_execute_context.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * A binding that represents a running executable on a wam.binding.fs. + * FileSystem. + * + * You should only create an ExecuteContext by calling an instance of + * wam.binding.fs.FileSystem..createExecuteContext(). + * + * @param {wam.binding.fs.FileSystem} The parent file system. + */ +wam.binding.fs.ExecuteContext = function(fileSystem) { + // We're a 'subclass' of wam.binding.Ready. + wam.binding.Ready.call(this); + + /** + * Parent file system. + */ + this.fileSystem = fileSystem; + + // If the parent file system is closed, we close too. + this.dependsOn(this.fileSystem); + + /** + * The wam.binding.fs.ExecuteContext we're currently calling out to, if any. + * + * See ..setCallee(). + */ + this.callee = null; + + /** + * Called by the execute() method of this instance. + */ + this.onExecute = new wam.Event(function() { + this.didExecute_ = true; + }.bind(this)); + + /** + * Events sourced by this binding in addition to the inherited events from + * wam.binding.Ready. + * + * These are raised after the corresponding method is invoked. For example, + * wam.binding.fs.signal(...) raises the onSignal event. + */ + this.onSignal = new wam.Event(); + this.onStdOut = new wam.Event(); + this.onStdErr = new wam.Event(); + this.onStdIn = new wam.Event(); + this.onTTYChange = new wam.Event(); + this.onTTYRequest = new wam.Event(); + + // An indication that the execute() method was called. + this.didExecute_ = false; + + /** + * The path provided to the execute() method of this ExecuteContext. + */ + this.path = null; + /** + * The arg provided to the execute() method of this ExecuteContext. + */ + this.arg = null; + + // The environtment variables for this execute context. + this.env_ = {}; + + // The tty state for this execute context. + this.tty_ = { + isatty: false, + rows: 0, + columns: 0, + interrupt: String.fromCharCode('C'.charCodeAt(0) - 64) // ^C + }; +}; + +wam.binding.fs.ExecuteContext.prototype = Object.create( + wam.binding.Ready.prototype); + +/** + * Set the given ExecuteContext as the callee for this instance. + * + * When calling another executable, incoming calls and outbound events are + * wired up to the caller as appropriate. This instance will not receive + * the stdio-like events while a call is in progress. The onSignal event, + * however, is delivered to this instance even when a call is in progress. + * + * If the callee is closed, events are rerouted back to this instance and the + * callee instance property is set to null. + */ +wam.binding.fs.ExecuteContext.prototype.setCallee = function(executeContext) { + if (this.callee) + throw new Error('Still waiting for call:', this.callee); + + this.callee = executeContext; + var previousInterruptChar = this.tty_.interrupt; + + var onClose = function() { + this.callee.onClose.removeListener(onClose); + this.callee.onStdOut.removeListener(this.onStdOut); + this.callee.onStdOut.removeListener(this.onStdErr); + this.callee.onTTYRequest.removeListener(this.onTTYRequest); + this.callee = null; + + if (this.tty_.interrupt != previousInterruptChar) + this.requestTTY({interrupt: previousInterruptChar}); + }.bind(this); + + this.callee.onClose.addListener(onClose); + this.callee.onStdOut.addListener(this.onStdOut); + this.callee.onStdErr.addListener(this.onStdErr); + this.callee.onTTYRequest.addListener(this.onTTYRequest); + this.callee.setEnvs(this.env_); + this.callee.setTTY(this.tty_); +}; + +/** + * Utility method to construct a new ExecuteContext, set it as the callee, and + * execute it with the given path and arg. + */ +wam.binding.fs.ExecuteContext.prototype.call = function(path, arg) { + this.setCallee(this.fileSystem.createExecuteContext()); + this.callee.execute(path, arg); + return this.callee; +}; + +/** + * Return a copy of the internal tty state. + */ +wam.binding.fs.ExecuteContext.prototype.getTTY = function() { + var rv = {}; + for (var key in this.tty_) { + rv[key] = this.tty_[key]; + } + + return rv; +}; + +/** + * Set the authoritative state of the tty. + * + * This should only be invoked in the direction of tty->executable. Calls in + * the reverse direction will only affect this instance and those derived (via + * setCallee) from it, and will be overwritten the next time the authoritative + * state changes. + * + * Executables should use requestTTY to request changes to the authoritative + * state. + * + * The tty state is an object with the following properties: + * + * tty { + * isatty: boolean, True if stdio-like methods are attached to a visual + * terminal. + * rows: integer, The number of rows in the tty. + * columns: integer, The number of columns in the tty. + * interrupt: string, The key used to raise an + * 'wam.FileSystem.Error.Interrupt' signal. + * } + * + * @param {Object} tty An object containing one or more of the properties + * described above. + */ +wam.binding.fs.ExecuteContext.prototype.setTTY = function(tty) { + this.assertReadyState('WAIT', 'READY'); + + if ('isatty' in tty) + this.tty_.isatty = !!tty.isatty; + if ('rows' in tty) + this.tty_.rows = tty.rows; + if ('columns' in tty) + this.tty_.columns = tty.columns; + + if (!this.tty_.rows || !this.tty_.columns) { + this.tty_.rows = 0; + this.tty_.columns = 0; + this.tty_.isatty = false; + } else { + this.tty_.isatty = true; + } + + if (tty.rows < 0 || tty.columns < 0) + throw new Error('Invalid tty size.'); + + if ('interrupt' in tty) + this.tty_.interrupt = tty.interrupt; + + this.onTTYChange(this.tty_); + + if (this.callee) + this.callee.setTTY(tty); +}; + +/** + * Request a change to the controlling tty. + * + * At the moment only the 'interrupt' property can be changed. + * + * @param {Object} tty An object containing a changeable property of the + * tty. + */ +wam.binding.fs.ExecuteContext.prototype.requestTTY = function(tty) { + this.assertReadyState('READY'); + + if (typeof tty.interrupt == 'string') + this.onTTYRequest({interrupt: tty.interrupt}); +}; + +/** + * Get a copy of the current environment variables. + */ +wam.binding.fs.ExecuteContext.prototype.getEnvs = function() { + var rv = {}; + for (var key in this.env_) { + rv[key] = this.env_[key]; + } + + return rv; +}; + +/** + * Get the value of the given environment variable, or the provided + * defaultValue if it is not set. + * + * @param {string} name + * @param {*} defaultValue + */ +wam.binding.fs.ExecuteContext.prototype.getEnv = function(name, defaultValue) { + if (this.env_.hasOwnProperty(name)) + return this.env_[name]; + + return defaultValue; +}; + +/** + * Overwrite the current environment. + * + * @param {Object} env + */ +wam.binding.fs.ExecuteContext.prototype.setEnvs = function(env) { + this.assertReadyState('WAIT', 'READY'); + for (var key in env) { + this.env_[key] = env[key]; + } +}; + +/** + * Set the given environment variable. + * + * @param {string} name + * @param {*} value + */ +wam.binding.fs.ExecuteContext.prototype.setEnv = function(name, value) { + this.assertReadyState('WAIT', 'READY'); + this.env_[name] = value; +}; + +/** + * Create a new open context using the wam.binding.fs.FileSystem for this + * execute context, bound to the lifetime of this context. + */ +wam.binding.fs.ExecuteContext.prototype.createOpenContext = function() { + var ocx = this.fileSystem.createOpenContext(); + ocx.dependsOn(this); + return ocx; +}; + +/** + * Same as wam.binding.fs.copyFile, except bound to the lifetime of this + * ExecuteContext. + */ +wam.binding.fs.ExecuteContext.prototype.copyFile = function( + sourcePath, targetPath, onSuccess, onError) { + this.readFile( + sourcePath, {}, {}, + function(result) { + this.writeFile( + targetPath, + {mode: {create: true, truncate: true}}, + {dataType: result.dataType, data: result.data}, + onSuccess, + onError); + }.bind(this), + onError); +}; + +/** + * Same as wam.binding.fs.readFile, except bound to the lifetime of this + * ExecuteContext. + */ +wam.binding.fs.ExecuteContext.prototype.readFile = function( + path, openArg, readArg, onSuccess, onError) { + var ocx = this.fileSystem.readFile( + path, openArg, readArg, onSuccess, onError); + ocx.dependsOn(this); + return ocx; +}; + +/** + * Same as wam.binding.fs.writeFile, except bound to the lifetime of this + * ExecuteContext. + */ +wam.binding.fs.ExecuteContext.prototype.writeFile = function( + path, openArg, writeArg, onSuccess, onError) { + var ocx = this.fileSystem.writeFile( + path, openArg, writeArg, onSuccess, onError); + ocx.dependsOn(this); + return ocx; +}; + +/** + * Attempt to execute the given path with the given argument. + * + * This can only be called once per OpenContext instance. + * + * This function attempts to execute a path. If the execute succeeds, the + * onReady event of this binding will fire and you're free to start + * communicating with the target process. + * + * When you're finished, call closeOk, closeError, or closeErrorValue to clean + * up the execution context. + * + * If the execute fails the context will be close with an 'error' reason. + * + * The onClose event of this binding will fire when the context is closed, + * regardless of which side of the context initiated the close. + * + * @param {string} The path to execute. + * @param {*} The arg to pass to the executable. + */ +wam.binding.fs.ExecuteContext.prototype.execute = function(path, arg) { + this.assertReadyState('WAIT'); + + if (this.didExecute_) + throw new Error('Already executed on this context'); + + this.path = path; + this.arg = arg; + + this.onExecute(); +}; + +/** + * Send a signal to the running executable. + * + * The only signal defined at this time has the name 'wam.FileSystem.Signal. + * Interrupt' and a null value. + * + * @param {name} + * @param {value} + */ +wam.binding.fs.ExecuteContext.prototype.signal = function(name, value) { + this.assertReady(); + if (this.callee) { + this.callee.closeError('wam.FileSystem.Error.Interrupt', []); + } else { + this.onSignal(name, value); + } +}; + +/** + * Send stdout from this executable. + * + * This is not restricted to string values. Recipients should filter out + * non-string values in their onStdOut handler if necessary. + * + * TODO(rginda): Add numeric argument onAck to support partial consumption. + * + * @param {*} value The value to send. + * @param {function()} opt_onAck The optional function to invoke when the + * recipient acknowledges receipt. + */ +wam.binding.fs.ExecuteContext.prototype.stdout = function(value, opt_onAck) { + if (!this.isReadyState('READY')) { + console.warn('Dropping stdout to closed execute context:', value); + return; + } + + this.onStdOut(value, opt_onAck); +}; + +/** + * Send stderr from this executable. + * + * This is not restricted to string values. Recipients should filter out + * non-string values in their onStdErr handler if necessary. + * + * TODO(rginda): Add numeric argument onAck to support partial consumption. + * + * @param {*} value The value to send. + * @param {function()} opt_onAck The optional function to invoke when the + * recipient acknowledges receipt. + */ +wam.binding.fs.ExecuteContext.prototype.stderr = function(value, opt_onAck) { + if (!this.isReadyState('READY')) { + console.warn('Dropping stderr to closed execute context:', value); + return; + } + + this.onStdErr(value, opt_onAck); +}; + +/** + * Send stdout to this executable. + * + * This is not restricted to string values. Recipients should filter out + * non-string values in their onStdIn handler if necessary. + * + * TODO(rginda): Add opt_onAck. + * + * @param {*} value The value to send. + */ +wam.binding.fs.ExecuteContext.prototype.stdin = function(value) { + this.assertReady(); + if (this.callee) { + this.callee.stdin(value); + } else { + this.onStdIn(value); + } +}; +// SOURCE FILE: wam/js/wam_binding_fs_open_context.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * A binding that represents an open file on a wam.binding.fs.FileSystem. + * + * You should only create an OpenContext by calling an instance of + * wam.binding.fs.FileSystem..createOpenContext(). + * + * @param {wam.binding.fs.FileSystem} The parent file system. + */ +wam.binding.fs.OpenContext = function(fileSystem) { + // We're a 'subclass' of wam.binding.Ready. + wam.binding.Ready.call(this); + + /** + * Parent file system. + */ + this.fileSystem = fileSystem; + + // If the parent file system is closed, we close too. + this.dependsOn(this.fileSystem); + + // When the open context is marked as ready is should include a wam.fs stat + // result for the target file. + this.onReady.addListener(function(value) { this.wamStat = value }.bind(this)); + + /** + * Events sourced by this binding in addition to the inherited events from + * wam.binding.Ready. + * + * These are raised after the corresponding method is invoked. For example, + * wam.binding.fs.open(...) raises the onOpen event. + */ + this.onOpen = new wam.Event(function() { this.didOpen_ = true }.bind(this)); + this.onSeek = new wam.Event(); + this.onRead = new wam.Event(); + this.onWrite = new wam.Event(); + + // An indication that the open() method was called. + this.didOpen_ = false; + + /** + * That path that this OpenContext was opened for. + */ + this.path = null; + /** + * The wam stat result we received when the file was opened. + */ + this.wamStat = null; + + this.mode = { + create: false, + exclusive: false, + truncate: false, + read: false, + write: false + }; +}; + +wam.binding.fs.OpenContext.prototype = Object.create( + wam.binding.Ready.prototype); + +/** + * List of acceptable values for the 'dataType' parameter used in stat and read + * operations. + */ +wam.binding.fs.OpenContext.dataTypes = [ + /** + * Not used in stat results. + * + * When a dataType of 'arraybuffer' is used on read and write requests, the + * data is expected to be an ArrayBuffer instance. + * + * NOTE(rginda): ArrayBuffer objects don't work over wam.transport. + * ChromePort, due to http://crbug.com/374454. + */ + 'arraybuffer', + + /** + * Not used in stat results. + * + * When used in read and write requests, the data will be a base64 encoded + * string. Note that decoding this value to a UTF8 string may result in + * invalid UTF8 sequences or data corruption. + */ + 'base64-string', + + /** + * In stat results, a dataType of 'blob' means that the file contains a set + * of random access bytes. + * + * When a dataType of 'blob' is used on a read request, the data is expected + * to be an instance of an opened Blob object. + * + * NOTE(rginda): Blobs can't cross origin over wam.transport.ChromePort. + * Need to test over HTML5 MessageChannels. + */ + 'blob', + + /** + * Not used in stat results. + * + * When used in read and write requests, the data will be a UTF-8 + * string. Note that if the underlying file contains sequences that cannot + * be encoded in UTF-8, the result may contain invalid sequences or may + * not match the actual contents of the file. + */ + 'utf8-string', + + /** + * In stat results, a dataType of 'value' means that the file contains a + * single value which can be of any type. + * + * When a dataType of 'value' is used on a read request, the results of + * the read will be the native type stored in the file. If the file + * natively stores a blob, the result will be a string. + */ + 'value', + ]; + +/** + * Open a file. + * + * This can only be called once per OpenContext instance. + * + * This function attempts to open a path. If the open succeeds, the onReady + * event of this binding will fire, and will include the wam 'stat' value + * for the target file. From there you can call the OpenContext seek, read, + * and write methods to operate on the target. When you're finished, call + * closeOk, closeError, or closeErrorValue to clean up the context. + * + * If the open fails, the onClose event of this binding will fire and will + * include a wam error value. + * + * The arg parameter should be an object. The only recognized property + * is 'mode', and may contain one or more of the following properties to + * override the default open mode. + * + * mode { + * create: false, True to create the file if it doesn't exist, + * exclusive: false, True to fail if create && file exists. + * truncate: false, True to empty the file after opening. + * read: true, True to enable read operations. + * write: false, True to enable write operations. + * } + * + * @param {string} path The path to open. + * @param {Object} arg The open arguments. + */ +wam.binding.fs.OpenContext.prototype.open = function(path, arg) { + this.assertReadyState('WAIT'); + + if (this.didOpen_) + throw new Error('Already opened on this context'); + + this.path = path; + if (arg && arg.mode && typeof arg.mode == 'object') { + this.mode.create = !!arg.mode.create; + this.mode.exclusive = !!arg.mode.exclusive; + this.mode.truncate = !!arg.mode.truncate; + this.mode.read = !!arg.mode.read; + this.mode.write = !!arg.mode.write; + } else { + this.mode.read = true; + } + + this.onOpen(); +}; + +/** + * Sanity check an inbound arguments. + * + * @param {Object} arg The arguments object to check. + * @param {function(wam.Error)} onError the callback to invoke if the + * check fails. + * + * @return {boolean} True if the arg object is valid, false if it failed. + */ +wam.binding.fs.OpenContext.prototype.checkArg_ = function(arg, onError) { + // If there's an offset, it must be a number. + if ('offset' in arg && typeof arg.offset != 'number') { + wam.async(onError, [null, 'wam.FileSystem.Error.BadOrMissingArgument', + ['offset', 'number']]); + return false; + } + + // If there's a count, it must be a number. + if ('count' in arg && typeof arg.count != 'number') { + wam.async(onError, [null, 'wam.FileSystem.Error.BadOrMissingArgument', + ['count', 'number']]); + return false; + } + + // If there's a whence, it's got to match this regexp. + if ('whence' in arg && !/^(begin|current|end)$/.test(arg.whence)) { + wam.async(onError, [null, 'wam.FileSystem.Error.BadOrMissingArgument', + ['whence', '(begin | current | end)']]); + return false; + } + + // If there's a whence, there's got to be an offset. + if (arg.whence && !('offset' in arg)) { + wam.async(onError, [null, 'wam.FileSystem.Error.BadOrMissingArgument', + ['offset', 'number']]); + return false; + } + + // If there's an offset, there's got to be a whence. + if (('offset' in arg) && !arg.whence) { + wam.async(onError, [null, 'wam.FileSystem.Error.BadOrMissingArgument', + ['whence', '(begin | current | end)']]); + return false; + } + + // If there's a dataType, it's got to be valid. + if ('dataType' in arg && + wam.binding.fs.OpenContext.dataTypes.indexOf(arg.dataType) == -1) { + wam.async(onError, + [null, 'wam.FileSystem.Error.BadOrMissingArgument', + ['dataType', + '(' + wam.binding.fs.OpenContext.dataTypes.join(' | ') + ')']]); + return false; + } + + return true; +}; + +/** + * Seek to a new position in the file. + * + * The arg object should be an object with the following properties: + * + * arg { + * offset: 0, An integer position to seek to. + * whence: ('begin', 'current', 'end'), A string specifying the origin of + * the seek. + * } + * + * @param {Object} arg The seek arg. + * @param {function()} onSuccess The callback to invoke if the seek succeeds. + * @param {function(wam.Error)} onError The callback to invoke if the seek + * fails. + */ +wam.binding.fs.OpenContext.prototype.seek = function(arg, onSuccess, onError) { + this.assertReady(); + + if (!this.checkArg_(arg, onError)) + return; + + this.onRead(arg, onSuccess, onError); +}; + +/** + * Read from the file. + * + * The arg object should be an object with the following properties: + * + * arg { + * offset: 0, An integer position to seek to before reading. + * whence: ('begin', 'current', 'end'), A string specifying the origin of + * the seek. + * dataType: The data type you would prefer to receive. Mus be one of + * wam.binding.fs.OpenContext.dataTypes. If the target cannot provide + * the requested format it should fail the read. If you leave this + * unspecified the target will choose a dataType. + * } + * + * @param {Object} arg The read arg. + * @param {function()} onSuccess The callback to invoke if the read succeeds. + * @param {function(wam.Error)} onError The callback to invoke if the read + * fails. + */ +wam.binding.fs.OpenContext.prototype.read = function(arg, onSuccess, onError) { + this.assertReady(); + + if (!this.mode.read) { + wam.async(onError, [null, 'wam.FileSystem.Error.OperationNotSupported', + []]); + return; + } + + if (!this.checkArg_(arg, onError)) + return; + + this.onRead(arg, onSuccess, onError); +}; + +/** + * Write to a file. + * + * The arg object should be an object with the following properties: + * + * arg { + * offset: 0, An integer position to seek to before writing. + * whence: ('begin', 'current', 'end'), A string specifying the origin of + * the seek. + * data: The data you want to write. + * dataType: The type of data you're providing. Must be one of + * wam.binding.fs.OpenContext.dataTypes. If the 'data' argument is an + * instance of a Blob or ArrayBuffer instance, this argument has no + * effect. + * } + * + * @param {Object} arg The write arg. + * @param {function()} onSuccess The callback to invoke if the write succeeds. + * @param {function(wam.Error)} onError The callback to invoke if the write + * fails. + */ +wam.binding.fs.OpenContext.prototype.write = function(arg, onSuccess, onError) { + this.assertReady(); + + if (!this.mode.write) { + wam.async(onError, + [null, 'wam.FileSystem.Error.OperationNotSupported', []]); + return; + } + + if (!this.checkArg_(arg, onError)) + return; + + this.onWrite(arg, onSuccess, onError); +}; +// SOURCE FILE: wam/js/wam_remote_ready.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * Request/Response classes to marshal a wam.binding.Ready over a wam channel. + */ +wam.remote.ready = {}; + +wam.remote.ready.Request = function(opt_readyBinding) { + /** + * The binding we'll use to communicate ready state. + */ + this.readyBinding = opt_readyBinding || new wam.binding.Ready(); + this.readyBinding.onClose.addListener(this.onReadyBindingClose_.bind(this)); + + /** + * Fired for replies to outMessage. + * + * This will not fire for the initial 'ready' message, only for the subsequent + * messages. + */ + this.onMessage = new wam.Event(); + + /** + * The message we're sending that expects a 'ready' reply. + */ + this.outMessage = null; + + /** + * The 'ready' reply message from the remote end. + */ + this.inReady = null; + + /** + * The final message received from the remote end. + */ + this.inFinal = null; + + /** + * Messages we've sent that are still awaiting replies. + * + * If the remote end closes out this ready context, we close these out with + * synthetic error replies to clean up. + */ + this.openOutMessages_ = {}; +}; + +/** + * Send the initial message that will request the 'ready' reply. + */ +wam.remote.ready.Request.prototype.sendRequest = function(outMessage) { + if (this.outMessage) + throw new Error('Request already sent.'); + + this.readyBinding.dependsOn(outMessage.channel.readyBinding); + this.outMessage = outMessage; + this.outMessage.onReply.addListener(this.onOutMessageReply_.bind(this)); + this.outMessage.send(); +}; + +wam.remote.ready.Request.prototype.createMessage = function(name, arg) { + this.readyBinding.assertReady(); + return this.inReady.createReply(name, arg); +}; + +/** + * Send a message to the other end of this context. + */ +wam.remote.ready.Request.prototype.send = function(name, arg, opt_onReply) { + this.readyBinding.assertReady(); + return this.inReady.reply(name, arg, opt_onReply); +}; + +wam.remote.ready.Request.prototype.onReadyBindingClose_ = function( + reason, value) { + if (this.outMessage && this.outMessage.isOpen) { + // Upon receipt of our 'ok'/'error' reply the remote end is required to + // acknowledge by sending a final reply to our outMessage (unless it has + // already done so). If the remotes final reply doesn't arrive within + // `wam.remote.closeTimeoutMs` milliseconds, we'll manually close the + // outMessage and log a warning. + if (this.outMessage.channel.readyBinding.isOpen) { + setTimeout(function() { + if (this.outMessage.isOpen) { + console.warn('Request: Manually closing "' + + this.outMessage.name + '" message.'); + this.outMessage.channel.injectMessage( + 'error', + wam.mkerr('wam.Error.CloseTimeout', []), + this.outMessage.subject); + } + }.bind(this), wam.remote.closeTimeoutMs); + } + } + + if (this.inReady && this.inReady.isOpen && + this.inReady.channel.readyBinding.isOpen) { + if (reason == 'ok') { + this.inReady.replyOk(null); + } else if (this.inFinal) { + this.inReady.replyError('wam.Error.ReadyAbort', [this.inFinal.arg]); + } else { + this.inReady.replyErrorValue(value); + } + } +}; + +/** + * Internal handler for replies to the outMessage. + */ +wam.remote.ready.Request.prototype.onOutMessageReply_ = function(inMessage) { + if (this.readyBinding.isReadyState('WAIT')) { + if (inMessage.name == 'ready') { + this.inReady = inMessage; + this.readyBinding.ready(inMessage.arg); + } else { + if (inMessage.name == 'error') { + this.readyBinding.closeErrorValue(inMessage.arg); + } else { + if (this.inReady.isOpen) { + this.readyBinding.closeError('wam.UnexpectedMessage', + [inMessage.name, inMessage.arg]); + } + } + } + } else if (this.readyBinding.isReadyState('READY')) { + this.onMessage(inMessage); + + if (inMessage.isFinalReply) { + if (inMessage.name == 'ok') { + this.readyBinding.closeOk(inMessage.arg); + } else { + this.readyBinding.closeErrorValue(inMessage.arg); + } + } + } +}; + +/** + * @param {lib.wam.InMessage} inMessage The inbound message that expects a + * 'ready' reply. + */ +wam.remote.ready.Response = function(inMessage, opt_readyBinding) { + /** + * The inbound message that expects a 'ready' reply. + */ + this.inMessage = inMessage; + + this.readyBinding = opt_readyBinding || new wam.binding.Ready(); + this.readyBinding.dependsOn(inMessage.channel.readyBinding); + this.readyBinding.onClose.addListener(this.onReadyBindingClose_.bind(this)); + this.readyBinding.onReady.addListener(this.onReadyBindingReady_.bind(this)); + + /** + * Our 'ready' reply. + */ + this.outReady = null; + + /** + * The final reply to our 'ready' message, saved for posterity. + */ + this.inFinal = null; + + /** + * Fired for replies to our 'ready' message, including any final 'ok' or + * 'error' reply. + */ + this.onMessage = new wam.Event(); +}; + +wam.remote.ready.Response.prototype.createMessage = function(name, arg) { + return this.inMessage.createReply(name, arg); +}; + +/** + * Send an arbitrary message to the other end of this context. + * + * You must call replyReady() once before sending additional messages. + */ +wam.remote.ready.Response.prototype.send = function(name, arg, opt_onReply) { + this.readyBinding.assertReady(); + return this.inMessage.reply(name, arg, opt_onReply); +}; + +/** + */ +wam.remote.ready.Response.prototype.onReadyBindingReady_ = function(value) { + this.outReady = this.inMessage.reply('ready', value, + this.onOutReadyReply_.bind(this)); +}; + +wam.remote.ready.Response.prototype.onReadyBindingClose_ = function( + reason, value) { + if (this.inMessage && this.inMessage.isOpen && + this.inMessage.channel.readyBinding.isOpen) { + if (reason == 'ok') { + this.inMessage.replyOk(value); + } else if (this.inFinal) { + this.inMessage.replyError('wam.Error.ReadyAbort', [this.inFinal.arg]); + } else { + this.inMessage.replyErrorValue(value); + } + } + + if (this.outReady && this.outReady.isOpen) { + if (this.outReady.channel.readyBinding.isOpen) { + setTimeout(function() { + if (this.outReady.isOpen) { + console.warn('Response: Manually closing "' + + this.outReady.name + '" message.'); + this.outReady.channel.injectMessage( + 'error', + lib.wam.errorManager.createMessageArg( + 'wam.Error.CloseTimeout', []), + this.outReady.subject); + } + }.bind(this), wam.remote.closeTimeoutMs); + } + } +}; + +wam.remote.ready.Response.prototype.onOutReadyReply_ = function(inMessage) { + if (this.readyBinding.isReadyState('READY')) { + this.onMessage(inMessage); + + if (inMessage.isFinalReply) { + this.inFinal = inMessage; + + if (inMessage.name == 'ok') { + this.readyBinding.closeOk(inMessage.arg); + } else { + this.readyBinding.closeErrorValue(inMessage.arg); + } + } + } +}; +// SOURCE FILE: wam/js/wam_remote_fs.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +wam.remote.fs = {}; + +wam.remote.fs.protocolName = 'x.wam.FileSystem'; + +/** + * Check to see of the given message is a wam.FileSystem handshake offer. + * + * @return {boolean} + */ +wam.remote.fs.testOffer = function(inMessage) { + if (!wam.changelogVersion) + throw new Error('Unknown changelog version'); + + if (!inMessage.arg.payload || typeof inMessage.arg.payload != 'object') + return false; + + var payload = inMessage.arg.payload; + if (payload.protocol != wam.remote.fs.protocolName) + return false; + + var pos = wam.changelogVersion.indexOf('.'); + var expectedMajor = wam.changelogVersion.substr(0, pos); + + pos = payload.version.indexOf('.'); + var offeredMajor = payload.version.substr(0, pos); + + return (expectedMajor == offeredMajor); +}; + +/** + * Context for a wam.FileSystem handshake request. + */ +wam.remote.fs.mount = function(channel) { + var handshakeRequest = new wam.remote.fs.handshake.Request(channel); + handshakeRequest.sendRequest(); + return handshakeRequest.fileSystem; +}; +// SOURCE FILE: wam/js/wam_remote_fs_handshake.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * Request/Response classes to connect a wam.binding.fs.FileSystem over a wam + * channel. + */ +wam.remote.fs.handshake = {}; + +/** + * Back a wam.binding.fs.FileSystem binding with a wam.FileSystem handshake + * request. + * + * Events sourced by the wam.binding.fs.FileSystem will become messages sent + * regarding a wam.FileSystem handshake on the given channel. + */ +wam.remote.fs.handshake.Request = function(channel) { + this.channel = channel; + + this.fileSystem = new wam.binding.fs.FileSystem(); + this.fileSystem.dependsOn(channel.readyBinding); + + this.fileSystem.onStat.addListener( + this.proxySingleMessage_.bind(this, 'stat')); + this.fileSystem.onUnlink.addListener( + this.proxySingleMessage_.bind(this, 'unlink')); + this.fileSystem.onList.addListener( + this.proxySingleMessage_.bind(this, 'list')); + + this.fileSystem.onExecuteContextCreated.addListener( + this.onExecuteContextCreated_.bind(this)); + this.fileSystem.onOpenContextCreated.addListener( + this.onOpenContextCreated_.bind(this)); + + this.readyRequest = new wam.remote.ready.Request(this.fileSystem); +}; + +/** + * Send the handshake offer message. + */ +wam.remote.fs.handshake.Request.prototype.sendRequest = function() { + if (!wam.changelogVersion) + throw new Error('Unknown changelog version'); + + var outMessage = this.channel.createHandshakeMessage + ({ protocol: wam.remote.fs.protocolName, + version: wam.changelogVersion + }); + + this.readyRequest.sendRequest(outMessage); +}; + +/** + * Proxy a wam.binding.fs.FileSystem event which maps to a single wam + * message that expects an immediate 'ok' or 'error' reply. + */ +wam.remote.fs.handshake.Request.prototype.proxySingleMessage_ = function( + name, arg, onSuccess, onError) { + this.readyRequest.send(name, {path: arg.path}, function(inMessage) { + if (inMessage.name == 'ok') { + onSuccess(inMessage.arg); + } else { + onError(inMessage.arg); + } + }); +}; + +/** + * Create a wam.remote.fs.execute.Request instance to handle the proxying of an + * execute context. + */ +wam.remote.fs.handshake.Request.prototype.onExecuteContextCreated_ = function( + executeContext) { + new wam.remote.fs.execute.Request(this, executeContext); +}; + +/** + * Create a wam.remote.fs.open.Request instance to handle the proxying of an + * open context. + */ +wam.remote.fs.handshake.Request.prototype.onOpenContextCreated_ = function( + openContext) { + new wam.remote.fs.open.Request(this, openContext); +}; + +/** + * Front a wam.binding.fs.FileSystem binding with a wam.FileSystem handshake + * response. + * + * Inbound messages to the handshake will raise events on the binding. + * + * @param {wam.InMessage} inMessage The inbound 'handshake' message. + * @param {wam.binding.fs.FileSystem} The binding to excite. + */ +wam.remote.fs.handshake.Response = function(inMessage, fileSystem) { + this.inMessage = inMessage; + this.fileSystem = fileSystem; + + this.readyResponse = new wam.remote.ready.Response(inMessage); + this.readyResponse.readyBinding.dependsOn(fileSystem); + this.readyResponse.onMessage.addListener(this.onMessage_.bind(this)); + + this.readyBinding = this.readyResponse.readyBinding; +}; + +/** + * Mark the binding as ready. + * + * @param {Object} value The ready value to provide. This may be an object + * with a 'name' property, suggesting a short name for this file system. + */ +wam.remote.fs.handshake.Response.prototype.sendReady = function(value) { + this.readyResponse.readyBinding.ready(value || null); +}; + +/** + * Handle inbound messages regarding the handshake. + */ +wam.remote.fs.handshake.Response.prototype.onMessage_ = function(inMessage) { + switch (inMessage.name) { + case 'stat': + this.fileSystem.stat( + inMessage.arg, + function(value) { inMessage.replyOk(value) }, + function(value) { inMessage.replyErrorValue(value) }); + break; + + case 'unlink': + this.fileSystem.unlink( + inMessage.arg, + function(value) { inMessage.replyOk(value) }, + function(value) { inMessage.replyErrorValue(value) }); + break; + + case 'list': + this.fileSystem.list( + inMessage.arg, + function(value) { inMessage.replyOk(value) }, + function(value) { inMessage.replyErrorValue(value) }); + break; + + case 'execute': + var executeContext = this.fileSystem.createExecuteContext(); + var executeReply = new wam.remote.fs.execute.Response( + inMessage, executeContext); + executeContext.setEnvs(inMessage.arg.execEnv); + executeContext.setTTY(inMessage.arg.tty || {}); + executeContext.execute(inMessage.arg.path, inMessage.arg.execArg); + break; + + case 'open': + var openContext = this.fileSystem.createOpenContext(); + var openReply = new wam.remote.fs.open.Response( + inMessage, openContext); + openContext.open(inMessage.arg.path, inMessage.arg.openArg); + break; + } +}; +// SOURCE FILE: wam/js/wam_remote_fs_execute.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * Request/Response classes to marshal an wam.binding.fs.ExecuteContext over a + * wam channel. + */ +wam.remote.fs.execute = {}; + +wam.remote.fs.execute.Request = function(handshakeRequest, executeContext) { + this.handshakeRequest = handshakeRequest; + this.executeContext = executeContext; + + this.readyRequest = new wam.remote.ready.Request(executeContext); + this.readyRequest.onMessage.addListener(this.onMessage_.bind(this)); + + executeContext.dependsOn(handshakeRequest.readyRequest.readyBinding); + executeContext.onExecute.addListener(this.onExecute_.bind(this)); + executeContext.onTTYChange.addListener(this.onTTYChange_.bind(this)); + executeContext.onStdIn.addListener(this.onStdIn_.bind(this)); + executeContext.onSignal.addListener(this.onSignal_.bind(this)); +}; + +wam.remote.fs.execute.Request.prototype.onExecute_ = function() { + var outMessage = this.handshakeRequest.readyRequest.createMessage( + 'execute', + {'path': this.executeContext.path, + 'execArg': this.executeContext.arg, + 'execEnv': this.executeContext.env_, + 'tty': this.executeContext.tty_ + }); + + this.readyRequest.sendRequest(outMessage); +}; + +wam.remote.fs.execute.Request.prototype.onStdIn_ = function(value) { + this.readyRequest.send('stdin', value); +}; + +wam.remote.fs.execute.Request.prototype.onSignal_ = function(name) { + this.readyRequest.send('signal', name); +}; + +wam.remote.fs.execute.Request.prototype.onTTYChange_ = function(tty) { + if (this.readyRequest.readyBinding.isOpen) + this.readyRequest.send('tty-change', tty); +}; + +wam.remote.fs.execute.Request.prototype.onMessage_ = function(inMessage) { + if (inMessage.name == 'stdout' || inMessage.name == 'stderr') { + var onAck = null; + if (inMessage.isOpen) { + onAck = function(value) { + inMessage.replyOk(typeof value == 'undefined' ? null : value); + }; + } + + if (inMessage.name == 'stdout') { + this.executeContext.stdout(inMessage.arg, onAck); + } else { + this.executeContext.stderr(inMessage.arg, onAck); + } + + } else if (inMessage.name == 'tty-request') { + this.executeContext.requestTTY(inMessage.arg); + + } else if (inMessage.name != 'stdout' && inMessage.name != 'stderr' && + !inMessage.isFinalReply) { + console.warn('remote execute request received unexpected message: ' + + inMessage.name, inMessage.arg); + if (inMessage.isOpen) { + inMessage.replyError('wam.UnexpectedMessage', + [inMessage.name, inMessage.arg]); + } + } +}; + +/** + * + */ +wam.remote.fs.execute.Response = function(inMessage, executeContext) { + this.inMessage = inMessage; + + this.executeContext = executeContext; + this.executeContext.onStdOut.addListener(this.onStdOut_, this); + this.executeContext.onStdErr.addListener(this.onStdErr_, this); + this.executeContext.onTTYRequest.addListener(this.onTTYRequest_.bind(this)); + + this.readyResponse = new wam.remote.ready.Response(inMessage, executeContext); + this.readyResponse.onMessage.addListener(this.onMessage_.bind(this)); +}; + +wam.remote.fs.execute.Response.prototype.onMessage_ = function(inMessage) { + switch (inMessage.name) { + case 'stdin': + var onAck = null; + if (inMessage.isOpen) { + onAck = function(value) { + inMessage.replyOk(typeof value == 'undefined' ? null : value); + }; + } + + this.executeContext.stdin(inMessage.arg, onAck); + break; + + case 'tty-change': + this.executeContext.setTTY(inMessage.arg); + break; + + case 'signal': + this.executeContext.signal(inMessage.arg.name, inMessage.arg.value); + break; + } +}; + +wam.remote.fs.execute.Response.prototype.onTTYRequest_ = function(value) { + this.readyResponse.send('tty-request', value); +}; + +wam.remote.fs.execute.Response.prototype.onStdOut_ = function(value, onAck) { + this.readyResponse.send('stdout', value, + (onAck ? + function(inMessage) { onAck(inMessage.arg) } : + null)); +}; + +wam.remote.fs.execute.Response.prototype.onStdErr_ = function(value, onAck) { + this.readyResponse.send('stderr', value, + (onAck ? + function(inMessage) { onAck(inMessage.arg) } : + null)); +}; +// SOURCE FILE: wam/js/wam_remote_fs_open.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * Request/Response classes to marshal an wam.binding.fs.OpenContext over a + * wam channel. + */ +wam.remote.fs.open = {}; + +/** + * Install event listeners on the supplied wam.binding.fs.OpenContext so that + * requests for open/seek/read/write are send over the an established + * wam.remote.fs.handshake.Request. + * + * @param {wam.remote.fs.handshake.Request} handshakeRequest An established + * 'wam.FileSystem' handshake which should service the open context. + * @param {wam.binding.fs.OpenContext} openContext + */ +wam.remote.fs.open.Request = function(handshakeRequest, openContext) { + this.handshakeRequest = handshakeRequest; + this.openContext = openContext; + + this.readyRequest = new wam.remote.ready.Request(openContext); + this.readyRequest.onMessage.addListener(this.onMessage_.bind(this)); + + openContext.dependsOn(handshakeRequest.readyRequest.readyBinding); + openContext.onOpen.addListener(this.onOpen_.bind(this)); + openContext.onSeek.addListener(this.onSeek_.bind(this)); + openContext.onRead.addListener(this.onRead_.bind(this)); + openContext.onWrite.addListener(this.onWrite_.bind(this)); +}; + +/** + * Handle the wam.binding.fs.OpenContext onOpen event. + */ +wam.remote.fs.open.Request.prototype.onOpen_ = function() { + var outMessage = this.handshakeRequest.readyRequest.createMessage( + 'open', + {'path': this.openContext.path, + 'openArg': { + mode: this.openContext.mode + } + }); + + this.readyRequest.sendRequest(outMessage); +}; + +/** + * Handle the wam.binding.fs.OpenContext onSeek event. + */ +wam.remote.fs.open.Request.prototype.onSeek_ = function( + value, onSuccess, onError) { + this.readyRequest.send('seek', value, function(inMessage) { + if (inMessage.name == 'ok') { + onSuccess(inMessage.arg); + } else { + onError(inMessage.arg); + } + }); +}; + +/** + * Handle the wam.binding.fs.OpenContext onRead event. + */ +wam.remote.fs.open.Request.prototype.onRead_ = function( + value, onSuccess, onError) { + this.readyRequest.send('read', value, function(inMessage) { + if (inMessage.name == 'ok') { + onSuccess(inMessage.arg); + } else { + onError(inMessage.arg); + } + }); +}; + +/** + * Handle the wam.binding.fs.OpenContext onWrite event. + */ +wam.remote.fs.open.Request.prototype.onWrite_ = function( + value, onSuccess, onError) { + this.readyRequest.send('write', value, function(inMessage) { + if (inMessage.name == 'ok') { + onSuccess(inMessage.arg); + } else { + onError(inMessage.arg); + } + }); +}; + +/** + * Handle inbound messages on the open context. + * + * We don't actually expect any of these at the moment, so we just make sure + * to close out any open messages with an error reply. + */ +wam.remote.fs.open.Request.prototype.onMessage_ = function(inMessage) { + if (!inMessage.isFinalReply) { + console.warn('remote open request received unexpected message: ' + + inMessage.name, inMessage.arg); + if (inMessage.isOpen) { + inMessage.replyError('wam.UnexpectedMessage', + [inMessage.name, inMessage.arg]); + } + } +}; + +/** + * Connect an inbound 'open' message to the given wam.binding.fs.OpenContext. + * + * When the OpenContext becomes ready, this will send the 'ready' reply. + * Additional 'seek', 'read', or 'write' replies to the 'ready' message will + * fire onSeek/Read/Write on the OpenContext binding. + * + * @param {wam.InMessage} inMessage An 'open' message received in the context + * of a wam.FileSystem handshake. + * @param {wam.binding.fs.OpenContext} openContext + */ +wam.remote.fs.open.Response = function(inMessage, openContext) { + this.inMessage = inMessage; + this.openContext = openContext; + this.readyResponse = new wam.remote.ready.Response(inMessage, openContext); + this.readyResponse.onMessage.addListener(this.onMessage_.bind(this)); +}; + +/** + * Route additional messages in the scope of this open context to the binding. + */ +wam.remote.fs.open.Response.prototype.onMessage_ = function(inMessage) { + var onSuccess = function(value) { inMessage.replyOk(value) }; + var onError = function(value) { inMessage.replyError(value) }; + + var checkOpen = function() { + if (inMessage.isOpen) + return true; + + console.log('Received "' + inMessage.name + '" message without a subject.'); + return false; + }; + + switch (inMessage.name) { + case 'seek': + if (!checkOpen()) + return; + + this.openContext.seek(inMessage.arg, onSuccess, onError); + break; + + case 'read': + if (!checkOpen()) + return; + + this.openContext.read(inMessage.arg, onSuccess, onError); + break; + + case 'write': + if (!checkOpen()) + return; + + this.openContext.write(inMessage.arg, onSuccess, onError); + break; + } +}; +// SOURCE FILE: wam/js/wam_jsfs.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +wam.jsfs = {}; + +/** + * Convert the given ArrayBuffer into a utf8 string. + * + * @param {ArrayBuffer} buffer. + * @return {string} + */ +wam.jsfs.arrayBufferToUTF8 = function(buffer) { + var view = new DataView(buffer); + var ary = []; + ary.length = buffer.byteLength; + for (var i = 0; i < buffer.byteLength; i++) { + ary[i] = String.fromCharCode(view.getUint8(i)); + } + + return ary.join(''); +}; +// SOURCE FILE: wam/js/wam_jsfs_file_system.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * An object that connects a wam.binding.fs.FileSystem to an in-memory file + * system composed of objects derived from wam.jsfs.Entry. + * + * See wam.jsfs.Directory, wam.jsfs.Executable, wam.jsfs.RemoteFileSystem, + * and wam.jsfs.dom.FileSystem for examples of entries that can be used + * with one of these. + * + * @param {wam.jsfs.Directory} opt_rootDirectory An optional directory instance + * to use as the root. + */ +wam.jsfs.FileSystem = function(opt_rootDirectory) { + this.rootDirectory_ = opt_rootDirectory || new wam.jsfs.Directory(); + this.defaultBinding = new wam.binding.fs.FileSystem(); + this.addBinding(this.defaultBinding); + this.defaultBinding.ready(); +}; + +/** + * Connect a file system binding to this file system implementation. + * + * We'll subscribe to events on the binding and provide the implementation for + * stat, unlink, list, execute, and open related functionality. + * + * @param {wam.binding.fs.FileSystem} binding + */ +wam.jsfs.FileSystem.prototype.addBinding = function(binding) { + binding.onStat.addListener(this.onStat_, this); + binding.onUnlink.addListener(this.onUnlink_, this); + binding.onList.addListener(this.onList_, this); + binding.onExecuteContextCreated.addListener( + this.onExecuteContextCreated_, this); + binding.onOpenContextCreated.addListener( + this.onOpenContextCreated_, this); + + binding.onClose.addListener(this.removeBinding.bind(this, binding)); +}; + +/** + * Remove a binding. + * + * @param {wam.binding.fs.FileSystem} binding + */ +wam.jsfs.FileSystem.prototype.removeBinding = function(binding) { + binding.onStat.removeListener(this.onStat_, this); + binding.onUnlink.removeListener(this.onUnlink_, this); + binding.onList.removeListener(this.onStat_, this); + binding.onExecuteContextCreated.removeListener( + this.onExecuteContextCreated_, this); + binding.onOpenContextCreated.removeListener( + this.onOpenContextCreated_, this); +}; + +/** + * Publish this file system on the given wam.Channel. + * + * If the other end of the channel offers a 'wam.Filesystem' handshake, we'll + * accept it on behalf of this file system. + * + * @param {wam.Channel} channel The channel to publish on. + * @param {string} name A short name to identify this file system to the other + * party. This is sent to the other party when we accept their handshake + * offer. There is currently no provision for selecting a file system by + * name as part of the handshake offer. + */ +wam.jsfs.FileSystem.prototype.publishOn = function(channel, name) { + var readyValue = name ? {name: name} : null; + + channel.onHandshakeOffered.addListener(function(offerEvent) { + if (offerEvent.response || + !wam.remote.fs.testOffer(offerEvent.inMessage)) { + return; + } + + this.handshakeResponse = new wam.remote.fs.handshake.Response( + offerEvent.inMessage, this.defaultBinding); + + this.handshakeResponse.sendReady(readyValue); + offerEvent.response = this.handshakeResponse; + }.bind(this)); +}; + +/** + * Ensure that the given path exists. + * + * Any missing directories are created as wam.jsfs.Directory instances. + * The onSuccess handler will be passed the final directory instance. If + * an error occurs, the path may have been partially constructed. + */ +wam.jsfs.FileSystem.prototype.makePath = function( + path, onSuccess, onError) { + var makeNextPath = function(directoryEntry, pathList) { + if (pathList.length == 0) { + onSuccess(directoryEntry); + return; + } + + var childDir = new wam.jsfs.Directory(); + directoryEntry.addEntry(pathList.shift(), + childDir, + makeNextPath.bind(null, childDir, pathList), + onError); + }; + + this.partialResolve + (path, + function (prefixList, pathList, resolvedEntry) { + if (!resolvedEntry) { + onError(wam.mkerr('wam.FileSystem.Error.NotFound', [path])); + return; + } + + if (!resolvedEntry.can('LIST')) { + onError(wam.mkerr('wam.FileSystem.Error.NotListable', + ['/' + prefixList.join('/')])); + return; + } + + if (pathList.length == 0) { + onSuccess(resolvedEntry); + return; + } + + makeNextPath(resolvedEntry, pathList); + }, + onError); +}; + +/** + * Call ..makePath sequentially, once for each path in pathList. + * + * If any path fails, stop the sequence and call onError. + * + * @param {Array} pathList The list of paths to create. + * @param {function()} onSuccess The function to invoke if all paths are created + * successfully. + * @param {function(wam.Error)} onError The function to invoke if a path fails. + * Remaning paths will not be created. + */ +wam.jsfs.FileSystem.prototype.makePaths = function( + pathList, onSuccess, onError) { + var makeNextPath = function(i, directoryEntry) { + if (i == pathList.length) { + onSuccess(directoryEntry); + return; + } + + this.makePath(pathList[i], makeNextPath.bind(null, i + 1), onError); + }.bind(this); + + makeNextPath(0, null); +}; + +/** + * Add a wam.jsfs.Entry subclass to the file system at the specified path. + * + * If necessary, wam.jsfs.Directory entries will be created for missing + * path elements. + * + * @param {string} path The path to the entry. + * @param {wam.jsfs.Entry} entry The wam.jsfs.Entry subclass to place at the + * path. + * @param {function()} onSuccess The function to invoke on success. + * @param {function(wam.Error)} onError The function to invoke on error. + */ +wam.jsfs.FileSystem.prototype.makeEntry = function( + path, entry, onSuccess, onError) { + var dirName = wam.binding.fs.dirName(path); + var baseName = wam.binding.fs.baseName(path); + var map = {}; + map[baseName] = entry; + this.makeEntries(dirName, map, onSuccess, onError); +}; + +/** + * Ensure that the given path exists, then add the given entries to it. + * + * @param {string} path The path to the parent directory for these entries. + * Will be created if necessary. + * @param {Object} entryMap A map of one or more {name: wam.jsfs.Entry}. + * @param {function()} onSuccess The function to invoke on success. + * @param {function(wam.Error)} onError The function to invoke on error. + */ +wam.jsfs.FileSystem.prototype.makeEntries = function( + path, entryMap, onSuccess, onError) { + + var entryNames = Object.keys(entryMap); + var makeNextEntry = function(directoryEntry) { + if (entryNames.length == 0) { + onSuccess(directoryEntry); + return; + } + + var name = entryNames.shift() + directoryEntry.addEntry(name, entryMap[name], + makeNextEntry.bind(null, directoryEntry), + onError); + }; + + this.makePath(path, makeNextEntry, onError); +}; + +/** + * Resolve the given path as far as possible. + * + * The success callback will receive three arguments: + * prefixList - An array of path names that were successfully resolved. + * pathList - The remaining path names, starting with the first that could not + * be found. + * entry - The entry instance that represents the final element of prefixList. + * This is not guaranteed to be a directory entry. + * + * If the partialResolve succeeds, it means that all of the path elements on the + * prefixList were found, and the elements on the pathList are yet-to-be + * resolved. The entry is the final wam.jsfs.Entry that was resolved. + * + * The meaning of this success depends on the context. If the resolved Entry + * can 'FORWARD', then this isn't necessarily a completed success or failure + * yet. + * + * @param {string} path The path to resolve. + * @param {function(Array, Array, wam.jsfs.Entry)} The function to invoke on + * success. + * @param {function(wam.Error)} onError The function to invoke on error. + */ +wam.jsfs.FileSystem.prototype.partialResolve = function( + path, onSuccess, onError) { + if (!onSuccess || !onError) + throw new Error('Missing onSuccess or onError'); + + if (!path || path == '/') { + wam.async(onSuccess, [null, [], [], this.rootDirectory_]); + return; + } + + var ary = path.match(/^\/?([^/]+)(.*)/); + if (!ary) { + wam.async(onError, + [null, wam.mkerr('wam.FileSystem.Error.InvalidPath', [path])]); + return; + } + + if (path.substr(0, 1) == '/') + path = path.substr(1); + + this.rootDirectory_.partialResolve( + [], wam.binding.fs.splitPath(path), + onSuccess, onError); +}; + +/** + * Handle the onStat event for a wam.binding.fs.FileSystem. + */ +wam.jsfs.FileSystem.prototype.onStat_ = function(arg, onSuccess, onError) { + if (typeof arg.path != 'string') { + console.error('Missing argument: path'); + wam.async(onError, + [null, wam.mkerr('wam.Error.MissingArgument', ['path'])]); + return; + } + + var onPartialResolve = function(prefixList, pathList, entry) { + if (entry.can('FORWARD')) { + entry.forwardStat + ({fullPath: arg.path, forwardPath: pathList.join('/')}, + onSuccess, onError); + return; + } + + if (pathList.length) { + onError(wam.mkerr('wam.FileSystem.Error.NotFound', [arg.path])); + return; + } + + entry.getStat(onSuccess, onError); + }; + + this.partialResolve(arg.path, onPartialResolve, onError); +}; + +/** + * Handle the onUnlink event for a wam.binding.fs.FileSystem. + */ +wam.jsfs.FileSystem.prototype.onUnlink_ = function(arg, onSuccess, onError) { + if (typeof arg.path != 'string') { + console.error('Missing argument: path'); + wam.async(onError, + [null, wam.mkerr('wam.Error.MissingArgument', ['path'])]); + return; + } + + var onPartialResolve = function(prefixList, pathList, entry) { + if (entry.can('FORWARD')) { + entry.forwardUnlink + ({fullPath: arg.path, forwardPath: pathList.join('/') + '/' + targetName}, + onSuccess, onError); + return; + } + + if (pathList.length) { + onError(wam.mkerr('wam.FileSystem.Error.NotFound', [parentPath])); + return; + } + + if (!entry.can('LIST')) { + onError(wam.mkerr('wam.FileSystem.Error.NotListable', [parentPath])); + return; + } + + entry.doUnlink(targetName, onSuccess, onError); + }; + + var parentPath = wam.binding.fs.dirName(arg.path); + var targetName = wam.binding.fs.baseName(arg.path); + + this.partialResolve(parentPath, onPartialResolve, onError); +}; + +/** + * Handle the onList event for a wam.binding.fs.FileSystem. + */ +wam.jsfs.FileSystem.prototype.onList_ = function(arg, onSuccess, onError) { + if (!onSuccess || !onError) + throw new Error('Missing callback', onSuccess, onError); + + if (typeof arg.path != 'string') { + console.error('Missing argument: path'); + wam.async(onError, + [null, wam.mkerr('wam.FileSystem.Error.BadOrMissingArgument', + ['path'])]); + return; + } + + var onPartialResolve = function(prefixList, pathList, entry) { + if (entry.can('FORWARD')) { + entry.forwardList + ({fullPath: arg.path, forwardPath: pathList.join('/')}, + onSuccess, onError); + return; + } + + if (pathList.length) { + onError(wam.mkerr('wam.FileSystem.Error.NotFound', [arg.path])); + return; + } + + if (!entry.can('LIST')) { + onError(wam.mkerr('wam.FileSystem.Error.NotListable', [arg.path])); + return; + } + + entry.listEntryStats(onSuccess); + }; + + this.partialResolve(arg.path, onPartialResolve, onError); +}; + +/** + * Handle the onExecuteContextCreated event for a wam.binding.fs.FileSystem. + */ +wam.jsfs.FileSystem.prototype.onExecuteContextCreated_ = function( + executeContext) { + new wam.jsfs.ExecuteContext(this, executeContext); +}; + +/** + * Handle the onOpenContextCreated event for a wam.binding.fs.FileSystem. + */ +wam.jsfs.FileSystem.prototype.onOpenContextCreated_ = function(openContext) { + new wam.jsfs.OpenContext(this, openContext); +}; +// SOURCE FILE: wam/js/wam_jsfs_entry.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +wam.jsfs.Entry = function() {}; + +/** + * List of operation types that may be supported by a filesystem entry and the + * methods they imply. + * + * All entries must support: + * + * - getStat(onSuccess, onError) + * + * Listable entries must support: + * + * - addEntry(name, entry, onSuccess, onError) + * - listEntryStats(onSuccess) + * - partialResolve(prefixList, pathList, onSuccess, onError) + * - doUnlink(name, onSuccess, onError) + * + * Forwardable entries must support: + * + * - forwardExecute(arg) + * @param {Object} arg The forward argument. Contains 'executeContext' and + * 'forwardPath' properties for the local executeContext and target file + * relative to the containing file system, and the forwarded file system. + * + * - forwardList(arg, onSuccess, onError) + * @param {Object} arg The forward argument. Contains 'fullPath' and + * 'forwardPath' properties locating the target file relative to the + * containing file system, and the forwarded file system. + * @param {function(Object)} onSuccess The function to invoke with the wam + * 'list' result if the call succeeds. + * @param {function(wam.Error)} onError + * + * - forwardOpen(arg) + * @param {Object} arg The forward argument. Contains 'openContext' and + * 'forwardPath' properties for the local wam.binding.fs.OpenContext and + * target file relative to the containing file system, and the forwarded + * file system. + * + * - forwardStat(arg, onSuccess, onError) + * @param {Object} arg The forward argument. Contains 'fullPath' and + * 'forwardPath' properties locating the target file relative to the + * containing file system, and the forwarded file system. + * @param {function(Object)} onSuccess The function to invoke with the wam + * 'stat' result if the call succeeds. + * @param {function(wam.Error)} onError + * + * - forwardUnlink(arg, onSuccess, onError) + * @param {Object} arg The forward argument. Contains 'fullPath' and + * 'forwardPath' properties locating the target file relative to the + * containing file system, and the forwarded file system. + * @param {function(Object)} onSuccess The function to invoke with the wam + * 'unlink' result if the call succeeds. + * @param {function(wam.Error)} onError + */ +wam.jsfs.Entry.ability = { + 'LIST': ['addEntry', 'getStat', 'listEntryStats', 'partialResolve', + 'doUnlink'], + 'OPEN': ['getStat'], + 'EXECUTE': ['getStat'], + 'FORWARD': ['forwardExecute', 'forwardList', 'forwardOpen', + 'forwardStat', 'forwardUnlink', 'getStat'] +}; + +/** + * Create a prototype object to use for a wam.jsfs.Entry subclass, mark it as + * supporting the given abilities, and verify that it has the required methods. + */ +wam.jsfs.Entry.subclass = function(abilities) { + var proto = Object.create(wam.jsfs.Entry.prototype); + proto.abilities = abilities; + wam.async(wam.jsfs.Entry.checkMethods_, [null, proto, (new Error()).stack]); + + return proto; +}; + +/** + * Check that a wam.jsfs.Entry subclass has all the methods it's supposed to + * have. + */ +wam.jsfs.Entry.checkMethods_ = function(proto, stack) { + var abilities = proto.abilities; + if (!abilities || abilities.length == 0) + throw new Error('Missing abilities property, ' + stack); + + if (abilities.indexOf('FORWARD') != -1) { + // Entries marked for FORWARD only need to support the FORWARD methods. + // Additional abilities only advise what can be forwarded. + abilities = ['FORWARD']; + } + + var checkMethods = function(opname, nameList) { + for (var i = 0; i < nameList.length; i++) { + if (typeof proto[nameList[i]] != 'function') + throw new Error('Missing ' + opname + ' method: ' + nameList[i]); + } + }; + + for (var i = 0; i < abilities.length; i++) { + if (abilities[i] in wam.jsfs.Entry.ability) { + checkMethods(abilities[i], wam.jsfs.Entry.ability[abilities[i]]); + } else { + throw new Error('Unknown operation: ' + abilities[i]); + } + } +}; + +/** + * Check if this entry supports the given ability. + */ +wam.jsfs.Entry.prototype.can = function(name) { + return (this.abilities.indexOf(name) != -1); +}; +// SOURCE FILE: wam/js/wam_jsfs_remote_file_system.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * A jsfs.Entry subclass that proxies to a wam file system connected via a + * wam.Channel. + * + * @param {wam.Channel} channel The channel hosting the wam file system. + */ +wam.jsfs.RemoteFileSystem = function(channel) { + wam.jsfs.Entry.call(this); + + this.channel = channel; + this.channel.readyBinding.onReady.addListener(this.onChannelReady_, this); + + this.remoteName = null; + + this.handshakeRequest_ = null; + this.remoteFileSystem_ = null; + + this.pendingOperations_ = []; + + this.onReady = new wam.Event(); + this.onClose = new wam.Event(); + + if (this.channel.readyBinding.isReadyState('READY')) + this.offerHandshake(); +}; + +/** + * We're an Entry subclass that is able to FORWARD and LIST. + */ +wam.jsfs.RemoteFileSystem.prototype = wam.jsfs.Entry.subclass( + ['FORWARD', 'LIST']); + +/** + * Return a wam 'stat' value for the FileSystem itself. + * + * This is a jsfs.Entry method needed as part of the 'LIST' action. + */ +wam.jsfs.RemoteFileSystem.prototype.getStat = function(onSuccess, onError) { + var readyState = 'UNEDFINED'; + if (this.remoteFileSystem_) + readyState = this.remoteFileSystem_.readyState; + + wam.async(onSuccess, + [null, + {abilities: this.abilities, + state: readyState, + channel: this.channel.name, + source: 'wamfs' + }]); +}; + +/** + * Reconnect the wam.Channel if necessary, then offer a wam.FileSystem + * 'handshake' message. + * + * @param {function()} onSuccess + * @param {function(wam.Error)} onError + */ +wam.jsfs.RemoteFileSystem.prototype.connect = function(onSuccess, onError) { + if (this.remoteFileSystem_ && this.remoteFileSystem_.isReadyState('READY')) + throw new Error('Already connected'); + + this.pendingOperations_.push([onSuccess, onError]); + + if (this.remoteFileSystem_ && this.remoteFileSystem_.isReadyState('WAIT')) + return; + + if (this.channel.readyBinding.isReadyState('READY')) { + this.offerHandshake(); + } else { + this.channel.reconnect(); + } +}; + +/** + * Offer a wam.FileSystem 'handshake' message over the associated channel. + */ +wam.jsfs.RemoteFileSystem.prototype.offerHandshake = function() { + if (this.remoteFileSystem_) { + if (this.remoteFileSystem_.isReadyState('READY')) + throw new Error('Already ready.'); + + this.remoteFileSystem_.onReady.removeListener( + this.onFileSystemReady_, this); + } + + this.handshakeRequest_ = new wam.remote.fs.handshake.Request(this.channel); + this.remoteFileSystem_ = this.handshakeRequest_.fileSystem; + this.remoteFileSystem_.onReady.addListener(this.onFileSystemReady_, this); + this.remoteFileSystem_.onClose.addListener(this.onFileSystemClose_, this); + this.handshakeRequest_.sendRequest(); +}; + +/** + * Handle the onReady event from the channel's ready binding. + */ +wam.jsfs.RemoteFileSystem.prototype.onChannelReady_ = function() { + this.offerHandshake(); +}; + +/** + * Handle the onReady event from the handshake offer. + */ +wam.jsfs.RemoteFileSystem.prototype.onFileSystemReady_ = function(value) { + if (typeof value == 'object' && value.name) + this.remoteName = value.name; + + while (this.pendingOperations_.length) { + var onSuccess = this.pendingOperations_.shift()[0]; + onSuccess(); + } + + this.onReady(value); +}; + +/** + * Handle an onClose from the handshake offer. + */ +wam.jsfs.RemoteFileSystem.prototype.onFileSystemClose_ = function( + reason, value) { + this.remoteFileSystem_.onReady.removeListener(this.onFileSystemReady_, this); + this.remoteFileSystem_.onClose.removeListener(this.onFileSystemClose_, this); + + this.onClose(reason, value); + + this.handshakeRequest_ = null; + this.remoteFileSystem_ = null; + + if (reason == 'error') { + while (this.pendingOperations_.length) { + var onError = this.pendingOperations_.shift()[1]; + onError(); + } + } +}; + +/** + * If this FileSystem isn't ready, try to make it ready and queue the callback + * for later, otherwise call it right now. + * + * @param {function()} callback The function to invoke when the file system + * becomes ready. + * @param {function(wam.Error)} onError The function to invoke if the + * file system fails to become ready. + */ +wam.jsfs.RemoteFileSystem.prototype.doOrQueue_ = function(callback, onError) { + if (this.remoteFileSystem_ && this.remoteFileSystem_.isReadyState('READY')) { + callback(); + } else { + this.connect(callback, onError); + } +}; + +/** + * Forward a stat call to the file system. + * + * This is a jsfs.Entry method needed as part of the 'FORWARD' action. + */ +wam.jsfs.RemoteFileSystem.prototype.forwardStat = function( + arg, onSuccess, onError) { + this.doOrQueue_(function() { + this.remoteFileSystem_.stat({path: arg.forwardPath}, onSuccess, onError); + }.bind(this), onError); +}; + +/** + * Forward an unlink call to the file system. + * + * This is a jsfs.Entry method needed as part of the 'FORWARD' action. + */ +wam.jsfs.RemoteFileSystem.prototype.forwardUnlink = function( + arg, onSuccess, onError) { + this.doOrQueue_(function() { + this.remoteFileSystem_.unlink({path: arg.forwardPath}, + onSuccess, onError); + }.bind(this), + onError); +}; + +/** + * Forward a list call to the LocalFileSystem. + * + * This is a jsfs.Entry method needed as part of the 'FORWARD' action. + */ +wam.jsfs.RemoteFileSystem.prototype.forwardList = function( + arg, onSuccess, onError) { + this.doOrQueue_(function() { + this.remoteFileSystem_.list({path: arg.forwardPath}, onSuccess, onError); + }.bind(this), + onError); +}; + +/** + * Forward a wam 'execute' to this file system. + * + * This is a jsfs.Entry method needed as part of the 'FORWARD' action. + */ +wam.jsfs.RemoteFileSystem.prototype.forwardExecute = function(arg) { + this.doOrQueue_(function() { + arg.executeContext.path = arg.forwardPath; + var executeRequest = new wam.remote.fs.execute.Request( + this.handshakeRequest_, arg.executeContext); + executeRequest.onExecute_(); + }.bind(this), + function(value) { arg.executeContext.closeError(value) }); +}; + +/** + * Forward a wam 'open' to this file system. + * + * This is a jsfs.Entry method needed as part of the 'FORWARD' action. + */ +wam.jsfs.RemoteFileSystem.prototype.forwardOpen = function(arg) { + this.doOrQueue_(function() { + arg.openContext.path = arg.forwardPath; + var openRequest = new wam.remote.fs.open.Request( + this.handshakeRequest_, arg.openContext); + openRequest.onOpen_(); + }.bind(this), + function(value) { arg.openContext.closeError(value) }); +}; +// SOURCE FILE: wam/js/wam_jsfs_directory.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * A wam.jsfs.Entry subclass that represents a directory. + */ +wam.jsfs.Directory = function() { + wam.jsfs.Entry.call(this); + this.entries_ = {}; + this.mtime_ = 0; +}; + +/** + * We're an Entry subclass that is able to LIST. + */ +wam.jsfs.Directory.prototype = wam.jsfs.Entry.subclass(['LIST']); + +/** + * Add a new wam.jsfs.Entry to this directory. + * + * This is a jsfs.Entry method needed as part of the 'LIST' action. + * + * @param {string} name The name of the entry to add. + * @param {wam.jsfs.Entry} entry The Entry subclass to add. + * @param {function()} onSuccess + * @param {function(wam.Error)} onError + */ +wam.jsfs.Directory.prototype.addEntry = function( + name, entry, onSuccess, onError) { + if (!name) { + wam.async(onError, + [null, wam.mkerr('wam.FileSystem.Error.InvalidPath', [name])]); + return; + } + + if (name in this.entries_) { + wam.async(onError, + [null, wam.mkerr('wam.FileSystem.Error.FileExists', [name])]); + return; + } + + wam.async(function() { + this.entries_[name] = entry; + onSuccess(); + }.bind(this)); +}; + +/** + * Remove an entry from this directory. + * + * This is a jsfs.Entry method needed as part of the 'LIST' action. + * + * @param {string} name The name of the entry to remove. + * @param {function()} onSuccess + * @param {function(wam.Error)} onError + */ +wam.jsfs.Directory.prototype.doUnlink = function(name, onSuccess, onError) { + wam.async(function() { + if (name in this.entries_) { + delete this.entries_[name]; + onSuccess(null); + } else { + onError(wam.mkerror('wam.FileSystem.Error.NotFound', [name])); + } + }.bind(this)); +}; + +wam.jsfs.Directory.prototype.listEntryStats = function(onSuccess) { + var rv = {}; + + var statCount = Object.keys(this.entries_).length; + if (statCount == 0) + wam.async(onSuccess, [null, rv]); + + var onStat = function(name, stat) { + rv[name] = {stat: stat}; + if (--statCount == 0) + onSuccess(rv); + }; + + for (var key in this.entries_) { + this.entries_[key].getStat(onStat.bind(null, key), + onStat.bind(null, key, null)); + } +}; + +wam.jsfs.Directory.prototype.getStat = function(onSuccess, onError) { + wam.async(onSuccess, + [null, + { abilities: this.abilities, + count: Object.keys(this.entries_).length, + source: 'jsfs' + }]); +}; + +wam.jsfs.Directory.prototype.partialResolve = function( + prefixList, pathList, onSuccess, onError) { + var entry = this.entries_[pathList[0]]; + if (!entry) { + // The path doesn't exist past this point, signal our partial success. + wam.async(onSuccess, [null, prefixList, pathList, this]); + + } else { + prefixList.push(pathList.shift()); + + if (pathList.length == 0) { + // We've found the full path. + wam.async(onSuccess, [null, prefixList, pathList, entry]); + + } else if (entry.can('LIST') && !entry.can('FORWARD')) { + // We're not done, descend into a child directory to look for more. + entry.partialResolve(prefixList, pathList, onSuccess, onError); + } else { + // We found a non-directory entry, but there are still remaining path + // elements. We'll signal a partial success and let the caller decide + // if this is fatal or not. + wam.async(onSuccess, [null, prefixList, pathList, entry]); + } + } +}; +// SOURCE FILE: wam/js/wam_jsfs_execute_context.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +wam.jsfs.ExecuteContext = function(jsfsFileSystem, executeContextBinding) { + this.jsfsFileSystem = jsfsFileSystem; + this.executeContextBinding = executeContextBinding; + executeContextBinding.onExecute.addListener(this.onExecute_, this); +}; + +wam.jsfs.ExecuteContext.prototype.onExecute_ = function() { + var path = this.executeContextBinding.path; + + var onError = function(value) { + this.executeContextBinding.closeErrorValue(value); + }.bind(this); + + var onPartialResolve = function(prefixList, pathList, entry) { + if (entry.can('FORWARD')) { + entry.forwardExecute + ({executeContext: this.executeContextBinding, + forwardPath: pathList.join('/')}); + return; + } + + if (pathList.length) { + onError(wam.mkerr('wam.FileSystem.Error.NotFound', [path])); + return; + } + + if (!entry.can('EXECUTE')) { + onError(wam.mkerr('wam.FileSystem.Error.NotExecutable', [path])); + return; + } + + entry.execute(this.executeContextBinding, this); + }.bind(this); + + this.jsfsFileSystem.partialResolve(path, onPartialResolve, onError); +}; +// SOURCE FILE: wam/js/wam_jsfs_open_context.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +wam.jsfs.OpenContext = function(jsfsFileSystem, openContextBinding) { + this.jsfsFileSystem = jsfsFileSystem; + this.openContextBinding = openContextBinding; + openContextBinding.onOpen.addListener(this.onOpen_, this); +}; + +wam.jsfs.OpenContext.prototype.onOpen_ = function() { + var path = this.openContextBinding.path; + + var onError = function(value) { + this.openContextBinding.closeErrorValue(value); + }.bind(this); + + var onPartialResolve = function(prefixList, pathList, entry) { + if (entry.can('FORWARD')) { + entry.forwardOpen + ({openContext: this.openContextBinding, + forwardPath: pathList.join('/')}); + return; + } + + if (pathList.length) { + onError(wam.mkerr('wam.FileSystem.Error.NotFound', [path])); + return; + } + + if (!entry.can('OPEN')) { + onError(wam.mkerr('wam.FileSystem.Error.NotOpenable', [path])); + return; + } + + entry.open(this.openContextBinding, this); + }.bind(this); + + this.jsfsFileSystem.partialResolve(path, onPartialResolve, onError); +}; +// SOURCE FILE: wam/js/wam_jsfs_executable.js +// Copyright (c) 2013 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + */ +wam.jsfs.Executable = function(callback) { + wam.jsfs.Entry.call(this); + this.callback_ = callback; +}; + +wam.jsfs.Executable.prototype = wam.jsfs.Entry.subclass(['EXECUTE']); + +wam.jsfs.Executable.prototype.getStat = function(onSuccess, onError) { + wam.async(onSuccess, + [null, + { abilities: this.abilities, + source: 'jsfs'}]); +}; + +wam.jsfs.Executable.prototype.execute = function(executeContext, arg) { + this.callback_(executeContext); +}; +// SOURCE FILE: wam/js/wam_jsfs_dom.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * Namespace for stuff related to the DOM FileSystem<->jsfs proxy layer. + */ +wam.jsfs.dom = {}; + +/** + * Convert an HTML5 FileError object into an appropriate wam.FileSystem.Error + * value. + * + * This should be used for errors that were raised in the context of a + * FileEntry. + * + * @param {FileError} error + * @param {string} path The path that this error relates to. + */ +wam.jsfs.dom.convertFileError = function(error, path) { + if (error.name == 'TypeMismatchError') + return wam.mkerr('wam.FileSystem.Error.NotOpenable', [path]); + + if (error.name == 'NotFoundError') + return wam.mkerr('wam.FileSystem.Error.NotFound', [path]); + + if (error.name == 'PathExistsError') + return wam.mkerr('wam.FileSystem.Error.PathExists', [path]); + + return wam.mkerr('wam.FileSystem.Error.RuntimeError', [error.name]); +}; + +/** + * Convert an HTML5 FileError object into an appropriate wam.FileSystem.Error + * value. + * + * This should be used for errors that were raised in the context of a + * DirEntry. + * + * @param {FileError} error + * @param {string} path The path that this error relates to. + */ +wam.jsfs.dom.convertDirError = function(error, path) { + if (error.name == 'TypeMismatchError') + return wam.mkerr('wam.FileSystem.Error.NotListable', [path]); + + return wam.jsfs.dom.convertFileError(error); +}; + +/** + * Get an appropriate wam 'stat' value for the given HTML5 FileEntry or + * DirEntry object. + */ +wam.jsfs.dom.statEntry = function(entry, onSuccess, onError) { + var onMetadata = function(entry, metadata) { + if (entry.isFile) { + onSuccess({ + source: 'domfs', + abilities: ['OPEN'], + dataType: 'blob', + mtime: new Date(metadata.modificationTime).getTime(), + size: metadata.size + }); + } else { + onSuccess({ + source: 'domfs', + abilities: ['LIST'], + mtime: new Date(metadata.modificationTime).getTime(), + }); + } + }; + + if ('getMetadata' in entry) { + entry.getMetadata(onMetadata.bind(null, entry), onError); + } else { + onSuccess({abilities: [], source: 'domfs'}); + } +}; +// SOURCE FILE: wam/js/wam_jsfs_dom_file_system.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * A jsfs.Entry subclass that proxies to a DOM LocalFileSystem. + */ +wam.jsfs.dom.FileSystem = function(opt_capacity) { + wam.jsfs.Entry.call(this); + + this.capacity_ = opt_capacity || 16 * 1024 * 1024; + + this.domfs_ = null; + this.pendingOperations_ = []; + + this.readyBinding = new wam.binding.Ready(); + this.readyBinding.onReady.addListener(this.onBindingReady_, this); + this.readyBinding.onClose.addListener(this.onBindingClose_, this); + + var onFileSystemFound = function (fileSystem) { + this.domfs_ = fileSystem; + this.readyBinding.ready(); + }.bind(this); + + var onFileSystemError = function (error) { + console.log('Error getting html5 file system: ' + error); + this.readyBinding.closeError(wam.jsfs.dom.convertError(error)); + }.bind(this); + + var requestFS = window.requestFileSystem || window.webkitRequestFileSystem; + requestFS(window.PERSISTENT, this.capacity_, + onFileSystemFound, onFileSystemError); +}; + +/** + * We're an Entry subclass that is able to FORWARD and LIST. + */ +wam.jsfs.dom.FileSystem.prototype = + wam.jsfs.Entry.subclass(['FORWARD', 'LIST']); + +/** + * Return a wam 'stat' value for the FileSystem itself. + * + * This is a jsfs.Entry method needed as part of the 'LIST' action. + */ +wam.jsfs.dom.FileSystem.prototype.getStat = function(onSuccess, onError) { + wam.async(onSuccess, + [null, + { abilities: this.abilities, + state: this.readyBinding.readyState, + capacity: this.capacity_, + source: 'domfs' + }]); +}; + +/** + * If this FileSystem isn't ready, try to make it ready and queue the callback + * for later, otherwise call it right now. + * + * @param {function()} callback The function to invoke when the file system + * becomes ready. + * @param {function(wam.Error)} onError The function to invoke if the + * file system fails to become ready. + */ +wam.jsfs.dom.FileSystem.prototype.doOrQueue_ = function(callback, onError) { + if (this.readyBinding.isReadyState('READY')) { + callback(); + } else { + this.connect(callback, onError); + } +}; + +/** + * Utility method converts a DOM FileError and a path into an appropriate + * 'wam.FileSystem.Error' value and passes it to the given onError function. + * + * The signature for this method is backwards because it's typically used + * in conjunction with onFileError_.bind(this, onError, path), where the final + * error parameter will be supplied later. + * + * @param {function(wam.Error)} onError The function to invoke with the + * converted error. + * @param {string} path The path associated with the with the error. + * @param {FileError} The DOM FileError to convert. + */ +wam.jsfs.dom.FileSystem.prototype.onFileError_ = function( + onError, path, error) { + onError(wam.jsfs.dom.convertFileError(error, path)); +}; + +/** + * Same as ..onFileError_, except used when reporting an error about a DirEntry. + */ +wam.jsfs.dom.FileSystem.prototype.onDirError_ = function( + onError, path, error) { + onError(wam.jsfs.dom.convertDirError(error, path)); +}; + +/** + * Forward a stat call to the LocalFileSystem. + * + * This is a jsfs.Entry method needed as part of the 'FORWARD' action. + */ +wam.jsfs.dom.FileSystem.prototype.forwardStat = function( + arg, onSuccess, onError) { + var onFileFound = function(entry) { + wam.jsfs.dom.statEntry( + entry, onSuccess, + this.onFileError_.bind(this, onError, arg.forwardPath)); + }.bind(this); + + var onDirFound = function(entry) { + wam.jsfs.dom.statEntry( + entry, onSuccess, + this.onDirError_.bind(this, onError, arg.forwardPath)); + }.bind(this); + + var onFileResolveError = function(error) { + if (error.name == 'TypeMismatchError') { + this.domfs_.root.getDirectory( + arg.path, {create: false}, + onDirFound, + this.onDirError_.bind(this, onError, arg.forwardPath)); + } else { + this.onFileError_(onError, arg.forwardPath, error); + } + }.bind(this); + + var stat = function() { + this.domfs_.root.getFile(arg.forwardPath, {create: false}, + onFileFound, onFileResolveError); + }.bind(this); + + this.doOrQueue_(stat, onError); +}; + +/** + * Forward an unlink call to the LocalFileSystem. + * + * This is a jsfs.Entry method needed as part of the 'FORWARD' action. + */ +wam.jsfs.dom.FileSystem.prototype.forwardUnlink = function( + arg, onSuccess, onError) { + var onFileFound = function(entry) { + entry.remove( + onSuccess, + this.onFileError_.bind(this, onError, arg.forwardPath)); + }.bind(this); + + var onDirFound = function(entry) { + entry.removeRecursively( + onSuccess, + this.onDirError_.bind(this, onError, arg.forwardPath)); + }.bind(this); + + var onFileResolveError = function(error) { + if (error.name == 'TypeMismatchError') { + this.domfs_.root.getDirectory( + arg.path, {create: false}, + onDirFound, + this.onDirError_.bind(this, onError, arg.forwardPath)); + } else { + this.onFileError_(onError, arg.forwardPath, error); + } + }.bind(this); + + this.doOrQueue_(function() { + this.domfs_.root.getFile(arg.forwardPath, {create: false}, + onFileFound, onFileResolveError); + }.bind(this), + onError); +}; + +/** + * Forward a list call to the LocalFileSystem. + * + * This is a jsfs.Entry method needed as part of the 'FORWARD' action. + */ +wam.jsfs.dom.FileSystem.prototype.forwardList = function( + arg, onSuccess, onError) { + // List of Entry object we'll need to stat. + var entries = []; + // Number of Entry objects we've got metadata results for so far. + var mdgot = 0; + // The wam 'list' result. + var rv = {}; + + // Called once per entry to deliver the successful stat result. + var onStat = function(name, stat) { + rv[name] = {stat: stat}; + if (++mdgot == entries.length) + onSuccess(rv); + }; + + // DirEntry.readEntries callback. + var onReadEntries = function(reader, results) { + if (!results.length) { + // If we're called back with no results it means we're done. + if (!entries.length) { + onSuccess(rv); + return; + } + + for (var i = 0; i < entries.length; i++) { + wam.jsfs.dom.statEntry( + entries[i], + onStat.bind(null, entries[i].name), + this.onFileError_.bind(this, onError, + arg.forwardPath + '/' + entries[i])); + } + } else { + entries = entries.concat(results); + reader.readEntries(onReadEntries.bind(null, reader)); + } + }.bind(this); + + // Delivers the DirEntry for the target directory. + var onDirectoryFound = function(dirEntry) { + var reader = dirEntry.createReader(); + reader.readEntries(onReadEntries.bind(null, reader)); + }; + + this.doOrQueue_(function() { + this.domfs_.root.getDirectory( + arg.forwardPath, {create: false}, + onDirectoryFound, + this.onDirError_.bind(this, onError, arg.forwardPath)); + }.bind(this), + onError); +}; + +/** + * Forward a wam 'execute' to this file system. + * + * Executables are not supported on the DOM file system. + * + * This is a jsfs.Entry method needed as part of the 'FORWARD' action. + * + * TODO(rginda): We could add support for running nmf files, or wash + * scripts, or even respect shebangs for shell scripts. Maybe? + */ +wam.jsfs.dom.FileSystem.prototype.forwardExecute = function(arg) { + arg.executeContext.closeError('wam.FileSystem.Error.NotExecutable', []); +}; + +/** + * Forward a wam 'open' to this file system. + * + * This is a jsfs.Entry method needed as part of the 'FORWARD' action. + */ +wam.jsfs.dom.FileSystem.prototype.forwardOpen = function(arg) { + this.doOrQueue_(function() { + arg.openContext.path = arg.forwardPath; + var domoc = new wam.jsfs.dom.OpenContext(this.domfs_, arg.openContext); + domoc.onOpen_({path: arg.forwardPath, arg: arg.arg}); + }.bind(this), + function(value) { arg.openContext.closeError(value) }); +}; + +/** + * Drain with success any pending doOrQueue_'s when we become ready. + */ +wam.jsfs.dom.FileSystem.prototype.onBindingReady_ = function() { + while (this.pendingOperations_.length) { + var onSuccess = this.pendingOperations_.shift()[0]; + onSuccess(); + } +}; + +/** + * Drain with error any pending doOrQueue_'s if we close due to an error. + */ +wam.jsfs.dom.FileSystem.prototype.onBindingClose_ = function(reason, value) { + if (reason == 'error') { + while (this.pendingOperations_.length) { + var onError = this.pendingOperations_.shift()[1]; + onError(); + } + } +}; +// SOURCE FILE: wam/js/wam_jsfs_dom_open_context.js +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +'use strict'; + +/** + * An object that connects a wam.binding.fs.OpenContext to an open file on a + * DOM LocalFileSystem. + */ +wam.jsfs.dom.OpenContext = function(domfs, openContextBinding) { + // Raw DOM LocalFileSystem instance, not the wam.jsfs.dom.FileSystem. + this.domfs_ = domfs; + + // The current read/write position. + this.position_ = 0; + // The DOM FileEntry we're operating on. + this.entry_ = null; + // The DOM File we're operating on. + this.file_ = null; + + /** + * The wam.binding.fs.OpenContext we're working for. + */ + this.openContextBinding = openContextBinding; + openContextBinding.onSeek.addListener(this.onSeek_, this); + openContextBinding.onRead.addListener(this.onRead_, this); + openContextBinding.onWrite.addListener(this.onWrite_, this); + + /** + * The path we were opened for. + */ + this.path = openContextBinding.path; +}; + +/** + * Utility function to perform a seek (update this.position_). + * + * Invokes onError with a wam.FileSystem.Error value and returns false on + * error. + * + * If the arg object does not have a 'whence' property, this call succeeds + * with no side effects. + * + * @param {Object} arg An object containing 'whence' and 'offset' arguments + * describing the seek operation. + */ +wam.jsfs.dom.OpenContext.prototype.seek_ = function(arg, onError) { + var fileSize = this.file_.size; + var start = this.position_; + + if (!arg.whence) + return true; + + if (arg.whence == 'begin') { + start = arg.offset; + + } else if (arg.whence == 'current') { + start += arg.offset; + + } else if (arg.whence == 'end') { + start = fileSize + arg.offset; + } + + if (start > fileSize) { + onError(wam.mkerr('wam.FileSystem.Error.EndOfFile', [])); + return false; + } + + if (start < 0) { + onError(wam.mkerr('wam.FileSystem.Error.BeginningOfFile', [])); + return false; + } + + this.position_ = start; + return true; +}; + +/** + * Convenience method to close out this context with a wam.Error value. + */ +wam.jsfs.dom.OpenContext.prototype.onWamError_ = function(wamError) { + this.openContextBinding.closeErrorValue(wamError); +}; + +/** + * Convenience method to convert a FileError to a wam.FileSystem.Error value + * close this context with it. + * + * Used in the context of a FileEntry. + */ +wam.jsfs.dom.OpenContext.prototype.onFileError_ = function(error) { + this.onWamError_(wam.jsfs.dom.convertFileError(error, this.path)); +}; + +/** + * Convenience method to convert a FileError to a wam.FileSystem.Error value + * close this context with it. + * + * Used in the context of a DirEntry. + */ +wam.jsfs.dom.OpenContext.prototype.onDirError_ = function(error) { + this.onWamError_(wam.jsfs.dom.convertDirError(error, this.path)); +}; + +/** + * Called directly by the parent wam.jsfs.dom.FileSystem to initate the + * open. + */ +wam.jsfs.dom.OpenContext.prototype.onOpen_ = function() { + var onFileError = this.onFileError_.bind(this); + var mode = this.openContextBinding.mode; + + var onStat = function(stat) { + this.entry_.file(function(f) { + this.file_ = f; + this.openContextBinding.ready(stat); + }.bind(this), + onFileError); + }.bind(this); + + var onFileFound = function(entry) { + this.entry_ = entry; + if (mode.write && mode.truncate) { + this.entry_.createWriter( + function(writer) { + writer.truncate(0); + wam.jsfs.dom.statEntry(entry, onStat, onFileError); + }, + onFileError); + } else { + wam.jsfs.dom.statEntry(entry, onStat, onFileError); + } + }.bind(this); + + this.domfs_.root.getFile( + this.path, + {create: mode.create, + exclusive: mode.exclusive + }, + onFileFound, onFileError); +}; + +/** + * Handle a seek event from the binding. + */ +wam.jsfs.dom.OpenContext.prototype.onSeek_ = function(arg, onSuccess, onError) { + if (!this.seek_(arg, onError)) + return; + + onSuccess({position: this.position_}); +}; + +/** + * Handle a read event from the binding. + */ +wam.jsfs.dom.OpenContext.prototype.onRead_ = function(arg, onSuccess, onError) { + if (!this.seek_(arg, onError)) + return; + + var fileSize = this.file_.size; + var end; + if (arg.count) { + end = this.position_ + count; + } else { + end = fileSize; + } + + var dataType = arg.dataType || 'utf8-string'; + var reader = new FileReader(this.entry_.file); + + reader.onload = function(e) { + this.position_ = end + 1; + var data = reader.result; + + if (dataType == 'base64-string') { + // TODO: By the time we read this into a string the data may already have + // been munged. We need an ArrayBuffer->Base64 string implementation to + // make this work for real. + data = btoa(data); + } + + onSuccess({dataType: dataType, data: data}); + }.bind(this); + + reader.onerror = function(error) { + onError(wam.jsfs.dom.convertFileError(error, this.path)); + }; + + var slice = this.file_.slice(this.position_, end); + if (dataType == 'blob') { + onSuccess({dataType: dataType, data: slice}); + } else if (dataType == 'arraybuffer') { + reader.readAsArrayBuffer(slice); + } else { + reader.readAsText(slice); + } +}; + +/** + * Handle a write event from the binding. + */ +wam.jsfs.dom.OpenContext.prototype.onWrite_ = function( + arg, onSuccess, onError) { + if (!this.seek_(arg, onError)) + return; + + var onWriterReady = function(writer) { + var blob; + if (arg.data instanceof Blob) { + blob = arg.data; + } else if (arg.data instanceof ArrayBuffer) { + blob = new Blob([arg.data], {type: 'application/octet-stream'}); + } else if (arg.dataType == 'base64-string') { + // TODO: Once we turn this into a string the data may already have + // been munged. We need an ArrayBuffer->Base64 string implementation to + // make this work for real. + blob = new Blob([atob(arg.data)], {type: 'application/octet-stream'}); + } else if (arg.dataType == 'utf8-string') { + blob = new Blob([arg.data], {type: 'text/plain'}); + } else if (arg.dataType == 'value') { + blob = new Blob([JSON.stringify(arg.data)], {type: 'text/json'}); + } + + writer.onerror = function(error) { + onError(wam.jsfs.dom.convertFileError(error, this.path)); + }.bind(this); + + writer.onwrite = function() { + this.position_ = this.position_ + blob.size; + onSuccess(null); + }.bind(this); + + writer.seek(this.position_); + writer.write(blob); + }; + + this.entry_.createWriter( + onWriterReady, + this.onFileError_.bind(this), + function(error) { + onError(wam.jsfs.dom.convertFileError(error, this.path)); + }); +}; +