PEIconExtractor.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.tika.parser.executable;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.io.IOUtils;
import org.xml.sax.SAXException;
import org.apache.tika.exception.TikaException;
import org.apache.tika.extractor.EmbeddedDocumentExtractor;
import org.apache.tika.extractor.EmbeddedDocumentUtil;
import org.apache.tika.io.EndianUtils;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.HttpHeaders;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.executable.ExecutableParser.CoffHeader;
import org.apache.tika.sax.EmbeddedContentHandler;
import org.apache.tika.sax.XHTMLContentHandler;
/**
* Extracts the icons of a PE file (EXE/DLL) from its resource section and
* hands each icon group to the {@link EmbeddedDocumentExtractor} as a
* standalone <code>.ico</code> file.
* <p>
* Windows stores an icon as a group resource ({@code RT_GROUP_ICON}) that
* lists the individual images, which are stored as {@code RT_ICON} resources.
* The {@code .ico} file format is nearly identical to the group resource;
* the only difference is that the group refers to its images by resource id
* whereas the file refers to them by file offset. This class rebuilds the
* file from the two resource types, with the largest image first.
* <p>
* File-backed input is read where the resource tree points, so only the
* directories and the icons themselves are touched. Anything else has to be
* read from the start: up to the resource section, then the section as far
* as its icons reach.
* Hostile input is contained by bounds checking every offset and by capping
* the buffered part of a section, the number of directory entries visited,
* the icons per group, the size of a rebuilt icon and the size of all
* rebuilt icons together. The tree is walked to its three levels and no
* deeper, so a cycle cannot loop.
*/
class PEIconExtractor {
static final String ICON_MIME_TYPE = "image/vnd.microsoft.icon";
private static final int RT_ICON = 3;
private static final int RT_GROUP_ICON = 14;
private static final int IMAGE_DIRECTORY_ENTRY_RESOURCE = 2;
private static final int PE32_MAGIC = 0x10b;
private static final int PE32PLUS_MAGIC = 0x20b;
private static final int SECTION_HEADER_SIZE = 40;
private static final int RESOURCE_DIRECTORY_SIZE = 16;
private static final int RESOURCE_DIRECTORY_ENTRY_SIZE = 8;
private static final int RESOURCE_DATA_ENTRY_SIZE = 16;
private static final byte[] PNG_SIGNATURE =
{(byte) 0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a};
private static final int PNG_IHDR = 0x49484452;
/** the signature, the IHDR chunk's length and type, then its width and height */
private static final int PNG_DIMENSIONS_END = 8 + 8 + 8;
private static final int BITMAP_INFO_HEADER_SIZE = 40;
private static final int GRP_ICON_DIR_SIZE = 6;
private static final int GRP_ICON_DIR_ENTRY_SIZE = 14;
private static final int ICON_DIR_ENTRY_SIZE = 16;
private static final long HIGH_BIT = 0x80000000L;
// Sanity limits for hostile input
private static final int MAX_SECTIONS = 96; // the PE spec's own limit
private static final int MAX_BUFFERED_SECTION_MB = 64;
private static final int MAX_DIRECTORY_ENTRIES = 20000; // visited across the whole tree
private static final int MAX_RESOURCE_NAME_LENGTH = 256;
private static final int MAX_ICONS_PER_GROUP = 256;
private static final long MAX_ICO_SIZE = 64 * 1024 * 1024;
// Groups may share images, so all icons together may outgrow the section, but not by much
private static final int MAX_OUTPUT_FACTOR = 4;
private static final int SECTION_BUFFER_FLOOR = 8192;
private PEIconExtractor() {
}
/**
* Continues reading the PE file directly after the COFF file header and
* emits its icon groups as embedded documents.
*
* @param stream the input positioned right after the COFF header
* @param metadata the PE file's own metadata, receives a note if the
* resource section is cut or broken and a warning if not
* all of the tree or of the icons could be handled
*/
static void extract(TikaInputStream stream, CoffHeader header, XHTMLContentHandler xhtml,
Metadata metadata, ParseContext context, boolean allGroups)
throws IOException, SAXException {
ResourceTree tree;
try {
tree = ResourceTree.read(stream, header);
} catch (SecurityException e) {
throw e;
} catch (EOFException | RuntimeException e) {
// A cut or broken resource section must not cost the caller the
// header metadata; any other IOException is the source failing
EmbeddedDocumentUtil.recordEmbeddedStreamException(e, metadata, context);
return;
}
if (tree == null) {
return;
}
if (tree.budgetExceeded) {
EmbeddedDocumentUtil.recordException(new TikaException(
"PE resource directory has more than " + MAX_DIRECTORY_ENTRIES +
" entries; icon extraction stopped early"), metadata, context);
}
emitIcons(tree, xhtml, metadata, context, allGroups);
if (tree.section.beyondLimit()) {
EmbeddedDocumentUtil.recordException(new TikaException(
"PE resource section is larger than " + MAX_BUFFERED_SECTION_MB +
" MB and the input is not a file; icons beyond that were not" +
" extracted"), metadata, context);
}
}
/**
* Rebuilds an <code>.ico</code> file for the first usable icon group, or
* every group if {@code allGroups}, and passes it on as an embedded
* document. The first usable group in resource order is the one Windows
* shows for the file itself. The icon adds nothing to the text
* output; its name is Tika's invention, not content of the file.
*/
private static void emitIcons(ResourceTree tree, XHTMLContentHandler xhtml,
Metadata parentMetadata, ParseContext context,
boolean allGroups) throws IOException, SAXException {
if (tree.groups.isEmpty()) {
return;
}
EmbeddedDocumentExtractor extractor =
EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context);
// A group that comes in several languages carries the language in its name
Map<String, Set<Integer>> languages = new HashMap<>();
for (Resource group : tree.groups) {
languages.computeIfAbsent(group.displayName(), k -> new HashSet<>())
.add(group.language);
}
Set<String> relationshipIds = new HashSet<>();
long emitted = 0;
boolean first = true;
for (Resource group : tree.groups) {
if (!first && !allGroups) {
return;
}
Icon icon = resolveGroup(group, tree);
String relationshipId =
RT_GROUP_ICON + "/" + group.displayName() + "/" + group.language;
// A name can spell an id, or two names can clean up to the same one;
// what cannot be told apart comes out once
if (icon == null || !relationshipIds.add(relationshipId)) {
continue;
}
String name = "icon_" + group.displayName();
if (languages.get(group.displayName()).size() > 1) {
name += "_" + group.language;
}
Metadata metadata = Metadata.newInstance(context);
metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, name + ".ico");
metadata.set(TikaCoreProperties.RESOURCE_NAME_EXTENSION_INFERRED, true);
metadata.set(HttpHeaders.CONTENT_TYPE, ICON_MIME_TYPE);
metadata.set(TikaCoreProperties.EMBEDDED_RELATIONSHIP_ID, relationshipId);
metadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, first ?
TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString() :
TikaCoreProperties.EmbeddedResourceType.ATTACHMENT.toString());
first = false;
if (!extractor.shouldParseEmbedded(metadata, context)) {
continue;
}
emitted += icoSize(icon.images);
if (emitted > MAX_OUTPUT_FACTOR * tree.section.available()) {
EmbeddedDocumentUtil.recordException(new TikaException(
"PE icons add up to more than " + MAX_OUTPUT_FACTOR +
" times the resource section they were read from; icon" +
" extraction stopped early"), parentMetadata, context);
return;
}
byte[] ico = buildIco(icon, tree.section);
if (ico == null) {
continue;
}
try (TikaInputStream tis = TikaInputStream.get(ico)) {
extractor.parseEmbedded(tis, new EmbeddedContentHandler(xhtml), metadata, context,
false);
}
}
}
/**
* Checks a {@code GRPICONDIR} and looks up the images it references.
*
* @return the directory and its images in directory order, or null if the
* group is unusable
*/
private static Icon resolveGroup(Resource group, ResourceTree tree) throws IOException {
Section section = tree.section;
byte[] dir = section.read(group.offset, (int) Math.min(group.size,
GRP_ICON_DIR_SIZE + MAX_ICONS_PER_GROUP * GRP_ICON_DIR_ENTRY_SIZE));
if (dir == null || dir.length < GRP_ICON_DIR_SIZE || EndianUtils.getUShortLE(dir, 0) != 0 ||
EndianUtils.getUShortLE(dir, 2) != 1) {
return null;
}
int count = EndianUtils.getUShortLE(dir, 4);
if (count == 0 || count > MAX_ICONS_PER_GROUP ||
GRP_ICON_DIR_SIZE + count * GRP_ICON_DIR_ENTRY_SIZE > group.size) {
return null;
}
List<Resource> images = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
int id = EndianUtils.getUShortLE(dir, groupEntry(i) + 12);
Resource image = tree.findIcon(id, group.language);
if (image == null) {
return null;
}
images.add(image);
}
if (shareBytes(images) || icoSize(images) > MAX_ICO_SIZE) {
return null;
}
for (Resource image : images) {
if (!section.canRead(image.offset, image.size)) {
return null;
}
}
return new Icon(dir, images);
}
/**
* Images that share bytes, the same one listed twice being the plain
* case, would let a tiny group inflate into a huge file.
*/
private static boolean shareBytes(List<Resource> images) {
List<Resource> sorted = new ArrayList<>(images);
sorted.sort(Comparator.comparingLong(image -> image.offset));
for (int i = 1; i < sorted.size(); i++) {
Resource previous = sorted.get(i - 1);
if (sorted.get(i).offset < previous.offset + previous.size) {
return true;
}
}
return false;
}
private static long icoSize(List<Resource> images) {
long size = GRP_ICON_DIR_SIZE + (long) images.size() * ICON_DIR_ENTRY_SIZE;
for (Resource image : images) {
size += image.size;
}
return size;
}
/**
* Converts a {@code GRPICONDIR} plus its {@code RT_ICON} images into an
* {@code ICONDIR} based <code>.ico</code> file.
*
* @return the file, or null if an image could not be read after all
*/
private static byte[] buildIco(Icon icon, Section section) throws IOException {
int count = icon.images.size();
int imageOffset = GRP_ICON_DIR_SIZE + count * ICON_DIR_ENTRY_SIZE;
ByteBuffer ico = ByteBuffer.allocate((int) icoSize(icon.images))
.order(ByteOrder.LITTLE_ENDIAN);
List<Integer> order = largestFirst(icon, section);
// ICONDIR: reserved, type, count - identical to the GRPICONDIR
ico.put(icon.dir, 0, GRP_ICON_DIR_SIZE);
for (int index : order) {
int size = (int) icon.images.get(index).size;
// width, height, colours, reserved, planes and bit count are shared
ico.put(icon.dir, groupEntry(index), 8);
// the group's BytesInRes may disagree with the actual resource; trust the resource
ico.putInt(size);
ico.putInt(imageOffset);
imageOffset += size;
}
for (int index : order) {
Resource image = icon.images.get(index);
if (!section.read(image.offset, ico.array(), ico.position(), (int) image.size)) {
return null;
}
ico.position(ico.position() + (int) image.size);
}
return ico.array();
}
/**
* Orders a group's images, largest first. The order a resource compiler
* wrote carries no meaning - Windows looks up the size it needs - but a
* reader that treats an icon as a multi-page image hands out the first
* entry, and for a single image out of an icon that should be the best one.
*
* @return indices into the group, by descending pixel count and colour depth
*/
private static List<Integer> largestFirst(Icon icon, Section section) throws IOException {
int count = icon.images.size();
long[] pixels = new long[count];
List<Integer> order = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
pixels[i] = pixelCount(icon, i, section);
order.add(i);
}
// a stable sort, so images of one size and depth keep the order the group has
order.sort(Comparator.<Integer>comparingLong(index -> pixels[index])
.thenComparingInt(icon::bitCount).reversed());
return order;
}
/**
* The pixels an image covers, read from the image itself: a directory entry
* holds one byte per side and reads 0 for 256 or above, so a 1024 pixel PNG
* and a 256 pixel bitmap are indistinguishable there.
*
* @return the pixel count, falling back to what the directory entry claims
*/
private static long pixelCount(Icon icon, int index, Section section) throws IOException {
Resource image = icon.images.get(index);
byte[] head = section.read(image.offset,
(int) Math.min(image.size, BITMAP_INFO_HEADER_SIZE));
long width = 0;
long height = 0;
if (head != null && startsWithPngHeader(head)) {
width = EndianUtils.getUIntBE(head, 16);
height = EndianUtils.getUIntBE(head, 20);
} else if (head != null && head.length >= BITMAP_INFO_HEADER_SIZE &&
EndianUtils.getUIntLE(head, 0) == BITMAP_INFO_HEADER_SIZE) {
width = EndianUtils.getIntLE(head, 4);
// the height covers the XOR bitmap and the AND mask below it; a
// negative one means the rows are stored top-down
height = Math.abs((long) EndianUtils.getIntLE(head, 8)) / 2;
}
if (width <= 0 || height <= 0 || width > Integer.MAX_VALUE ||
height > Integer.MAX_VALUE) {
return icon.claimedPixelCount(index);
}
return width * height;
}
private static boolean startsWithPngHeader(byte[] head) {
return head.length >= PNG_DIMENSIONS_END &&
Arrays.equals(head, 0, PNG_SIGNATURE.length, PNG_SIGNATURE, 0,
PNG_SIGNATURE.length) &&
EndianUtils.getIntBE(head, 12) == PNG_IHDR;
}
/**
* @return the offset of an entry in a {@code GRPICONDIR}
*/
private static int groupEntry(int index) {
return GRP_ICON_DIR_SIZE + index * GRP_ICON_DIR_ENTRY_SIZE;
}
/**
* The three level resource tree (type / name / language), reduced to its
* icons and icon groups.
*/
private static final class ResourceTree {
final Section section;
private final long sectionVa;
private final long rootOffset;
// icon id -> language -> icon, in directory order
private final Map<Integer, Map<Integer, Resource>> icons = new HashMap<>();
final List<Resource> groups = new ArrayList<>();
private int budget = MAX_DIRECTORY_ENTRIES;
boolean budgetExceeded;
private ResourceTree(Section section, long sectionVa, long rootOffset) {
this.section = section;
this.sectionVa = sectionVa;
this.rootOffset = rootOffset;
}
/**
* Finds the resource section through the optional header and the
* section table, which follow the COFF header, and walks its tree.
*
* @return the tree, or null if the file has no resource section
*/
static ResourceTree read(TikaInputStream stream, CoffHeader header) throws IOException {
if (header.numSections() <= 0 || header.numSections() > MAX_SECTIONS) {
return null;
}
// The optional header holds the data directories, of which we
// need the one pointing at the resource tree
byte[] optHdr = new byte[header.sizeOptHdrs()];
IOUtils.readFully(stream, optHdr);
int dataDirOffset;
switch (optHdr.length >= 2 ? EndianUtils.getUShortLE(optHdr, 0) : 0) {
case PE32_MAGIC:
dataDirOffset = 96;
break;
case PE32PLUS_MAGIC:
dataDirOffset = 112;
break;
default:
return null;
}
int rsrcEntry = dataDirOffset + IMAGE_DIRECTORY_ENTRY_RESOURCE * 8;
if (rsrcEntry + 8 > optHdr.length) {
return null;
}
long numDataDirs = EndianUtils.getUIntLE(optHdr, dataDirOffset - 4);
if (numDataDirs <= IMAGE_DIRECTORY_ENTRY_RESOURCE) {
return null;
}
long rsrcRva = EndianUtils.getUIntLE(optHdr, rsrcEntry);
long rsrcSize = EndianUtils.getUIntLE(optHdr, rsrcEntry + 4);
if (rsrcRva == 0 || rsrcSize == 0) {
return null;
}
// The section table tells us where in the file the resource RVA lives
byte[] sections = new byte[header.numSections() * SECTION_HEADER_SIZE];
IOUtils.readFully(stream, sections);
long sectionVa = -1;
long sectionRawPtr = -1;
long sectionRawSize = -1;
for (int off = 0; off < sections.length; off += SECTION_HEADER_SIZE) {
long va = EndianUtils.getUIntLE(sections, off + 12);
long rawSize = EndianUtils.getUIntLE(sections, off + 16);
long rawPtr = EndianUtils.getUIntLE(sections, off + 20);
if (rsrcRva >= va && rsrcRva < va + rawSize) {
sectionVa = va;
sectionRawPtr = rawPtr;
sectionRawSize = rawSize;
break;
}
}
if (sectionVa < 0) {
return null;
}
Section section;
if (stream.hasFile()) {
section = new FileSection(stream.getFileChannel(), sectionRawPtr, sectionRawSize);
} else {
long position = header.end() + optHdr.length + sections.length;
if (sectionRawPtr < position) {
return null;
}
IOUtils.skipFully(stream, sectionRawPtr - position);
section = new BufferedSection(stream, sectionRawSize);
}
// Offsets inside the tree are relative to its root, which normally
// but not necessarily sits at the start of the section
ResourceTree tree = new ResourceTree(section, sectionVa, rsrcRva - sectionVa);
tree.readTypes();
return tree;
}
private void readTypes() throws IOException {
long groupDir = -1;
long iconDir = -1;
for (Entry entry : readEntries(rootOffset)) {
// Only icons are interesting, and a well-formed tree lists each type once
if (entry.subdirectory && entry.nameField == RT_GROUP_ICON && groupDir < 0) {
groupDir = entry.target;
} else if (entry.subdirectory && entry.nameField == RT_ICON && iconDir < 0) {
iconDir = entry.target;
}
}
// Groups first: should the entry budget run out among the icons,
// the groups whose icons were reached still come out
if (groupDir >= 0) {
readNames(groupDir, RT_GROUP_ICON);
}
if (iconDir >= 0) {
readNames(iconDir, RT_ICON);
}
}
private void readNames(long dirOffset, int type) throws IOException {
for (Entry entry : readEntries(dirOffset)) {
if (!entry.subdirectory) {
continue;
}
String name = null;
if (entry.named()) {
name = readName(rootOffset + (entry.nameField & ~HIGH_BIT));
if (name == null) {
continue;
}
}
readLanguages(entry.target, type, entry.id(), name);
}
}
private void readLanguages(long dirOffset, int type, int id, String name)
throws IOException {
for (Entry entry : readEntries(dirOffset)) {
// Language ids are always numeric, and a subdirectory below the
// language level is malformed; nothing to find there
if (!entry.subdirectory && !entry.named()) {
readDataEntry(entry.target, new Resource(type, id, name, entry.id()));
}
}
}
/**
* @return the entries of the directory, as many as the entry budget still allows
*/
private List<Entry> readEntries(long dirOffset) throws IOException {
byte[] dir = section.read(dirOffset, RESOURCE_DIRECTORY_SIZE);
if (dir == null) {
return List.of();
}
int numEntries = EndianUtils.getUShortLE(dir, 12) + EndianUtils.getUShortLE(dir, 14);
int count = Math.min(numEntries, budget);
budget -= count;
budgetExceeded |= count < numEntries;
byte[] bytes = section.read(dirOffset + RESOURCE_DIRECTORY_SIZE,
count * RESOURCE_DIRECTORY_ENTRY_SIZE);
if (bytes == null) {
return List.of();
}
List<Entry> entries = new ArrayList<>(count);
for (int i = 0; i < bytes.length; i += RESOURCE_DIRECTORY_ENTRY_SIZE) {
long dataField = EndianUtils.getUIntLE(bytes, i + 4);
entries.add(new Entry(EndianUtils.getUIntLE(bytes, i),
(dataField & HIGH_BIT) != 0, rootOffset + (dataField & ~HIGH_BIT)));
}
return entries;
}
private void readDataEntry(long offset, Resource resource) throws IOException {
byte[] entry = section.read(offset, RESOURCE_DATA_ENTRY_SIZE);
if (entry == null) {
return;
}
// Resource data normally lives in the same section as the tree;
// data elsewhere is out of reach
resource.offset = EndianUtils.getUIntLE(entry, 0) - sectionVa;
resource.size = EndianUtils.getUIntLE(entry, 4);
if (!section.contains(resource.offset, resource.size)) {
return;
}
if (resource.type == RT_GROUP_ICON) {
groups.add(resource);
} else if (resource.name == null) {
// Groups reference icons by numeric id, so a named icon is unreachable
icons.computeIfAbsent(resource.id, k -> new LinkedHashMap<>())
.putIfAbsent(resource.language, resource);
}
}
/**
* @return the name with path separators and control characters
* replaced: it ends up in a file name and in an id whose parts are
* separated by slashes
*/
private String readName(long offset) throws IOException {
byte[] prefix = section.read(offset, 2);
if (prefix == null) {
return null;
}
int length = EndianUtils.getUShortLE(prefix, 0);
if (length == 0 || length > MAX_RESOURCE_NAME_LENGTH) {
return null;
}
byte[] chars = section.read(offset + 2, length * 2);
if (chars == null) {
return null;
}
StringBuilder name = new StringBuilder(new String(chars, StandardCharsets.UTF_16LE));
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (c == '/' || c == '\\' || Character.isISOControl(c)) {
name.setCharAt(i, '_');
}
}
return name.toString();
}
/**
* @return the icon with the given id, preferring the group's language
*/
Resource findIcon(int id, int language) {
Map<Integer, Resource> byLanguage = icons.get(id);
if (byLanguage == null) {
return null;
}
Resource icon = byLanguage.get(language);
return icon != null ? icon : byLanguage.values().iterator().next();
}
}
/**
* One entry of a resource directory.
*
* @param nameField the id, or with the high bit set the offset of the name
* @param subdirectory whether the target is a directory rather than a data entry
* @param target the section offset the entry points to
*/
private record Entry(long nameField, boolean subdirectory, long target) {
boolean named() {
return (nameField & HIGH_BIT) != 0;
}
int id() {
return (int) (nameField & 0xffff);
}
}
/**
* A leaf of the resource tree: type, name (or id) and language, plus the
* location of its data within the resource section.
*/
private static final class Resource {
final int type;
final int id;
final String name;
final int language;
long offset;
long size;
Resource(int type, int id, String name, int language) {
this.type = type;
this.id = id;
this.name = name;
this.language = language;
}
String displayName() {
return name != null ? name : Integer.toString(id);
}
}
/**
* A usable icon group: its {@code GRPICONDIR} and the images it lists.
*/
private record Icon(byte[] dir, List<Resource> images) {
int bitCount(int index) {
return EndianUtils.getUShortLE(dir, groupEntry(index) + 6);
}
long claimedPixelCount(int index) {
return (long) claimedSide(dir[groupEntry(index)]) *
claimedSide(dir[groupEntry(index) + 1]);
}
/** A side of 0 in a directory entry means 256 or above. */
private static int claimedSide(byte side) {
int value = side & 0xff;
return value == 0 ? 256 : value;
}
}
/**
* The bytes of the resource section, addressed by their offset in it.
*/
abstract static class Section {
private final long declaredSize;
Section(long declaredSize) {
this.declaredSize = declaredSize;
}
/**
* @return whether the range lies inside the section as declared
*/
final boolean contains(long offset, long length) {
return offset >= 0 && length >= 0 && offset + length <= declaredSize;
}
/**
* @return the number of bytes of the section known to exist
*/
abstract long available();
/**
* @return whether the range can be read; finding out may read the input that far
*/
abstract boolean canRead(long offset, long length) throws IOException;
/**
* Copies a range that {@link #canRead(long, long)} has confirmed.
*
* @return false if the bytes are not there after all
*/
abstract boolean copy(long offset, byte[] target, int targetOffset, int length)
throws IOException;
/**
* @return whether a range was refused that the section declares but
* this class does not reach
*/
boolean beyondLimit() {
return false;
}
/**
* @return false if the range cannot be read
*/
final boolean read(long offset, byte[] target, int targetOffset, int length)
throws IOException {
return canRead(offset, length) && copy(offset, target, targetOffset, length);
}
/**
* @return the bytes, or null if the range cannot be read
*/
final byte[] read(long offset, int length) throws IOException {
if (!canRead(offset, length)) {
return null;
}
byte[] bytes = new byte[length];
return copy(offset, bytes, 0, length) ? bytes : null;
}
}
/**
* A section of a file, read where the walk points.
*/
static final class FileSection extends Section {
private final FileChannel channel;
private final long start;
private final long present;
/**
* @param start the file offset of the section
*/
FileSection(FileChannel channel, long start, long declaredSize) throws IOException {
super(declaredSize);
if (start > channel.size()) {
throw new EOFException("The file ends before its resource section");
}
this.channel = channel;
this.start = start;
this.present = Math.min(declaredSize, channel.size() - start);
}
@Override
long available() {
return present;
}
@Override
boolean canRead(long offset, long length) {
return contains(offset, length) && offset + length <= present;
}
@Override
boolean copy(long offset, byte[] target, int targetOffset, int length)
throws IOException {
ByteBuffer buffer = ByteBuffer.wrap(target, targetOffset, length);
long position = start + offset;
while (buffer.hasRemaining()) {
int n = channel.read(buffer, position);
if (n < 0) {
return false;
}
position += n;
}
return true;
}
}
/**
* A section that can only be read forward. It is buffered from its start
* as far as the walk has reached; the buffer grows with the bytes that
* arrive, never with a size the file merely declares.
*/
static final class BufferedSection extends Section {
private final InputStream source;
private final long limit;
byte[] buf = new byte[0];
private int length;
private boolean eof;
private boolean beyondLimit;
/**
* @param source a stream positioned at the start of the section
*/
BufferedSection(InputStream source, long declaredSize) {
super(declaredSize);
this.source = source;
this.limit = Math.min(declaredSize, MAX_BUFFERED_SECTION_MB * 1024L * 1024L);
}
@Override
long available() {
return length;
}
@Override
boolean canRead(long offset, long length) throws IOException {
if (!contains(offset, length)) {
return false;
}
if (offset + length > limit) {
beyondLimit = true;
return false;
}
return ensure((int) (offset + length));
}
@Override
boolean copy(long offset, byte[] target, int targetOffset, int length) {
System.arraycopy(buf, (int) offset, target, targetOffset, length);
return true;
}
@Override
boolean beyondLimit() {
return beyondLimit;
}
private boolean ensure(int end) throws IOException {
while (length < end && !eof) {
if (length == buf.length) {
buf = Arrays.copyOf(buf, (int) Math.min(limit,
Math.max(SECTION_BUFFER_FLOOR, 2L * buf.length)));
}
int n = source.read(buf, length, Math.min(end, buf.length) - length);
if (n < 0) {
eof = true;
} else {
length += n;
}
}
return end <= length;
}
}
}