1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/addon-sdk/source/lib/sdk/io/text-streams.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,242 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +module.metadata = { 1.11 + "stability": "experimental" 1.12 +}; 1.13 + 1.14 +const {Cc,Ci,Cu,components} = require("chrome"); 1.15 +var NetUtil = {}; 1.16 +Cu.import("resource://gre/modules/NetUtil.jsm", NetUtil); 1.17 +NetUtil = NetUtil.NetUtil; 1.18 + 1.19 +// NetUtil.asyncCopy() uses this buffer length, and since we call it, for best 1.20 +// performance we use it, too. 1.21 +const BUFFER_BYTE_LEN = 0x8000; 1.22 +const PR_UINT32_MAX = 0xffffffff; 1.23 +const DEFAULT_CHARSET = "UTF-8"; 1.24 + 1.25 +exports.TextReader = TextReader; 1.26 +exports.TextWriter = TextWriter; 1.27 + 1.28 +/** 1.29 + * An input stream that reads text from a backing stream using a given text 1.30 + * encoding. 1.31 + * 1.32 + * @param inputStream 1.33 + * The stream is backed by this nsIInputStream. It must already be 1.34 + * opened. 1.35 + * @param charset 1.36 + * Text in inputStream is expected to be in this character encoding. If 1.37 + * not given, "UTF-8" is assumed. See nsICharsetConverterManager.idl for 1.38 + * documentation on how to determine other valid values for this. 1.39 + */ 1.40 +function TextReader(inputStream, charset) { 1.41 + const self = this; 1.42 + charset = checkCharset(charset); 1.43 + 1.44 + let stream = Cc["@mozilla.org/intl/converter-input-stream;1"]. 1.45 + createInstance(Ci.nsIConverterInputStream); 1.46 + stream.init(inputStream, charset, BUFFER_BYTE_LEN, 1.47 + Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); 1.48 + 1.49 + let manager = new StreamManager(this, stream); 1.50 + 1.51 + /** 1.52 + * Reads a string from the stream. If the stream is closed, an exception is 1.53 + * thrown. 1.54 + * 1.55 + * @param numChars 1.56 + * The number of characters to read. If not given, the remainder of 1.57 + * the stream is read. 1.58 + * @return The string read. If the stream is already at EOS, returns the 1.59 + * empty string. 1.60 + */ 1.61 + this.read = function TextReader_read(numChars) { 1.62 + manager.ensureOpened(); 1.63 + 1.64 + let readAll = false; 1.65 + if (typeof(numChars) === "number") 1.66 + numChars = Math.max(numChars, 0); 1.67 + else 1.68 + readAll = true; 1.69 + 1.70 + let str = ""; 1.71 + let totalRead = 0; 1.72 + let chunkRead = 1; 1.73 + 1.74 + // Read in numChars or until EOS, whichever comes first. Note that the 1.75 + // units here are characters, not bytes. 1.76 + while (true) { 1.77 + let chunk = {}; 1.78 + let toRead = readAll ? 1.79 + PR_UINT32_MAX : 1.80 + Math.min(numChars - totalRead, PR_UINT32_MAX); 1.81 + if (toRead <= 0 || chunkRead <= 0) 1.82 + break; 1.83 + 1.84 + // The converter stream reads in at most BUFFER_BYTE_LEN bytes in a call 1.85 + // to readString, enough to fill its byte buffer. chunkRead will be the 1.86 + // number of characters encoded by the bytes in that buffer. 1.87 + chunkRead = stream.readString(toRead, chunk); 1.88 + str += chunk.value; 1.89 + totalRead += chunkRead; 1.90 + } 1.91 + 1.92 + return str; 1.93 + }; 1.94 +} 1.95 + 1.96 +/** 1.97 + * A buffered output stream that writes text to a backing stream using a given 1.98 + * text encoding. 1.99 + * 1.100 + * @param outputStream 1.101 + * The stream is backed by this nsIOutputStream. It must already be 1.102 + * opened. 1.103 + * @param charset 1.104 + * Text will be written to outputStream using this character encoding. 1.105 + * If not given, "UTF-8" is assumed. See nsICharsetConverterManager.idl 1.106 + * for documentation on how to determine other valid values for this. 1.107 + */ 1.108 +function TextWriter(outputStream, charset) { 1.109 + const self = this; 1.110 + charset = checkCharset(charset); 1.111 + 1.112 + let stream = outputStream; 1.113 + 1.114 + // Buffer outputStream if it's not already. 1.115 + let ioUtils = Cc["@mozilla.org/io-util;1"].getService(Ci.nsIIOUtil); 1.116 + if (!ioUtils.outputStreamIsBuffered(outputStream)) { 1.117 + stream = Cc["@mozilla.org/network/buffered-output-stream;1"]. 1.118 + createInstance(Ci.nsIBufferedOutputStream); 1.119 + stream.init(outputStream, BUFFER_BYTE_LEN); 1.120 + } 1.121 + 1.122 + // I'd like to use nsIConverterOutputStream. But NetUtil.asyncCopy(), which 1.123 + // we use below in writeAsync(), naturally expects its sink to be an instance 1.124 + // of nsIOutputStream, which nsIConverterOutputStream's only implementation is 1.125 + // not. So we use uconv and manually convert all strings before writing to 1.126 + // outputStream. 1.127 + let uconv = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. 1.128 + createInstance(Ci.nsIScriptableUnicodeConverter); 1.129 + uconv.charset = charset; 1.130 + 1.131 + let manager = new StreamManager(this, stream); 1.132 + 1.133 + /** 1.134 + * Flushes the backing stream's buffer. 1.135 + */ 1.136 + this.flush = function TextWriter_flush() { 1.137 + manager.ensureOpened(); 1.138 + stream.flush(); 1.139 + }; 1.140 + 1.141 + /** 1.142 + * Writes a string to the stream. If the stream is closed, an exception is 1.143 + * thrown. 1.144 + * 1.145 + * @param str 1.146 + * The string to write. 1.147 + */ 1.148 + this.write = function TextWriter_write(str) { 1.149 + manager.ensureOpened(); 1.150 + let istream = uconv.convertToInputStream(str); 1.151 + let len = istream.available(); 1.152 + while (len > 0) { 1.153 + stream.writeFrom(istream, len); 1.154 + len = istream.available(); 1.155 + } 1.156 + istream.close(); 1.157 + }; 1.158 + 1.159 + /** 1.160 + * Writes a string on a background thread. After the write completes, the 1.161 + * backing stream's buffer is flushed, and both the stream and the backing 1.162 + * stream are closed, also on the background thread. If the stream is already 1.163 + * closed, an exception is thrown immediately. 1.164 + * 1.165 + * @param str 1.166 + * The string to write. 1.167 + * @param callback 1.168 + * An optional function. If given, it's called as callback(error) when 1.169 + * the write completes. error is an Error object or undefined if there 1.170 + * was no error. Inside callback, |this| is the stream object. 1.171 + */ 1.172 + this.writeAsync = function TextWriter_writeAsync(str, callback) { 1.173 + manager.ensureOpened(); 1.174 + let istream = uconv.convertToInputStream(str); 1.175 + NetUtil.asyncCopy(istream, stream, function (result) { 1.176 + let err = components.isSuccessCode(result) ? undefined : 1.177 + new Error("An error occured while writing to the stream: " + result); 1.178 + if (err) 1.179 + console.error(err); 1.180 + 1.181 + // asyncCopy() closes its output (and input) stream. 1.182 + manager.opened = false; 1.183 + 1.184 + if (typeof(callback) === "function") { 1.185 + try { 1.186 + callback.call(self, err); 1.187 + } 1.188 + catch (exc) { 1.189 + console.exception(exc); 1.190 + } 1.191 + } 1.192 + }); 1.193 + }; 1.194 +} 1.195 + 1.196 +// This manages the lifetime of stream, a TextReader or TextWriter. It defines 1.197 +// closed and close() on stream and registers an unload listener that closes 1.198 +// rawStream if it's still opened. It also provides ensureOpened(), which 1.199 +// throws an exception if the stream is closed. 1.200 +function StreamManager(stream, rawStream) { 1.201 + const self = this; 1.202 + this.rawStream = rawStream; 1.203 + this.opened = true; 1.204 + 1.205 + /** 1.206 + * True iff the stream is closed. 1.207 + */ 1.208 + stream.__defineGetter__("closed", function stream_closed() { 1.209 + return !self.opened; 1.210 + }); 1.211 + 1.212 + /** 1.213 + * Closes both the stream and its backing stream. If the stream is already 1.214 + * closed, an exception is thrown. For TextWriters, this first flushes the 1.215 + * backing stream's buffer. 1.216 + */ 1.217 + stream.close = function stream_close() { 1.218 + self.ensureOpened(); 1.219 + self.unload(); 1.220 + }; 1.221 + 1.222 + require("../system/unload").ensure(this); 1.223 +} 1.224 + 1.225 +StreamManager.prototype = { 1.226 + ensureOpened: function StreamManager_ensureOpened() { 1.227 + if (!this.opened) 1.228 + throw new Error("The stream is closed and cannot be used."); 1.229 + }, 1.230 + unload: function StreamManager_unload() { 1.231 + // TextWriter.writeAsync() causes rawStream to close and therefore sets 1.232 + // opened to false, so check that we're still opened. 1.233 + if (this.opened) { 1.234 + // Calling close() on both an nsIUnicharInputStream and 1.235 + // nsIBufferedOutputStream closes their backing streams. It also forces 1.236 + // nsIOutputStreams to flush first. 1.237 + this.rawStream.close(); 1.238 + this.opened = false; 1.239 + } 1.240 + } 1.241 +}; 1.242 + 1.243 +function checkCharset(charset) { 1.244 + return typeof(charset) === "string" ? charset : DEFAULT_CHARSET; 1.245 +}