diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b6d0ae5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,34 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Java sources +*.java text diff=java eol=lf +*.gradle text diff=java eol=lf + +# These files are text and should be normalized (Convert crlf => lf) +*.css text diff=css eol=lf +*.html text diff=html eol=lf +*.md text diff=markdown eol=lf +*.js text eol=lf +*.csv text eol=lf +*.json text eol=lf +*.properties text eol=lf +*.svg text eol=lf +*.xml text eol=lf +*.yaml text eol=lf +*.yml text eol=lf +*.toml text eol=lf +*.lang text eol=lf + +# These files are binary and should be left untouched +*.png binary +*.gif binary +*.jpg binary +*.jpeg binary + +# Common build-tool wrapper scripts +mvnw text eol=lf +gradlew text eol=lf +*.sh text eol=lf +*.bat text eol=crlf +*.cmd text eol=crlf \ No newline at end of file diff --git a/pom.xml b/pom.xml index 5bd2284..bc8c5d3 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ software.coley lljzip - 2.6.2 + 2.7.0 LL Java ZIP Lower level ZIP support for Java diff --git a/src/main/java/software/coley/lljzip/ZipIO.java b/src/main/java/software/coley/lljzip/ZipIO.java index f599900..3ad57ad 100644 --- a/src/main/java/software/coley/lljzip/ZipIO.java +++ b/src/main/java/software/coley/lljzip/ZipIO.java @@ -3,7 +3,11 @@ import software.coley.lljzip.format.model.CentralDirectoryFileHeader; import software.coley.lljzip.format.model.EndOfCentralDirectory; import software.coley.lljzip.format.model.ZipArchive; -import software.coley.lljzip.format.read.*; +import software.coley.lljzip.format.read.AdaptingZipReader; +import software.coley.lljzip.format.read.ForwardScanZipReader; +import software.coley.lljzip.format.read.JvmZipReader; +import software.coley.lljzip.format.read.NaiveLocalFileZipReader; +import software.coley.lljzip.format.read.ZipReader; import java.io.FileNotFoundException; import java.io.IOException; @@ -172,7 +176,8 @@ public static ZipArchive readJvm(Path path) throws IOException { * * @return Archive from path. * - * @throws IOException When the archive cannot be read. + * @throws IOException + * When the archive cannot be read. */ public static ZipArchive readAdaptingIO(Path path) throws IOException { ZipArchive archive = new ZipArchive(); diff --git a/src/main/java/software/coley/lljzip/format/model/AbstractZipFileHeader.java b/src/main/java/software/coley/lljzip/format/model/AbstractZipFileHeader.java index 8603548..49ee979 100644 --- a/src/main/java/software/coley/lljzip/format/model/AbstractZipFileHeader.java +++ b/src/main/java/software/coley/lljzip/format/model/AbstractZipFileHeader.java @@ -3,13 +3,13 @@ import software.coley.lljzip.format.compression.Decompressor; import software.coley.lljzip.format.compression.ZipCompressions; import software.coley.lljzip.util.MemorySegmentUtil; -import software.coley.lljzip.util.lazy.LazyMemorySegment; -import software.coley.lljzip.util.lazy.LazyInt; -import software.coley.lljzip.util.lazy.LazyLong; +import software.coley.lljzip.util.data.MemorySegmentData; +import software.coley.lljzip.util.data.StringData; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.lang.foreign.MemorySegment; +import java.util.Objects; /** * Common base for shared elements of {@link CentralDirectoryFileHeader} and {@link LocalFileHeader}. @@ -17,19 +17,19 @@ * @author Matt Coley */ public abstract class AbstractZipFileHeader implements ZipPart, ZipRead { - // Zip spec elements, all lazily read, common between central/local file headers - protected LazyInt versionNeededToExtract; - protected LazyInt generalPurposeBitFlag; - protected LazyInt compressionMethod; - protected LazyInt lastModFileTime; - protected LazyInt lastModFileDate; - protected LazyInt crc32; - protected LazyLong compressedSize; - protected LazyLong uncompressedSize; - protected LazyInt fileNameLength; - protected LazyInt extraFieldLength; - protected LazyMemorySegment fileName; - protected LazyMemorySegment extraField; + // Zip spec elements, common between central/local file headers + protected int versionNeededToExtract; + protected int generalPurposeBitFlag; + protected int compressionMethod; + protected int lastModFileTime; + protected int lastModFileDate; + protected int crc32; + protected long compressedSize; + protected long uncompressedSize; + protected int fileNameLength; + protected int extraFieldLength; + protected StringData fileName; + protected MemorySegmentData extraField; // Offset into the data this part is read from protected transient long offset = -1L; @@ -37,9 +37,6 @@ public abstract class AbstractZipFileHeader implements ZipPart, ZipRead { // Data source that contents were read from. protected transient MemorySegment data; - // String cache values - private transient String fileNameCache; - private transient String extraFieldCache; /** * @return The associated backing data that this file header was read from. @@ -55,7 +52,7 @@ public long offset() { } @Override - public void read(@Nonnull MemorySegment data, long offset) { + public void read(@Nonnull MemorySegment data, long offset) throws ZipParseException { this.data = data; this.offset = offset; } @@ -64,7 +61,7 @@ public void read(@Nonnull MemorySegment data, long offset) { * @return Version of zip software required to read the archive features. */ public int getVersionNeededToExtract() { - return versionNeededToExtract.get(); + return versionNeededToExtract; } /** @@ -72,14 +69,14 @@ public int getVersionNeededToExtract() { * Version of zip software required to read the archive features. */ public void setVersionNeededToExtract(int versionNeededToExtract) { - this.versionNeededToExtract.set(versionNeededToExtract); + this.versionNeededToExtract = versionNeededToExtract; } /** * @return Used primarily to expand on details of file compression. */ public int getGeneralPurposeBitFlag() { - return generalPurposeBitFlag.get(); + return generalPurposeBitFlag; } /** @@ -87,7 +84,7 @@ public int getGeneralPurposeBitFlag() { * Used primarily to expand on details of file compression. */ public void setGeneralPurposeBitFlag(int generalPurposeBitFlag) { - this.generalPurposeBitFlag.set(generalPurposeBitFlag); + this.generalPurposeBitFlag = generalPurposeBitFlag; } /** @@ -96,7 +93,7 @@ public void setGeneralPurposeBitFlag(int generalPurposeBitFlag) { * @see ZipCompressions Possible methods. */ public int getCompressionMethod() { - return compressionMethod.get(); + return compressionMethod; } /** @@ -106,14 +103,14 @@ public int getCompressionMethod() { * @see ZipCompressions Possible methods. */ public void setCompressionMethod(int compressionMethod) { - this.compressionMethod.set(compressionMethod); + this.compressionMethod = compressionMethod; } /** * @return Modification time of the file. */ public int getLastModFileTime() { - return lastModFileTime.get(); + return lastModFileTime; } /** @@ -121,14 +118,14 @@ public int getLastModFileTime() { * Modification time of the file. */ public void setLastModFileTime(int lastModFileTime) { - this.lastModFileTime.set(lastModFileTime); + this.lastModFileTime = lastModFileTime; } /** * @return Modification date of the file. */ public int getLastModFileDate() { - return lastModFileDate.get(); + return lastModFileDate; } /** @@ -136,14 +133,14 @@ public int getLastModFileDate() { * Modification date of the file. */ public void setLastModFileDate(int lastModFileDate) { - this.lastModFileDate.set(lastModFileDate); + this.lastModFileDate = lastModFileDate; } /** * @return File checksum. */ public int getCrc32() { - return crc32.get(); + return crc32; } /** @@ -151,7 +148,7 @@ public int getCrc32() { * File checksum. */ public void setCrc32(int crc32) { - this.crc32.set(crc32); + this.crc32 = crc32; } /** @@ -164,7 +161,7 @@ public void setCrc32(int crc32) { * @return Compressed size of {@link LocalFileHeader#getFileData()}. */ public long getCompressedSize() { - return compressedSize.get(); + return compressedSize; } /** @@ -172,7 +169,7 @@ public long getCompressedSize() { * Compressed size of {@link LocalFileHeader#getFileData()}. */ public void setCompressedSize(long compressedSize) { - this.compressedSize.set(compressedSize & 0xFFFFFFFFL); + this.compressedSize = compressedSize & 0xFFFFFFFFL; } /** @@ -183,7 +180,7 @@ public void setCompressedSize(long compressedSize) { * @return Uncompressed size after {@link LocalFileHeader#decompress(Decompressor)} is used on {@link LocalFileHeader#getFileData()}. */ public long getUncompressedSize() { - return uncompressedSize.get(); + return uncompressedSize; } /** @@ -191,14 +188,14 @@ public long getUncompressedSize() { * Uncompressed size after {@link LocalFileHeader#decompress(Decompressor)} is used on {@link LocalFileHeader#getFileData()}. */ public void setUncompressedSize(long uncompressedSize) { - this.uncompressedSize.set(uncompressedSize & 0xFFFFFFFFL); + this.uncompressedSize = uncompressedSize & 0xFFFFFFFFL; } /** * @return Length of {@link #getFileName()}. */ public int getFileNameLength() { - return fileNameLength.get(); + return fileNameLength; } /** @@ -206,14 +203,14 @@ public int getFileNameLength() { * Length of {@link #getFileName()}. */ public void setFileNameLength(int fileNameLength) { - this.fileNameLength.set(fileNameLength & 0xFFFF); + this.fileNameLength = fileNameLength & 0xFFFF; } /** * @return Length of {@link #getExtraField()} */ public int getExtraFieldLength() { - return extraFieldLength.get(); + return extraFieldLength; } /** @@ -221,60 +218,91 @@ public int getExtraFieldLength() { * Length of {@link #getExtraField()} */ public void setExtraFieldLength(int extraFieldLength) { - this.extraFieldLength.set(extraFieldLength & 0xFFFF); + this.extraFieldLength = extraFieldLength & 0xFFFF; } /** * @return File name. */ - public MemorySegment getFileName() { - return fileName.get(); + public StringData getFileName() { + return fileName; } /** * @param fileName * File name. */ - public void setFileName(MemorySegment fileName) { - this.fileName.set(fileName); - fileNameCache = null; + public void setFileName(StringData fileName) { + this.fileName = fileName; } /** * @return File name. */ public String getFileNameAsString() { - String fileNameCache = this.fileNameCache; - if (fileNameCache == null && fileName != null) { - return this.fileNameCache = MemorySegmentUtil.toString(fileName.get()); - } - return fileNameCache; + return fileName.get(); } /** * @return May be used for extra compression information, * depending on the {@link #getCompressionMethod() compression method} used. */ - public MemorySegment getExtraField() { - return extraField.get(); + public MemorySegmentData getExtraField() { + return extraField; } /** * @param extraField * Extra field bytes. */ - public void setExtraField(MemorySegment extraField) { - this.extraField.set(extraField); + public void setExtraField(MemorySegmentData extraField) { + this.extraField = extraField; } /** * @return Extra field. */ public String getExtraFieldAsString() { - String fileCommentCache = this.extraFieldCache; - if (fileCommentCache == null && extraField != null) { - return this.extraFieldCache = MemorySegmentUtil.toString(extraField.get()); - } - return fileCommentCache; + return MemorySegmentUtil.toString(extraField.get()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AbstractZipFileHeader that)) return false; + + if (versionNeededToExtract != that.versionNeededToExtract) return false; + if (generalPurposeBitFlag != that.generalPurposeBitFlag) return false; + if (compressionMethod != that.compressionMethod) return false; + if (lastModFileTime != that.lastModFileTime) return false; + if (lastModFileDate != that.lastModFileDate) return false; + if (crc32 != that.crc32) return false; + if (compressedSize != that.compressedSize) return false; + if (uncompressedSize != that.uncompressedSize) return false; + if (fileNameLength != that.fileNameLength) return false; + if (extraFieldLength != that.extraFieldLength) return false; + if (offset != that.offset) return false; + if (!Objects.equals(fileName, that.fileName)) return false; + if (!Objects.equals(extraField, that.extraField)) return false; + return Objects.equals(data, that.data); + } + + @Override + public int hashCode() { + int result = versionNeededToExtract; + result = 31 * result + generalPurposeBitFlag; + result = 31 * result + compressionMethod; + result = 31 * result + lastModFileTime; + result = 31 * result + lastModFileDate; + result = 31 * result + crc32; + result = 31 * result + (int) (compressedSize ^ (compressedSize >>> 32)); + result = 31 * result + (int) (uncompressedSize ^ (uncompressedSize >>> 32)); + result = 31 * result + fileNameLength; + result = 31 * result + extraFieldLength; + result = 31 * result + (fileName != null ? fileName.hashCode() : 0); + result = 31 * result + (extraField != null ? extraField.hashCode() : 0); + result = 31 * result + (int) (offset ^ (offset >>> 32)); + result = 31 * result + (data != null ? data.hashCode() : 0); + return result; } } diff --git a/src/main/java/software/coley/lljzip/format/model/AdaptingLocalFileHeader.java b/src/main/java/software/coley/lljzip/format/model/AdaptingLocalFileHeader.java index 3f9d6af..0da6625 100644 --- a/src/main/java/software/coley/lljzip/format/model/AdaptingLocalFileHeader.java +++ b/src/main/java/software/coley/lljzip/format/model/AdaptingLocalFileHeader.java @@ -1,14 +1,13 @@ package software.coley.lljzip.format.model; -import software.coley.lljzip.util.lazy.LazyMemorySegment; -import software.coley.lljzip.util.lazy.LazyInt; -import software.coley.lljzip.util.lazy.LazyLong; +import software.coley.lljzip.util.MemorySegmentUtil; +import software.coley.lljzip.util.data.MemorySegmentData; +import software.coley.lljzip.util.data.StringData; import javax.annotation.Nonnull; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.lang.foreign.MemorySegment; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -39,25 +38,21 @@ public AdaptingLocalFileHeader(@Nonnull ZipFile archive, @Nonnull ZipEntry entry byte[] entryData = outputStream.toByteArray(); String entryName = entry.getName(); byte[] extra = entry.getExtra(); - versionNeededToExtract = new LazyInt(() -> 0); - generalPurposeBitFlag = new LazyInt(() -> 0); - lastModFileTime = new LazyInt(() -> 0); - lastModFileDate = new LazyInt(() -> 0); - fileNameLength = new LazyInt(entryName::length); - fileName = new LazyMemorySegment(() -> MemorySegment.ofArray(entryName.getBytes())); - fileDataLength = new LazyLong(() -> entryData.length); - fileData = new LazyMemorySegment(() -> MemorySegment.ofArray(entryData)); - compressionMethod = new LazyInt(() -> 0); - uncompressedSize = new LazyLong(() -> entryData.length); - compressedSize = new LazyLong(() -> entryData.length); - crc32 = new LazyInt(() -> (int) entry.getCrc()); - if (extra != null) { - extraFieldLength = new LazyInt(() -> extra.length); - extraField = new LazyMemorySegment(() -> MemorySegment.ofArray(extra)); + + fileNameLength = entryName.length(); + fileName = StringData.of(entryName); + fileData = MemorySegmentData.of(entryData); + compressionMethod = 0; + uncompressedSize = entryData.length; + compressedSize = entryData.length; + crc32 = (int) entry.getCrc(); + if (extra != null && extra.length > 0) { + extraFieldLength = extra.length; + extraField = MemorySegmentData.of(extra); } else { - extraFieldLength = new LazyInt(() -> 0); - extraField = new LazyMemorySegment(() -> MemorySegment.ofArray(new byte[0])); + extraFieldLength = 0; + extraField = MemorySegmentData.empty(); } - data = MemorySegment.ofArray(new byte[0]); + data = MemorySegmentUtil.EMPTY; } } diff --git a/src/main/java/software/coley/lljzip/format/model/CentralDirectoryFileHeader.java b/src/main/java/software/coley/lljzip/format/model/CentralDirectoryFileHeader.java index c101761..1f29b98 100644 --- a/src/main/java/software/coley/lljzip/format/model/CentralDirectoryFileHeader.java +++ b/src/main/java/software/coley/lljzip/format/model/CentralDirectoryFileHeader.java @@ -1,14 +1,14 @@ package software.coley.lljzip.format.model; -import software.coley.lljzip.util.MemorySegmentUtil; -import software.coley.lljzip.util.lazy.LazyMemorySegment; -import software.coley.lljzip.util.lazy.LazyInt; -import software.coley.lljzip.util.lazy.LazyLong; +import software.coley.lljzip.util.data.MemorySegmentData; +import software.coley.lljzip.util.data.StringData; import javax.annotation.Nonnull; import java.lang.foreign.MemorySegment; import java.util.Objects; +import static software.coley.lljzip.util.MemorySegmentUtil.*; + /** * ZIP CentralDirectoryFileHeader structure. *
@@ -41,16 +41,13 @@ public class CentralDirectoryFileHeader extends AbstractZipFileHeader {
 	private transient LocalFileHeader linkedFileHeader;
 
 	// CentralDirectoryFileHeader spec (plus common elements between this and local file)
-	private LazyInt versionMadeBy;
-	private LazyInt fileCommentLength;
-	private LazyMemorySegment fileComment;
-	private LazyInt diskNumberStart;
-	private LazyInt internalFileAttributes;
-	private LazyInt externalFileAttributes;
-	private LazyLong relativeOffsetOfLocalHeader;
-
-	// String cache values
-	private transient String fileCommentCache;
+	private int versionMadeBy;
+	private int fileCommentLength;
+	private StringData fileComment;
+	private int diskNumberStart;
+	private int internalFileAttributes;
+	private int externalFileAttributes;
+	private long relativeOffsetOfLocalHeader;
 
 	/**
 	 * @return Copy.
@@ -61,22 +58,22 @@ public CentralDirectoryFileHeader copy() {
 		copy.data = data;
 		copy.offset = offset;
 		copy.linkedFileHeader = linkedFileHeader;
-		copy.versionMadeBy = versionMadeBy.copy();
-		copy.versionNeededToExtract = versionNeededToExtract.copy();
-		copy.generalPurposeBitFlag = generalPurposeBitFlag.copy();
-		copy.compressionMethod = compressionMethod.copy();
-		copy.lastModFileTime = lastModFileTime.copy();
-		copy.lastModFileDate = lastModFileDate.copy();
-		copy.crc32 = crc32.copy();
-		copy.compressedSize = compressedSize.copy();
-		copy.uncompressedSize = uncompressedSize.copy();
-		copy.fileNameLength = fileNameLength.copy();
-		copy.extraFieldLength = extraFieldLength.copy();
-		copy.fileCommentLength = fileCommentLength.copy();
-		copy.diskNumberStart = diskNumberStart.copy();
-		copy.internalFileAttributes = internalFileAttributes.copy();
-		copy.externalFileAttributes = externalFileAttributes.copy();
-		copy.relativeOffsetOfLocalHeader = relativeOffsetOfLocalHeader.copy();
+		copy.versionMadeBy = versionMadeBy;
+		copy.versionNeededToExtract = versionNeededToExtract;
+		copy.generalPurposeBitFlag = generalPurposeBitFlag;
+		copy.compressionMethod = compressionMethod;
+		copy.lastModFileTime = lastModFileTime;
+		copy.lastModFileDate = lastModFileDate;
+		copy.crc32 = crc32;
+		copy.compressedSize = compressedSize;
+		copy.uncompressedSize = uncompressedSize;
+		copy.fileNameLength = fileNameLength;
+		copy.extraFieldLength = extraFieldLength;
+		copy.fileCommentLength = fileCommentLength;
+		copy.diskNumberStart = diskNumberStart;
+		copy.internalFileAttributes = internalFileAttributes;
+		copy.externalFileAttributes = externalFileAttributes;
+		copy.relativeOffsetOfLocalHeader = relativeOffsetOfLocalHeader;
 		copy.fileName = fileName.copy();
 		copy.extraField = extraField.copy();
 		copy.fileComment = fileComment.copy();
@@ -84,35 +81,57 @@ public CentralDirectoryFileHeader copy() {
 	}
 
 	@Override
-	public void read(@Nonnull MemorySegment data, long offset) {
+	public void read(@Nonnull MemorySegment data, long offset) throws ZipParseException {
 		super.read(data, offset);
-		versionMadeBy = MemorySegmentUtil.readLazyWord(data, offset, 4).withId("versionMadeBy");
-		versionNeededToExtract = MemorySegmentUtil.readLazyWord(data, offset, 6).withId("versionNeededToExtract");
-		generalPurposeBitFlag = MemorySegmentUtil.readLazyWord(data, offset, 8).withId("generalPurposeBitFlag");
-		compressionMethod = MemorySegmentUtil.readLazyWord(data, offset, 10).withId("compressionMethod");
-		lastModFileTime = MemorySegmentUtil.readLazyWord(data, offset, 12).withId("lastModFileTime");
-		lastModFileDate = MemorySegmentUtil.readLazyWord(data, offset, 14).withId("lastModFileDate");
-		crc32 = MemorySegmentUtil.readLazyQuad(data, offset, 16).withId("crc32");
-		compressedSize = MemorySegmentUtil.readLazyMaskedLongQuad(data, offset, 20).withId("compressedSize");
-		uncompressedSize = MemorySegmentUtil.readLazyMaskedLongQuad(data, offset, 24).withId("uncompressedSize");
-		fileNameLength = MemorySegmentUtil.readLazyWord(data, offset, 28).withId("fileNameLength");
-		extraFieldLength = MemorySegmentUtil.readLazyWord(data, offset, 30).withId("extraFieldLength");
-		fileCommentLength = MemorySegmentUtil.readLazyWord(data, offset, 32).withId("fileCommentLength");
-		diskNumberStart = MemorySegmentUtil.readLazyWord(data, offset, 34).withId("diskNumberStart");
-		internalFileAttributes = MemorySegmentUtil.readLazyWord(data, offset, 36).withId("internalFileAttributes");
-		externalFileAttributes = MemorySegmentUtil.readLazyQuad(data, offset, 38).withId("externalFileAttributes");
-		relativeOffsetOfLocalHeader = MemorySegmentUtil.readLazyMaskedLongQuad(data, offset, 42).withId("relativeOffsetOfLocalHeader");
-		fileName = MemorySegmentUtil.readLazySlice(data, offset, new LazyInt(() -> 46), fileNameLength).withId("fileName");
-		extraField = MemorySegmentUtil.readLazySlice(data, offset, fileNameLength.add(46), extraFieldLength).withId("extraField");
-		fileComment = MemorySegmentUtil.readLazySlice(data, offset, fileNameLength.add(46).add(extraFieldLength), fileCommentLength).withId("fileComment");
+		try {
+			versionMadeBy = readWord(data, offset, 4);
+			versionNeededToExtract = readWord(data, offset, 6);
+			generalPurposeBitFlag = readWord(data, offset, 8);
+			compressionMethod = readWord(data, offset, 10);
+			lastModFileTime = readWord(data, offset, 12);
+			lastModFileDate = readWord(data, offset, 14);
+			crc32 = readQuad(data, offset, 16);
+			compressedSize = readMaskedLongQuad(data, offset, 20);
+			uncompressedSize = readMaskedLongQuad(data, offset, 24);
+			fileNameLength = readWord(data, offset, 28);
+			extraFieldLength = readWord(data, offset, 30);
+			fileCommentLength = readWord(data, offset, 32);
+			diskNumberStart = readWord(data, offset, 34);
+			internalFileAttributes = readWord(data, offset, 36);
+			externalFileAttributes = readQuad(data, offset, 38);
+			relativeOffsetOfLocalHeader = readMaskedLongQuad(data, offset, 42);
+		} catch (Throwable t) {
+			throw new ZipParseException(ZipParseException.Type.OTHER);
+		}
+		try {
+			fileName = StringData.of(readSlice(data, offset, 46, fileNameLength));
+		} catch (IndexOutOfBoundsException ex) {
+			throw new ZipParseException(ex, ZipParseException.Type.IOOBE_FILE_NAME);
+		} catch (Throwable t) {
+			throw new ZipParseException(ZipParseException.Type.OTHER);
+		}
+		try {
+			extraField = MemorySegmentData.of(data, offset + 46 + fileNameLength, extraFieldLength);
+		} catch (IndexOutOfBoundsException ex) {
+			throw new ZipParseException(ex, ZipParseException.Type.IOOBE_FILE_EXTRA);
+		} catch (Throwable t) {
+			throw new ZipParseException(ZipParseException.Type.OTHER);
+		}
+		try {
+			fileComment = StringData.of(readSlice(data, offset, 46 + fileNameLength + extraFieldLength, fileCommentLength));
+		} catch (IndexOutOfBoundsException ex) {
+			throw new ZipParseException(ex, ZipParseException.Type.IOOBE_CEN_COMMENT);
+		} catch (Throwable t) {
+			throw new ZipParseException(ZipParseException.Type.OTHER);
+		}
 	}
 
 	@Override
 	public long length() {
 		return MIN_FIXED_SIZE +
-				fileNameLength.get() +
-				extraFieldLength.get() +
-				fileCommentLength.get();
+				fileNameLength +
+				extraFieldLength +
+				fileCommentLength;
 	}
 
 	@Nonnull
@@ -139,7 +158,7 @@ public int getFileNameLength() {
 	 * @return File name.
 	 */
 	@Override
-	public MemorySegment getFileName() {
+	public StringData getFileName() {
 		return super.getFileName();
 	}
 
@@ -173,7 +192,7 @@ public void link(LocalFileHeader header) {
 	 * @return Version of zip software used to make the archive.
 	 */
 	public int getVersionMadeBy() {
-		return versionMadeBy.get();
+		return versionMadeBy;
 	}
 
 	/**
@@ -181,14 +200,14 @@ public int getVersionMadeBy() {
 	 * 		Version of zip software used to make the archive.
 	 */
 	public void setVersionMadeBy(int versionMadeBy) {
-		this.versionMadeBy.set(versionMadeBy);
+		this.versionMadeBy = versionMadeBy;
 	}
 
 	/**
 	 * @return Disk number where the archive starts from, or {@code 0xFFFF} for ZIP64.
 	 */
 	public int getDiskNumberStart() {
-		return diskNumberStart.get();
+		return diskNumberStart;
 	}
 
 	/**
@@ -196,7 +215,7 @@ public int getDiskNumberStart() {
 	 * 		Disk number where the archive starts from, or {@code 0xFFFF} for ZIP64.
 	 */
 	public void setDiskNumberStart(int diskNumberStart) {
-		this.diskNumberStart.set(diskNumberStart);
+		this.diskNumberStart = diskNumberStart;
 	}
 
 	/**
@@ -211,7 +230,7 @@ public void setDiskNumberStart(int diskNumberStart) {
 	 * @return Internal attributes used for inferring content type.
 	 */
 	public int getInternalFileAttributes() {
-		return internalFileAttributes.get();
+		return internalFileAttributes;
 	}
 
 	/**
@@ -219,7 +238,7 @@ public int getInternalFileAttributes() {
 	 * 		Internal attributes used for inferring content type.
 	 */
 	public void setInternalFileAttributes(int internalFileAttributes) {
-		this.internalFileAttributes.set(internalFileAttributes);
+		this.internalFileAttributes = internalFileAttributes;
 	}
 
 	/**
@@ -229,7 +248,7 @@ public void setInternalFileAttributes(int internalFileAttributes) {
 	 * @return Host system dependent attributes.
 	 */
 	public int getExternalFileAttributes() {
-		return externalFileAttributes.get();
+		return externalFileAttributes;
 	}
 
 	/**
@@ -237,7 +256,7 @@ public int getExternalFileAttributes() {
 	 * 		Host system dependent attributes.
 	 */
 	public void setExternalFileAttributes(int externalFileAttributes) {
-		this.externalFileAttributes.set(externalFileAttributes);
+		this.externalFileAttributes = externalFileAttributes;
 	}
 
 	/**
@@ -245,7 +264,7 @@ public void setExternalFileAttributes(int externalFileAttributes) {
 	 * This should also be where the {@link LocalFileHeader} is located.  Or {@code 0xFFFFFFFF} for ZIP64.
 	 */
 	public long getRelativeOffsetOfLocalHeader() {
-		return relativeOffsetOfLocalHeader.get();
+		return relativeOffsetOfLocalHeader;
 	}
 
 	/**
@@ -254,14 +273,14 @@ public long getRelativeOffsetOfLocalHeader() {
 	 * 		This should also be where the {@link LocalFileHeader} is located.  Or {@code 0xFFFFFFFF} for ZIP64.
 	 */
 	public void setRelativeOffsetOfLocalHeader(long relativeOffsetOfLocalHeader) {
-		this.relativeOffsetOfLocalHeader.set(relativeOffsetOfLocalHeader & 0xFFFFFFFFL);
+		this.relativeOffsetOfLocalHeader = relativeOffsetOfLocalHeader & 0xFFFFFFFFL;
 	}
 
 	/**
 	 * @return Length of {@link #getFileComment()}.
 	 */
 	public int getFileCommentLength() {
-		return fileCommentLength.get();
+		return fileCommentLength;
 	}
 
 
@@ -270,33 +289,29 @@ public int getFileCommentLength() {
 	 * 		Length of {@link #getFileComment()}.
 	 */
 	public void setFileCommentLength(int fileCommentLength) {
-		this.fileCommentLength.set(fileCommentLength & 0xFFFF);
+		this.fileCommentLength = fileCommentLength & 0xFFFF;
 	}
 
 	/**
 	 * @return File comment.
 	 */
-	public MemorySegment getFileComment() {
-		return fileComment.get();
+	public StringData getFileComment() {
+		return fileComment;
 	}
 
 	/**
 	 * @param fileComment
 	 * 		File comment.
 	 */
-	public void setFileComment(MemorySegment fileComment) {
-		this.fileComment.set(fileComment);
+	public void setFileComment(StringData fileComment) {
+		this.fileComment = fileComment;
 	}
 
 	/**
 	 * @return File comment.
 	 */
 	public String getFileCommentAsString() {
-		String fileCommentCache = this.fileCommentCache;
-		if (fileCommentCache == null) {
-			return this.fileCommentCache = MemorySegmentUtil.toString(fileComment.get());
-		}
-		return fileCommentCache;
+		return fileComment.get();
 	}
 
 	@Override
@@ -327,54 +342,30 @@ public String toString() {
 	@Override
 	public boolean equals(Object o) {
 		if (this == o) return true;
-		if (o == null || getClass() != o.getClass()) return false;
-
-		CentralDirectoryFileHeader that = (CentralDirectoryFileHeader) o;
-
+		if (!(o instanceof CentralDirectoryFileHeader that)) return false;
+		if (!super.equals(o)) return false;
+
+		if (versionMadeBy != that.versionMadeBy) return false;
+		if (fileCommentLength != that.fileCommentLength) return false;
+		if (diskNumberStart != that.diskNumberStart) return false;
+		if (internalFileAttributes != that.internalFileAttributes) return false;
+		if (externalFileAttributes != that.externalFileAttributes) return false;
+		if (relativeOffsetOfLocalHeader != that.relativeOffsetOfLocalHeader) return false;
 		if (!Objects.equals(linkedFileHeader, that.linkedFileHeader)) return false;
-		if (!Objects.equals(versionMadeBy, that.versionMadeBy)) return false;
-		if (!Objects.equals(versionNeededToExtract, that.versionNeededToExtract)) return false;
-		if (!Objects.equals(generalPurposeBitFlag, that.generalPurposeBitFlag)) return false;
-		if (!Objects.equals(compressionMethod, that.compressionMethod)) return false;
-		if (!Objects.equals(lastModFileTime, that.lastModFileTime)) return false;
-		if (!Objects.equals(lastModFileDate, that.lastModFileDate)) return false;
-		if (!Objects.equals(crc32, that.crc32)) return false;
-		if (!Objects.equals(compressedSize, that.compressedSize)) return false;
-		if (!Objects.equals(uncompressedSize, that.uncompressedSize)) return false;
-		if (!Objects.equals(fileNameLength, that.fileNameLength)) return false;
-		if (!Objects.equals(extraFieldLength, that.extraFieldLength)) return false;
-		if (!Objects.equals(fileCommentLength, that.fileCommentLength)) return false;
-		if (!Objects.equals(diskNumberStart, that.diskNumberStart)) return false;
-		if (!Objects.equals(internalFileAttributes, that.internalFileAttributes)) return false;
-		if (!Objects.equals(externalFileAttributes, that.externalFileAttributes)) return false;
-		if (!Objects.equals(relativeOffsetOfLocalHeader, that.relativeOffsetOfLocalHeader)) return false;
-		if (!Objects.equals(fileName, that.fileName)) return false;
-		if (!Objects.equals(extraField, that.extraField)) return false;
 		return Objects.equals(fileComment, that.fileComment);
 	}
 
 	@Override
 	public int hashCode() {
-		int result = linkedFileHeader != null ? linkedFileHeader.hashCode() : 0;
-		result = 31 * result + (versionMadeBy != null ? versionMadeBy.hashCode() : 0);
-		result = 31 * result + (versionNeededToExtract != null ? versionNeededToExtract.hashCode() : 0);
-		result = 31 * result + (generalPurposeBitFlag != null ? generalPurposeBitFlag.hashCode() : 0);
-		result = 31 * result + (compressionMethod != null ? compressionMethod.hashCode() : 0);
-		result = 31 * result + (lastModFileTime != null ? lastModFileTime.hashCode() : 0);
-		result = 31 * result + (lastModFileDate != null ? lastModFileDate.hashCode() : 0);
-		result = 31 * result + (crc32 != null ? crc32.hashCode() : 0);
-		result = 31 * result + (compressedSize != null ? compressedSize.hashCode() : 0);
-		result = 31 * result + (uncompressedSize != null ? uncompressedSize.hashCode() : 0);
-		result = 31 * result + (fileNameLength != null ? fileNameLength.hashCode() : 0);
-		result = 31 * result + (extraFieldLength != null ? extraFieldLength.hashCode() : 0);
-		result = 31 * result + (fileCommentLength != null ? fileCommentLength.hashCode() : 0);
-		result = 31 * result + (diskNumberStart != null ? diskNumberStart.hashCode() : 0);
-		result = 31 * result + (internalFileAttributes != null ? internalFileAttributes.hashCode() : 0);
-		result = 31 * result + (externalFileAttributes != null ? externalFileAttributes.hashCode() : 0);
-		result = 31 * result + (relativeOffsetOfLocalHeader != null ? relativeOffsetOfLocalHeader.hashCode() : 0);
-		result = 31 * result + (fileName != null ? fileName.hashCode() : 0);
-		result = 31 * result + (extraField != null ? extraField.hashCode() : 0);
+		int result = super.hashCode();
+		result = 31 * result + (linkedFileHeader != null ? linkedFileHeader.hashCode() : 0);
+		result = 31 * result + versionMadeBy;
+		result = 31 * result + fileCommentLength;
 		result = 31 * result + (fileComment != null ? fileComment.hashCode() : 0);
+		result = 31 * result + diskNumberStart;
+		result = 31 * result + internalFileAttributes;
+		result = 31 * result + externalFileAttributes;
+		result = 31 * result + (int) (relativeOffsetOfLocalHeader ^ (relativeOffsetOfLocalHeader >>> 32));
 		return result;
 	}
 }
diff --git a/src/main/java/software/coley/lljzip/format/model/EndOfCentralDirectory.java b/src/main/java/software/coley/lljzip/format/model/EndOfCentralDirectory.java
index db3069a..420e661 100644
--- a/src/main/java/software/coley/lljzip/format/model/EndOfCentralDirectory.java
+++ b/src/main/java/software/coley/lljzip/format/model/EndOfCentralDirectory.java
@@ -1,11 +1,13 @@
 package software.coley.lljzip.format.model;
 
-import software.coley.lljzip.util.MemorySegmentUtil;
+import software.coley.lljzip.util.data.StringData;
 
 import javax.annotation.Nonnull;
 import java.lang.foreign.MemorySegment;
 import java.util.Objects;
 
+import static software.coley.lljzip.util.MemorySegmentUtil.readQuad;
+import static software.coley.lljzip.util.MemorySegmentUtil.readWord;
 
 /**
  * ZIP EndOfCentralDirectory structure.
@@ -35,8 +37,7 @@ public class EndOfCentralDirectory implements ZipPart, ZipRead {
 	private long centralDirectorySize;
 	private long centralDirectoryOffset;
 	private int zipCommentLength;
-	private MemorySegment zipComment;
-	private transient String zipCommentCache;
+	private StringData zipComment;
 
 	/**
 	 * @return Copy.
@@ -53,26 +54,25 @@ public EndOfCentralDirectory copy() {
 		copy.centralDirectoryOffset = centralDirectoryOffset;
 		copy.zipCommentLength = zipCommentLength;
 		copy.zipComment = zipComment;
-		copy.zipCommentCache = zipCommentCache;
 		return copy;
 	}
 
 	@Override
 	public void read(@Nonnull MemorySegment data, long offset) {
 		this.offset = offset;
-		diskNumber = MemorySegmentUtil.readWord(data, offset + 4);
-		centralDirectoryStartDisk = MemorySegmentUtil.readWord(data, offset + 6);
-		centralDirectoryStartOffset = MemorySegmentUtil.readWord(data, offset + 8);
-		numEntries = MemorySegmentUtil.readWord(data, offset + 10);
-		setCentralDirectorySize(MemorySegmentUtil.readQuad(data, offset + 12));
-		setCentralDirectoryOffset(MemorySegmentUtil.readQuad(data, offset + 16));
-		setZipCommentLength(MemorySegmentUtil.readWord(data, offset + 20));
-		zipComment = data.asSlice(offset + 22, zipCommentLength);
+		diskNumber = readWord(data, offset + 4);
+		centralDirectoryStartDisk = readWord(data, offset + 6);
+		centralDirectoryStartOffset = readWord(data, offset + 8);
+		numEntries = readWord(data, offset + 10);
+		setCentralDirectorySize(readQuad(data, offset + 12));
+		setCentralDirectoryOffset(readQuad(data, offset + 16));
+		setZipCommentLength(readWord(data, offset + 20));
+		zipComment = StringData.of(data.asSlice(offset + 22, zipCommentLength));
 	}
 
 	@Override
 	public long length() {
-		return 22L + zipComment.byteSize();
+		return 22L + zipComment.get().length();
 	}
 
 	@Nonnull
@@ -196,7 +196,7 @@ public void setZipCommentLength(int zipCommentLength) {
 	/**
 	 * @return Optional comment, or empty string.
 	 */
-	public MemorySegment getZipComment() {
+	public StringData getZipComment() {
 		return zipComment;
 	}
 
@@ -204,9 +204,7 @@ public MemorySegment getZipComment() {
 	 * @param zipComment
 	 * 		Optional comment, or empty string.
 	 */
-	public void setZipComment(MemorySegment zipComment) {
-		if (zipComment == null)
-			zipComment = MemorySegment.ofArray(new byte[0]);
+	public void setZipComment(StringData zipComment) {
 		this.zipComment = zipComment;
 	}
 
@@ -214,11 +212,7 @@ public void setZipComment(MemorySegment zipComment) {
 	 * @return Optional comment, or empty string.
 	 */
 	public String getZipCommentAsString() {
-		String zipCommentCache = this.zipCommentCache;
-		if (zipCommentCache == null) {
-			return this.zipCommentCache = MemorySegmentUtil.toString(zipComment);
-		}
-		return zipCommentCache;
+		return zipComment.get();
 	}
 
 	@Override
@@ -239,21 +233,30 @@ public String toString() {
 	@Override
 	public boolean equals(Object o) {
 		if (this == o) return true;
-		if (o == null || getClass() != o.getClass()) return false;
-		EndOfCentralDirectory that = (EndOfCentralDirectory) o;
-		return diskNumber == that.diskNumber &&
-				centralDirectoryStartDisk == that.centralDirectoryStartDisk &&
-				centralDirectoryStartOffset == that.centralDirectoryStartOffset &&
-				numEntries == that.numEntries &&
-				centralDirectorySize == that.centralDirectorySize &&
-				centralDirectoryOffset == that.centralDirectoryOffset &&
-				zipCommentLength == that.zipCommentLength &&
-				zipComment.equals(that.zipComment);
+		if (!(o instanceof EndOfCentralDirectory that)) return false;
+
+		if (offset != that.offset) return false;
+		if (diskNumber != that.diskNumber) return false;
+		if (centralDirectoryStartDisk != that.centralDirectoryStartDisk) return false;
+		if (centralDirectoryStartOffset != that.centralDirectoryStartOffset) return false;
+		if (numEntries != that.numEntries) return false;
+		if (centralDirectorySize != that.centralDirectorySize) return false;
+		if (centralDirectoryOffset != that.centralDirectoryOffset) return false;
+		if (zipCommentLength != that.zipCommentLength) return false;
+		return Objects.equals(zipComment, that.zipComment);
 	}
 
 	@Override
 	public int hashCode() {
-		return Objects.hash(diskNumber, centralDirectoryStartDisk, centralDirectoryStartOffset,
-				numEntries, centralDirectorySize, centralDirectoryOffset, zipCommentLength, zipComment);
+		int result = (int) (offset ^ (offset >>> 32));
+		result = 31 * result + diskNumber;
+		result = 31 * result + centralDirectoryStartDisk;
+		result = 31 * result + centralDirectoryStartOffset;
+		result = 31 * result + numEntries;
+		result = 31 * result + (int) (centralDirectorySize ^ (centralDirectorySize >>> 32));
+		result = 31 * result + (int) (centralDirectoryOffset ^ (centralDirectoryOffset >>> 32));
+		result = 31 * result + zipCommentLength;
+		result = 31 * result + (zipComment != null ? zipComment.hashCode() : 0);
+		return result;
 	}
 }
diff --git a/src/main/java/software/coley/lljzip/format/model/JvmLocalFileHeader.java b/src/main/java/software/coley/lljzip/format/model/JvmLocalFileHeader.java
index 7994422..d987321 100644
--- a/src/main/java/software/coley/lljzip/format/model/JvmLocalFileHeader.java
+++ b/src/main/java/software/coley/lljzip/format/model/JvmLocalFileHeader.java
@@ -3,7 +3,7 @@
 import software.coley.lljzip.format.ZipPatterns;
 import software.coley.lljzip.format.compression.ZipCompressions;
 import software.coley.lljzip.util.MemorySegmentUtil;
-import software.coley.lljzip.util.lazy.LazyLong;
+import software.coley.lljzip.util.data.MemorySegmentData;
 
 import javax.annotation.Nonnull;
 import java.lang.foreign.MemorySegment;
@@ -32,8 +32,15 @@ public void setOffsets(@Nonnull NavigableSet offsets) {
 	}
 
 	@Override
-	public void read(@Nonnull MemorySegment data, long offset) {
-		super.read(data, offset);
+	public void read(@Nonnull MemorySegment data, long offset) throws ZipParseException {
+		try {
+			super.read(data, offset);
+		} catch (ZipParseException ex) {
+			// If the error was that the data couldn't be read, that's OK because we're going to try reading
+			// that in a slightly different way below.
+			if (ex.getType() != ZipParseException.Type.IOOBE_FILE_DATA)
+				throw ex;
+		}
 
 		// JVM file data reading does NOT use the compressed/uncompressed fields.
 		// Instead, it scans data until the next header.
@@ -77,23 +84,27 @@ public void read(@Nonnull MemorySegment data, long offset) {
 		// Update the file data ranges
 		this.relativeDataOffsetStart = relativeDataOffsetStart;
 		this.relativeDataOffsetEnd = relativeDataOffsetEnd;
-		fileDataLength = new LazyLong(() -> relativeDataOffsetEnd - relativeDataOffsetStart).withId("fileDataLength");
-
-		fileData = MemorySegmentUtil.readLazyLongSlice(data, offset,
-				new LazyLong(() -> relativeDataOffsetStart), fileDataLength).withId("fileData");
+		long fileDataLength = relativeDataOffsetEnd - relativeDataOffsetStart;
+
+		try {
+			fileData = MemorySegmentData.of(MemorySegmentUtil.readLongSlice(data, offset, relativeDataOffsetStart, fileDataLength));
+		} catch (IndexOutOfBoundsException ex) {
+			throw new ZipParseException(ex, ZipParseException.Type.IOOBE_FILE_DATA);
+		} catch (Throwable t) {
+			throw new ZipParseException(ZipParseException.Type.OTHER);
+		}
 
 		// Update sizes where possible
-		long size = fileDataLength.get();
 		if (getCompressionMethod() == ZipCompressions.STORED) {
-			setUncompressedSize(size);
-			setCompressedSize(size);
+			setUncompressedSize(fileDataLength);
+			setCompressedSize(fileDataLength);
 		} else {
-			setCompressedSize(size);
+			setCompressedSize(fileDataLength);
 		}
 
 		// If we have a size, we can assume we found some data.
 		// Whether its valid, who really knows?
-		foundData = size != 0;
+		foundData = fileDataLength != 0;
 	}
 
 	@Override
@@ -103,7 +114,6 @@ public void adoptLinkedCentralDirectoryValues() {
 			return;
 
 		// JVM trusts central directory file header contents over local
-		//  - Using fields as this maintains the lazy model
 		versionNeededToExtract = linkedDirectoryFileHeader.versionNeededToExtract;
 		generalPurposeBitFlag = linkedDirectoryFileHeader.generalPurposeBitFlag;
 		compressionMethod = linkedDirectoryFileHeader.compressionMethod;
@@ -137,9 +147,7 @@ public void adoptLinkedCentralDirectoryValues() {
 			// Data should not be overflowing into adjacent header entries.
 			// - If it is, the data here is likely intentionally tampered with to screw with parsers
 			if (fileDataLength < relativeDataOffsetEnd) {
-				fileData = MemorySegmentUtil.readLazyLongSlice(data, offset,
-						new LazyLong(() -> relativeDataOffsetStart - offset),
-						new LazyLong(() -> fileDataLength)).withId("fileData");
+				fileData = MemorySegmentData.of(MemorySegmentUtil.readLongSlice(data, offset, relativeDataOffsetStart - offset, fileDataLength));
 			}
 		}
 	}
diff --git a/src/main/java/software/coley/lljzip/format/model/LocalFileHeader.java b/src/main/java/software/coley/lljzip/format/model/LocalFileHeader.java
index 6883c43..fb96cd7 100644
--- a/src/main/java/software/coley/lljzip/format/model/LocalFileHeader.java
+++ b/src/main/java/software/coley/lljzip/format/model/LocalFileHeader.java
@@ -2,10 +2,8 @@
 
 import software.coley.lljzip.format.compression.Decompressor;
 import software.coley.lljzip.format.read.ZipReader;
-import software.coley.lljzip.util.MemorySegmentUtil;
-import software.coley.lljzip.util.lazy.LazyMemorySegment;
-import software.coley.lljzip.util.lazy.LazyInt;
-import software.coley.lljzip.util.lazy.LazyLong;
+import software.coley.lljzip.util.data.MemorySegmentData;
+import software.coley.lljzip.util.data.StringData;
 
 import javax.annotation.Nonnull;
 import java.io.IOException;
@@ -13,6 +11,7 @@
 import java.util.Objects;
 
 import static software.coley.lljzip.format.compression.ZipCompressions.STORED;
+import static software.coley.lljzip.util.MemorySegmentUtil.*;
 
 /**
  * ZIP LocalFileHeader structure.
@@ -38,14 +37,11 @@
  * @author Matt Coley
  */
 public class LocalFileHeader extends AbstractZipFileHeader {
-	protected static final int MIN_FIXED_SIZE = 30;
+	public static final int MIN_FIXED_SIZE = 30;
 	protected transient CentralDirectoryFileHeader linkedDirectoryFileHeader;
 
 	// LocalFileHeader spec (plus common elements between this and central file)
-	protected LazyMemorySegment fileData;
-
-	// Caches
-	protected transient LazyLong fileDataLength;
+	protected MemorySegmentData fileData;
 
 	/**
 	 * @return Copy.
@@ -56,53 +52,63 @@ public LocalFileHeader copy() {
 		copy.data = data;
 		copy.offset = offset;
 		copy.linkedDirectoryFileHeader = linkedDirectoryFileHeader;
-		copy.versionNeededToExtract = versionNeededToExtract.copy();
-		copy.generalPurposeBitFlag = generalPurposeBitFlag.copy();
-		copy.compressionMethod = compressionMethod.copy();
-		copy.lastModFileTime = lastModFileTime.copy();
-		copy.lastModFileDate = lastModFileDate.copy();
-		copy.crc32 = crc32.copy();
-		copy.compressedSize = compressedSize.copy();
-		copy.uncompressedSize = uncompressedSize.copy();
-		copy.fileNameLength = fileNameLength.copy();
-		copy.extraFieldLength = extraFieldLength.copy();
+		copy.versionNeededToExtract = versionNeededToExtract;
+		copy.generalPurposeBitFlag = generalPurposeBitFlag;
+		copy.compressionMethod = compressionMethod;
+		copy.lastModFileTime = lastModFileTime;
+		copy.lastModFileDate = lastModFileDate;
+		copy.crc32 = crc32;
+		copy.compressedSize = compressedSize;
+		copy.uncompressedSize = uncompressedSize;
+		copy.fileNameLength = fileNameLength;
+		copy.extraFieldLength = extraFieldLength;
 		copy.fileName = fileName.copy();
 		copy.extraField = extraField.copy();
-		copy.fileDataLength = fileDataLength.copy();
 		copy.fileData = fileData.copy();
 		return copy;
 	}
 
 	@Override
-	public void read(@Nonnull MemorySegment data, long offset) {
+	public void read(@Nonnull MemorySegment data, long offset) throws ZipParseException {
 		super.read(data, offset);
-		versionNeededToExtract = MemorySegmentUtil.readLazyWord(data, offset, 4).withId("versionNeededToExtract");
-		generalPurposeBitFlag = MemorySegmentUtil.readLazyWord(data, offset, 6).withId("generalPurposeBitFlag");
-		compressionMethod = MemorySegmentUtil.readLazyWord(data, offset, 8).withId("compressionMethod");
-		lastModFileTime = MemorySegmentUtil.readLazyWord(data, offset, 10).withId("lastModFileTime");
-		lastModFileDate = MemorySegmentUtil.readLazyWord(data, offset, 12).withId("lastModFileDate");
-		crc32 = MemorySegmentUtil.readLazyQuad(data, offset, 14).withId("crc32");
-		compressedSize = MemorySegmentUtil.readLazyMaskedLongQuad(data, offset, 18).withId("compressedSize");
-		uncompressedSize = MemorySegmentUtil.readLazyMaskedLongQuad(data, offset, 22).withId("uncompressedSize");
-		fileNameLength = MemorySegmentUtil.readLazyWord(data, offset, 26).withId("fileNameLength");
-		extraFieldLength = MemorySegmentUtil.readLazyWord(data, offset, 28).withId("extraFieldLength");
-		fileName = MemorySegmentUtil.readLazySlice(data, offset, new LazyInt(() -> MIN_FIXED_SIZE), fileNameLength).withId("fileName");
-		extraField = MemorySegmentUtil.readLazySlice(data, offset, fileNameLength.add(MIN_FIXED_SIZE), extraFieldLength).withId("extraField");
-		fileDataLength = new LazyLong(() -> {
-			long fileDataLength;
-			if (compressionMethod.get() == STORED) {
-				fileDataLength = uncompressedSize.get();
-			} else {
-				fileDataLength = compressedSize.get();
-			}
-			return fileDataLength;
-		}).withId("fileDataLength");
-		fileData = MemorySegmentUtil.readLazyLongSlice(data, offset,
-				fileNameLength.add(extraFieldLength).add(MIN_FIXED_SIZE), fileDataLength).withId("fileData");
+		try {
+			versionNeededToExtract = readWord(data, offset, 4);
+			generalPurposeBitFlag = readWord(data, offset, 6);
+			compressionMethod = readWord(data, offset, 8);
+			lastModFileTime = readWord(data, offset, 10);
+			lastModFileDate = readWord(data, offset, 12);
+			crc32 = readQuad(data, offset, 14);
+			compressedSize = readMaskedLongQuad(data, offset, 18);
+			uncompressedSize = readMaskedLongQuad(data, offset, 22);
+			fileNameLength = readWord(data, offset, 26);
+			extraFieldLength = readWord(data, offset, 28);
+		} catch (Throwable t) {
+			throw new ZipParseException(ZipParseException.Type.OTHER);
+		}
+		try {
+			fileName = StringData.of(readSlice(data, offset, MIN_FIXED_SIZE, fileNameLength));
+		} catch (IndexOutOfBoundsException ex) {
+			throw new ZipParseException(ex, ZipParseException.Type.IOOBE_FILE_NAME);
+		} catch (Throwable t) {
+			throw new ZipParseException(ZipParseException.Type.OTHER);
+		}
+		try {
+			extraField = MemorySegmentData.of(data, offset + MIN_FIXED_SIZE + fileNameLength, extraFieldLength);
+		} catch (IndexOutOfBoundsException ex) {
+			throw new ZipParseException(ex, ZipParseException.Type.IOOBE_FILE_EXTRA);
+		} catch (Throwable t) {
+			throw new ZipParseException(ZipParseException.Type.OTHER);
+		}
+		long fileDataLength = (compressionMethod == STORED) ? uncompressedSize : compressedSize;
+		try {
+			fileData = MemorySegmentData.of(readLongSlice(data, offset, MIN_FIXED_SIZE + fileNameLength + extraFieldLength, fileDataLength));
+		} catch (IndexOutOfBoundsException ex) {
+			throw new ZipParseException(ex, ZipParseException.Type.IOOBE_FILE_DATA);
+		} catch (Throwable t) {
+			throw new ZipParseException(ZipParseException.Type.OTHER);
+		}
 	}
 
-
-
 	/**
 	 * Should match {@link CentralDirectoryFileHeader#getFileNameLength()} but is not a strict requirement.
 	 * If they do not match, the central directory file name length should be trusted instead.
@@ -121,8 +127,7 @@ public int getFileNameLength() {
 	 * @return File name.
 	 */
 	@Override
-	public MemorySegment getFileName() {
-
+	public StringData getFileName() {
 		return super.getFileName();
 	}
 
@@ -168,7 +173,7 @@ public boolean hasDifferentValuesThanCentralDirectoryHeader() {
 	 * In some cases the {@link LocalFileHeader} file size may be 0, but the authoritative CEN states a non-0 value,
 	 * which you may want to adopt.
 	 */
-	public void adoptLinkedCentralDirectoryValues() {
+	public void adoptLinkedCentralDirectoryValues() throws ZipParseException {
 		if (linkedDirectoryFileHeader != null) {
 			versionNeededToExtract = linkedDirectoryFileHeader.versionNeededToExtract;
 			generalPurposeBitFlag = linkedDirectoryFileHeader.generalPurposeBitFlag;
@@ -183,55 +188,41 @@ public void adoptLinkedCentralDirectoryValues() {
 			extraField = linkedDirectoryFileHeader.extraField;
 			// We're using the same slices/data locations from the central directory.
 			// If we wanted to use local data but with updated offsets from the central directory it would look like this:
-			//  fileName = ByteDataUtil.readLazySlice(data, offset, new LazyInt(() -> MIN_FIXED_SIZE), fileNameLength).withId("fileName");
-			//  extraField = ByteDataUtil.readLazySlice(data, offset, fileNameLength.add(MIN_FIXED_SIZE), extraFieldLength).withId("extraField");
-			fileDataLength = new LazyLong(() -> {
-				long fileDataLength;
-				if (compressionMethod.get() == STORED) {
-					fileDataLength = uncompressedSize.get();
-				} else {
-					fileDataLength = compressedSize.get();
-				}
-				return fileDataLength;
-			}).withId("fileDataLength");
+			//  fileName = ByteDataUtil.readSlice(data, offset, MIN_FIXED_SIZE, fileNameLength)
+			//  extraField = ByteDataUtil.readSlice(data, offset, MIN_FIXED_SIZE + fileNameLength, extraFieldLength)
+			long fileDataLength = (compressionMethod == STORED) ? uncompressedSize : compressedSize;
 			if (data != null)
-				fileData = MemorySegmentUtil.readLazyLongSlice(data, offset, fileNameLength.add(extraFieldLength).add(MIN_FIXED_SIZE), fileDataLength).withId("fileData");
+				try {
+					fileData = MemorySegmentData.of(readLongSlice(data, offset, MIN_FIXED_SIZE + fileNameLength + extraFieldLength, fileDataLength));
+				} catch (IndexOutOfBoundsException ex) {
+					throw new ZipParseException(ex, ZipParseException.Type.IOOBE_FILE_DATA);
+				}
 		}
 	}
 
 	/**
 	 * Sets the file data length to go up to the given offset.
 	 *
-	 * @param endOffset New file data length.
+	 * @param endOffset
+	 * 		New file data length.
 	 */
 	public void setFileDataEndOffset(long endOffset) {
-		long fileDataStartOffset = offset + fileNameLength.add(extraFieldLength).add(MIN_FIXED_SIZE).get();
+		long fileDataStartOffset = offset + MIN_FIXED_SIZE + fileNameLength + extraFieldLength;
 		long length = endOffset - fileDataStartOffset;
 		setFileDataLength(length);
 	}
 
 	/**
-	 * @param newLength New file data length.
+	 * @param newLength
+	 * 		New file data length.
 	 */
 	public void setFileDataLength(long newLength) {
-		fileDataLength.set(newLength);
-		fileData = MemorySegmentUtil.readLazyLongSlice(data, offset, fileNameLength.add(extraFieldLength).add(MIN_FIXED_SIZE), newLength).withId("fileData");
-	}
-
-	/**
-	 * @param newLength New file data length.
-	 */
-	public void setFileDataLength(@Nonnull LazyLong newLength) {
-		fileDataLength = newLength;
-		fileData = MemorySegmentUtil.readLazyLongSlice(data, offset, fileNameLength.add(extraFieldLength).add(MIN_FIXED_SIZE), newLength).withId("fileData");
+		fileData = MemorySegmentData.of(readLongSlice(data, offset, MIN_FIXED_SIZE + fileNameLength + extraFieldLength, newLength));
 	}
 
 	@Override
 	public long length() {
-		return MIN_FIXED_SIZE +
-				fileNameLength.get() +
-				extraFieldLength.get() +
-				fileDataLength.get();
+		return MIN_FIXED_SIZE + fileNameLength + extraFieldLength + fileData.length();
 	}
 
 	@Nonnull
@@ -281,15 +272,14 @@ public MemorySegment getFileData() {
 	 * @param fileData
 	 * 		Compressed file contents.
 	 */
-	public void setFileData(MemorySegment fileData) {
-		this.fileData.set(fileData);
+	public void setFileData(MemorySegmentData fileData) {
+		this.fileData = fileData;
 	}
 
 	@Override
 	public String toString() {
 		return "LocalFileHeader{" +
 				"fileData=" + fileData +
-				", fileDataLength=" + fileDataLength +
 				", data=" + data +
 				", versionNeededToExtract=" + versionNeededToExtract +
 				", generalPurposeBitFlag=" + generalPurposeBitFlag +
@@ -309,42 +299,17 @@ public String toString() {
 	@Override
 	public boolean equals(Object o) {
 		if (this == o) return true;
-		if (o == null || getClass() != o.getClass()) return false;
-
-		LocalFileHeader that = (LocalFileHeader) o;
+		if (!(o instanceof LocalFileHeader that)) return false;
+		if (!super.equals(o)) return false;
 
-		if (!Objects.equals(versionNeededToExtract, that.versionNeededToExtract)) return false;
-		if (!Objects.equals(generalPurposeBitFlag, that.generalPurposeBitFlag)) return false;
-		if (!Objects.equals(compressionMethod, that.compressionMethod)) return false;
-		if (!Objects.equals(lastModFileTime, that.lastModFileTime)) return false;
-		if (!Objects.equals(lastModFileDate, that.lastModFileDate)) return false;
-		if (!Objects.equals(crc32, that.crc32)) return false;
-		if (!Objects.equals(compressedSize, that.compressedSize)) return false;
-		if (!Objects.equals(uncompressedSize, that.uncompressedSize)) return false;
-		if (!Objects.equals(fileNameLength, that.fileNameLength)) return false;
-		if (!Objects.equals(extraFieldLength, that.extraFieldLength)) return false;
-		if (!Objects.equals(fileName, that.fileName)) return false;
-		if (!Objects.equals(extraField, that.extraField)) return false;
-		if (!Objects.equals(fileDataLength, that.fileDataLength)) return false;
+		if (!Objects.equals(linkedDirectoryFileHeader, that.linkedDirectoryFileHeader)) return false;
 		return Objects.equals(fileData, that.fileData);
 	}
 
 	@Override
 	public int hashCode() {
-		int result = 0;
-		result = 31 * result + (versionNeededToExtract != null ? versionNeededToExtract.hashCode() : 0);
-		result = 31 * result + (generalPurposeBitFlag != null ? generalPurposeBitFlag.hashCode() : 0);
-		result = 31 * result + (compressionMethod != null ? compressionMethod.hashCode() : 0);
-		result = 31 * result + (lastModFileTime != null ? lastModFileTime.hashCode() : 0);
-		result = 31 * result + (lastModFileDate != null ? lastModFileDate.hashCode() : 0);
-		result = 31 * result + (crc32 != null ? crc32.hashCode() : 0);
-		result = 31 * result + (compressedSize != null ? compressedSize.hashCode() : 0);
-		result = 31 * result + (uncompressedSize != null ? uncompressedSize.hashCode() : 0);
-		result = 31 * result + (fileNameLength != null ? fileNameLength.hashCode() : 0);
-		result = 31 * result + (extraFieldLength != null ? extraFieldLength.hashCode() : 0);
-		result = 31 * result + (fileName != null ? fileName.hashCode() : 0);
-		result = 31 * result + (extraField != null ? extraField.hashCode() : 0);
-		result = 31 * result + (fileDataLength != null ? fileDataLength.hashCode() : 0);
+		int result = super.hashCode();
+		result = 31 * result + (linkedDirectoryFileHeader != null ? linkedDirectoryFileHeader.hashCode() : 0);
 		result = 31 * result + (fileData != null ? fileData.hashCode() : 0);
 		return result;
 	}
diff --git a/src/main/java/software/coley/lljzip/format/model/ZipArchive.java b/src/main/java/software/coley/lljzip/format/model/ZipArchive.java
index 5ec7daf..50f1a66 100644
--- a/src/main/java/software/coley/lljzip/format/model/ZipArchive.java
+++ b/src/main/java/software/coley/lljzip/format/model/ZipArchive.java
@@ -9,7 +9,12 @@
 import java.io.Closeable;
 import java.io.IOException;
 import java.lang.foreign.MemorySegment;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
@@ -160,6 +165,7 @@ public List getNameFilteredLocalFiles(Predicate nameFil
 			return centralDirectories.stream()
 					.filter(c -> nameFilter.test(c.getFileNameAsString()))
 					.map(CentralDirectoryFileHeader::getLinkedFileHeader)
+					.filter(Objects::nonNull)
 					.collect(Collectors.toList());
 
 		// Fallback to using local entries
diff --git a/src/main/java/software/coley/lljzip/format/model/ZipParseException.java b/src/main/java/software/coley/lljzip/format/model/ZipParseException.java
new file mode 100644
index 0000000..b5a3866
--- /dev/null
+++ b/src/main/java/software/coley/lljzip/format/model/ZipParseException.java
@@ -0,0 +1,52 @@
+package software.coley.lljzip.format.model;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Exception thrown when parsing {@link ZipPart} contents from a data source.
+ *
+ * @author Matt Coley
+ * @see Type
+ */
+public class ZipParseException extends Exception {
+	private final Type type;
+
+	public ZipParseException(@Nullable IndexOutOfBoundsException cause, @Nonnull Type type) {
+		super(type.getMessage(), cause);
+		this.type = type;
+	}
+
+	public ZipParseException(@Nonnull Type type) {
+		this(null, type);
+	}
+
+	/**
+	 * @return Parse failure case.
+	 */
+	@Nonnull
+	public Type getType() {
+		return type;
+	}
+
+	/**
+	 * Enum of common parse failure cases.
+	 */
+	public enum Type {
+		IOOBE_FILE_NAME("Bounds check failed reading file name"),
+		IOOBE_FILE_DATA("Bounds check failed reading file data"),
+		IOOBE_FILE_EXTRA("Bounds check failed reading file extra"),
+		IOOBE_CEN_COMMENT("Bounds check failed reading directory comment"),
+		OTHER("Unknown zip parse error");
+
+		private final String message;
+
+		Type(String message) {
+			this.message = message;
+		}
+
+		String getMessage() {
+			return message;
+		}
+	}
+}
diff --git a/src/main/java/software/coley/lljzip/format/model/ZipRead.java b/src/main/java/software/coley/lljzip/format/model/ZipRead.java
index 3773f45..f5529aa 100644
--- a/src/main/java/software/coley/lljzip/format/model/ZipRead.java
+++ b/src/main/java/software/coley/lljzip/format/model/ZipRead.java
@@ -15,5 +15,5 @@ public interface ZipRead {
 	 * @param offset
 	 * 		Initial offset in data to start at.
 	 */
-	void read(@Nonnull MemorySegment data, long offset);
+	void read(@Nonnull MemorySegment data, long offset) throws ZipParseException;
 }
diff --git a/src/main/java/software/coley/lljzip/format/read/AbstractZipReader.java b/src/main/java/software/coley/lljzip/format/read/AbstractZipReader.java
index 81f9cf2..eb4ccf0 100644
--- a/src/main/java/software/coley/lljzip/format/read/AbstractZipReader.java
+++ b/src/main/java/software/coley/lljzip/format/read/AbstractZipReader.java
@@ -9,7 +9,8 @@
  */
 public abstract class AbstractZipReader extends DelegatingZipPartAllocator implements ZipReader {
 	/**
-	 * @param allocator Allocator to use.
+	 * @param allocator
+	 * 		Allocator to use.
 	 */
 	public AbstractZipReader(@Nonnull ZipPartAllocator allocator) {
 		super(allocator);
diff --git a/src/main/java/software/coley/lljzip/format/read/ForwardScanZipReader.java b/src/main/java/software/coley/lljzip/format/read/ForwardScanZipReader.java
index 8ef34fe..63b4690 100644
--- a/src/main/java/software/coley/lljzip/format/read/ForwardScanZipReader.java
+++ b/src/main/java/software/coley/lljzip/format/read/ForwardScanZipReader.java
@@ -7,13 +7,15 @@
 import software.coley.lljzip.format.model.EndOfCentralDirectory;
 import software.coley.lljzip.format.model.LocalFileHeader;
 import software.coley.lljzip.format.model.ZipArchive;
+import software.coley.lljzip.format.model.ZipParseException;
 import software.coley.lljzip.util.MemorySegmentUtil;
 import software.coley.lljzip.util.OffsetComparator;
 
 import javax.annotation.Nonnull;
 import java.io.IOException;
 import java.lang.foreign.MemorySegment;
-import java.util.*;
+import java.util.NavigableSet;
+import java.util.TreeSet;
 
 /**
  * The standard read strategy that should work with standard zip archives.
@@ -33,7 +35,8 @@ public ForwardScanZipReader() {
 	/**
 	 * New reader with given allocator.
 	 *
-	 * @param allocator Allocator to use.
+	 * @param allocator
+	 * 		Allocator to use.
 	 */
 	public ForwardScanZipReader(@Nonnull ZipPartAllocator allocator) {
 		super(allocator);
@@ -59,7 +62,12 @@ public void read(@Nonnull ZipArchive zip, @Nonnull MemorySegment data) throws IO
 		long centralDirectoryOffset = zipStart + end.getCentralDirectoryOffset();
 		while (centralDirectoryOffset < len && MemorySegmentUtil.readQuad(data, centralDirectoryOffset) == ZipPatterns.CENTRAL_DIRECTORY_FILE_HEADER_QUAD) {
 			CentralDirectoryFileHeader directory = new CentralDirectoryFileHeader();
-			directory.read(data, centralDirectoryOffset);
+			try {
+				directory.read(data, centralDirectoryOffset);
+			} catch (ZipParseException ex) {
+				// When the CEN cannot be read, we can't recover
+				throw new IOException(ex);
+			}
 			centralDirectoryOffset += directory.length();
 			zip.addPart(directory);
 		}
@@ -73,7 +81,12 @@ public void read(@Nonnull ZipArchive zip, @Nonnull MemorySegment data) throws IO
 				LocalFileHeader file = newLocalFileHeader();
 				directory.link(file);
 				file.link(directory);
-				file.read(data, offset);
+				try {
+					file.read(data, offset);
+				} catch (ZipParseException ex) {
+					// Unlike the other readers, we aren't going to fall back to using CEN data, so we won't recover from this.
+					throw new IOException(ex);
+				}
 				zip.addPart(file);
 				postProcessLocalFileHeader(file);
 				offsets.add(offset);
diff --git a/src/main/java/software/coley/lljzip/format/read/JvmZipReader.java b/src/main/java/software/coley/lljzip/format/read/JvmZipReader.java
index 47a3576..bc8516c 100644
--- a/src/main/java/software/coley/lljzip/format/read/JvmZipReader.java
+++ b/src/main/java/software/coley/lljzip/format/read/JvmZipReader.java
@@ -3,7 +3,12 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import software.coley.lljzip.format.ZipPatterns;
-import software.coley.lljzip.format.model.*;
+import software.coley.lljzip.format.model.CentralDirectoryFileHeader;
+import software.coley.lljzip.format.model.EndOfCentralDirectory;
+import software.coley.lljzip.format.model.JvmLocalFileHeader;
+import software.coley.lljzip.format.model.LocalFileHeader;
+import software.coley.lljzip.format.model.ZipArchive;
+import software.coley.lljzip.format.model.ZipParseException;
 import software.coley.lljzip.util.MemorySegmentUtil;
 import software.coley.lljzip.util.OffsetComparator;
 
@@ -27,12 +32,13 @@
 public class JvmZipReader extends AbstractZipReader {
 	private static final Logger logger = LoggerFactory.getLogger(JvmZipReader.class);
 	private final boolean skipRevisitedCenToLocalLinks;
+	private final boolean allowBasicJvmBaseOffsetZeroCheck;
 
 	/**
 	 * New reader with jvm allocator.
 	 */
 	public JvmZipReader() {
-		this(new JvmZipPartAllocator(), true);
+		this(new JvmZipPartAllocator(), true, true);
 	}
 
 	/**
@@ -41,9 +47,11 @@ public JvmZipReader() {
 	 * @param skipRevisitedCenToLocalLinks
 	 * 		Flag to skip creating duplicate {@link LocalFileHeader} entries if multiple
 	 *        {@link CentralDirectoryFileHeader} point to the same location.
+	 * @param allowBasicJvmBaseOffsetZeroCheck
+	 * 		Flag to check for {@code jvmBaseFileOffset == 0} before using the logic adapted from {@code ZipFile.Source#findEND()}.
 	 */
-	public JvmZipReader(boolean skipRevisitedCenToLocalLinks) {
-		this(new JvmZipPartAllocator(), skipRevisitedCenToLocalLinks);
+	public JvmZipReader(boolean skipRevisitedCenToLocalLinks, boolean allowBasicJvmBaseOffsetZeroCheck) {
+		this(new JvmZipPartAllocator(), skipRevisitedCenToLocalLinks, allowBasicJvmBaseOffsetZeroCheck);
 	}
 
 	/**
@@ -54,10 +62,13 @@ public JvmZipReader(boolean skipRevisitedCenToLocalLinks) {
 	 * @param skipRevisitedCenToLocalLinks
 	 * 		Flag to skip creating duplicate {@link LocalFileHeader} entries if multiple
 	 *        {@link CentralDirectoryFileHeader} point to the same location.
+	 * @param allowBasicJvmBaseOffsetZeroCheck
+	 * 		Flag to check for {@code jvmBaseFileOffset == 0} before using the logic adapted from {@code ZipFile.Source#findEND()}.
 	 */
-	public JvmZipReader(@Nonnull ZipPartAllocator allocator, boolean skipRevisitedCenToLocalLinks) {
+	public JvmZipReader(@Nonnull ZipPartAllocator allocator, boolean skipRevisitedCenToLocalLinks, boolean allowBasicJvmBaseOffsetZeroCheck) {
 		super(allocator);
 		this.skipRevisitedCenToLocalLinks = skipRevisitedCenToLocalLinks;
+		this.allowBasicJvmBaseOffsetZeroCheck = allowBasicJvmBaseOffsetZeroCheck;
 	}
 
 	@Override
@@ -85,7 +96,12 @@ public void read(@Nonnull ZipArchive zip, @Nonnull MemorySegment data) throws IO
 			centralDirectoryOffset = MemorySegmentUtil.lastIndexOfQuad(data, centralDirectoryOffset - 1L, ZipPatterns.CENTRAL_DIRECTORY_FILE_HEADER_QUAD);
 			if (centralDirectoryOffset >= 0L) {
 				CentralDirectoryFileHeader directory = newCentralDirectoryFileHeader();
-				directory.read(data, centralDirectoryOffset);
+				try {
+					directory.read(data, centralDirectoryOffset);
+				} catch (ZipParseException ex) {
+					// We cannot recover from the CEN reading encountering failures.
+					throw new IOException(ex);
+				}
 				zip.addPart(directory);
 				if (directory.getRelativeOffsetOfLocalHeader() > maxRelativeOffset)
 					maxRelativeOffset = directory.getRelativeOffsetOfLocalHeader();
@@ -122,29 +138,42 @@ public void read(@Nonnull ZipArchive zip, @Nonnull MemorySegment data) throws IO
 
 		// Search for the first valid PK header if there was either no prior ZIP file
 		// or if the prior ZIP detection was bogus.
-		if (priorZipEndWasBogus || precedingEndOfCentralDirectory == -1L) {
-			// There was no match for a prior end part. We will seek forwards until finding a *VALID* PK starting header.
-			//  - Java's zip parser does not always start from zero. It uses the computation:
-			//      locpos = (end.endpos - end.cenlen) - end.cenoff;
-			//  - This computation is taken from: ZipFile.Source#initCEN
-			jvmBaseFileOffset = (end.offset() - end.getCentralDirectorySize()) - end.getCentralDirectoryOffset();
-
-			// Now that we have the start offset, scan forward. We can match the current value as well.
-			jvmBaseFileOffset = MemorySegmentUtil.indexOfWord(data, jvmBaseFileOffset, ZipPatterns.PK_WORD);
-			while (jvmBaseFileOffset >= 0L) {
-				// Check that the PK discovered represents a valid zip part
-				try {
-					if (MemorySegmentUtil.readQuad(data, jvmBaseFileOffset) == ZipPatterns.LOCAL_FILE_HEADER_QUAD)
-						new LocalFileHeader().read(data, jvmBaseFileOffset);
-					else if (MemorySegmentUtil.readQuad(data, jvmBaseFileOffset) == ZipPatterns.CENTRAL_DIRECTORY_FILE_HEADER_QUAD)
-						new CentralDirectoryFileHeader().read(data, jvmBaseFileOffset);
-					else
+		scan:
+		{
+			if (priorZipEndWasBogus || precedingEndOfCentralDirectory == -1L) {
+				// If the start of the file is valid, we don't have to actually do much more anti-bogus work.
+				// This whole 'scan' block is incredibly yucky but seems to work for all of our edge case inputs.
+				if (allowBasicJvmBaseOffsetZeroCheck && MemorySegmentUtil.readQuad(data, 0) == ZipPatterns.LOCAL_FILE_HEADER_QUAD) {
+					if (MemorySegmentUtil.indexOfQuad(data, 1, ZipPatterns.LOCAL_FILE_HEADER_QUAD) > LocalFileHeader.MIN_FIXED_SIZE) {
+						break scan;
+					}
+				}
+
+				// There was no match for a prior end part. We will seek forwards until finding a *VALID* PK starting header.
+				//  - Java's zip parser does not always start from zero. It uses the computation:
+				//      locpos = (end.endpos - end.cenlen) - end.cenoff;
+				//  - This computation is taken from: ZipFile.Source#initCEN
+				long endPos = end.offset();
+				long cenLen = end.getCentralDirectorySize();
+				long cenOff = end.getCentralDirectoryOffset();
+				jvmBaseFileOffset = (endPos - cenLen) - cenOff;
+
+				// Now that we have the start offset, scan forward. We can match the current value as well.
+				jvmBaseFileOffset = MemorySegmentUtil.indexOfWord(data, jvmBaseFileOffset, ZipPatterns.PK_WORD);
+				while (jvmBaseFileOffset >= 0L) {
+					// Check that the PK discovered represents a valid zip part
+					try {
+						if (MemorySegmentUtil.readQuad(data, jvmBaseFileOffset) == ZipPatterns.LOCAL_FILE_HEADER_QUAD)
+							break;
+						else if (MemorySegmentUtil.readQuad(data, jvmBaseFileOffset) == ZipPatterns.CENTRAL_DIRECTORY_FILE_HEADER_QUAD)
+							break;
+
+						// Not an expected value...
 						throw new IllegalStateException("No match for LocalFileHeader/CentralDirectoryFileHeader");
-					// Valid, we're good to go
-					break;
-				} catch (Exception ex) {
-					// Invalid, seek forward
-					jvmBaseFileOffset = MemorySegmentUtil.indexOfWord(data, jvmBaseFileOffset + 1L, ZipPatterns.PK_WORD);
+					} catch (Exception ex) {
+						// Invalid, seek forward
+						jvmBaseFileOffset = MemorySegmentUtil.indexOfWord(data, jvmBaseFileOffset + 1L, ZipPatterns.PK_WORD);
+					}
 				}
 			}
 		}
@@ -195,7 +224,11 @@ else if (MemorySegmentUtil.readQuad(data, jvmBaseFileOffset) == ZipPatterns.CENT
 				if (file instanceof JvmLocalFileHeader) {
 					((JvmLocalFileHeader) file).setOffsets(entryOffsets);
 				}
-				file.read(data, offset);
+				try {
+					file.read(data, offset);
+				} catch (IndexOutOfBoundsException t) {
+					// Its intended that if this fails the adopting of CEN values below will work instead.
+				}
 				directory.link(file);
 				file.link(directory);
 				file.adoptLinkedCentralDirectoryValues();
@@ -209,7 +242,8 @@ else if (MemorySegmentUtil.readQuad(data, jvmBaseFileOffset) == ZipPatterns.CENT
 		// Record any data appearing at the front of the file not associated with the ZIP file contents.
 		if (!entryOffsets.isEmpty()) {
 			long firstOffset = entryOffsets.first();
-			zip.setPrefixData(data.asSlice(0, firstOffset));
+			if (firstOffset > 0)
+				zip.setPrefixData(data.asSlice(0, firstOffset));
 		}
 
 		// Sort based on order
diff --git a/src/main/java/software/coley/lljzip/format/read/NaiveLocalFileZipReader.java b/src/main/java/software/coley/lljzip/format/read/NaiveLocalFileZipReader.java
index c422618..82667e7 100644
--- a/src/main/java/software/coley/lljzip/format/read/NaiveLocalFileZipReader.java
+++ b/src/main/java/software/coley/lljzip/format/read/NaiveLocalFileZipReader.java
@@ -3,6 +3,7 @@
 import software.coley.lljzip.format.ZipPatterns;
 import software.coley.lljzip.format.model.LocalFileHeader;
 import software.coley.lljzip.format.model.ZipArchive;
+import software.coley.lljzip.format.model.ZipParseException;
 import software.coley.lljzip.util.MemorySegmentUtil;
 
 import javax.annotation.Nonnull;
@@ -43,7 +44,11 @@ public void read(@Nonnull ZipArchive zip, @Nonnull MemorySegment data) throws IO
 		}
 		do {
 			LocalFileHeader file = newLocalFileHeader();
-			file.read(data, localFileOffset);
+			try {
+				file.read(data, localFileOffset);
+			} catch (ZipParseException ex) {
+				throw new IOException(ex);
+			}
 			zip.addPart(file);
 			postProcessLocalFileHeader(file);
 		} while ((localFileOffset = MemorySegmentUtil.indexOfQuad(data, localFileOffset + 1, ZipPatterns.LOCAL_FILE_HEADER_QUAD)) >= 0);
diff --git a/src/main/java/software/coley/lljzip/format/read/SimpleZipPartAllocator.java b/src/main/java/software/coley/lljzip/format/read/SimpleZipPartAllocator.java
index d103f26..8ff8a28 100644
--- a/src/main/java/software/coley/lljzip/format/read/SimpleZipPartAllocator.java
+++ b/src/main/java/software/coley/lljzip/format/read/SimpleZipPartAllocator.java
@@ -11,7 +11,7 @@
  *
  * @author Matt Coley
  */
-public class SimpleZipPartAllocator implements ZipPartAllocator{
+public class SimpleZipPartAllocator implements ZipPartAllocator {
 	@Nonnull
 	@Override
 	public LocalFileHeader newLocalFileHeader() {
diff --git a/src/main/java/software/coley/lljzip/format/read/ZipReader.java b/src/main/java/software/coley/lljzip/format/read/ZipReader.java
index 2ae3465..ea10a3f 100644
--- a/src/main/java/software/coley/lljzip/format/read/ZipReader.java
+++ b/src/main/java/software/coley/lljzip/format/read/ZipReader.java
@@ -25,7 +25,8 @@ public interface ZipReader {
 	void read(@Nonnull ZipArchive zip, @Nonnull MemorySegment data) throws IOException;
 
 	/**
-	 * @param file File to post-process.
+	 * @param file
+	 * 		File to post-process.
 	 */
 	default void postProcessLocalFileHeader(@Nonnull LocalFileHeader file) {
 		// no-op by default
diff --git a/src/main/java/software/coley/lljzip/format/transform/CentralAdoptingMapper.java b/src/main/java/software/coley/lljzip/format/transform/CentralAdoptingMapper.java
index a2133a6..debe625 100644
--- a/src/main/java/software/coley/lljzip/format/transform/CentralAdoptingMapper.java
+++ b/src/main/java/software/coley/lljzip/format/transform/CentralAdoptingMapper.java
@@ -3,6 +3,7 @@
 import software.coley.lljzip.format.model.CentralDirectoryFileHeader;
 import software.coley.lljzip.format.model.LocalFileHeader;
 import software.coley.lljzip.format.model.ZipArchive;
+import software.coley.lljzip.format.model.ZipParseException;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -26,7 +27,11 @@ public CentralAdoptingMapper(@Nonnull ZipPartMapper delegate) {
 	public LocalFileHeader mapLocal(@Nonnull ZipArchive archive, @Nonnull LocalFileHeader localFileHeader) {
 		if (localFileHeader.getLinkedDirectoryFileHeader() != null) {
 			LocalFileHeader copy = localFileHeader.copy();
-			copy.adoptLinkedCentralDirectoryValues();
+			try {
+				copy.adoptLinkedCentralDirectoryValues();
+			} catch (ZipParseException ex) {
+				throw new IllegalStateException(ex);
+			}
 			return copy;
 		}
 		return super.mapLocal(archive, localFileHeader);
diff --git a/src/main/java/software/coley/lljzip/format/transform/JvmClassDirectoryMapper.java b/src/main/java/software/coley/lljzip/format/transform/JvmClassDirectoryMapper.java
index 42f0e10..c5c5f91 100644
--- a/src/main/java/software/coley/lljzip/format/transform/JvmClassDirectoryMapper.java
+++ b/src/main/java/software/coley/lljzip/format/transform/JvmClassDirectoryMapper.java
@@ -28,7 +28,7 @@ public LocalFileHeader mapLocal(@Nonnull ZipArchive archive, @Nonnull LocalFileH
 		if (name.endsWith(".class/")) {
 			int newLength = name.length() - 1;
 			LocalFileHeader copy = localFileHeader.copy();
-			copy.setFileName(copy.getFileName().asSlice(0, newLength));
+			copy.setFileName(copy.getFileName().substring(0, newLength));
 			copy.setFileNameLength(newLength);
 			localFileHeader = copy;
 		}
@@ -42,7 +42,7 @@ public CentralDirectoryFileHeader mapCentral(@Nonnull ZipArchive archive, @Nonnu
 		if (name.endsWith(".class/")) {
 			int newLength = name.length() - 1;
 			CentralDirectoryFileHeader copy = centralDirectoryFileHeader.copy();
-			copy.setFileName(copy.getFileName().asSlice(0, newLength));
+			copy.setFileName(copy.getFileName().substring(0, newLength));
 			copy.setFileNameLength(newLength);
 			centralDirectoryFileHeader = copy;
 		}
diff --git a/src/main/java/software/coley/lljzip/format/transform/ZipPartMapper.java b/src/main/java/software/coley/lljzip/format/transform/ZipPartMapper.java
index 7aed95d..d9df231 100644
--- a/src/main/java/software/coley/lljzip/format/transform/ZipPartMapper.java
+++ b/src/main/java/software/coley/lljzip/format/transform/ZipPartMapper.java
@@ -1,6 +1,10 @@
 package software.coley.lljzip.format.transform;
 
-import software.coley.lljzip.format.model.*;
+import software.coley.lljzip.format.model.CentralDirectoryFileHeader;
+import software.coley.lljzip.format.model.EndOfCentralDirectory;
+import software.coley.lljzip.format.model.LocalFileHeader;
+import software.coley.lljzip.format.model.ZipArchive;
+import software.coley.lljzip.format.model.ZipPart;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
diff --git a/src/main/java/software/coley/lljzip/format/write/DelegatingZipWriter.java b/src/main/java/software/coley/lljzip/format/write/DelegatingZipWriter.java
index 64d1a10..ecece63 100644
--- a/src/main/java/software/coley/lljzip/format/write/DelegatingZipWriter.java
+++ b/src/main/java/software/coley/lljzip/format/write/DelegatingZipWriter.java
@@ -19,7 +19,8 @@ public class DelegatingZipWriter implements ZipWriter {
 	private final ZipWriter delegate;
 
 	/**
-	 * @param delegate Delegate writer.
+	 * @param delegate
+	 * 		Delegate writer.
 	 */
 	public DelegatingZipWriter(@Nullable ZipWriter delegate) {
 		this.delegate = delegate;
diff --git a/src/main/java/software/coley/lljzip/format/write/DirectZipWriter.java b/src/main/java/software/coley/lljzip/format/write/DirectZipWriter.java
index 5be8497..daaf20f 100644
--- a/src/main/java/software/coley/lljzip/format/write/DirectZipWriter.java
+++ b/src/main/java/software/coley/lljzip/format/write/DirectZipWriter.java
@@ -46,8 +46,8 @@ protected void writeLocalFile(@Nonnull LocalFileHeader fileHeader, @Nonnull Outp
 		writeIntLE(os, (int) fileHeader.getUncompressedSize());
 		writeShortLE(os, fileHeader.getFileNameLength());
 		writeShortLE(os, fileHeader.getExtraFieldLength());
-		os.write(MemorySegmentUtil.toByteArray(fileHeader.getFileName()));
-		os.write(MemorySegmentUtil.toByteArray(fileHeader.getExtraField()));
+		os.write(fileHeader.getFileName().get().getBytes());
+		os.write(MemorySegmentUtil.toByteArray(fileHeader.getExtraField().get()));
 		os.write(MemorySegmentUtil.toByteArray(fileHeader.getFileData()));
 	}
 
@@ -69,9 +69,9 @@ protected void writeCentralDirectory(@Nonnull CentralDirectoryFileHeader directo
 		writeShortLE(os, directory.getInternalFileAttributes());
 		writeIntLE(os, directory.getExternalFileAttributes());
 		writeIntLE(os, (int) directory.getRelativeOffsetOfLocalHeader());
-		os.write(MemorySegmentUtil.toByteArray(directory.getFileName()));
-		os.write(MemorySegmentUtil.toByteArray(directory.getExtraField()));
-		os.write(MemorySegmentUtil.toByteArray(directory.getFileComment()));
+		os.write(directory.getFileName().get().getBytes());
+		os.write(MemorySegmentUtil.toByteArray(directory.getExtraField().get()));
+		os.write(directory.getFileComment().get().getBytes());
 	}
 
 	protected void writeEnd(@Nonnull EndOfCentralDirectory end, @Nonnull OutputStream os) throws IOException {
@@ -83,7 +83,7 @@ protected void writeEnd(@Nonnull EndOfCentralDirectory end, @Nonnull OutputStrea
 		writeIntLE(os, (int) end.getCentralDirectorySize());
 		writeIntLE(os, (int) end.getCentralDirectoryOffset());
 		writeShortLE(os, end.getZipCommentLength());
-		os.write(MemorySegmentUtil.toByteArray(end.getZipComment()));
+		os.write(end.getZipComment().get().getBytes());
 	}
 
 	protected static void writeShortLE(OutputStream os, int value) throws IOException {
diff --git a/src/main/java/software/coley/lljzip/util/ExtraFieldTime.java b/src/main/java/software/coley/lljzip/util/ExtraFieldTime.java
index f7c48bd..75566cf 100644
--- a/src/main/java/software/coley/lljzip/util/ExtraFieldTime.java
+++ b/src/main/java/software/coley/lljzip/util/ExtraFieldTime.java
@@ -26,7 +26,7 @@ public class ExtraFieldTime {
 	public static TimeWrapper read(@Nonnull CentralDirectoryFileHeader header) {
 		int extraLen = header.getExtraFieldLength();
 		if (extraLen > 0 && extraLen < 0xFFFF) {
-			MemorySegment extra = header.getExtraField();
+			MemorySegment extra = header.getExtraField().get();
 			return read(extra);
 		}
 		return null;
@@ -42,7 +42,7 @@ public static TimeWrapper read(@Nonnull CentralDirectoryFileHeader header) {
 	public static TimeWrapper read(@Nonnull LocalFileHeader header) {
 		int extraLen = header.getExtraFieldLength();
 		if (extraLen > 0 && extraLen < 0xFFFF) {
-			MemorySegment extra = header.getExtraField();
+			MemorySegment extra = header.getExtraField().get();
 			return read(extra);
 		}
 		return null;
diff --git a/src/main/java/software/coley/lljzip/util/MemorySegmentUtil.java b/src/main/java/software/coley/lljzip/util/MemorySegmentUtil.java
index 7ce25cc..8b06156 100644
--- a/src/main/java/software/coley/lljzip/util/MemorySegmentUtil.java
+++ b/src/main/java/software/coley/lljzip/util/MemorySegmentUtil.java
@@ -1,9 +1,6 @@
 package software.coley.lljzip.util;
 
 import software.coley.lljzip.format.model.ZipPart;
-import software.coley.lljzip.util.lazy.LazyMemorySegment;
-import software.coley.lljzip.util.lazy.LazyInt;
-import software.coley.lljzip.util.lazy.LazyLong;
 
 import java.lang.foreign.MemorySegment;
 import java.lang.foreign.ValueLayout;
@@ -17,6 +14,7 @@
  */
 public class MemorySegmentUtil {
 	public static final int WILDCARD = Integer.MIN_VALUE;
+	public static final MemorySegment EMPTY = MemorySegment.ofArray(new byte[0]);
 	private static final ValueLayout.OfInt LITTLE_INT = ValueLayout.JAVA_INT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
 	private static final ValueLayout.OfShort LITTLE_SHORT = ValueLayout.JAVA_SHORT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
 
@@ -319,12 +317,10 @@ public static String toString(MemorySegment data) {
 	 * @param localOffset
 	 * 		Local offset from the header offset.
 	 *
-	 * @return Lazily populated word.
+	 * @return Value of word.
 	 */
-	public static LazyInt readLazyWord(MemorySegment data, long headerOffset, int localOffset) {
-		return new LazyInt(() -> {
-			return readWord(data, headerOffset + localOffset);
-		});
+	public static int readWord(MemorySegment data, long headerOffset, int localOffset) {
+		return readWord(data, headerOffset + localOffset);
 	}
 
 	/**
@@ -335,12 +331,10 @@ public static LazyInt readLazyWord(MemorySegment data, long headerOffset, int lo
 	 * @param localOffset
 	 * 		Local offset from the header offset.
 	 *
-	 * @return Lazily populated quad.
+	 * @return Value of quad.
 	 */
-	public static LazyInt readLazyQuad(MemorySegment data, long headerOffset, int localOffset) {
-		return new LazyInt(() -> {
-			return readQuad(data, headerOffset + localOffset);
-		});
+	public static int readQuad(MemorySegment data, long headerOffset, int localOffset) {
+		return readQuad(data, headerOffset + localOffset);
 	}
 
 	/**
@@ -351,12 +345,10 @@ public static LazyInt readLazyQuad(MemorySegment data, long headerOffset, int lo
 	 * @param localOffset
 	 * 		Local offset from the header offset.
 	 *
-	 * @return Lazily populated masked quad.
+	 * @return Value of masked quad.
 	 */
-	public static LazyInt readLazyMaskedQuad(MemorySegment data, long headerOffset, int localOffset) {
-		return new LazyInt(() -> {
-			return readQuad(data, headerOffset + localOffset) & 0xFFFF;
-		});
+	public static int readMaskedQuad(MemorySegment data, long headerOffset, int localOffset) {
+		return readQuad(data, headerOffset + localOffset) & 0xFFFF;
 	}
 
 	/**
@@ -367,12 +359,10 @@ public static LazyInt readLazyMaskedQuad(MemorySegment data, long headerOffset,
 	 * @param localOffset
 	 * 		Local offset from the header offset.
 	 *
-	 * @return Lazily populated long word.
+	 * @return Value of long word.
 	 */
-	public static LazyLong readLazyLongWord(MemorySegment data, long headerOffset, int localOffset) {
-		return new LazyLong(() -> {
-			return readWord(data, headerOffset + localOffset);
-		});
+	public static long readLongWord(MemorySegment data, long headerOffset, int localOffset) {
+		return readWord(data, headerOffset + localOffset);
 	}
 
 	/**
@@ -383,12 +373,10 @@ public static LazyLong readLazyLongWord(MemorySegment data, long headerOffset, i
 	 * @param localOffset
 	 * 		Local offset from the header offset.
 	 *
-	 * @return Lazily populated masked long quad.
+	 * @return Value of masked long quad.
 	 */
-	public static LazyLong readLazyMaskedLongQuad(MemorySegment data, long headerOffset, int localOffset) {
-		return new LazyLong(() -> {
-			return readQuad(data, headerOffset + localOffset) & 0xFFFFFFFFL;
-		});
+	public static long readMaskedLongQuad(MemorySegment data, long headerOffset, int localOffset) {
+		return readQuad(data, headerOffset + localOffset) & 0xFFFFFFFFL;
 	}
 
 	/**
@@ -399,28 +387,10 @@ public static LazyLong readLazyMaskedLongQuad(MemorySegment data, long headerOff
 	 * @param localOffset
 	 * 		Local offset from the header offset.
 	 *
-	 * @return Lazily populated slice.
-	 */
-	public static LazyMemorySegment readLazySlice(MemorySegment data, long headerOffset, LazyInt localOffset, LazyInt length) {
-		return new LazyMemorySegment(() -> {
-			return data.asSlice(headerOffset + localOffset.get(), length.get());
-		});
-	}
-
-	/**
-	 * @param data
-	 * 		Content to get long slice from.
-	 * @param headerOffset
-	 * 		Offset of {@link ZipPart} header.
-	 * @param localOffset
-	 * 		Local offset from the header offset.
-	 *
-	 * @return Lazily populated long slice.
+	 * @return Value of slice.
 	 */
-	public static LazyMemorySegment readLazyLongSlice(MemorySegment data, long headerOffset, LazyInt localOffset, LazyLong length) {
-		return new LazyMemorySegment(() -> {
-			return data.asSlice(headerOffset + localOffset.get(), length.get());
-		});
+	public static MemorySegment readSlice(MemorySegment data, long headerOffset, int localOffset, int length) {
+		return data.asSlice(headerOffset + localOffset, length);
 	}
 
 	/**
@@ -431,12 +401,10 @@ public static LazyMemorySegment readLazyLongSlice(MemorySegment data, long heade
 	 * @param localOffset
 	 * 		Local offset from the header offset.
 	 *
-	 * @return Lazily populated long slice.
+	 * @return Value of long slice.
 	 */
-	public static LazyMemorySegment readLazyLongSlice(MemorySegment data, long headerOffset, LazyLong localOffset, LazyLong length) {
-		return new LazyMemorySegment(() -> {
-			return data.asSlice(headerOffset + localOffset.get(), length.get());
-		});
+	public static MemorySegment readLongSlice(MemorySegment data, long headerOffset, int localOffset, long length) {
+		return data.asSlice(headerOffset + localOffset, length);
 	}
 
 	/**
@@ -447,11 +415,9 @@ public static LazyMemorySegment readLazyLongSlice(MemorySegment data, long heade
 	 * @param localOffset
 	 * 		Local offset from the header offset.
 	 *
-	 * @return Lazily populated long slice.
+	 * @return Value of long slice.
 	 */
-	public static LazyMemorySegment readLazyLongSlice(MemorySegment data, long headerOffset, LazyInt localOffset, long length) {
-		return new LazyMemorySegment(() -> {
-			return data.asSlice(headerOffset + localOffset.get(), length);
-		});
+	public static MemorySegment readLongSlice(MemorySegment data, long headerOffset, long localOffset, long length) {
+		return data.asSlice(headerOffset + localOffset, length);
 	}
 }
diff --git a/src/main/java/software/coley/lljzip/util/data/MemorySegmentData.java b/src/main/java/software/coley/lljzip/util/data/MemorySegmentData.java
new file mode 100644
index 0000000..8b1607c
--- /dev/null
+++ b/src/main/java/software/coley/lljzip/util/data/MemorySegmentData.java
@@ -0,0 +1,107 @@
+package software.coley.lljzip.util.data;
+
+import software.coley.lljzip.util.MemorySegmentUtil;
+
+import javax.annotation.Nonnull;
+import java.lang.foreign.MemorySegment;
+
+/**
+ * Wrapper for reading {@link MemorySegment} content.
+ *
+ * @author Matt Coley
+ */
+public interface MemorySegmentData {
+	@Nonnull
+	MemorySegment get();
+
+	default long length() {
+		return get().byteSize();
+	}
+
+	@Nonnull
+	default MemorySegmentData copy() {
+		return of(get());
+	}
+
+	@Nonnull
+	static MemorySegmentData empty() {
+		return FullSegment.EMPTY;
+	}
+
+	@Nonnull
+	static MemorySegmentData of(@Nonnull byte[] source) {
+		if (source.length == 0)
+			return empty();
+		return of(MemorySegment.ofArray(source));
+	}
+
+	@Nonnull
+	static MemorySegmentData of(@Nonnull MemorySegment segment) {
+		if (segment.byteSize() == 0)
+			return empty();
+		return new FullSegment(segment);
+	}
+
+	@Nonnull
+	static MemorySegmentData of(@Nonnull MemorySegment segment, long offset, long length) {
+		if (length == 0)
+			return empty();
+		return new Caching(new PartialSegment(segment, offset, length));
+	}
+
+	class Caching implements MemorySegmentData {
+		private final MemorySegmentData delegate;
+		private MemorySegment cached;
+
+		public Caching(@Nonnull MemorySegmentData delegate) {
+			this.delegate = delegate;
+		}
+
+		@Nonnull
+		@Override
+		public MemorySegment get() {
+			if (cached == null)
+				cached = delegate.get();
+			return cached;
+		}
+	}
+
+	class PartialSegment implements MemorySegmentData {
+		private final MemorySegment segment;
+		private final long offset;
+		private final long length;
+
+		public PartialSegment(@Nonnull MemorySegment segment, long offset, long length) {
+			this.segment = segment;
+			this.offset = offset;
+			this.length = length;
+		}
+
+		@Nonnull
+		@Override
+		public MemorySegment get() {
+			return segment.asSlice(offset, length);
+		}
+	}
+
+	class FullSegment implements MemorySegmentData {
+		private static final FullSegment EMPTY = new FullSegment(MemorySegmentUtil.EMPTY);
+		private final MemorySegment segment;
+
+		public FullSegment(@Nonnull MemorySegment segment) {
+			this.segment = segment;
+		}
+
+		@Nonnull
+		@Override
+		public MemorySegment get() {
+			return segment;
+		}
+
+		@Nonnull
+		@Override
+		public MemorySegmentData copy() {
+			return this;
+		}
+	}
+}
diff --git a/src/main/java/software/coley/lljzip/util/data/StringData.java b/src/main/java/software/coley/lljzip/util/data/StringData.java
new file mode 100644
index 0000000..ce004ce
--- /dev/null
+++ b/src/main/java/software/coley/lljzip/util/data/StringData.java
@@ -0,0 +1,123 @@
+package software.coley.lljzip.util.data;
+
+import software.coley.lljzip.util.MemorySegmentUtil;
+
+import javax.annotation.Nonnull;
+import java.lang.foreign.MemorySegment;
+
+/**
+ * Wrapper for reading {@link String} content from a variety of sources.
+ *
+ * @author Matt Coley
+ */
+public interface StringData {
+	@Nonnull
+	String get();
+
+	@Nonnull
+	default StringData substring(int begin, int end) {
+		return of(get().substring(begin, end));
+	}
+
+	@Nonnull
+	default StringData copy() {
+		return of(get());
+	}
+
+	@Nonnull
+	static StringData empty() {
+		return Literal.EMPTY;
+	}
+
+	@Nonnull
+	static StringData of(@Nonnull String source) {
+		if (source.isEmpty())
+			return empty();
+		return new Literal(source);
+	}
+
+	@Nonnull
+	static StringData of(@Nonnull MemorySegment segment) {
+		if (segment.byteSize() == 0)
+			return empty();
+		return new Caching(new FullSegment(segment));
+	}
+
+	@Nonnull
+	static StringData of(@Nonnull MemorySegment segment, long offset, long length) {
+		if (length == 0)
+			return empty();
+		return new Caching(new PartialSegment(segment, offset, length));
+	}
+
+	class Caching implements StringData {
+
+		private final StringData delegate;
+		private String cached;
+
+		public Caching(@Nonnull StringData delegate) {
+			this.delegate = delegate;
+		}
+
+		@Nonnull
+		@Override
+		public String get() {
+			if (cached == null)
+				cached = delegate.get();
+			return cached;
+		}
+	}
+
+	class PartialSegment implements StringData {
+		private final MemorySegment segment;
+		private final long offset;
+		private final long length;
+
+		public PartialSegment(@Nonnull MemorySegment segment, long offset, long length) {
+			this.segment = segment;
+			this.offset = offset;
+			this.length = length;
+		}
+
+		@Nonnull
+		@Override
+		public String get() {
+			return MemorySegmentUtil.readString(segment, offset, length);
+		}
+	}
+
+	class FullSegment implements StringData {
+		private final MemorySegment segment;
+
+		public FullSegment(@Nonnull MemorySegment segment) {
+			this.segment = segment;
+		}
+
+		@Nonnull
+		@Override
+		public String get() {
+			return MemorySegmentUtil.toString(segment);
+		}
+	}
+
+	class Literal implements StringData {
+		private static final Literal EMPTY = new Literal("");
+		private final String content;
+
+		public Literal(@Nonnull String content) {
+			this.content = content;
+		}
+
+		@Nonnull
+		@Override
+		public String get() {
+			return content;
+		}
+
+		@Nonnull
+		@Override
+		public StringData copy() {
+			return this;
+		}
+	}
+}
diff --git a/src/main/java/software/coley/lljzip/util/lazy/Lazy.java b/src/main/java/software/coley/lljzip/util/lazy/Lazy.java
deleted file mode 100644
index daeb566..0000000
--- a/src/main/java/software/coley/lljzip/util/lazy/Lazy.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package software.coley.lljzip.util.lazy;
-
-import javax.annotation.Nonnull;
-
-/**
- * Common lazy type.
- *
- * @param 
- * 		Lazy supplier type.
- */
-public abstract class Lazy {
-	protected final S lookup;
-	protected boolean set;
-	protected String id = "";
-
-	public Lazy(@Nonnull S lookup) {
-		this.lookup = lookup;
-	}
-
-	/**
-	 * @param id
-	 * 		Value id.
-	 * @param 
-	 * 		Self type.
-	 *
-	 * @return Self.
-	 */
-	@SuppressWarnings("unchecked")
-	public > L withId(String id) {
-		this.id = id;
-		return (L) this;
-	}
-}
diff --git a/src/main/java/software/coley/lljzip/util/lazy/LazyInt.java b/src/main/java/software/coley/lljzip/util/lazy/LazyInt.java
deleted file mode 100644
index 28dcd46..0000000
--- a/src/main/java/software/coley/lljzip/util/lazy/LazyInt.java
+++ /dev/null
@@ -1,90 +0,0 @@
-package software.coley.lljzip.util.lazy;
-
-import javax.annotation.Nonnull;
-import java.util.function.IntSupplier;
-
-/**
- * Lazy int getter.
- */
-public class LazyInt extends Lazy {
-	private int value;
-
-	/**
-	 * @param lookup
-	 * 		Lazy lookup.
-	 */
-	public LazyInt(@Nonnull IntSupplier lookup) {
-		super(lookup);
-	}
-
-	/**
-	 * @return Copy.
-	 */
-	@Nonnull
-	public LazyInt copy() {
-		LazyInt copy = new LazyInt(lookup);
-		copy.id = id;
-		if (set) copy.set(value);
-		return copy;
-	}
-
-	/**
-	 * @param value
-	 * 		Value to add to the current.
-	 *
-	 * @return New value that maps the current value plus the additional value.
-	 */
-	public LazyInt add(int value) {
-		return new LazyInt(() -> value + lookup.getAsInt());
-	}
-
-	/**
-	 * @param value
-	 * 		Value to add to the current.
-	 *
-	 * @return New value that maps the current value plus the additional value.
-	 */
-	public LazyInt add(@Nonnull LazyInt value) {
-		return new LazyInt(() -> value.get() + lookup.getAsInt());
-	}
-
-	/**
-	 * @param value
-	 * 		Int value.
-	 */
-	public void set(int value) {
-		set = true;
-		this.value = value;
-	}
-
-	/**
-	 * @return Int value.
-	 */
-	public int get() {
-		if (!set) {
-			value = lookup.getAsInt();
-			set = true;
-		}
-		return value;
-	}
-
-	@Override
-	public String toString() {
-		return id + " " + get();
-	}
-
-	@Override
-	public boolean equals(Object o) {
-		if (this == o) return true;
-		if (o == null || getClass() != o.getClass()) return false;
-
-		LazyInt lazyInt = (LazyInt) o;
-
-		return get() == lazyInt.get();
-	}
-
-	@Override
-	public int hashCode() {
-		return get();
-	}
-}
diff --git a/src/main/java/software/coley/lljzip/util/lazy/LazyIntChain.java b/src/main/java/software/coley/lljzip/util/lazy/LazyIntChain.java
deleted file mode 100644
index ab93f12..0000000
--- a/src/main/java/software/coley/lljzip/util/lazy/LazyIntChain.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package software.coley.lljzip.util.lazy;
-
-import javax.annotation.Nonnull;
-
-/**
- * Lazy int getter.
- */
-public class LazyIntChain extends LazyInt {
-	/**
-	 * @param values
-	 * 		Chained values to combined as a lookup.
-	 */
-	public LazyIntChain(@Nonnull LazyInt... values) {
-		super(() -> {
-			int sum = 0;
-			for (LazyInt lazy : values) {
-				sum += lazy.get();
-			}
-			return sum;
-		});
-	}
-}
diff --git a/src/main/java/software/coley/lljzip/util/lazy/LazyLong.java b/src/main/java/software/coley/lljzip/util/lazy/LazyLong.java
deleted file mode 100644
index 424671c..0000000
--- a/src/main/java/software/coley/lljzip/util/lazy/LazyLong.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package software.coley.lljzip.util.lazy;
-
-import javax.annotation.Nonnull;
-import java.util.function.LongSupplier;
-
-/**
- * Lazy int getter.
- */
-public class LazyLong extends Lazy {
-	private long value;
-
-	/**
-	 * @param lookup
-	 * 		Lazy lookup.
-	 */
-	public LazyLong(@Nonnull LongSupplier lookup) {
-		super(lookup);
-	}
-
-	/**
-	 * @return Copy.
-	 */
-	@Nonnull
-	public LazyLong copy() {
-		LazyLong copy = new LazyLong(lookup);
-		copy.id = id;
-		if (set) copy.set(value);
-		return copy;
-	}
-
-	/**
-	 * @param value
-	 * 		Long value.
-	 */
-	public void set(long value) {
-		set = true;
-		this.value = value;
-	}
-
-	/**
-	 * @return Long value.
-	 */
-	public long get() {
-		if (!set) {
-			value = lookup.getAsLong();
-			set = true;
-		}
-		return value;
-	}
-
-	@Override
-	public String toString() {
-		return id + " " + get();
-	}
-
-	@Override
-	public boolean equals(Object o) {
-		if (this == o) return true;
-		if (o == null || getClass() != o.getClass()) return false;
-
-		LazyLong lazyLong = (LazyLong) o;
-
-		return get() == lazyLong.get();
-	}
-
-	@Override
-	public int hashCode() {
-		long v = get();
-		return (int) (v ^ (v >>> 32));
-	}
-}
diff --git a/src/main/java/software/coley/lljzip/util/lazy/LazyMemorySegment.java b/src/main/java/software/coley/lljzip/util/lazy/LazyMemorySegment.java
deleted file mode 100644
index 983f2fb..0000000
--- a/src/main/java/software/coley/lljzip/util/lazy/LazyMemorySegment.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package software.coley.lljzip.util.lazy;
-
-import javax.annotation.Nonnull;
-import java.lang.foreign.MemorySegment;
-import java.util.Objects;
-import java.util.function.Supplier;
-
-/**
- * Lazy {@link MemorySegment} getter.
- */
-public class LazyMemorySegment extends Lazy> {
-	private MemorySegment value;
-
-	/**
-	 * @param lookup
-	 * 		Lazy lookup.
-	 */
-	public LazyMemorySegment(@Nonnull Supplier lookup) {
-		super(lookup);
-	}
-
-	/**
-	 * @return Copy.
-	 */
-	@Nonnull
-	public LazyMemorySegment copy() {
-		LazyMemorySegment copy = new LazyMemorySegment(lookup);
-		copy.id = id;
-		if (set) copy.set(value);
-		return copy;
-	}
-
-	/**
-	 * @param value
-	 * 		Data value.
-	 */
-	public void set(@Nonnull MemorySegment value) {
-		set = true;
-		this.value = value;
-	}
-
-	/**
-	 * @return Data value.
-	 */
-	@Nonnull
-	public MemorySegment get() {
-		if (!set) {
-			value = lookup.get();
-			set = true;
-		}
-		return value;
-	}
-
-	@Override
-	public String toString() {
-		return id + " data[" + get().byteSize() + "]";
-	}
-
-	@Override
-	public boolean equals(Object o) {
-		if (this == o) return true;
-		if (o == null || getClass() != o.getClass()) return false;
-
-		LazyMemorySegment that = (LazyMemorySegment) o;
-
-		return Objects.equals(get(), that.get());
-	}
-
-	@Override
-	public int hashCode() {
-		return Objects.hashCode(get());
-	}
-}
diff --git a/src/test/java/software/coley/lljzip/JvmVsZipFileEqualityEdgeCaseTests.java b/src/test/java/software/coley/lljzip/JvmVsZipFileEqualityEdgeCaseTests.java
index 80a9e19..95b8104 100644
--- a/src/test/java/software/coley/lljzip/JvmVsZipFileEqualityEdgeCaseTests.java
+++ b/src/test/java/software/coley/lljzip/JvmVsZipFileEqualityEdgeCaseTests.java
@@ -32,7 +32,7 @@ public void test(String name) {
 		Path path = Paths.get("src/test/resources/" + name);
 
 		try {
-			ZipArchive zipJvm = ZipIO.read(path, new JvmZipReader(false));
+			ZipArchive zipJvm = ZipIO.read(path, new JvmZipReader(false, false));
 			ZipArchive zipAdapting = ZipIO.readAdaptingIO(path);
 			int sizeDel = zipAdapting.getLocalFiles().size();
 			int sizeJvm = zipJvm.getLocalFiles().size();
diff --git a/src/test/java/software/coley/lljzip/PartParseTests.java b/src/test/java/software/coley/lljzip/PartParseTests.java
index ee0ee11..17b4d1d 100644
--- a/src/test/java/software/coley/lljzip/PartParseTests.java
+++ b/src/test/java/software/coley/lljzip/PartParseTests.java
@@ -171,7 +171,7 @@ public void testLocalHeaderDetectMismatch() {
 			ZipArchive zipStdAndAdopt = ZipIO.read(path, new ForwardScanZipReader() {
 				@Override
 				public void postProcessLocalFileHeader(@Nonnull LocalFileHeader file) {
-					file.adoptLinkedCentralDirectoryValues();
+					assertDoesNotThrow(() -> file.adoptLinkedCentralDirectoryValues());
 				}
 			});
 			LocalFileHeader helloAdopted = zipStdAndAdopt.getLocalFileByName("Hello.class");