michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: module.metadata = { michael@0: "stability": "experimental" michael@0: }; michael@0: michael@0: const {Cc,Ci,Cu,components} = require("chrome"); michael@0: var NetUtil = {}; michael@0: Cu.import("resource://gre/modules/NetUtil.jsm", NetUtil); michael@0: NetUtil = NetUtil.NetUtil; michael@0: michael@0: // NetUtil.asyncCopy() uses this buffer length, and since we call it, for best michael@0: // performance we use it, too. michael@0: const BUFFER_BYTE_LEN = 0x8000; michael@0: const PR_UINT32_MAX = 0xffffffff; michael@0: const DEFAULT_CHARSET = "UTF-8"; michael@0: michael@0: exports.TextReader = TextReader; michael@0: exports.TextWriter = TextWriter; michael@0: michael@0: /** michael@0: * An input stream that reads text from a backing stream using a given text michael@0: * encoding. michael@0: * michael@0: * @param inputStream michael@0: * The stream is backed by this nsIInputStream. It must already be michael@0: * opened. michael@0: * @param charset michael@0: * Text in inputStream is expected to be in this character encoding. If michael@0: * not given, "UTF-8" is assumed. See nsICharsetConverterManager.idl for michael@0: * documentation on how to determine other valid values for this. michael@0: */ michael@0: function TextReader(inputStream, charset) { michael@0: const self = this; michael@0: charset = checkCharset(charset); michael@0: michael@0: let stream = Cc["@mozilla.org/intl/converter-input-stream;1"]. michael@0: createInstance(Ci.nsIConverterInputStream); michael@0: stream.init(inputStream, charset, BUFFER_BYTE_LEN, michael@0: Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); michael@0: michael@0: let manager = new StreamManager(this, stream); michael@0: michael@0: /** michael@0: * Reads a string from the stream. If the stream is closed, an exception is michael@0: * thrown. michael@0: * michael@0: * @param numChars michael@0: * The number of characters to read. If not given, the remainder of michael@0: * the stream is read. michael@0: * @return The string read. If the stream is already at EOS, returns the michael@0: * empty string. michael@0: */ michael@0: this.read = function TextReader_read(numChars) { michael@0: manager.ensureOpened(); michael@0: michael@0: let readAll = false; michael@0: if (typeof(numChars) === "number") michael@0: numChars = Math.max(numChars, 0); michael@0: else michael@0: readAll = true; michael@0: michael@0: let str = ""; michael@0: let totalRead = 0; michael@0: let chunkRead = 1; michael@0: michael@0: // Read in numChars or until EOS, whichever comes first. Note that the michael@0: // units here are characters, not bytes. michael@0: while (true) { michael@0: let chunk = {}; michael@0: let toRead = readAll ? michael@0: PR_UINT32_MAX : michael@0: Math.min(numChars - totalRead, PR_UINT32_MAX); michael@0: if (toRead <= 0 || chunkRead <= 0) michael@0: break; michael@0: michael@0: // The converter stream reads in at most BUFFER_BYTE_LEN bytes in a call michael@0: // to readString, enough to fill its byte buffer. chunkRead will be the michael@0: // number of characters encoded by the bytes in that buffer. michael@0: chunkRead = stream.readString(toRead, chunk); michael@0: str += chunk.value; michael@0: totalRead += chunkRead; michael@0: } michael@0: michael@0: return str; michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * A buffered output stream that writes text to a backing stream using a given michael@0: * text encoding. michael@0: * michael@0: * @param outputStream michael@0: * The stream is backed by this nsIOutputStream. It must already be michael@0: * opened. michael@0: * @param charset michael@0: * Text will be written to outputStream using this character encoding. michael@0: * If not given, "UTF-8" is assumed. See nsICharsetConverterManager.idl michael@0: * for documentation on how to determine other valid values for this. michael@0: */ michael@0: function TextWriter(outputStream, charset) { michael@0: const self = this; michael@0: charset = checkCharset(charset); michael@0: michael@0: let stream = outputStream; michael@0: michael@0: // Buffer outputStream if it's not already. michael@0: let ioUtils = Cc["@mozilla.org/io-util;1"].getService(Ci.nsIIOUtil); michael@0: if (!ioUtils.outputStreamIsBuffered(outputStream)) { michael@0: stream = Cc["@mozilla.org/network/buffered-output-stream;1"]. michael@0: createInstance(Ci.nsIBufferedOutputStream); michael@0: stream.init(outputStream, BUFFER_BYTE_LEN); michael@0: } michael@0: michael@0: // I'd like to use nsIConverterOutputStream. But NetUtil.asyncCopy(), which michael@0: // we use below in writeAsync(), naturally expects its sink to be an instance michael@0: // of nsIOutputStream, which nsIConverterOutputStream's only implementation is michael@0: // not. So we use uconv and manually convert all strings before writing to michael@0: // outputStream. michael@0: let uconv = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. michael@0: createInstance(Ci.nsIScriptableUnicodeConverter); michael@0: uconv.charset = charset; michael@0: michael@0: let manager = new StreamManager(this, stream); michael@0: michael@0: /** michael@0: * Flushes the backing stream's buffer. michael@0: */ michael@0: this.flush = function TextWriter_flush() { michael@0: manager.ensureOpened(); michael@0: stream.flush(); michael@0: }; michael@0: michael@0: /** michael@0: * Writes a string to the stream. If the stream is closed, an exception is michael@0: * thrown. michael@0: * michael@0: * @param str michael@0: * The string to write. michael@0: */ michael@0: this.write = function TextWriter_write(str) { michael@0: manager.ensureOpened(); michael@0: let istream = uconv.convertToInputStream(str); michael@0: let len = istream.available(); michael@0: while (len > 0) { michael@0: stream.writeFrom(istream, len); michael@0: len = istream.available(); michael@0: } michael@0: istream.close(); michael@0: }; michael@0: michael@0: /** michael@0: * Writes a string on a background thread. After the write completes, the michael@0: * backing stream's buffer is flushed, and both the stream and the backing michael@0: * stream are closed, also on the background thread. If the stream is already michael@0: * closed, an exception is thrown immediately. michael@0: * michael@0: * @param str michael@0: * The string to write. michael@0: * @param callback michael@0: * An optional function. If given, it's called as callback(error) when michael@0: * the write completes. error is an Error object or undefined if there michael@0: * was no error. Inside callback, |this| is the stream object. michael@0: */ michael@0: this.writeAsync = function TextWriter_writeAsync(str, callback) { michael@0: manager.ensureOpened(); michael@0: let istream = uconv.convertToInputStream(str); michael@0: NetUtil.asyncCopy(istream, stream, function (result) { michael@0: let err = components.isSuccessCode(result) ? undefined : michael@0: new Error("An error occured while writing to the stream: " + result); michael@0: if (err) michael@0: console.error(err); michael@0: michael@0: // asyncCopy() closes its output (and input) stream. michael@0: manager.opened = false; michael@0: michael@0: if (typeof(callback) === "function") { michael@0: try { michael@0: callback.call(self, err); michael@0: } michael@0: catch (exc) { michael@0: console.exception(exc); michael@0: } michael@0: } michael@0: }); michael@0: }; michael@0: } michael@0: michael@0: // This manages the lifetime of stream, a TextReader or TextWriter. It defines michael@0: // closed and close() on stream and registers an unload listener that closes michael@0: // rawStream if it's still opened. It also provides ensureOpened(), which michael@0: // throws an exception if the stream is closed. michael@0: function StreamManager(stream, rawStream) { michael@0: const self = this; michael@0: this.rawStream = rawStream; michael@0: this.opened = true; michael@0: michael@0: /** michael@0: * True iff the stream is closed. michael@0: */ michael@0: stream.__defineGetter__("closed", function stream_closed() { michael@0: return !self.opened; michael@0: }); michael@0: michael@0: /** michael@0: * Closes both the stream and its backing stream. If the stream is already michael@0: * closed, an exception is thrown. For TextWriters, this first flushes the michael@0: * backing stream's buffer. michael@0: */ michael@0: stream.close = function stream_close() { michael@0: self.ensureOpened(); michael@0: self.unload(); michael@0: }; michael@0: michael@0: require("../system/unload").ensure(this); michael@0: } michael@0: michael@0: StreamManager.prototype = { michael@0: ensureOpened: function StreamManager_ensureOpened() { michael@0: if (!this.opened) michael@0: throw new Error("The stream is closed and cannot be used."); michael@0: }, michael@0: unload: function StreamManager_unload() { michael@0: // TextWriter.writeAsync() causes rawStream to close and therefore sets michael@0: // opened to false, so check that we're still opened. michael@0: if (this.opened) { michael@0: // Calling close() on both an nsIUnicharInputStream and michael@0: // nsIBufferedOutputStream closes their backing streams. It also forces michael@0: // nsIOutputStreams to flush first. michael@0: this.rawStream.close(); michael@0: this.opened = false; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: function checkCharset(charset) { michael@0: return typeof(charset) === "string" ? charset : DEFAULT_CHARSET; michael@0: }