In Mozilla code, a stream is an object which represents access to a sequence of characters. It is not that sequence of characters, though: the characters may not all be available when you read from the stream.
Think of a water tank with a spout: the tank may be full, or it may be half-full, or it may be empty. The spout controls how much water you can get out of the tank at a time. The spout is a source of water. If we think of the water as data, then the spout represents an input stream: a controller for data coming out of something. At the same time, there may be some way to put water into the tank - say, a main water line. That water line represents an output stream: a controller for data going into something.
The actual data going in to and coming out of an object isn't usually important to a stream. Streams simply provide a way to read or write data to some object.
In basic C++ programming for a console application, we usually talk about streams to access files, or to interact with the user. Mozilla's use of streams is more complex. On the one hand, we don't interact with the user through streams. On the other hand, we use streams to access files within a ZIP archive, to store and provide data coming from websites, even to talk to other programs on the same computer through "pipes" (more on that later). We even have streams that take input from other streams and transform the data within them to something more useful.
There are streams for several different types of data storage. Each of these implements nsIInputStream.
Type | Native Class | Contract ID | Interface | How to bind to a data source |
---|---|---|---|---|
Generic | nsStorageInputStream | N/A | nsIInputStream, nsISeekableStream |
storageStream.newInputStream(); |
String (8-bit characters) | nsStringStream | @mozilla.org/io/string-input-stream;1 | nsIStringInputStream | stream.setData(data, length); |
File | nsFileInputStream | @mozilla.org/network/file-input-stream;1 | nsIFileInputStream | stream.init(file, ioFlags, perm, behaviorFlags); |
ZIP | nsJARInputStream | N/A | nsIInputStream |
zipReader.getInputStream(zipEntry); |
Similarly, each of these implements nsIOutputStream.
Type | Native Class | Contract ID | Interface | How to bind to a data target |
---|---|---|---|---|
Generic | nsStorageStream | @mozilla.org/storagestream;1 | nsIStorageStream | stream.getOutputStream(); // returns nsIOutputStream |
File | nsFileOutputStream | @mozilla.org/network/file-output-stream;1 | nsIFileOutputStream | stream.init(file, ioFlags, perm, behaviorFlags); |
File | nsSafeFileOutputStream | @mozilla.org/network/safe-file-output-stream;1 |
nsISafeFileOutputStream, nsIFileOutputStream |
stream.init(file, ioFlags, perm, behaviorFlags); |
Any implementation of nsIChannel will have an input stream as well, but unless you own the channel, you shouldn't try to read from the input stream. There are two ways of getting the input stream: by calling the channel's .open() method for a synchronous read, or by calling the channel's .asyncOpen() method with an nsIStreamListener object.
You can get an nsIChannel object from the IO Service's newChannel(spec, charset, baseURI) method or its .newChannelFromURI(uri) method.
Mozilla provides a "safe file output stream" implementation. This implementation actually writes the contents of the file you're trying to create to a temporary file. If everything goes well, calling the stream's .finish() method overwrites the original file with the new file.
Input streams are not scriptable - you cannot directly call .read() on them, for example. To solve this, there is a special nsIScriptableInputStream interface and "scriptable stream" wrapper. If you have an input stream called nativeStream, you can use code like this:
var stream = Components.classes["@mozilla.org/scriptableinputstream;1"] .createInstance(Components.interfaces.nsIScriptableInputStream); stream.init(nativeStream);
The stream provides .read(count), .available(), and .close() methods.
Output streams are usually scriptable: you can call .write(chars, chars.length) as you wish. However, it is usually better to create an input stream that you then feed to the output stream:
var outStream = Components.classes["@mozilla.org/storagestream;1"] .createInstance(Components.interfaces.nsIStorageStream) .getOutputStream(); var inStream = Components.classes["@mozilla.org/io/string-input-stream;1"] .createInstance(Components.interfaces.nsIStringInputStream); var data = "Hello World"; inStream.setData(data, data.length); while (inStream.available()) { outStream.writeFrom(inStream, inStream.available()); }
Note this is an inefficient example: the only important part is how to feed the output stream. This is also a synchronous (blocking) operation, so if you're in JavaScript, consider using NetUtil.jsm as described below.
nsIInputStream and nsIOutputStream work with 8-bit characters. However, JavaScript strings contain 16-bit characters. This can mean if you have characters beyond ASCII code 255, you risk losing data using nsStringStream, for example.
To get an input stream for JavaScript strings safely, try this.
var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"] .createInstance(Components.interfaces.nsIScriptableUnicodeConverter); converter.charset = "UTF-8"; var stream = converter.convertToInputStream(string);
There are several useful JavaScript modules at your disposal. For streams, the most obvious are NetUtil.jsm and FileUtils.jsm. NetUtil.jsm provides APIs for copying an input stream to an output stream (the asyncCopy() method), getting an input stream from another source (the asyncFetch() method), and reading an input stream into a string (the readInputStreamToString() method). FileUtils.jsm provides APIs for getting output streams for files, with the .openFileOutputStream(file, modeFlags) and .openSafeFileOutputStream(file, modeFlags) methods, and for closing those output streams with the .closeSafeFileOutputStream(inputStream) method.
As we mentioned earlier, you can use the IO service to get any channel, and from there an input stream. The IO service is available through the Services.jsm module as the .io property.
A stream listener is an object you build to let you know when there is data in a stream ready for you to consume. Most streams are asynchronous: they make no assumptions that all the data a resource provides is available immediately. (This is particularly true of streams that reach out over a network connection, like HTTP and FTP channels.)
Stream listeners implement three methods. From the nsIStreamListener interface, the .onDataAvailable(request, context, inputStream, offset, count) method gives you the input stream and the number of bytes available. The stream listener must read exactly count bytes before exiting. From the nsIRequestObserver interface, the .onStartRequest(request, context) method tells you when the request begins, while the .onStopRequest(request, context) method tells you when the request ends. A request will have one .onStartRequest(request, context) call, followed by at least one .onDataAvailable(...) call, followed by one .onStopRequest(request, context) call.
The context argument will be something passed from whoever invokes the request to the .onStartRequest(), .onDataAvailable(), and .onStopRequest() methods of the listener.
As for passing in the stream listener and starting the request: that will vary depending on the use case.
Some streams are "seekable": they let you specify where in the stream you are reading from (instead of requiring it be from the beginning). They can also report their current position in the file. Seekable streams implement the nsISeekableStream interface.
The following stream types are known to implement nsISeekableStream:
Several stream types leverage primitive stream types to do specialized work. These work by taking the input from another stream, and providing a stream interface to access that underlying stream's data.
Type | Purpose | Native Class | Contract ID | Interface | How to bind to a primitive input stream |
---|---|---|---|---|---|
Multiplex | Concatenate multiple input streams into one. | nsMultiplexInputStream | @mozilla.org/io/multiplex-input-stream;1 | nsIMultiplexInputStream |
.appendStream(stream) .insertStream(stream, index) |
Buffered | Read ahead in the underlying stream into a buffer, so that calls to the underlying stream are minimized. | nsBufferedInputStream | @mozilla.org/network/buffered-input-stream;1 | nsIBufferedInputStream | .init(stream, bufferSize) |
Binary | Read binary data from the underlying stream, in "big-endian" order. | nsBinaryInputStream | @mozilla.org/binaryinputstream;1 | nsIBinaryInputStream | .setInputStream(stream) |
Object | Read a nsISupports object from the underlying stream. | nsBinaryInputStream | @mozilla.org/binaryinputstream;1 | nsIObjectInputStream | (inherits from nsIBinaryInputStream) |
Converter | Convert Unicode characters from an underlying stream. | nsConverterInputStream | @mozilla.org/intl/converter-input-stream;1 | nsIConverterInputStream | .init(stream, charset, bufferSize, replaceChar) |
MIME | Separate headers from data. | nsMIMEInputStream | @mozilla.org/network/mime-input-stream;1 | nsIMIMEInputStream | .setData(stream) |
Similarly, there are complex output streams which build from primitive output streams:
Type | Purpose | Native Class | Contract ID | Interface | How to bind to a primitive output stream |
---|---|---|---|---|---|
Buffered | Store data in a buffer until the buffer is full or the stream closes. Then, write that data to the underlying stream. | nsBufferedOutputStream | @mozilla.org/network/buffered-output-stream;1 | nsIBufferedOutputStream | .init(stream, bufferSize) |
Binary | Write binary data to the underlying stream, in "big-endian" order. | nsBinaryOutputStream | @mozilla.org/binaryoutputstream;1 | nsIBinaryOutputStream | .setOutputStream(stream) |
Object | Write an nsISupports object to the underlying stream. | nsBinaryOutputStream | @mozilla.org/binaryoutputstream;1 | nsIObjectOutputStream | (inherits from nsIBinaryOutputStream) |
Converter | Write to an underlying stream with automatic conversion of Unicode characters. | nsConverterOutputStream | @mozilla.org/intl/converter-output-stream;1 | nsIConverterOutputStream | .init(stream, charset, bufferSize, replaceChar) |
(TBD: @mozilla.org/streamConverters;1)
Suppose you already have an input stream, and something to read from that input stream...but the reader doesn't do anything with the stream. Suppose that reader also implements nsIStreamListener. Mozilla provides an "input stream pump" component to feed data from the stream into the reader.
There are two parts: initializing the pump, and telling it to asynchronously read data into the stream listener:
var pump = Components.classes["@mozilla.org/network/input-stream-pump;1"] .createInstance(Components.interfaces.nsIInputStreamPump); pump.init(stream, -1, -1, 0, 0, true); pump.asyncRead(listener, context);
For file input, see Code Snippets: Reading from a file. For file output, see Code Snippets: Writing to a file.
For getting an input stream from a ZIP archive, see the nsIZipReader interface:
// file is an nsIFile object mapping to a ZIP archive var zipReader = Components.classes["@mozilla.org/libjar/zip-reader;1"] .createInstance(Components.interfaces.nsIZipReader); zipReader.open(file); var stream = zipReader.getInputStream("/path/to/zipped/file"); // process the stream // when we don't need the zipReader anymore zipReader.close();
For writing from an input stream to a ZIP archive, see the nsIZipWriter interface:
// file is an nsIFile object mapping to a ZIP archive var zipWriter = Components.classes["@mozilla.org/zipwriter;1"] .createInstance(Components.interfaces.nsIZipWriter); zipWriter.open(file, ioFlags); // stream is the output stream zipWriter.addEntryStream("/path/to/zipped/file", modTime, compression, stream, queueForLater); // if queued for later operations, and all operations are queued zipWriter.processQueue(); // when we don't need the zipWriter anymore zipWriter.close();
var StringStream = Components.Constructor("@mozilla.org/io/string-input-stream;1", "nsIStringInputStream", "setData"); function buildStream(data) { return new StringStream(data, data.length); } function run_test() { var str1 = buildStream("We "); var str2 = buildStream("will "); var str3 = buildStream("rock "); var str4 = buildStream("you!"); var check = "We will rock you!"; var multi = Components.classes["@mozilla.org/io/multiplex-input-stream;1"] .createInstance(Components.interfaces.nsIMultiplexInputStream); multi.appendStream(str1); multi.appendStream(str2); multi.appendStream(str3); multi.appendStream(str4); var inStream = Components.classes["@mozilla.org/scriptableinputstream;1"] .createInstance(Components.interfaces.nsIScriptableInputStream); inStream.init(multi); var data = inStream.read(inStream.available()); // check == data; }
var StringStream = Components.Constructor("@mozilla.org/io/string-input-stream;1", "nsIStringInputStream", "setData"); var InputStream = Components.Constructor("@mozilla.org/scriptableinputstream;1", "nsIScriptableInputStream", "init"); function buildStream(data) { return new StringStream(data, data.length); } Components.utils.import("resource://gre/modules/NetUtil.jsm"); function run_test() { var check = "We will rock you!"; var baseInputStream = buildStream(check); var store = Components.classes["@mozilla.org/storagestream;1"] .createInstance(Components.interfaces.nsIStorageStream); /* In practice, your storage streams shouldn't be this small. The first argument, * which represents capacity per segment, is far too small for practical use. */ store.init(64, 64, null); var out = store.getOutputStream(0); NetUtil.asyncCopy(baseInputStream, out, function(status) { if (status != Components.results.NS_OK) return; /* Due to a crash, we can't create input streams until the storage output stream * has some data in it. * * Also, baseInputStream has been completely consumed at this point, so we * shouldn't read from it anymore. (However, because baseInputStream is an * nsStringInputStream, it is also a seekable stream...so we could go to * the beginning if we wanted to.) */ var str1 = store.newInputStream(0); var d1 = new InputStream(str1); // d1 has a complete copy of baseInputStream's original contents. var d2 = new InputStream(store.newInputStream(0)); // d2 has a complete copy of baseInputStream's original contents. }); }