summaryrefslogtreecommitdiff
path: root/toolkit/jetpack/sdk/io/text-streams.js
blob: ed4ec4972b2934a160e788df534218a362ef10b7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

module.metadata = {
  "stability": "experimental"
};

const { Cc, Ci, Cu, components } = require("chrome");
const { ensure } = require("../system/unload");
const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});

// NetUtil.asyncCopy() uses this buffer length, and since we call it, for best
// performance we use it, too.
const BUFFER_BYTE_LEN = 0x8000;
const PR_UINT32_MAX = 0xffffffff;
const DEFAULT_CHARSET = "UTF-8";


/**
 * An input stream that reads text from a backing stream using a given text
 * encoding.
 *
 * @param inputStream
 *        The stream is backed by this nsIInputStream.  It must already be
 *        opened.
 * @param charset
 *        Text in inputStream is expected to be in this character encoding.  If
 *        not given, "UTF-8" is assumed.  See nsICharsetConverterManager.idl for
 *        documentation on how to determine other valid values for this.
 */
function TextReader(inputStream, charset) {
  charset = checkCharset(charset);

  let stream = Cc["@mozilla.org/intl/converter-input-stream;1"].
               createInstance(Ci.nsIConverterInputStream);
  stream.init(inputStream, charset, BUFFER_BYTE_LEN,
              Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);

  let manager = new StreamManager(this, stream);

  /**
   * Reads a string from the stream.  If the stream is closed, an exception is
   * thrown.
   *
   * @param  numChars
   *         The number of characters to read.  If not given, the remainder of
   *         the stream is read.
   * @return The string read.  If the stream is already at EOS, returns the
   *         empty string.
   */
  this.read = function TextReader_read(numChars) {
    manager.ensureOpened();

    let readAll = false;
    if (typeof(numChars) === "number")
      numChars = Math.max(numChars, 0);
    else
      readAll = true;

    let str = "";
    let totalRead = 0;
    let chunkRead = 1;

    // Read in numChars or until EOS, whichever comes first.  Note that the
    // units here are characters, not bytes.
    while (true) {
      let chunk = {};
      let toRead = readAll ?
                   PR_UINT32_MAX :
                   Math.min(numChars - totalRead, PR_UINT32_MAX);
      if (toRead <= 0 || chunkRead <= 0)
        break;

      // The converter stream reads in at most BUFFER_BYTE_LEN bytes in a call
      // to readString, enough to fill its byte buffer.  chunkRead will be the
      // number of characters encoded by the bytes in that buffer.
      chunkRead = stream.readString(toRead, chunk);
      str += chunk.value;
      totalRead += chunkRead;
    }

    return str;
  };
}
exports.TextReader = TextReader;

/**
 * A buffered output stream that writes text to a backing stream using a given
 * text encoding.
 *
 * @param outputStream
 *        The stream is backed by this nsIOutputStream.  It must already be
 *        opened.
 * @param charset
 *        Text will be written to outputStream using this character encoding.
 *        If not given, "UTF-8" is assumed.  See nsICharsetConverterManager.idl
 *        for documentation on how to determine other valid values for this.
 */
function TextWriter(outputStream, charset) {
  charset = checkCharset(charset);

  let stream = outputStream;

  // Buffer outputStream if it's not already.
  let ioUtils = Cc["@mozilla.org/io-util;1"].getService(Ci.nsIIOUtil);
  if (!ioUtils.outputStreamIsBuffered(outputStream)) {
    stream = Cc["@mozilla.org/network/buffered-output-stream;1"].
             createInstance(Ci.nsIBufferedOutputStream);
    stream.init(outputStream, BUFFER_BYTE_LEN);
  }

  // I'd like to use nsIConverterOutputStream.  But NetUtil.asyncCopy(), which
  // we use below in writeAsync(), naturally expects its sink to be an instance
  // of nsIOutputStream, which nsIConverterOutputStream's only implementation is
  // not.  So we use uconv and manually convert all strings before writing to
  // outputStream.
  let uconv = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
              createInstance(Ci.nsIScriptableUnicodeConverter);
  uconv.charset = charset;

  let manager = new StreamManager(this, stream);

  /**
   * Flushes the backing stream's buffer.
   */
  this.flush = function TextWriter_flush() {
    manager.ensureOpened();
    stream.flush();
  };

  /**
   * Writes a string to the stream.  If the stream is closed, an exception is
   * thrown.
   *
   * @param str
   *        The string to write.
   */
  this.write = function TextWriter_write(str) {
    manager.ensureOpened();
    let istream = uconv.convertToInputStream(str);
    let len = istream.available();
    while (len > 0) {
      stream.writeFrom(istream, len);
      len = istream.available();
    }
    istream.close();
  };

  /**
   * Writes a string on a background thread.  After the write completes, the
   * backing stream's buffer is flushed, and both the stream and the backing
   * stream are closed, also on the background thread.  If the stream is already
   * closed, an exception is thrown immediately.
   *
   * @param str
   *        The string to write.
   * @param callback
   *        An optional function.  If given, it's called as callback(error) when
   *        the write completes.  error is an Error object or undefined if there
   *        was no error.  Inside callback, |this| is the stream object.
   */
  this.writeAsync = function TextWriter_writeAsync(str, callback) {
    manager.ensureOpened();
    let istream = uconv.convertToInputStream(str);
    NetUtil.asyncCopy(istream, stream, (result) => {
        let err = components.isSuccessCode(result) ? undefined :
        new Error("An error occured while writing to the stream: " + result);
      if (err)
        console.error(err);

      // asyncCopy() closes its output (and input) stream.
      manager.opened = false;

      if (typeof(callback) === "function") {
        try {
          callback.call(this, err);
        }
        catch (exc) {
          console.exception(exc);
        }
      }
    });
  };
}
exports.TextWriter = TextWriter;

// This manages the lifetime of stream, a TextReader or TextWriter.  It defines
// closed and close() on stream and registers an unload listener that closes
// rawStream if it's still opened.  It also provides ensureOpened(), which
// throws an exception if the stream is closed.
function StreamManager(stream, rawStream) {
  this.rawStream = rawStream;
  this.opened = true;

  /**
   * True iff the stream is closed.
   */
  stream.__defineGetter__("closed", () => !this.opened);

  /**
   * Closes both the stream and its backing stream.  If the stream is already
   * closed, an exception is thrown.  For TextWriters, this first flushes the
   * backing stream's buffer.
   */
  stream.close = () => {
    this.ensureOpened();
    this.unload();
  };

  ensure(this);
}

StreamManager.prototype = {
  ensureOpened: function StreamManager_ensureOpened() {
    if (!this.opened)
      throw new Error("The stream is closed and cannot be used.");
  },
  unload: function StreamManager_unload() {
    // TextWriter.writeAsync() causes rawStream to close and therefore sets
    // opened to false, so check that we're still opened.
    if (this.opened) {
      // Calling close() on both an nsIUnicharInputStream and
      // nsIBufferedOutputStream closes their backing streams.  It also forces
      // nsIOutputStreams to flush first.
      this.rawStream.close();
      this.opened = false;
    }
  }
};

function checkCharset(charset) {
  return typeof(charset) === "string" ? charset : DEFAULT_CHARSET;
}