/* ************************************************************************
#
# 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.ctp.f;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import io.netty.buffer.ByteBuf;
import divconq.ctp.CtpCommand;
import divconq.ctp.CtpConstants;
import divconq.ctp.IStreamCommand;
import divconq.hub.Hub;
import divconq.lang.chars.Utf8Decoder;
import divconq.lang.chars.Utf8Encoder;
import divconq.util.IOUtil;
public class BlockCommand extends CtpCommand implements IStreamCommand {
public Map<Integer, byte[]> headers = new HashMap<>();
public ByteBuf data = null;
public boolean eof = false;
public ByteBuf getData() {
return this.data;
}
public void setData(ByteBuf v) {
this.data = v;
}
public void setIsFolder(boolean v) {
this.headers.put(CtpConstants.CTP_F_ATTR_IS_FOLDER, v ? new byte[] { 0x01 } : new byte[] { 0x00 } );
}
public boolean isFolder() {
byte[] attr = headers.get(CtpConstants.CTP_F_ATTR_IS_FOLDER);
if (attr == null)
return false;
return attr[0] == 0x01;
}
public void setSize(long size) {
this.headers.put(CtpConstants.CTP_F_ATTR_SIZE, IOUtil.longToByteArray(size));
}
public long getSize() {
byte[] attr = headers.get(CtpConstants.CTP_F_ATTR_SIZE);
if (attr == null)
return 0;
return IOUtil.byteArrayToLong(attr);
}
public void setModTime(long millis) {
this.headers.put(CtpConstants.CTP_F_ATTR_MODTIME, IOUtil.longToByteArray(millis));
}
public void setPermissions(int v) {
this.headers.put(CtpConstants.CTP_F_ATTR_PERMISSIONS, IOUtil.intToByteArray(v));
}
public void setOffset(long v) {
this.headers.put(CtpConstants.CTP_F_ATTR_FILE_OFFSET, IOUtil.longToByteArray(v));
}
public void setPath(CharSequence v) {
this.headers.put(CtpConstants.CTP_F_ATTR_PATH, Utf8Encoder.encode(v));
}
public String getPath() {
byte[] attr = headers.get(CtpConstants.CTP_F_ATTR_PATH);
if (attr == null)
return null;
return Utf8Decoder.decode(attr).toString();
}
public void setEof(boolean v) {
this.eof = v;
}
public BlockCommand() {
super(CtpConstants.CTP_F_CMD_STREAM_BLOCK);
}
@Override
public ByteBuf encode() {
int size = 1 + 1; // code + CTP_F_BLOCK_TYPE_
int type = 0;
int hdrcnt = this.headers.size();
if (hdrcnt > 0) {
type |= CtpConstants.CTP_F_BLOCK_TYPE_HEADER;
size += hdrcnt * 4 + 2; // each header is at least 4 bytes (2 attr, 2 len) + ATTR_END is 2 bytes
for (byte[] val : this.headers.values())
size += val.length;
}
if (this.data != null) {
type |= CtpConstants.CTP_F_BLOCK_TYPE_CONTENT;
size += 3 + this.data.readableBytes();
}
if (this.eof)
type |= CtpConstants.CTP_F_BLOCK_TYPE_EOF;
// build buffer
ByteBuf bb = Hub.instance.getBufferAllocator().buffer(size);
bb.writeByte(this.cmdCode);
bb.writeByte(type);
// add header if any
if (hdrcnt > 0) {
for (Entry<Integer, byte[]> val : this.headers.entrySet()) {
bb.writeShort(val.getKey());
bb.writeShort(val.getValue().length);
bb.writeBytes(val.getValue());
}
// end of list of headers
bb.writeShort(CtpConstants.CTP_F_ATTR_END);
}
// add content if any
if (this.data != null) {
bb.writeLong(this.streamOffset);
bb.writeMedium(this.data.readableBytes());
bb.writeBytes(this.data);
}
return bb;
}
@Override
public void release() {
if (this.data != null)
this.data.release();
}
public void copyAttributes(BlockCommand cmd) {
for (Entry<Integer, byte[]> attr : cmd.headers.entrySet())
this.headers.put(attr.getKey(), attr.getValue());
this.eof = cmd.eof;
}
public void copyAttributes(FileDescriptor file) {
for (Entry<Integer, byte[]> attr : file.headers.entrySet())
this.headers.put(attr.getKey(), attr.getValue());
this.eof = file.eof;
}
////////////////////////////////////////////////////////////////////
// decode support
////////////////////////////////////////////////////////////////////
enum State {
BLOCK_TYPE,
HEADER_ATTR,
HEADER_SIZE,
HEADER_VALUE,
STREAM_OFFSET,
PAYLOAD_SIZE,
PAYLOAD,
DONE
}
protected State state = State.BLOCK_TYPE;
protected int blocktype = 0;
protected boolean skipHeaders = false;
protected boolean skipPayload= false;
protected int currattr = 0;
protected int currasize = 0;
protected long streamOffset = 0;
protected int paysize = 0;
@Override
public boolean decode(ByteBuf in) {
while (this.state != State.DONE) {
switch (this.state) {
case BLOCK_TYPE: {
if (in.readableBytes() < 1)
return false;
this.blocktype = in.readUnsignedByte();
this.eof = ((this.blocktype & CtpConstants.CTP_F_BLOCK_TYPE_EOF) != 0);
this.skipHeaders = ((this.blocktype & CtpConstants.CTP_F_BLOCK_TYPE_HEADER) == 0);
this.skipPayload = ((this.blocktype & CtpConstants.CTP_F_BLOCK_TYPE_CONTENT) == 0);
// completely done, exit the loop and decode
if (this.skipHeaders && this.skipPayload) {
this.state = State.DONE;
break;
}
// to skip headers, go back to loop
if (this.skipHeaders) {
this.state = State.STREAM_OFFSET;
break;
}
this.state = State.HEADER_ATTR;
// deliberate fall through
}
case HEADER_ATTR: {
if (in.readableBytes() < 2)
return false;
this.currattr = in.readShort();
// done with headers, go back to loop to skip down to payload
if (this.currattr == CtpConstants.CTP_F_ATTR_END) {
if (this.skipPayload)
this.state = State.DONE;
else
this.state = State.STREAM_OFFSET;
break;
}
this.state = State.HEADER_SIZE;
// deliberate fall through
}
case HEADER_SIZE: {
if (in.readableBytes() < 2)
return false;
this.currasize = in.readShort();
// an empty attribute is like a flag - present but no data
// go on to next header
if (this.currasize == 0) {
this.headers.put(this.currattr, new byte[0]);
this.currattr = 0;
this.state = State.HEADER_ATTR;
break;
}
this.state = State.HEADER_VALUE;
// deliberate fall through
}
case HEADER_VALUE: {
if (in.readableBytes() < this.currasize)
return false;
byte[] val = new byte[this.currasize];
in.readBytes(val);
this.headers.put(this.currattr, val);
this.currattr = 0;
this.currasize = 0;
this.state = State.HEADER_ATTR;
break;
}
case STREAM_OFFSET: {
if (in.readableBytes() < 8)
return false;
this.streamOffset = in.readLong();
this.state = State.PAYLOAD_SIZE;
// deliberate fall through
}
case PAYLOAD_SIZE: {
if (in.readableBytes() < 3)
return false;
this.paysize = in.readMedium();
this.state = State.PAYLOAD;
// deliberate fall through
}
case PAYLOAD: {
// return here, without any state reset, means we need more before we can decide what to do
if (in.readableBytes() < this.paysize)
return false;
// add Data only if there are some bytes, otherwise skip buffer allocation
if (this.paysize > 0) {
ByteBuf bb = in.readSlice(this.paysize);
bb.retain();
this.data = bb;
}
this.state = State.DONE;
// deliberate fall through
}
case DONE: {
break;
}
}
}
return true;
}
}