text-streams.js 7.7 KB

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