/* ************************************************************************ # # DivConq # # http://divconq.com/ # # Copyright: # Copyright 2014 eTimeline, LLC. All rights reserved. # # License: # See the license.txt file in the project's top-level directory for details. # # Authors: # * Andy White # ************************************************************************ */ package divconq.lang; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import divconq.io.IReader; import divconq.lang.chars.Utf8Decoder; import divconq.lang.chars.Utf8Encoder; import divconq.lang.op.OperationContext; import divconq.util.ArrayUtil; import divconq.util.StringUtil; /** * Memory is a flexible way to manipulate bytes or characters in RAM. Memory * is auto-expanding, read and write, and takes both byte and character input * (UTF-9) natively * * @author Andy * */ public class Memory implements IReader { protected int chunkSize = 4096; protected List<MemoryChunk> buffers = new ArrayList<MemoryChunk>(); protected int length = 0; // effective length of content, the capacity is total of all buffers protected int position = 0; protected Utf8Decoder decoder = null; public Memory() { } /** * @param chunkSize initial size, how much to grow by as memory auto-expands */ public Memory(int chunkSize) { this.chunkSize = chunkSize; } /** * Use for view on a now static memory chunk. Position is independent of original, but buffers are shared. * * @param memstream memory to create a view of, shares memory but will not expand with original */ public Memory(Memory memstream) { if (memstream == null) return; for (MemoryChunk mc : memstream.buffers) this.buffers.add(new MemoryChunk(mc)); this.length = memstream.length; //this.position = memstream.position; this.chunkSize = memstream.chunkSize; } /** * @param firstbuffer initial bytes */ public Memory(byte[] firstbuffer) { this.chunkSize = firstbuffer.length; this.length = chunkSize; this.position = firstbuffer.length; this.buffers.add(new MemoryChunk(firstbuffer)); } /** * @param firstbuffer initial bytes * @param length cropped to */ public Memory(byte[] firstbuffer, int length) { this.chunkSize = firstbuffer.length; if (length < 0) length = 0; this.length = length; this.position = length; this.buffers.add(new MemoryChunk(firstbuffer, length)); } public Memory(CharSequence chars) { // TODO this could be more efficient, see write below this(Utf8Encoder.encode(chars)); } /** * @return how much does memory grow by when current buffer is used up */ public int getChunkSize() { return this.chunkSize; } protected void allocateNext() { this.buffers.add(new MemoryChunk(this.chunkSize)); } /** * @return what is the total allocated memory so far */ public int getCapacity() { return this.buffers.size() * this.chunkSize; } /** * @param value force a greater allocation of memory (does not reduce) */ public void setCapacity(int value) { while ((this.buffers.size() * this.chunkSize) < value) this.allocateNext(); } /** * @return how many bytes have been written */ public int getLength() { return this.length; } /** * @return byte currently pointing to for a read or a write */ public int getPosition() { return this.position; } /** * @param value which byte to use for the next read or write */ public void setPosition(int value) { if (value < 0) value = 0; this.setCapacity(value); // if needed, grow capacity to accommodate position this.position = value; } public int readableBytes() { return this.length - this.position; } protected MemoryChunk linkPositionToBuffer() { int idx = (int)(this.position / this.chunkSize); if (idx >= this.buffers.size()) return null; MemoryChunk chunk = this.buffers.get(idx); chunk.setPosition((int)(this.position % this.chunkSize)); return chunk; } /** * Read from current position in memory into a byte array. Increments the * the position accordingly. * * @param buffer destination for copy * @param offset within destination * @param count of bytes to copy * @return number of bytes that were copied, less than count if end of memory was hit */ public int read(byte[] buffer, int offset, int count) { if (buffer == null) return 0; if ((offset < 0) || (count <= 0)) return 0; if ((buffer.length - offset) < count) return 0; if ((this.position + count) > this.length) count = this.length - this.position; int left = count; do { MemoryChunk chunk = this.linkPositionToBuffer(); if (chunk == null) break; int read = chunk.read(buffer, offset, left); if (read == 0) break; left -= read; this.position += read; offset += read; } while (left > 0); return count; } /** * Read the byte at current position and increment position. * * @return byte read or -1 if end of memory */ public int readByte() { if (this.position >= this.length) return -1; MemoryChunk chunk = this.linkPositionToBuffer(); if (chunk == null) return -1; int b = chunk.readByte(); this.position++; return b; } /** * Read the byte(s) at current position until a full character (utf-8) has been * read. Increment position accordingly. * * @return character read or -1 if end of memory */ public int readChar() { int b = this.readByte(); // decoder is created on demand only if (this.decoder == null) this.decoder = new Utf8Decoder(); else this.decoder.reset(); // in case last call ended mid character (see readCharRestricted) try { while (b != -1) { if (!this.decoder.readByteNeedMore((byte)b, true)) return this.decoder.getCharacterAndReset(); b = this.readByte(); } } catch (Exception x) { // TODO } return -1; } @Override public void close() { } /** * Read the bytes at current position until a full character (utf-8) has been * read. Increment position accordingly. Keep reading until end or number * of bytes has been read. This method will not leave memory position * part way through a string, but return to start of character that exceeds * "bytes". Special characters ", \, \n, \t all count as 2 chars in case * they are escaped later. * * @param bytes max bytes (not chars) to read * @return character read or -1 if end of memory */ public CharSequence readCharsCapped(int bytes) { StringBuilder sb = new StringBuilder(); if (bytes < 1) return sb; int finalpos = this.position + bytes; int charstartpos = this.position; int b = this.readByte(); // decoder is created on demand only if (this.decoder == null) this.decoder = new Utf8Decoder(); else this.decoder.reset(); // in case last call ended mid character try { while ((b != -1) && (this.position < finalpos)) { if (!this.decoder.readByteNeedMore((byte)b, true)) { char ch = (char)this.decoder.getCharacterAndReset(); sb.append(ch); charstartpos = this.position; if ((ch == '\\') || (ch == '\"') || (ch == '\n') || (ch == '\t')) finalpos--; // reign in for escape chars } b = this.readByte(); } if (this.position >= finalpos) this.position = charstartpos; } catch (Exception x) { // TODO } return sb; } /** * Copy the entire contents of a byte array into Memory. Increment position accordingly. * * @param buffer source bytes * @return number of bytes written */ public int write(byte[] buffer) { if (buffer != null) return this.write(buffer, 0, buffer.length); return 0; } /** * Copy the part of the contents of a byte array into Memory. Increment position accordingly. * * @param buffer source bytes * @param offset in source * @param count number of bytes to copy * @return number of bytes written */ public int write(byte[] buffer, int offset, int count) { if (buffer == null) return 0; if ((offset < 0) || (count <= 0)) return 0; if (buffer.length - offset < count) return 0; this.setCapacity(this.position + count); // ensure we can write this int left = count; do { MemoryChunk chunk = this.linkPositionToBuffer(); if (chunk == null) break; int write = (int)(this.chunkSize - chunk.getPosition()); if (write > left) write = left; write = chunk.write(buffer, offset, write); if (write < 1) break; left -= write; this.position += write; offset += write; } while (left > 0); if (this.position > this.length) this.length = this.position; return count - left; } /** * Copy the entire contents of a ByteBuffer into Memory. Increment position accordingly. * * @param buffer source buffer * @return number of bytes written */ public int write(ByteBuffer buffer) { if (buffer == null) return 0; int count = buffer.limit() - buffer.position(); this.setCapacity(this.position + count); // ensure we can write this int left = count; do { MemoryChunk chunk = this.linkPositionToBuffer(); if (chunk == null) break; int write = chunk.write(buffer); if (write < 1) break; left -= write; this.position += write; } while (left > 0); if (this.position > this.length) this.length = this.position; return count - left; } /** * Copy the entire contents of an InputStream into Memory. Increment position accordingly. * * @param s stream to read from * @return number of bytes written * * @throws IOException if stream is bad */ public int copyFromStream(InputStream s) throws IOException { return this.copyFromStream(s, Integer.MAX_VALUE); // read any amount } /** * Copy the some of the contents of an InputStream into Memory. Increment position accordingly. * * @param s stream to read from * @param count number of bytes to read from stream * @return number of bytes written * * @throws IOException if stream is bad */ public int copyFromStream(InputStream s, int count) throws IOException { if ((s == null) || (count <= 0)) return 0; int left = count; int amt = 0; byte[] buff = new byte[4096]; while (left > 0) { amt = s.read(buff, 0, (left > buff.length) ? buff.length : left); if (amt <= 0) break; this.write(buff, 0, amt); left -= amt; } return count - left; } /** * Write a single byte into Memory. Increment position accordingly. * * @param value byte to write * @return true if able to write */ public boolean writeByte(byte value) { this.setCapacity(this.position + 1); MemoryChunk chunk = this.linkPositionToBuffer(); if (chunk == null) return false; if (!chunk.writeByte(value)) return false; this.position++; if (this.position > this.length) this.length = this.position; return true; } /** * Write a single character into Memory as UTF-8. Increment position accordingly. * * @param ch character to write */ public void writeChar(int ch) { if (StringUtil.isRestrictedChar(ch)) { OperationContext.get().error("MEMORY got a bad char"); // TODO throw exception or something return; } this.write(Utf8Encoder.encode(ch)); } /** * Write a string into Memory as UTF-8. Increment position accordingly. * * @param str string to write */ public void write(CharSequence str) { if (StringUtil.containsRestrictedChars(str)) { OperationContext.get().error("MEMORY got a bad string"); // TODO throw exception or something return; } // TODO this could be more efficient - encode <= 64 bytes at a time and add this.write(Utf8Encoder.encode(str)); } public void writeLine(CharSequence str) { if (StringUtil.containsRestrictedChars(str)) { OperationContext.get().error("MEMORY got a bad string"); // TODO throw exception or something return; } // TODO this could be more efficient - encode <= 64 bytes at a time and add this.write(Utf8Encoder.encode(str)); this.writeChar('\n'); } public void writeLine() { this.writeChar('\n'); } /** * Change the apparent number of bytes written. If less than current length then * bytes are truncated. If greater than current then new bytes are added with * what ever value may have previously been in them (be careful). * * @param value number of bytes we claim to contain */ public void setLength(int value) { if (value < 0) value = 0; if (value > this.getCapacity()) this.setCapacity(value); if (value < this.position) this.position = value; this.length = value; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return this.toChars().toString(); } /** * Assumes the entire content is UTF-8 characters and formats them into a string. * Ignores position completely. * * @return memory as string */ public CharSequence toChars() { int oldpos = this.getPosition(); StringBuilder32 sb = new StringBuilder32(); // decoder is created on demand only if (this.decoder == null) this.decoder = new Utf8Decoder(); else this.decoder.reset(); // in case last call ended mid character this.setPosition(0); try { int b = this.readByte(); while (b != -1) { if (!this.decoder.readByteNeedMore((byte)b, true)) sb.append(this.decoder.getCharacterAndReset()); b = this.readByte(); } } catch (Exception x) { // TODO } this.setPosition(oldpos); return sb; } /** * Copies all of Memory into a single byte array. Ignores position. * * @return all bytes from Memory */ public byte[] toArray() { int count = this.length; if (count < 0) count = 0; byte[] outBuffer = new byte[count]; if (count == 0) return outBuffer; int dstpos = 0; for (MemoryChunk chunk : this.buffers) { int write = Math.min(chunk.getLength(), count - dstpos); if (!ArrayUtil.blockCopy(chunk.getBuffer(), 0, outBuffer, dstpos, write)) break; dstpos += write; } return outBuffer; } /** * Internal * * @param pos entry to get * @return bytes from entry */ public byte[] getBufferEntry(int pos) { return this.buffers.get(pos).getBuffer(); } /** * Access (read) Memory as an Input Stream * * @return the stream */ public InputStream getInputStream() { return new InputStream() { @Override public int read() throws IOException { return Memory.this.readByte(); } }; } /** * Copy Memory from current position forward into an output stream. * * @param stream destination stream * @return bytes copied */ public long copyToStream(OutputStream stream) { return this.copyToStream(stream, Long.MAX_VALUE); } /** * Copy Memory from current position forward into an output stream. * * @param stream destination stream * @param count number of bytes to copy into stream * @return bytes copied */ public long copyToStream(OutputStream stream, long count) { if ((stream == null) || (count <= 0) || (this.position >= this.length)) return 0; if (this.position > (this.length - count)) count = this.length - this.position; long left = count; do { MemoryChunk chunk = this.linkPositionToBuffer(); if (chunk == null) break; long write = chunk.copyToStream(stream, left); if (write == 0) break; left -= write; this.position += write; } while (left > 0); try { stream.flush(); } catch (IOException e) { // TODO logging } return count; } }