// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.util.io;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import org.infinity.util.Misc;
/**
* Collection of useful stream- and buffer-based operations.
*/
public class StreamUtils
{
/** Attempts to replace the file extension string of {@code fileName} by {@code newExt}. */
public static String replaceFileExtension(String fileName, String newExt)
{
String retVal = fileName;
if (retVal != null) {
if (newExt == null) {
newExt = "";
}
// making sure that our 'dot' belongs to the filename's extension
Path file = FileManager.resolve(retVal);
String name = file.getFileName().toString();
int pos = name.lastIndexOf('.');
if (pos > 0) {
pos = retVal.lastIndexOf('.');
if (pos > 0) {
retVal = retVal.substring(0, pos);
}
}
if (newExt.length() > 0 && newExt.charAt(0) != '.') {
retVal += ".";
}
retVal += newExt;
}
return retVal;
}
/** Attempts to replace the file extension string of {@code file} by {@code newExt}. */
public static Path replaceFileExtension(Path file, String newExt)
{
Path retVal = file;
if (file != null) {
if (newExt == null) {
newExt = "";
}
String name = file.getFileName().toString();
int pos = name.lastIndexOf('.');
if (pos > 0) {
// no need to replace if extensions are equal
if (newExt.length() > 0 && newExt.charAt(0) == '.') {
if (name.substring(pos).equalsIgnoreCase(newExt)) {
return retVal;
}
} else {
if (name.substring(pos+1).equalsIgnoreCase(newExt)) {
return retVal;
}
}
name = name.substring(0, pos);
}
if (newExt.length() > 0 && newExt.charAt(0) != '.') {
name += ".";
}
name += newExt;
if (file.getParent() != null) {
retVal = file.getParent().resolve(name);
} else {
retVal = file.getFileSystem().getPath(name);
}
}
return retVal;
}
/**
* Splits {@code fileName} into a path, base and extension part and returns them as String array.
* @param fileName The file name to split into its components.
* @return A String array that always consists of three components.
* String[0] contains the path component. Can be empty (but is never {@code null}).
* String[1] contains the file base without path and extension.
* String[2] contains the file extension. Can be empty (but is never {@code null}).
* The concatenated string components are equal to the original {@code fileName} if
* {@code fileName} is not {@code null}.
*/
public static String[] splitFileName(String fileName)
{
String[] retVal = {"", "", ""};
if (fileName != null) {
String temp = fileName.replace('\\', '/').replace(':', '/');
// splitting path
int p = temp.lastIndexOf('/');
if (p >= 0) {
retVal[0] = fileName.substring(0, p);
if (p+1 < temp.length()) {
temp = temp.substring(p+1);
} else {
temp = "";
}
}
// splitting file base
p = temp.lastIndexOf('.');
if (p > 0) { // p == 0 ? extension is file base
retVal[1] = temp.substring(0, p);
if (p < temp.length()) {
temp = temp.substring(p);
} else {
temp = "";
}
}
// determining file extension
if (temp.length() > 0) {
retVal[2] = temp;
}
}
return retVal;
}
/** Returns a fully initialized empty {@link ByteBuffer} in little endian order. */
public static ByteBuffer getByteBuffer(int size)
{
return ByteBuffer.allocate(Math.max(0, size)).order(ByteOrder.LITTLE_ENDIAN);
}
/** Returns a {@link ByteBuffer} based on {@code buffer} in little endian order. */
public static ByteBuffer getByteBuffer(byte[] buffer)
{
if (buffer == null) {
buffer = new byte[0];
}
return ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
}
/**
* Convenience method: Returns an input stream for the specified file.
* OpenOptions used: {@code StandardOpenOption.READ}
*/
public static InputStream getInputStream(Path file) throws IOException
{
return getInputStream(file, StandardOpenOption.READ);
}
/**
* Convenience method: Returns an input stream for the specified file using the specified
* {@link OpenOption}s.
*/
public static InputStream getInputStream(Path file, OpenOption... options) throws IOException
{
return new BufferedInputStream(Files.newInputStream(file, StandardOpenOption.READ));
}
/**
* Convenience method: Returns an output stream for the specified file.
* OpenOptions used: {@code StandardOpenOption.WRITE} and {@code StandardOpenOption.CREATE}.
*/
public static OutputStream getOutputStream(Path file) throws IOException
{
return getOutputStream(file, false);
}
/**
* Convenience method: Returns an output stream for the specified file.
* OpenOptions used: {@code StandardOpenOption.WRITE}, {@code StandardOpenOption.CREATE} and
* {@code StandardOpenOption.TRUNCATE_EXISTING) (if truncate is {@code true}).
*
*/
public static OutputStream getOutputStream(Path file, boolean truncate) throws IOException
{
OpenOption[] options = new OpenOption[truncate ? 3 : 2];
options[0] = StandardOpenOption.WRITE;
options[1] = StandardOpenOption.CREATE;
if (truncate) {
options[2] = StandardOpenOption.TRUNCATE_EXISTING;
}
return getOutputStream(file, options);
}
/**
* Convenience method: Returns an output stream for the specified file using the specified
* {@link OpenOption}s.
*/
public static OutputStream getOutputStream(Path file, OpenOption... options) throws IOException
{
return new BufferedOutputStream(Files.newOutputStream(file, options));
}
/**
* Copies data from {@code src} buffer to {@code dst} buffer using their current positions as offsets.
* Current positions of byte buffers will be advanted to the end of copied data.
*/
public static int copyBytes(ByteBuffer src, ByteBuffer dst, int length)
{
int retVal = copyBytes(src, src.position(), dst, dst.position(), length);
src.position(src.position() + retVal);
dst.position(dst.position() + retVal);
return retVal;
}
/**
* Copies data from {@code src} buffer to {@code dst} buffer using the specified offsets.
* Current positions of byte buffers are unaffected.
*/
public static int copyBytes(ByteBuffer src, int srcOffset, ByteBuffer dst, int dstOffset, int length)
{
int srcPos = src.position();
int dstPos = dst.position();
int maxLength = 0;
try {
src.position(srcOffset);
dst.position(dstOffset);
maxLength = Math.min(length, Math.min(src.remaining(), dst.remaining()));
ByteBuffer bufTmp = src.duplicate(); // to preserve limit
bufTmp.limit(bufTmp.position() + maxLength);
dst.put(bufTmp);
} catch (Throwable t) {
t.printStackTrace();
} finally {
src.position(srcPos);
dst.position(dstPos);
}
return maxLength;
}
/**
* Reads "length" number of bytes from the specified input stream and returns them
* as new {@link ByteBuffer} object.
*/
public static ByteBuffer readBytes(InputStream is, int length) throws IOException
{
ByteBuffer bb = null;
if (length > 0) {
bb = getByteBuffer(length);
readBytes(is, bb);
} else {
bb = getByteBuffer(0);
}
bb.position(0);
return bb;
}
/**
* Reads as many bytes from the input stream into the given ByteBuffer, starting at current
* buffer position and ending at the current buffer's limit.
*/
public static void readBytes(InputStream is, ByteBuffer buffer) throws IOException
{
byte[] buf = new byte[8192];
while (buffer.remaining() > 0) {
int len = Math.min(buf.length, buffer.remaining());
int n = is.read(buf, 0, len);
if (n < 0) {
break;
}
buffer.put(buf, 0, n);
}
}
/** Reads as many bytes from the input stream into the specified byte array. */
public static void readBytes(InputStream is, byte[] buffer) throws IOException
{
readBytes(is, buffer, 0, buffer.length);
}
/**
* Reads up to "length" bytes of data from the input stream into the given byte array,
* starting at the specified offset.
*/
public static void readBytes(InputStream is, byte[] buffer, int offset, int length) throws IOException
{
int bytesRead = 0;
offset = Math.max(0, Math.min(offset, length));
length = Math.min(buffer.length - offset, length);
while (bytesRead < length) {
int newRead = is.read(buffer, offset + bytesRead, length - bytesRead);
if (newRead == -1) {
throw new IOException("Unable to read remaining " + (buffer.length - bytesRead) + " bytes");
}
bytesRead += newRead;
}
}
/**
* Reads a byte (8 bit) from specified input stream.
* @param is The input stream to read from.
* @return The byte value from the stream.
*/
public static byte readByte(InputStream is) throws IOException
{
byte res = 0;
if (is != null) {
int n = is.read();
if (n != -1) {
res = (byte)n;
}
}
return res;
}
/**
* Reads an unsigned byte (8 bit) from specified input stream.
* @param is The input stream to read from.
* @return The unsigned byte value from the stream.
*/
public static short readUnsignedByte(InputStream is) throws IOException
{
return (short)(readByte(is) & 0xff);
}
/**
* Reads a short (16 bit) from the specified byte channel.
* @param channel The {@link ReadableByteChannel} to read from.
* @return The short value from the channel.
*/
public static short readShort(ReadableByteChannel channel) throws IOException
{
ByteBuffer bb2 = getByteBuffer(2);
for (int cnt = 0; cnt < 2;) {
int n = channel.read(bb2);
if (n < 0) {
throw new IOException("End of stream");
}
cnt += n;
}
bb2.position(0);
return bb2.getShort();
}
/**
* Reads a short (16 bit) from specified input stream.
* @param is The input stream to read from.
* @return The short value from the stream.
*/
public static short readShort(InputStream is) throws IOException
{
ByteBuffer bb2 = getByteBuffer(2);
for (int cnt = 0; cnt < 2;) {
int n = is.read(bb2.array(), cnt, bb2.array().length - cnt);
if (n < 0) {
throw new IOException("End of stream");
}
cnt += n;
}
bb2.position(0);
return bb2.getShort();
}
/**
* Reads an unsigned short (16 bit) from specified input stream.
* @param is The input stream to read from.
* @return The unsigned short value from the stream.
*/
public static int readUnsignedShort(InputStream is) throws IOException
{
return readShort(is) & 0xffff;
}
/**
* Reads an integer (32 bit) from the specified byte channel.
* @param channel The {@link ReadableByteChannel} to read from.
* @return The integer value from the channel.
*/
public static int readInt(ReadableByteChannel channel) throws IOException
{
ByteBuffer bb4 = getByteBuffer(4);
bb4.position(0);
for (int cnt = 0; cnt < 4;) {
int n = channel.read(bb4);
if (n < 0) {
throw new IOException("End of stream");
}
cnt += n;
}
bb4.position(0);
return bb4.getInt();
}
/**
* Reads an integer (32 bit) from specified input stream.
* @param is The input stream to read from.
* @return The integer value from the stream.
*/
public static int readInt(InputStream is) throws IOException
{
ByteBuffer bb4 = getByteBuffer(4);
for (int cnt = 0; cnt < 4;) {
int n = is.read(bb4.array(), cnt, bb4.array().length - cnt);
if (n < 0) {
throw new IOException("End of stream");
}
cnt += n;
}
bb4.position(0);
return bb4.getInt();
}
/**
* Reads an unsigned integer (32 bit) from specified input stream.
* @param is The input stream to read from.
* @return The unsigned integer value from the stream.
*/
public static long readUnsignedInt(InputStream is) throws IOException
{
return (long)readInt(is) & 0xffffffffL;
}
/**
* Reads an 24 bit integer from specified input stream.
* @param is The input stream to read from.
* @return The 24 bit integer value from the stream.
*/
public static int readInt24(InputStream is) throws IOException
{
return signExtend(readUnsignedInt24(is), 24);
}
/**
* Reads an unsigned 24 bit integer from specified input stream.
* @param is The input stream to read from.
* @return The unsigned 24 bit integer value from the stream.
*/
public static int readUnsignedInt24(InputStream is) throws IOException
{
ByteBuffer bb4 = getByteBuffer(4);
for (int cnt = 0; cnt < 3;) {
int n = is.read(bb4.array(), cnt, bb4.array().length - cnt - 1);
if (n < 0) {
throw new IOException("End of stream");
}
cnt += n;
}
bb4.position(0);
return bb4.getInt() & 0xffffff;
}
/**
* Sign extends the specified value consisting of specified number of bits.
* @param value The value to sign-extend
* @param bits Size of {@code value} in bits.
* @return A sign-extended version of {@code value}.
*/
public static int signExtend(int value, int bits)
{
return (value << (32 - bits)) >> (32 - bits);
}
/**
* Reads a string of given length from the specified byte channel using the
* default charset "windows-1252".
* @param channel The {@link ReadableByteChannel} to read from.
* @param length Number of bytes to read.
* @return The resulting String.
*/
public static String readString(ReadableByteChannel channel, int length) throws IOException
{
return readString(channel, length, Misc.CHARSET_DEFAULT);
}
/**
* Reads a string of given length and character set from the specified byte channel.
* @param channel The {@link ReadableByteChannel} to read from.
* @param length Number of bytes to read.
* @param charset The charset used to encode bytes into characters.
* @return The resulting String.
*/
public static String readString(ReadableByteChannel channel, int length, Charset charset) throws IOException
{
if (length > 0) {
ByteBuffer bb = ByteBuffer.wrap(new byte[length]);
for (int cnt = 0; cnt < length;) {
int n = channel.read(bb);
if (n < 0) {
throw new IOException("End of stream");
}
cnt += n;
}
return new String(bb.array(), charset);
} else {
return "";
}
}
public static String readString(ByteBuffer buffer, int length)
{
return readString(buffer, buffer.position(), length, Misc.CHARSET_DEFAULT);
}
/**
* Reads a string of given length from the specified {@link ByteBuffer}, starting at the
* specified offset using the default charset "windows-1252".
* @param buffer The buffer to read from.
* @param offset Start offset in buffer.
* @param length Number of bytes to read.
* @return The resulting String.
*/
public static String readString(ByteBuffer buffer, int offset, int length)
{
return readString(buffer, offset, length, Misc.CHARSET_DEFAULT);
}
/**
* Reads a string of given length and character set from the specified {@link ByteBuffer}.
* @param buffer The buffer to read from.
* @param length Number of bytes to read.
* @param charset The charset used to encode bytes into characters.
* @return The resulting String.
*/
public static String readString(ByteBuffer buffer, int length, Charset charset)
{
return readString(buffer, buffer.position(), length, charset);
}
/**
* Reads a string of given length and character set from the specified {@link ByteBuffer},
* starting at the specified offset.
* @param buffer The buffer to read from.
* @param offset Start offset in buffer.
* @param length Number of bytes to read.
* @param charset The charset used to encode bytes into characters.
* @return The resulting String.
*/
public static String readString(ByteBuffer buffer, int offset, int length, Charset charset)
{
if (length > 0 && offset >= 0 && offset < buffer.limit()) {
buffer.position(offset);
length = Math.min(length, buffer.remaining());
byte[] buf = new byte[length];
buffer.get(buf);
for (int i = 0; i < buf.length; i++) {
if (buf[i] == 0) {
length = i;
break;
}
}
return new String(buf, 0, length, charset);
}
return "";
}
/**
* Reads a string of given length from the specified input stream, using the default
* charset "windows-1252".
* @param is The input stream to read from.
* @param length Number of bytes to read.
* @return The resulting String.
*/
public static String readString(InputStream is, int length) throws IOException
{
return readString(is, length, Misc.CHARSET_DEFAULT);
}
/**
* Reads a string of given length and charset from the specified input stream.
* @param is The input stream to read from.
* @param length Number of bytes to read.
* @param charset The charset used to encode bytes into characters.
* @return The resulting String.
*/
public static String readString(InputStream is, int length, Charset charset) throws IOException
{
ByteBuffer buffer = readBytes(is, length);
return readString(buffer, length, charset);
}
/**
* Writes the buffer content to the output stream.
* @param os The output stream.
* @param buffer The buffer to write.
*/
public static void writeBytes(OutputStream os, byte[] buffer) throws IOException
{
os.write(buffer);
}
/**
* Writes the buffer content to the output stream.
* @param os The output stream.
* @param buffer The ByteBuffer.
*/
public static void writeBytes(OutputStream os, ByteBuffer buffer) throws IOException
{
WritableByteChannel ch = Channels.newChannel(os);
ch.write(buffer);
}
/**
* Writes all available data from the input stream to the output stream.
* @param os The output stream.
* @param is The input stream
* @return Actual number of bytes written.
*/
public static long writeBytes(OutputStream os, InputStream is) throws IOException
{
return writeBytes(os, is, -1L);
}
/**
* Writes all available data from the input stream to the output stream.
* @param os The output stream.
* @param is The input stream
* @param length Max. number of bytes to write. Specify {@code -1L} to ignore length.
* @return Actual number of bytes written.
*/
public static long writeBytes(OutputStream os, InputStream is, long length) throws IOException
{
long retVal = 0L;
byte[] buffer = new byte[8192];
while (length < 0L || retVal < length) {
int numBytes = (length < 0) ? buffer.length : (int)Math.min(buffer.length, length - retVal);
int n = is.read(buffer, 0, numBytes);
if (n < 0) {
break;
}
os.write(buffer, 0, n);
retVal += n;
}
return retVal;
}
/**
* Writes the specified byte value 'count' times to the output stream.
* @param os The output stream.
* @param value The value to write.
* @param count The number of times to write 'value'.
*/
public static void writeBytes(OutputStream os, byte value, int count) throws IOException
{
while (count-- > 0) {
os.write(value);
}
}
/**
* Writes a byte (8 bit) to specified output stream.
* @param out The output stream to write to.
* @param value The value to write.
* @return The actual number of bytes written.
*/
public static int writeByte(OutputStream out, byte value) throws IOException
{
int res = 0;
if (out != null) {
out.write(value);
res++;
}
return res;
}
/**
* Writes a short (16 bit) to specified output stream.
* @param out The output stream to write to.
* @param value The value to write.
* @return The actual number of bytes written.
*/
public static int writeShort(OutputStream out, short value) throws IOException
{
int res = 0;
if (out != null) {
for (int i = 0, shift = 0; i < 2; i++, shift+=8) {
out.write((value >>> shift) & 0xff);
res++;
}
}
return res;
}
/**
* Writes an integer (32 bit) to the specified byte channel.
* @param channel The {@link WritableByteChannel} to write to.
* @param value The value to write.
*/
public static void writeInt(WritableByteChannel channel, int value) throws IOException
{
ByteBuffer bb4 = getByteBuffer(4);
bb4.position(0);
bb4.putInt(value);
bb4.position(0);
for (int cnt = 0; cnt < 4;) {
int n = channel.write(bb4);
cnt += n;
}
}
/**
* Writes an integer (32 bit) to specified output stream.
* @param out The output stream to write to.
* @param value The value to write.
* @return The actual number of bytes written.
*/
public static int writeInt(OutputStream out, int value) throws IOException
{
int res = 0;
if (out != null) {
for (int i = 0, shift = 0; i < 4; i++, shift+=8) {
out.write((value >>> shift) & 0xff);
res++;
}
}
return res;
}
/**
* Writes a 24 bit integer to specified output stream.
* @param out The output stream to write to.
* @param value The value to write.
* @return The actual number of bytes written.
*/
public static int writeInt24(OutputStream out, int value) throws IOException
{
int res = 0;
if (out != null) {
for (int i = 0, shift = 0; i < 3; i++, shift+=8) {
out.write((value >>> shift) & 0xff);
res++;
}
}
return res;
}
/**
* Writes at least "length" bytes of the specified string into the output stream using the
* default charset "windows-1252".
* @param os The output stream to write to.
* @param s The string to write.
* @param length The minimum number of bytes to write. A shorter string will be padded with null bytes.
* @return The number of bytes written to the stream.
*/
public static int writeString(OutputStream os, String s, int length) throws IOException
{
return writeString(os, s, length, Misc.CHARSET_DEFAULT);
}
/**
* Writes at least "length" bytes of the specified string into the output stream.
* @param os The output stream to write to.
* @param s The string to write.
* @param length The minimum number of bytes to write. A shorter string will be padded with null bytes.
* @param charset The characet set used to convert characters into bytes.
* @return The number of bytes written to the stream.
*/
public static int writeString(OutputStream os, String s, int length, Charset charset) throws IOException
{
byte[] stringBytes = s.getBytes(charset);
writeBytes(os, stringBytes);
if (length > stringBytes.length) {
byte buffer[] = new byte[length - stringBytes.length];
writeBytes(os, buffer);
}
return Math.max(length, stringBytes.length);
}
/** Returns the content of {@code buffer} as byte array. */
public static byte[] toArray(ByteBuffer buffer)
{
byte[] retVal = null;
try {
retVal = buffer.array();
} catch (Throwable t) {
}
if (retVal == null || retVal.length != buffer.limit()) {
retVal = new byte[buffer.limit()];
int pos = buffer.position();
buffer.position(0);
buffer.get(retVal);
buffer.position(pos);
}
return retVal;
}
}