A small library for manipulating immutable binary blobs.
A Blob
is a generic representation for a chunk of binary data.
Suppose we have the following bytes:
byte[] bytes = {0x01,0x02};
Then, we can create a ByteBlob
from as follows:
Blob b1 = new ByteBlob(bytes);
Using this, we can read the original bytes in various ways.
// Read bytes individually
assert b1.readByte(0) == 0x01;
assert b1.readByte(1) == 0x02;
// Read bytes together
assert b1.readShort(0) == 0x102;
This extends to other primitive layouts, such as int
, long
, etc.
A Blob
is always immutable which means, for example, the byte
array behind a ByteBlob
cannot be modified the Blob
API. However,
the Blob
API supports functional writes. That is, writing to a
Blob
produces another Blob
. For example, consider the following
continuation from our example above:
Blob b2 = b1.writeByte(0,(byte) 0x02);
// Check nothing changed in b1
assert b1.readShort(0) == 0x102;
// Check contents of b2
assert b2.readShort(0) == 0x202;
A key aspect of the library is that such blobs are maintained as
instances of Blob.Diff
over their parents. Thus, the above write
does not clone the original array. We can see this by printing out
the blobs:
System.out.println("b1: " + b1);
System.out.println("b2: " + b2);
Running this gives the following output:
b1: [1, 2]
b2: {(0;1;[2])}[1, 2]
Here, b2
contains a single replacement over the contents of b1
---
namely, the bytes between 0
and upto (but not including) 1
are
replaced with [2]
(in this case).
The library current performs only limited optimisations. For example, consider this continuation:
Blob b3 = b2.writeByte(0,(byte) 0x03);
In this case, b3
has the representation {(0;1;[3])}[1, 2]
rather
than {(0;1;[3])}{(0;1;[2])}[1, 2]
as might be expected.
A Blob
can be resized in various ways. For example, we can insert
bytes into a Blob
as follows:
Blob b4 = b1.insertBytes(0,(byte) 0x03, (byte) 0x04);
Blob b5 = b1.insertByte(2,(byte) 0x03);
// Sanity check b4
assert b4.size() == 4;
assert b4.readShort(0) == 0x304;
// Sanity check b5
assert b5.size() == 3;
assert b5.readByte(2) == 0x3;
Here, the contents of b4
looks like [3,4,1,2]
, whilst b5
looks
like [1,2,3]
.
Another supported operation is replacing one sequence with another, as the following illustrates:
Blob b6 = b4.replaceBytes(1, 2, (byte) 0x05);
// Check
assert b6.size() == 3;
assert b6.readByte(1) == 0x05;
assert b6.readByte(2) == 0x02;
Here, the size of b6
has reduced to 3
because we've replaced two
bytes in b4
with just one byte.
Talk about merging.
Whilst reading / writing low-level data types is useful, ultimately the ability to read / write objects is important. Here, we look at the simplest way of getting started with this, and in the following sections consider more advanced approaches.
A proxy object is a essentially a wrapper around a Blob
which
provides a more useful (e.g. human readable) interface. A simple
example is the following Point
class:
public class Point {
private final Blob blob;
private final int offset;
public Point(Blob blob, int offset) {
if((blob.size() - offset) < 8) {
throw new IllegalArgumentException("insufficient space in blob");
}
this.blob = blob;
this.offset = offset;
}
public int getX() {
return blob.readInt(offset);
}
public int getY() {
return blob.readInt(offset + 4);
}
}
We can create a Point
from an existing Blob
(which must be big
enough), and use this proxy object to access data within the blob
(i.e. the X
and Y
coordinates). Observe that the layout of our
Point
is hard coded into our proxy (e.g. where an int
is 4
bytes, etc).
The following illustrates a simple test:
Blob blob = Blob(new byte[8]);
blob = blob.writeInt(0, 1);
blob = blob.writeInt(4, 2);
Point p = new Point(blob,0);
assert p.getX() == 1;
assert p.getY() == 2;
This simply initialises a Blob
of sufficient size, and creates a new
proxy Point
at address 0
. This proxy then provides convenient
methods for reading data out of the blob.
Whilst our Point
class above was helpful, it was still fairly
limited. The next step is to update our Point
class so we can
write to it as well. The following illustrates the changes
necessary:
class Point {
...
public Point setX(int x) {
Blob nblob = blob.writeInt(offset, x);
return new Point(nblob,offset);
}
public Point setY(int y) {
Blob nblob = blob.writeInt(offset + 4, y);
return new Point(nblob,offset);
}
}
Here, we have defined a functional API for Point
which reflects the
mechanics of the underlying Blob
. However, it is possible to create
an imperative API (e.g. which updates the blob
field) --- however,
care must be taken when dealing with nested proxies.