/* This code is part of Freenet. It is distributed under the GNU General
* Public License, version 2 (or at your option any later version). See
* http://www.gnu.org/ for further details of the GPL. */
package freenet.client.filter;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashMap;
import freenet.l10n.NodeL10n;
import freenet.support.io.FileUtil;
/**
* Content filter for GIF's.
* This throws out all optional non-raster data that it cannot validate.
*
* References:
* https://www.w3.org/Graphics/GIF/spec-gif87.txt
* https://www.w3.org/Graphics/GIF/spec-gif89a.txt
*/
public class GIFFilter implements ContentDataFilter {
static final int HEADER_SIZE = 6;
static final byte[] gif87aHeader =
{ (byte)'G', (byte)'I', (byte)'F', (byte)'8', (byte)'7', (byte)'a' };
static final byte[] gif89aHeader =
{ (byte)'G', (byte)'I', (byte)'F', (byte)'8', (byte)'9', (byte)'a' };
@Override
public void readFilter(InputStream input, OutputStream output, String charset, HashMap<String, String> otherParams,
FilterCallback cb) throws DataFilterException, IOException {
DataInputStream dis = new DataInputStream(input);
try {
// Check the header
byte[] headerCheck = new byte[HEADER_SIZE];
dis.readFully(headerCheck);
final boolean isGIF87a = Arrays.equals(headerCheck, gif87aHeader);
final boolean isGIF89a = Arrays.equals(headerCheck, gif89aHeader);
if (!isGIF87a && !isGIF89a) {
throwDataError(l10n("invalidHeaderTitle"), l10n("invalidHeader"));
}
output.write(headerCheck);
if (isGIF87a) {
GIF87aValidator.filter(input, output);
} else if (isGIF89a) {
GIF89aValidator.filter(input, output);
}
} catch (EOFException e) {
throwDataError(l10n("unexpectedEOFTitle"), l10n("unexpectedEOF"));
}
output.flush();
}
private static abstract class GIFValidator {
private final InputStream input;
private final OutputStream output;
// Screen descriptor data
protected int screenWidth;
protected int screenHeight;
protected int screenFlags;
protected int screenColors; // Parsed from screenFlags
protected int screenBackgroundColor;
protected int screenAspectRatio;
// ",": Image separator character
private static final int IMAGE_SEPARATOR = 0x2C;
// ";": GIF terminator
private static final int GIF_TERMINATOR = 0x3B;
// "!": GIF Extension Block Introducer
protected static final int EXTENSION_INTRODUCER = 0x21;
protected GIFValidator(InputStream input, OutputStream output) {
this.input = input;
this.output = output;
}
/** Checks whether the parsed screen descriptor is valid. */
protected boolean validateScreenDescriptor() {
// Not in the specification, but check whether the background color index is within
// the bounds of the Global Color Table just to be sure.
return screenBackgroundColor < screenColors;
}
/** Checks whether the given image flags are valid. */
protected boolean validateImageFlags(int imageFlags) {
return true;
}
/** Filters the next extension blocks, and skips it when it is unsupported or invalid. */
protected void filterExtensionBlock() throws IOException {
skipExtensionBlock();
}
/** Signals that image data is found. If the image data is valid, it will be written
* immediately after this method returns. */
protected void foundImageData(boolean valid) throws IOException {
// Do nothing.
}
/** Filters a complete GIF stream; assuming its header has already been read. */
protected final void filter() throws IOException, DataFilterException {
readScreenDescriptor();
if (!validateScreenDescriptor()) {
throwDataError(l10n("invalidHeaderTitle"), l10n("invalidHeader"));
}
writeScreenDescriptor();
final boolean hasGlobalColorMap = (screenFlags & 0x80) == 0x80;
if (hasGlobalColorMap) {
copy(3 * screenColors);
}
filterData();
}
/** Reads the screen descriptor from the input and parses it. */
private void readScreenDescriptor() throws IOException, DataFilterException {
screenWidth = readShort();
screenHeight = readShort();
screenFlags = readByte();
screenBackgroundColor = readByte();
screenAspectRatio = readByte();
final int bitsPerPixel = (screenFlags & 0x07) + 1;
screenColors = 1 << bitsPerPixel;
}
/** Writes the previously parsed and validated screen descriptor to the output. */
private void writeScreenDescriptor() throws IOException {
writeShort(screenWidth);
writeShort(screenHeight);
writeByte(screenFlags);
writeByte(screenBackgroundColor);
writeByte(screenAspectRatio);
}
/** Looks for data blocks and filters them according to their type. */
private void filterData() throws IOException, DataFilterException {
boolean imageSeen = false;
boolean terminated = false;
int lastByte;
while (!terminated && (lastByte = input.read()) != -1) {
switch(lastByte) {
case IMAGE_SEPARATOR:
imageSeen |= filterImage();
break;
case GIF_TERMINATOR:
terminated |= imageSeen;
break;
case EXTENSION_INTRODUCER:
filterExtensionBlock();
break;
default:
// The specification expects us to skip other data; we can simply omit it.
}
}
if (!imageSeen) {
throwDataError(l10n("noDataTitle"), l10n("noData"));
}
if (!terminated) {
throwDataError(l10n("unterminatedGifTitle"), l10n("unterminatedGif"));
}
// There may still be trailing data at this point; we can simply omit it.
writeByte(GIF_TERMINATOR);
}
/** Filters a render block. Actual LZW data is *not* checked. */
private boolean filterImage() throws IOException, DataFilterException {
final int imageLeft = readShort();
final int imageTop = readShort();
final int imageWidth = readShort();
final int imageHeight = readShort();
final int imageFlags = readByte();
final boolean hasLocalColorMap = (imageFlags & 0x80) == 0x80;
final byte[] localColorMap;
if (hasLocalColorMap) {
final int bitsPerPixel = (imageFlags & 0x07) + 1;
final int imageColors = 1 << bitsPerPixel;
localColorMap = readBytes(3 * imageColors);
} else {
localColorMap = new byte[0];
}
final int lzwCodeSize = readByte();
if (imageLeft + imageWidth > screenWidth || imageTop + imageHeight > screenHeight ||
lzwCodeSize < 2 || lzwCodeSize >= 12 || !validateImageFlags(imageFlags)) {
foundImageData(false);
skipSubBlocks();
return false;
} else {
foundImageData(true);
writeByte(IMAGE_SEPARATOR);
writeShort(imageLeft);
writeShort(imageTop);
writeShort(imageWidth);
writeShort(imageHeight);
writeByte(imageFlags);
writeBytes(localColorMap);
writeByte(lzwCodeSize);
copySubBlocks();
return true;
}
}
/** Skips an entire extension block; assumes the extension indicator is already read. */
private void skipExtensionBlock() throws IOException {
skip(1); // extension function
skipSubBlocks();
}
/** Skips all subblocks in the input, until the empty terminator subblock is found. */
protected final void skipSubBlocks() throws IOException {
int length;
while ((length = readByte()) != 0) {
skip(length);
}
}
/** Copies all subblocks to the output, until the empty terminator subblock is found. */
protected final void copySubBlocks() throws IOException {
int length;
while ((length = readByte()) != 0) {
writeByte(length);
copy(length);
}
writeByte(0);
}
/** Copy a small number of bytes from input to output. */
protected final void copy(int length) throws IOException {
for (int i = 0; i < length; i++) {
writeByte(readByte());
}
}
/** Read an unsigned byte from the input. */
protected final int readByte() throws IOException {
int val = input.read();
if (val == -1) {
throw new EOFException();
}
return val;
}
/** Write an unsigned byte to the output. */
protected final void writeByte(int val) throws IOException {
output.write(val & 0xFF);
}
/** Read a number of bytes from the input. */
protected final byte[] readBytes(int num) throws IOException {
byte[] buf = new byte[num];
int remaining = buf.length;
while (remaining > 0) {
int read = input.read(buf, buf.length - remaining, remaining);
if (read <= 0) {
throw new EOFException();
}
remaining -= read;
}
return buf;
}
/** Write all given bytes to the output. */
protected final void writeBytes(byte[] data) throws IOException {
output.write(data);
}
/** Read a little-endian unsigned short from the input. */
protected final int readShort() throws IOException {
int lsb = readByte();
int msb = readByte();
return (msb << 8) | lsb;
}
/** Write a little-endian unsigned short to the output. */
protected final void writeShort(int val) throws IOException {
output.write(val & 0xFF);
output.write((val >>> 8) & 0xFF);
}
/** Skip the given number of bytes of the input. */
protected final void skip(int num) throws IOException {
long remaining = num;
long skipped;
while (remaining > 0) {
skipped = input.skip(remaining);
remaining -= skipped;
if (skipped == 0) {
readByte();
remaining--;
}
}
}
}
private static class GIF87aValidator extends GIFValidator {
private GIF87aValidator(InputStream input, OutputStream output) {
super(input, output);
}
@Override
protected boolean validateScreenDescriptor() {
// The sort flag and aspect ratio indicator must be 0 in GIF87a.
final boolean sort = (screenFlags & 0x08) == 0x08;
if (sort || screenAspectRatio != 0) {
return false;
}
return super.validateScreenDescriptor();
}
@Override
protected boolean validateImageFlags(int imageFlags) {
// The disposal method must be 0 in GIF87a.
return (imageFlags & 0x38) == 0x00 && super.validateImageFlags(imageFlags);
}
static void filter(InputStream input, OutputStream output)
throws IOException, DataFilterException {
new GIF87aValidator(input, output).filter();
}
}
private static class GIF89aValidator extends GIFValidator {
// Whether we have a valid graphic control block to output.
private boolean hasGraphicControl = false;
private int gcFlags;
private int gcDelayTime;
private int gcTransparentColor;
// Whether the (render, extension) block is yet to be written.
private boolean firstBlock = true;
// Extension function label for the graphic control extension
private static final int GRAPHIC_CONTROL_LABEL = 0xF9;
// Extension function label for application extensions
private static final int APPLICATION_LABEL = 0xFF;
// Signatures for the Netscape 2.0 / AnimExts 1.0 extensions
private static final byte[] NETSCAPE2_0_SIG = new byte[] {
(byte)'N', (byte)'E', (byte)'T', (byte)'S', (byte)'C', (byte)'A', (byte)'P',
(byte)'E', (byte)'2', (byte)'.', (byte)'0'
};
private static final byte[] ANIMEXTS1_0_SIG = new byte[] {
(byte)'A', (byte)'N', (byte)'I', (byte)'M', (byte)'E', (byte)'X', (byte)'T',
(byte)'S', (byte)'1', (byte)'.', (byte)'0'
};
private GIF89aValidator(InputStream input, OutputStream output) {
super(input, output);
}
static void filter(InputStream input, OutputStream output)
throws IOException, DataFilterException {
new GIF89aValidator(input, output).filter();
}
@Override
protected void filterExtensionBlock() throws IOException {
int label = readByte();
switch (label) {
case GRAPHIC_CONTROL_LABEL:
readGraphicControl();
break;
case APPLICATION_LABEL:
filterApplicationBlock();
break;
default:
skipSubBlocks();
}
}
@Override
protected void foundImageData(boolean success) throws IOException {
if (success && hasGraphicControl) {
writeGraphicControl();
}
// Graphic control only applies to the single following render block; we must drop
// it when we encounter an invalid render block.
hasGraphicControl = false;
if (success) {
firstBlock = false;
}
}
/** Filters an application extension block; assuming its indicator and label are already
* read. Currently the only supported extension is the Loop Extension. */
private void filterApplicationBlock() throws IOException {
final int length = readByte();
final byte[] signature = readBytes(length);
final int remaining;
if (Arrays.equals(signature, ANIMEXTS1_0_SIG) ||
Arrays.equals(signature, NETSCAPE2_0_SIG)) {
// Netscape 2.0 / AnimExts 1.0 extension.
final int subLength = readByte();
if (subLength == 3) {
final int subID = readByte();
final int loopCount = readShort();
remaining = readByte();
if (remaining == 0 && subID == 0x01 && firstBlock) {
// Valid Loop Extension, and this is the first block.
writeByte(EXTENSION_INTRODUCER);
writeByte(APPLICATION_LABEL);
writeByte(NETSCAPE2_0_SIG.length);
writeBytes(NETSCAPE2_0_SIG);
writeByte(subLength);
writeByte(subID);
writeShort(loopCount);
writeByte(0);
firstBlock = false;
}
} else {
remaining = subLength;
}
} else {
remaining = readByte();
}
if (remaining != 0) {
skip(remaining);
skipSubBlocks();
}
}
/** Reads a graphic control block; assuming its indicator and label are already read. */
private void readGraphicControl() throws IOException {
if (hasGraphicControl) {
// Graphic control may only appear once per render block.
skipSubBlocks();
return;
}
final int length = readByte();
if (length != 4) {
// Length must be 4.
skip(length);
skipSubBlocks();
return;
}
gcFlags = readByte();
gcDelayTime = readShort();
gcTransparentColor = readByte();
final int terminator = readByte();
if (terminator != 0) {
// There is more data: this should not happen. Skip the rest of the stream.
skip(terminator);
skipSubBlocks();
return;
}
final int disposalMethod = (gcFlags & 0x1C) >>> 2;
if (disposalMethod >= 4) {
// Undefined disposal method.
return;
}
hasGraphicControl = true;
}
/** Writes a complete graphic control block. */
private void writeGraphicControl() throws IOException {
writeByte(EXTENSION_INTRODUCER);
writeByte(GRAPHIC_CONTROL_LABEL);
writeByte(4);
writeByte(gcFlags);
writeShort(gcDelayTime);
writeByte(gcTransparentColor);
writeByte(0);
firstBlock = false;
}
}
private static String l10n(String key) {
return NodeL10n.getBase().getString("GIFFilter."+key);
}
private static void throwDataError(String shortReason, String reason) throws DataFilterException {
// Throw an exception
String message = l10n("notGif");
if (reason != null) {
message += ' ' + reason;
}
if (shortReason != null) {
message += " - (" + shortReason + ')';
}
throw new DataFilterException(shortReason, shortReason, message);
}
@Override
public void writeFilter(InputStream input, OutputStream output, String charset, HashMap<String, String> otherParams,
FilterCallback cb) throws DataFilterException, IOException {
output.write(input.read());
}
}