Skip to content

Commit

Permalink
Merge pull request #7 from marc-christian-schulze/optimizations
Browse files Browse the repository at this point in the history
Optimizations
  • Loading branch information
marc-christian-schulze authored Nov 28, 2024
2 parents 0d37158 + 1fe0f7e commit 86b68b2
Show file tree
Hide file tree
Showing 18 changed files with 593 additions and 44 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,11 @@ Below is a full example configuration, including the default values, of the plug
</configuration>
```

# Examples

* [examples/zip-file-format - ZipFileReadingTest](blob/master/examples/zip-file-format/src/test/java/examples/zip/ZipFileReadingTest.java) - Shows how to read a ZIP file using structs4java
* [examples/zip-file-format - ZipFileWritingTest](blob/master/examples/zip-file-format/src/test/java/examples/zip/ZipFileWritingTest.java) - Shows how to write a ZIP files using structs4java

# Comparison to Javolution
If you do not want to rely on code generation you should have a look at [Javolution](http://javolution.org/) which is a plain Java implementation.

Expand Down
12 changes: 12 additions & 0 deletions examples/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.marc-christian-schulze.structs4java</groupId>
<artifactId>examples</artifactId>
<version>1.0.54-SNAPSHOT</version>
<packaging>pom</packaging>

<modules>
<module>zip-file-format</module>
</modules>
</project>
42 changes: 42 additions & 0 deletions examples/zip-file-format/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.marc-christian-schulze.structs4java</groupId>
<artifactId>examples-zip-file-format</artifactId>
<version>1.0.54-SNAPSHOT</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>

<description>Example project showing how to define file structures using the example of ZIP files.</description>

<build>
<plugins>
<plugin>
<groupId>com.github.marc-christian-schulze.structs4java</groupId>
<artifactId>structs4java-maven-plugin</artifactId>
<version>1.0.54-SNAPSHOT</version>
<executions>
<execution>
<id>compile-structs</id>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
62 changes: 62 additions & 0 deletions examples/zip-file-format/src/main/structs/zip.structs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package examples.zip;

struct LocalFileHeader {
uint8_t signature[4];
uint16_t version;
bitfield uint16_t {
uint16_t generalPurposeFlags : 16;
}
uint16_t compressionMethod;
uint16_t lastModificationTime;
uint16_t lastModificationDate;
uint32_t crc32OfUncompressedData;
uint32_t compressedSizeInBytes;
uint32_t uncompressedSizeInBytes;
uint16_t fileNameLengthInBytes sizeof(fileName);
uint16_t extraFieldLengthInBytes sizeof(extraFields);
char fileName[];
ExtraFieldRecord extraFields[];
}

struct ExtraFieldRecord {
uint8_t signature[2];
uint16_t length sizeof(data);
uint8_t data[];
}

struct CentralDirectoryFileHeader {
uint8_t signature[4];
uint16_t versionMadeBy;
uint16_t versionNeededToExtract;
bitfield uint16_t {
uint16_t generalPurposeFlags : 16;
}
uint16_t compressionMethod;
uint16_t fileLastModificationTime;
uint16_t fileLastModificationDate;
uint32_t crc32OfUncompressedData;
uint32_t compressedSizeInBytes;
uint32_t uncompressedSizeInBytes;
uint16_t fileNameLengthInBytes sizeof(fileName);
uint16_t extraFieldLengthInBytes sizeof(extraFields);
uint16_t fileCommentLengthInBytes sizeof(fileComment);
uint16_t diskNumber;
uint16_t internalAttributes;
uint32_t externalAttributes;
uint32_t relativeOffsetOfLocalFileHeader;
char fileName[];
uint8_t extraFields[];
uint8_t fileComment[];
}

struct EndOfCentralDirectoryRecord {
uint8_t signature[4];
uint16_t diskNumber;
uint16_t diskWhereCentralDirectoryStarts;
uint16_t numberOfCentralDirectoryRecordsOnThisDisk;
uint16_t totalNumberOfCentralDirectoryRecords;
uint32_t sizeOfCentralDirectoryInBytes;
uint32_t offsetOfStartOfCentralDirectory;
uint16_t commentLength sizeof(comment);
uint8_t comment[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package examples.zip;

import org.junit.Test;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class ZipFileReadingTest {

@Test
public void testUsingExampleZipFile() throws IOException {
try(RandomAccessFile raf = new RandomAccessFile("src/test/resources/example.zip", "r")) {
ByteBuffer buffer = raf.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, raf.length());

// ZIP file specification requires little endian encoding
buffer.order(ByteOrder.LITTLE_ENDIAN);

// first, we need to search backwards in the ZIP file for the end of central directory
assertTrue("No valid central directory found", seekToEndOfCentralDirectory(buffer));

// read from the buffer using the generated Java classes from the zip.structs file
EndOfCentralDirectoryRecord eocd = EndOfCentralDirectoryRecord.read(buffer);

assertEquals("We expect 3 entries in the ZIP file", 3, eocd.getNumberOfCentralDirectoryRecordsOnThisDisk());
assertEquals("We expect 3 entries in the ZIP file", 3, eocd.getTotalNumberOfCentralDirectoryRecords());

// seek to first entry of the central directory
buffer.position((int) eocd.getOffsetOfStartOfCentralDirectory());

// let's retrieve the file names from the directory
CentralDirectoryFileHeader cdfhDirectory = CentralDirectoryFileHeader.read(buffer);
assertEquals(cdfhDirectory.getFileName(), "zip-content/");

CentralDirectoryFileHeader cdfhFile2 = CentralDirectoryFileHeader.read(buffer);
assertEquals(cdfhFile2.getFileName(), "zip-content/file2.txt");

CentralDirectoryFileHeader cdfhFile1 = CentralDirectoryFileHeader.read(buffer);
assertEquals(cdfhFile1.getFileName(), "zip-content/file1.txt");

// finally, let's read the file contents
// first, we need to seek to the location of the local file header
buffer.position((int) cdfhFile1.getRelativeOffsetOfLocalFileHeader());
// next, we read the local file header using our generated Java class
LocalFileHeader fhFile1 = LocalFileHeader.read(buffer);
// right after the header, the actual file content is placed
byte[] contentFile1 = new byte[(int) fhFile1.getCompressedSizeInBytes()];
buffer.get(contentFile1);
assertEquals("content file 1\n", new String(contentFile1));

// same for file 2
buffer.position((int) cdfhFile2.getRelativeOffsetOfLocalFileHeader());
LocalFileHeader fhFile2 = LocalFileHeader.read(buffer);
byte[] contentFile2 = new byte[(int) fhFile2.getCompressedSizeInBytes()];
buffer.get(contentFile2);
assertEquals("content file 2\n", new String(contentFile2));
}
}

private static boolean seekToEndOfCentralDirectory(ByteBuffer buffer) throws IOException {
int value;

// we start scanning from the end of the file
buffer.position(buffer.limit());

// first search for 0x06
value = readPreviousByteFromBuffer(buffer);
while(value != 0x06) {
if(buffer.position() < 4) {
return false;
}
value = readPreviousByteFromBuffer(buffer);
}

// now we search for 0x05
value = readPreviousByteFromBuffer(buffer);
while(value != 0x05) {
if(buffer.position() < 3) {
return false;
}
value = readPreviousByteFromBuffer(buffer);
}

// now we search for 0x4b
value = readPreviousByteFromBuffer(buffer);
while(value != 0x4b) {
if(buffer.position() < 2) {
return false;
}
value = readPreviousByteFromBuffer(buffer);
}

// finally we search for 0x50
value = readPreviousByteFromBuffer(buffer);
while(value != 0x50) {
if(buffer.position() < 1) {
return false;
}
value = readPreviousByteFromBuffer(buffer);
}

return true;
}

private static int readPreviousByteFromBuffer(ByteBuffer buffer) throws IOException {
buffer.position(buffer.position() - 1);
int value = buffer.get() & 0xFF;
buffer.position(buffer.position() - 1);
return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package examples.zip;

import org.junit.Test;
import static org.junit.Assert.assertEquals;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.zip.CRC32;
import java.util.zip.Checksum;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class ZipFileWritingTest {

@Test
public void testWritingZipFile() throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(4096);
buffer.order(ByteOrder.LITTLE_ENDIAN);

// our file content to store
byte[] fileData = "example file content".getBytes("UTF-8");

// compute the CRC32 checksum of the file content
Checksum checksum = new CRC32();
checksum.update(fileData, 0, fileData.length);
long crc32 = checksum.getValue();

// first we create and write the local file header
long positionOfLocalFileHeader = buffer.position();
LocalFileHeader fhFile = new LocalFileHeader();
fhFile.setSignature(ByteBuffer.wrap(new byte[]{ 0x50, 0x4b, 0x03, 0x04 }));
fhFile.setFileName("example.txt");
fhFile.setCompressedSizeInBytes(fileData.length);
fhFile.setUncompressedSizeInBytes(fileData.length);
fhFile.setCrc32OfUncompressedData(crc32);
fhFile.write(buffer);
// right after the local file header, the actual file content needs to be stored
buffer.put(fileData);

// now we need to create the central directory header
long positionOfFirstCentralDirectoryHeader = buffer.position();
CentralDirectoryFileHeader cdfh = new CentralDirectoryFileHeader();
cdfh.setSignature(ByteBuffer.wrap(new byte[]{ 0x50, 0x4b, 0x01, 0x02 }));
cdfh.setFileName("example.txt");
cdfh.setCrc32OfUncompressedData(crc32);
cdfh.setCompressedSizeInBytes(fileData.length);
cdfh.setUncompressedSizeInBytes(fileData.length);
cdfh.setRelativeOffsetOfLocalFileHeader(positionOfLocalFileHeader);
cdfh.write(buffer);

// finally, we need to write the closing end of central directory header
EndOfCentralDirectoryRecord eocdr = new EndOfCentralDirectoryRecord();
eocdr.setSignature(ByteBuffer.wrap(new byte[]{ 0x50, 0x4b, 0x05, 0x06 }));
eocdr.setOffsetOfStartOfCentralDirectory(positionOfFirstCentralDirectoryHeader);
eocdr.setNumberOfCentralDirectoryRecordsOnThisDisk(1);
eocdr.setTotalNumberOfCentralDirectoryRecords(1);
eocdr.write(buffer);

// now we read the ZIP file using Java on-board classes and verify
ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(buffer.array()));
ZipEntry fileEntry = zis.getNextEntry();
assertEquals("example.txt", fileEntry.getName());
byte[] fileContentRead = zis.readAllBytes();
assertEquals("example file content", new String(fileContentRead, "UTF-8"));
}
}
4 changes: 4 additions & 0 deletions examples/zip-file-format/src/test/resources/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The `example.zip` file was created without compression using
```
zip -0 -r example.zip zip-content/
```
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
content file 1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
content file 2
Loading

0 comments on commit 86b68b2

Please sign in to comment.