|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 module.metadata = { |
|
8 "stability": "experimental" |
|
9 }; |
|
10 |
|
11 const {Cc,Ci,Cu,components} = require("chrome"); |
|
12 var NetUtil = {}; |
|
13 Cu.import("resource://gre/modules/NetUtil.jsm", NetUtil); |
|
14 NetUtil = NetUtil.NetUtil; |
|
15 |
|
16 // NetUtil.asyncCopy() uses this buffer length, and since we call it, for best |
|
17 // performance we use it, too. |
|
18 const BUFFER_BYTE_LEN = 0x8000; |
|
19 const PR_UINT32_MAX = 0xffffffff; |
|
20 const DEFAULT_CHARSET = "UTF-8"; |
|
21 |
|
22 exports.TextReader = TextReader; |
|
23 exports.TextWriter = TextWriter; |
|
24 |
|
25 /** |
|
26 * An input stream that reads text from a backing stream using a given text |
|
27 * encoding. |
|
28 * |
|
29 * @param inputStream |
|
30 * The stream is backed by this nsIInputStream. It must already be |
|
31 * opened. |
|
32 * @param charset |
|
33 * Text in inputStream is expected to be in this character encoding. If |
|
34 * not given, "UTF-8" is assumed. See nsICharsetConverterManager.idl for |
|
35 * documentation on how to determine other valid values for this. |
|
36 */ |
|
37 function TextReader(inputStream, charset) { |
|
38 const self = this; |
|
39 charset = checkCharset(charset); |
|
40 |
|
41 let stream = Cc["@mozilla.org/intl/converter-input-stream;1"]. |
|
42 createInstance(Ci.nsIConverterInputStream); |
|
43 stream.init(inputStream, charset, BUFFER_BYTE_LEN, |
|
44 Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); |
|
45 |
|
46 let manager = new StreamManager(this, stream); |
|
47 |
|
48 /** |
|
49 * Reads a string from the stream. If the stream is closed, an exception is |
|
50 * thrown. |
|
51 * |
|
52 * @param numChars |
|
53 * The number of characters to read. If not given, the remainder of |
|
54 * the stream is read. |
|
55 * @return The string read. If the stream is already at EOS, returns the |
|
56 * empty string. |
|
57 */ |
|
58 this.read = function TextReader_read(numChars) { |
|
59 manager.ensureOpened(); |
|
60 |
|
61 let readAll = false; |
|
62 if (typeof(numChars) === "number") |
|
63 numChars = Math.max(numChars, 0); |
|
64 else |
|
65 readAll = true; |
|
66 |
|
67 let str = ""; |
|
68 let totalRead = 0; |
|
69 let chunkRead = 1; |
|
70 |
|
71 // Read in numChars or until EOS, whichever comes first. Note that the |
|
72 // units here are characters, not bytes. |
|
73 while (true) { |
|
74 let chunk = {}; |
|
75 let toRead = readAll ? |
|
76 PR_UINT32_MAX : |
|
77 Math.min(numChars - totalRead, PR_UINT32_MAX); |
|
78 if (toRead <= 0 || chunkRead <= 0) |
|
79 break; |
|
80 |
|
81 // The converter stream reads in at most BUFFER_BYTE_LEN bytes in a call |
|
82 // to readString, enough to fill its byte buffer. chunkRead will be the |
|
83 // number of characters encoded by the bytes in that buffer. |
|
84 chunkRead = stream.readString(toRead, chunk); |
|
85 str += chunk.value; |
|
86 totalRead += chunkRead; |
|
87 } |
|
88 |
|
89 return str; |
|
90 }; |
|
91 } |
|
92 |
|
93 /** |
|
94 * A buffered output stream that writes text to a backing stream using a given |
|
95 * text encoding. |
|
96 * |
|
97 * @param outputStream |
|
98 * The stream is backed by this nsIOutputStream. It must already be |
|
99 * opened. |
|
100 * @param charset |
|
101 * Text will be written to outputStream using this character encoding. |
|
102 * If not given, "UTF-8" is assumed. See nsICharsetConverterManager.idl |
|
103 * for documentation on how to determine other valid values for this. |
|
104 */ |
|
105 function TextWriter(outputStream, charset) { |
|
106 const self = this; |
|
107 charset = checkCharset(charset); |
|
108 |
|
109 let stream = outputStream; |
|
110 |
|
111 // Buffer outputStream if it's not already. |
|
112 let ioUtils = Cc["@mozilla.org/io-util;1"].getService(Ci.nsIIOUtil); |
|
113 if (!ioUtils.outputStreamIsBuffered(outputStream)) { |
|
114 stream = Cc["@mozilla.org/network/buffered-output-stream;1"]. |
|
115 createInstance(Ci.nsIBufferedOutputStream); |
|
116 stream.init(outputStream, BUFFER_BYTE_LEN); |
|
117 } |
|
118 |
|
119 // I'd like to use nsIConverterOutputStream. But NetUtil.asyncCopy(), which |
|
120 // we use below in writeAsync(), naturally expects its sink to be an instance |
|
121 // of nsIOutputStream, which nsIConverterOutputStream's only implementation is |
|
122 // not. So we use uconv and manually convert all strings before writing to |
|
123 // outputStream. |
|
124 let uconv = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. |
|
125 createInstance(Ci.nsIScriptableUnicodeConverter); |
|
126 uconv.charset = charset; |
|
127 |
|
128 let manager = new StreamManager(this, stream); |
|
129 |
|
130 /** |
|
131 * Flushes the backing stream's buffer. |
|
132 */ |
|
133 this.flush = function TextWriter_flush() { |
|
134 manager.ensureOpened(); |
|
135 stream.flush(); |
|
136 }; |
|
137 |
|
138 /** |
|
139 * Writes a string to the stream. If the stream is closed, an exception is |
|
140 * thrown. |
|
141 * |
|
142 * @param str |
|
143 * The string to write. |
|
144 */ |
|
145 this.write = function TextWriter_write(str) { |
|
146 manager.ensureOpened(); |
|
147 let istream = uconv.convertToInputStream(str); |
|
148 let len = istream.available(); |
|
149 while (len > 0) { |
|
150 stream.writeFrom(istream, len); |
|
151 len = istream.available(); |
|
152 } |
|
153 istream.close(); |
|
154 }; |
|
155 |
|
156 /** |
|
157 * Writes a string on a background thread. After the write completes, the |
|
158 * backing stream's buffer is flushed, and both the stream and the backing |
|
159 * stream are closed, also on the background thread. If the stream is already |
|
160 * closed, an exception is thrown immediately. |
|
161 * |
|
162 * @param str |
|
163 * The string to write. |
|
164 * @param callback |
|
165 * An optional function. If given, it's called as callback(error) when |
|
166 * the write completes. error is an Error object or undefined if there |
|
167 * was no error. Inside callback, |this| is the stream object. |
|
168 */ |
|
169 this.writeAsync = function TextWriter_writeAsync(str, callback) { |
|
170 manager.ensureOpened(); |
|
171 let istream = uconv.convertToInputStream(str); |
|
172 NetUtil.asyncCopy(istream, stream, function (result) { |
|
173 let err = components.isSuccessCode(result) ? undefined : |
|
174 new Error("An error occured while writing to the stream: " + result); |
|
175 if (err) |
|
176 console.error(err); |
|
177 |
|
178 // asyncCopy() closes its output (and input) stream. |
|
179 manager.opened = false; |
|
180 |
|
181 if (typeof(callback) === "function") { |
|
182 try { |
|
183 callback.call(self, err); |
|
184 } |
|
185 catch (exc) { |
|
186 console.exception(exc); |
|
187 } |
|
188 } |
|
189 }); |
|
190 }; |
|
191 } |
|
192 |
|
193 // This manages the lifetime of stream, a TextReader or TextWriter. It defines |
|
194 // closed and close() on stream and registers an unload listener that closes |
|
195 // rawStream if it's still opened. It also provides ensureOpened(), which |
|
196 // throws an exception if the stream is closed. |
|
197 function StreamManager(stream, rawStream) { |
|
198 const self = this; |
|
199 this.rawStream = rawStream; |
|
200 this.opened = true; |
|
201 |
|
202 /** |
|
203 * True iff the stream is closed. |
|
204 */ |
|
205 stream.__defineGetter__("closed", function stream_closed() { |
|
206 return !self.opened; |
|
207 }); |
|
208 |
|
209 /** |
|
210 * Closes both the stream and its backing stream. If the stream is already |
|
211 * closed, an exception is thrown. For TextWriters, this first flushes the |
|
212 * backing stream's buffer. |
|
213 */ |
|
214 stream.close = function stream_close() { |
|
215 self.ensureOpened(); |
|
216 self.unload(); |
|
217 }; |
|
218 |
|
219 require("../system/unload").ensure(this); |
|
220 } |
|
221 |
|
222 StreamManager.prototype = { |
|
223 ensureOpened: function StreamManager_ensureOpened() { |
|
224 if (!this.opened) |
|
225 throw new Error("The stream is closed and cannot be used."); |
|
226 }, |
|
227 unload: function StreamManager_unload() { |
|
228 // TextWriter.writeAsync() causes rawStream to close and therefore sets |
|
229 // opened to false, so check that we're still opened. |
|
230 if (this.opened) { |
|
231 // Calling close() on both an nsIUnicharInputStream and |
|
232 // nsIBufferedOutputStream closes their backing streams. It also forces |
|
233 // nsIOutputStreams to flush first. |
|
234 this.rawStream.close(); |
|
235 this.opened = false; |
|
236 } |
|
237 } |
|
238 }; |
|
239 |
|
240 function checkCharset(charset) { |
|
241 return typeof(charset) === "string" ? charset : DEFAULT_CHARSET; |
|
242 } |