initial commit

This commit is contained in:
Jörg Prante 2020-05-28 12:09:56 +02:00
commit 9945b77f55
259 changed files with 38619 additions and 0 deletions

12
.gitignore vendored Normal file
View file

@ -0,0 +1,12 @@
/.idea
/target
/.settings
/.classpath
/.project
/.gradle
build
out
logs
*~
*.iml
.DS_Store

33
build.gradle Normal file
View file

@ -0,0 +1,33 @@
plugins {
id "de.marcphilipp.nexus-publish" version "0.4.0"
id "io.codearte.nexus-staging" version "0.21.1"
}
wrapper {
gradleVersion = "${project.property('gradle.wrapper.version')}"
distributionType = Wrapper.DistributionType.ALL
}
ext {
user = 'xbib'
name = 'archive'
description = 'Archive algorithms for Java'
inceptionYear = '2016'
url = 'https://github.com/' + user + '/' + name
scmUrl = 'https://github.com/' + user + '/' + name
scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git'
scmDeveloperConnection = 'scm:git:ssh://git@github.com:' + user + '/' + name + '.git'
issueManagementSystem = 'Github'
issueManagementUrl = ext.scmUrl + '/issues'
licenseName = 'The Apache License, Version 2.0'
licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
subprojects {
apply plugin: 'java-library'
apply from: rootProject.file('gradle/ide/idea.gradle')
apply from: rootProject.file('gradle/compile/java.gradle')
apply from: rootProject.file('gradle/test/junit5.gradle')
apply from: rootProject.file('gradle/publishing/publication.gradle')
}
apply from: rootProject.file('gradle/publishing/sonatype.gradle')

5
gradle.properties Normal file
View file

@ -0,0 +1,5 @@
group = org.xbib
name = archive
version = 1.0.0
gradle.wrapper.version = 6.4.1

View file

@ -0,0 +1,35 @@
apply plugin: 'java-library'
java {
modularity.inferModulePath.set(true)
}
compileJava {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
compileTestJava {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
jar {
manifest {
attributes('Implementation-Version': project.version)
}
}
task sourcesJar(type: Jar, dependsOn: classes) {
classifier 'sources'
from sourceSets.main.allSource
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier 'javadoc'
}
artifacts {
archives sourcesJar, javadocJar
}

View file

@ -0,0 +1,55 @@
apply plugin: 'org.xbib.gradle.plugin.asciidoctor'
configurations {
asciidoclet
}
dependencies {
asciidoclet "org.asciidoctor:asciidoclet:${project.property('asciidoclet.version')}"
}
asciidoctor {
backends 'html5'
outputDir = file("${rootProject.projectDir}/docs")
separateOutputDirs = false
attributes 'source-highlighter': 'coderay',
idprefix: '',
idseparator: '-',
toc: 'left',
doctype: 'book',
icons: 'font',
encoding: 'utf-8',
sectlink: true,
sectanchors: true,
linkattrs: true,
imagesdir: 'img',
stylesheet: "${projectDir}/src/docs/asciidoc/css/foundation.css"
}
/*javadoc {
options.docletpath = configurations.asciidoclet.files.asType(List)
options.doclet = 'org.asciidoctor.Asciidoclet'
//options.overview = "src/docs/asciidoclet/overview.adoc"
options.addStringOption "-base-dir", "${projectDir}"
options.addStringOption "-attribute",
"name=${project.name},version=${project.version},title-link=https://github.com/xbib/${project.name}"
configure(options) {
noTimestamp = true
}
}*/
/*javadoc {
options.docletpath = configurations.asciidoclet.files.asType(List)
options.doclet = 'org.asciidoctor.Asciidoclet'
options.overview = "${rootProject.projectDir}/src/docs/asciidoclet/overview.adoc"
options.addStringOption "-base-dir", "${projectDir}"
options.addStringOption "-attribute",
"name=${project.name},version=${project.version},title-link=https://github.com/xbib/${project.name}"
options.destinationDirectory(file("${projectDir}/docs/javadoc"))
configure(options) {
noTimestamp = true
}
}*/

13
gradle/ide/idea.gradle Normal file
View file

@ -0,0 +1,13 @@
apply plugin: 'idea'
idea {
module {
outputDir file('build/classes/java/main')
testOutputDir file('build/classes/java/test')
}
}
if (project.convention.findPlugin(JavaPluginConvention)) {
//sourceSets.main.output.classesDirs = file("build/classes/java/main")
//sourceSets.test.output.classesDirs = file("build/classes/java/test")
}

View file

@ -0,0 +1,64 @@
apply plugin: "de.marcphilipp.nexus-publish"
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
artifact sourcesJar
artifact javadocJar
pom {
name = project.name
description = rootProject.ext.description
url = rootProject.ext.url
inceptionYear = rootProject.ext.inceptionYear
packaging = 'jar'
organization {
name = 'xbib'
url = 'https://xbib.org'
}
developers {
developer {
id = 'jprante'
name = 'Jörg Prante'
email = 'joergprante@gmail.com'
url = 'https://github.com/jprante'
}
}
scm {
url = rootProject.ext.scmUrl
connection = rootProject.ext.scmConnection
developerConnection = rootProject.ext.scmDeveloperConnection
}
issueManagement {
system = rootProject.ext.issueManagementSystem
url = rootProject.ext.issueManagementUrl
}
licenses {
license {
name = rootProject.ext.licenseName
url = rootProject.ext.licenseUrl
distribution = 'repo'
}
}
}
}
}
}
if (project.hasProperty("signing.keyId")) {
apply plugin: 'signing'
signing {
sign publishing.publications.mavenJava
}
}
nexusPublishing {
repositories {
sonatype {
username = project.property('ossrhUsername')
password = project.property('ossrhPassword')
packageGroup = "org.xbib"
}
}
}

View file

@ -0,0 +1,11 @@
if (project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword')) {
apply plugin: 'io.codearte.nexus-staging'
nexusStaging {
username = project.property('ossrhUsername')
password = project.property('ossrhPassword')
packageGroup = "org.xbib"
}
}

27
gradle/test/junit5.gradle Normal file
View file

@ -0,0 +1,27 @@
def junitVersion = project.hasProperty('junit.version')?project.property('junit.version'):'5.6.2'
def hamcrestVersion = project.hasProperty('hamcrest.version')?project.property('hamcrest.version'):'2.2'
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}"
testImplementation "org.hamcrest:hamcrest-library:${hamcrestVersion}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
}
test {
useJUnitPlatform()
failFast = true
testLogging {
events 'STARTED', 'PASSED', 'FAILED', 'SKIPPED'
}
afterSuite { desc, result ->
if (!desc.parent) {
println "\nTest result: ${result.resultType}"
println "Test summary: ${result.testCount} tests, " +
"${result.successfulTestCount} succeeded, " +
"${result.failedTestCount} failed, " +
"${result.skippedTestCount} skipped"
}
}
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Executable file
View file

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

104
gradlew.bat vendored Normal file
View file

@ -0,0 +1,104 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -0,0 +1,3 @@
dependencies {
api project(':io-archive')
}

View file

@ -0,0 +1,4 @@
module org.xbib.io.archive.ar {
exports org.xbib.io.archive.ar;
requires org.xbib.io.archive;
}

View file

@ -0,0 +1,181 @@
package org.xbib.io.archive.ar;
import org.xbib.io.archive.entry.ArchiveEntry;
import java.io.File;
import java.util.Date;
/**
* Represents an archive entry in the "ar" format.
* Each AR archive starts with "!<arch>" followed by a LF. After these 8 bytes
* the archive entries are listed. The format of an entry header is as it follows:
* <pre>
* START BYTE END BYTE NAME FORMAT LENGTH
* 0 15 File name ASCII 16
* 16 27 Modification timestamp Decimal 12
* 28 33 Owner ID Decimal 6
* 34 39 Group ID Decimal 6
* 40 47 File mode Octal 8
* 48 57 File size (bytes) Decimal 10
* 58 59 File magic \140\012 2
* </pre>
* This specifies that an ar archive entry header contains 60 bytes.
* Due to the limitation of the file name length to 16 bytes GNU and
* BSD has their own variants of this format. Currently this code
* can read but not write the GNU variant and doesn't support
* the BSD variant at all.
*
* <a href="http://www.freebsd.org/cgi/man.cgi?query=ar&sektion=5">ar man page</a>
*/
public class ArArchiveEntry implements ArchiveEntry {
/**
* The header for each entry
*/
public static final String HEADER = "!<arch>\n";
/**
* The trailer for each entry
*/
public static final String TRAILER = "`\012";
private static final int DEFAULT_MODE = 33188; // = (octal) 0100644
/**
* SVR4/GNU adds a trailing / to names; BSD does not.
* They also vary in how names longer than 16 characters are represented.
* (Not yet fully supported by this implementation)
*/
private String name;
private int userId;
private int groupId;
private int mode;
private long lastModified;
private long length;
public ArArchiveEntry() {
}
/**
* Create a new instance using a couple of default values.
* Sets userId and groupId to 0, the octal file mode to 644 and
* the last modified time to the current time.
*
* @param name name of the entry
* @param length length of the entry in bytes
*/
public ArArchiveEntry(String name, long length) {
this(name, length, 0, 0, DEFAULT_MODE,
System.currentTimeMillis() / 1000);
}
/**
* Create a new instance.
*
* @param name name of the entry
* @param length length of the entry in bytes
* @param userId numeric user id
* @param groupId numeric group id
* @param mode file mode
* @param lastModified last modified time in seconds since the epoch
*/
public ArArchiveEntry(String name, long length, int userId, int groupId,
int mode, long lastModified) {
this.name = name;
this.length = length;
this.userId = userId;
this.groupId = groupId;
this.mode = mode;
this.lastModified = lastModified;
}
/**
* Create a new instance using the attributes of the given file
*/
public ArArchiveEntry(File inputFile, String entryName) {
// TODO sort out mode
this(entryName, inputFile.isFile() ? inputFile.length() : 0,
0, 0, DEFAULT_MODE, inputFile.lastModified() / 1000);
}
public ArArchiveEntry setEntrySize(long size) {
this.length = size;
return this;
}
public long getEntrySize() {
return this.getLength();
}
public ArArchiveEntry setName(String name) {
this.name = name;
return this;
}
public String getName() {
return name;
}
public int getUserId() {
return userId;
}
public int getGroupId() {
return groupId;
}
public int getMode() {
return mode;
}
public ArArchiveEntry setLastModified(Date date) {
this.lastModified = date.getTime() / 1000;
return this;
}
/**
* Last modified time in seconds since the epoch.
*/
public Date getLastModified() {
return new Date(1000 * lastModified);
}
public long getLength() {
return length;
}
public boolean isDirectory() {
return false;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ArArchiveEntry other = (ArArchiveEntry) obj;
if (name == null) {
if (other.name != null) {
return false;
}
} else if (!name.equals(other.name)) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,329 @@
package org.xbib.io.archive.ar;
import org.xbib.io.archive.stream.ArchiveInputStream;
import org.xbib.io.archive.util.ArchiveUtils;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
/**
* Implements the "ar" archive format as an input stream.
*/
public class ArArchiveInputStream extends ArchiveInputStream<ArArchiveEntry> {
static final String BSD_LONGNAME_PREFIX = "#1/";
private static final int BSD_LONGNAME_PREFIX_LEN =
BSD_LONGNAME_PREFIX.length();
private static final String BSD_LONGNAME_PATTERN =
"^" + BSD_LONGNAME_PREFIX + "\\d+";
private final InputStream input;
private long offset = 0;
private boolean closed;
/*
* If getNextEnxtry has been called, the entry metadata is stored in
* currentEntry.
*/
private ArArchiveEntry currentEntry = null;
// Storage area for extra long names (GNU ar)
private byte[] namebuffer = null;
/*
* The offset where the current entry started. -1 if no entry has been
* called
*/
private long entryOffset = -1;
/**
* Constructs an Ar input stream with the referenced stream
*
* @param pInput the ar input stream
*/
public ArArchiveInputStream(final InputStream pInput) {
input = pInput;
closed = false;
}
/**
* Returns the next AR entry in this stream.
*
* @return the next AR entry.
* @throws java.io.IOException if the entry could not be read
*/
public ArArchiveEntry getNextArEntry() throws IOException {
if (currentEntry != null) {
final long entryEnd = entryOffset + currentEntry.getLength();
while (offset < entryEnd) {
int x = read();
if (x == -1) {
// hit EOF before previous entry was complete
// TODO: throw an exception instead?
return null;
}
}
currentEntry = null;
}
if (offset == 0) {
final byte[] expected = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER);
final byte[] realized = new byte[expected.length];
final int read = read(realized);
if (read != expected.length) {
throw new IOException("failed to read header");
}
for (int i = 0; i < expected.length; i++) {
if (expected[i] != realized[i]) {
throw new IOException("invalid header " + ArchiveUtils.toAsciiString(realized));
}
}
}
if (offset % 2 != 0 && read() < 0) {
// hit eof
return null;
}
if (input.available() == 0) {
return null;
}
final byte[] name = new byte[16];
final byte[] lastmodified = new byte[12];
final byte[] userid = new byte[6];
final byte[] groupid = new byte[6];
final byte[] filemode = new byte[8];
final byte[] length = new byte[10];
read(name);
read(lastmodified);
read(userid);
read(groupid);
read(filemode);
read(length);
{
final byte[] expected = ArchiveUtils.toAsciiBytes(ArArchiveEntry.TRAILER);
final byte[] realized = new byte[expected.length];
final int read = read(realized);
if (read != expected.length) {
throw new IOException("failed to read entry trailer");
}
for (int i = 0; i < expected.length; i++) {
if (expected[i] != realized[i]) {
throw new IOException("invalid entry trailer. not read the content?");
}
}
}
entryOffset = offset;
// GNU ar uses a '/' to mark the end of the filename; this allows for the use of spaces without the use of an extended filename.
// entry name is stored as ASCII string
String temp = ArchiveUtils.toAsciiString(name).trim();
long len = asLong(length);
if (isGNUStringTable(temp)) { // GNU extended filenames entry
currentEntry = readGNUStringTable(length);
return getNextArEntry();
} else if (temp.endsWith("/")) { // GNU terminator
temp = temp.substring(0, temp.length() - 1);
} else if (isGNULongName(temp)) {
int offset = Integer.parseInt(temp.substring(1));// get the offset
temp = getExtendedName(offset); // convert to the long name
} else if (isBSDLongName(temp)) {
temp = getBSDLongName(temp);
// entry length contained the length of the file name in
// addition to the real length of the entry.
// assume file name was ASCII, there is no "standard" otherwise
int nameLen = temp.length();
len -= nameLen;
entryOffset += nameLen;
}
currentEntry = new ArArchiveEntry(temp, len, asInt(userid, true),
asInt(groupid, true), asInt(filemode, 8),
asLong(lastmodified));
return currentEntry;
}
/**
* Get an extended name from the GNU extended name buffer.
*
* @param offset pointer to entry within the buffer
* @return the extended file name; without trailing "/" if present.
* @throws java.io.IOException if name not found or buffer not set up
*/
private String getExtendedName(int offset) throws IOException {
if (namebuffer == null) {
throw new IOException("Cannot process GNU long filename as no // record was found");
}
for (int i = offset; i < namebuffer.length; i++) {
if (namebuffer[i] == '\012') {
if (namebuffer[i - 1] == '/') {
i--; // drop trailing /
}
return ArchiveUtils.toAsciiString(namebuffer, offset, i - offset);
}
}
throw new IOException("Failed to read entry: " + offset);
}
private long asLong(byte[] input) {
return Long.parseLong(ArchiveUtils.toAsciiString(input).trim());
}
private int asInt(byte[] input) {
return asInt(input, 10, false);
}
private int asInt(byte[] input, boolean treatBlankAsZero) {
return asInt(input, 10, treatBlankAsZero);
}
private int asInt(byte[] input, int base) {
return asInt(input, base, false);
}
private int asInt(byte[] input, int base, boolean treatBlankAsZero) {
String string = ArchiveUtils.toAsciiString(input).trim();
if (string.length() == 0 && treatBlankAsZero) {
return 0;
}
return Integer.parseInt(string, base);
}
@Override
public ArArchiveEntry getNextEntry() throws IOException {
return getNextArEntry();
}
@Override
public void close() throws IOException {
if (!closed) {
closed = true;
input.close();
}
currentEntry = null;
}
@Override
public int read(byte[] b, final int off, final int len) throws IOException {
int toRead = len;
if (currentEntry != null) {
final long entryEnd = entryOffset + currentEntry.getLength();
if (len > 0 && entryEnd > offset) {
toRead = (int) Math.min(len, entryEnd - offset);
} else {
return -1;
}
}
final int ret = this.input.read(b, off, toRead);
offset += (ret > 0 ? ret : 0);
return ret;
}
/**
* Does the name look like it is a long name (or a name containing
* spaces) as encoded by BSD ar?
* <p/>
* <p>From the FreeBSD ar(5) man page:</p>
* <pre>
* BSD In the BSD variant, names that are shorter than 16
* characters and without embedded spaces are stored
* directly in this field. If a name has an embedded
* space, or if it is longer than 16 characters, then
* the string "#1/" followed by the decimal represen-
* tation of the length of the file name is placed in
* this field. The actual file name is stored immedi-
* ately after the archive header. The content of the
* archive member follows the file name. The ar_size
* field of the header (see below) will then hold the
* sum of the size of the file name and the size of
* the member.
* </pre>
*/
private static boolean isBSDLongName(String name) {
return name != null && name.matches(BSD_LONGNAME_PATTERN);
}
/**
* Reads the real name from the current stream assuming the very
* first bytes to be read are the real file name.
*
* @see #isBSDLongName
*/
private String getBSDLongName(String bsdLongName) throws IOException {
int nameLen =
Integer.parseInt(bsdLongName.substring(BSD_LONGNAME_PREFIX_LEN));
byte[] name = new byte[nameLen];
int read = 0, readNow = 0;
while ((readNow = input.read(name, read, nameLen - read)) >= 0) {
read += readNow;
if (read == nameLen) {
break;
}
}
if (read != nameLen) {
throw new EOFException();
}
return ArchiveUtils.toAsciiString(name);
}
private static final String GNU_STRING_TABLE_NAME = "//";
/**
* Is this the name of the "Archive String Table" as used by
* SVR4/GNU to store long file names?
* <p/>
* <p>GNU ar stores multiple extended filenames in the data section
* of a file with the name "//", this record is referred to by
* future headers.</p>
* <p/>
* <p>A header references an extended filename by storing a "/"
* followed by a decimal offset to the start of the filename in
* the extended filename data section.</p>
* <p/>
* <p>The format of the "//" file itself is simply a list of the
* long filenames, each separated by one or more LF
* characters. Note that the decimal offsets are number of
* characters, not line or string number within the "//" file.</p>
*/
private static boolean isGNUStringTable(String name) {
return GNU_STRING_TABLE_NAME.equals(name);
}
/**
* Reads the GNU archive String Table.
*
* @see #isGNUStringTable
*/
private ArArchiveEntry readGNUStringTable(byte[] length) throws IOException {
int bufflen = asInt(length); // Assume length will fit in an int
namebuffer = new byte[bufflen];
int read = read(namebuffer, 0, bufflen);
if (read != bufflen) {
throw new IOException("Failed to read complete // record: expected="
+ bufflen + " read=" + read);
}
return new ArArchiveEntry(GNU_STRING_TABLE_NAME, bufflen);
}
private static final String GNU_LONGNAME_PATTERN = "^/\\d+";
/**
* Does the name look like it is a long name (or a name containing
* spaces) as encoded by SVR4/GNU ar?
*
* @see #isGNUStringTable
*/
private boolean isGNULongName(String name) {
return name != null && name.matches(GNU_LONGNAME_PATTERN);
}
}

View file

@ -0,0 +1,215 @@
package org.xbib.io.archive.ar;
import org.xbib.io.archive.stream.ArchiveOutputStream;
import org.xbib.io.archive.util.ArchiveUtils;
import java.io.IOException;
import java.io.OutputStream;
/**
* Implements the "ar" archive format as an output stream.
*/
public class ArArchiveOutputStream extends ArchiveOutputStream<ArArchiveEntry> {
/**
* Fail if a long file name is required in the archive.
*/
public static final int LONGFILE_ERROR = 0;
/**
* BSD ar extensions are used to store long file names in the archive.
*/
public static final int LONGFILE_BSD = 1;
private final OutputStream out;
private long entryOffset = 0;
private ArArchiveEntry prevEntry;
private boolean haveUnclosedEntry = false;
private int longFileMode = LONGFILE_ERROR;
/**
* indicates if this archive is finished
*/
private boolean finished = false;
public ArArchiveOutputStream(final OutputStream pOut) {
this.out = pOut;
}
/**
* Set the long file mode.
* This can be LONGFILE_ERROR(0) or LONGFILE_BSD(1).
* This specifies the treatment of long file names (names &gt;= 16).
* Default is LONGFILE_ERROR.
*
* @param longFileMode the mode to use
*/
public void setLongFileMode(int longFileMode) {
this.longFileMode = longFileMode;
}
private long writeArchiveHeader() throws IOException {
byte[] header = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER);
out.write(header);
return header.length;
}
@Override
public void closeArchiveEntry() throws IOException {
if (finished) {
throw new IOException("Stream has already been finished");
}
if (prevEntry == null || !haveUnclosedEntry) {
throw new IOException("No current entry to close");
}
if ((entryOffset % 2) != 0) {
out.write('\n'); // Pad byte
}
haveUnclosedEntry = false;
}
@Override
public ArArchiveEntry newArchiveEntry() {
return new ArArchiveEntry();
}
@Override
public void putArchiveEntry(final ArArchiveEntry pEntry) throws IOException {
if (finished) {
throw new IOException("Stream has already been finished");
}
if (prevEntry == null) {
writeArchiveHeader();
} else {
if (prevEntry.getLength() != entryOffset) {
throw new IOException("length does not match entry (" + prevEntry.getLength() + " != " + entryOffset);
}
if (haveUnclosedEntry) {
closeArchiveEntry();
}
}
prevEntry = pEntry;
writeEntryHeader(pEntry);
entryOffset = 0;
haveUnclosedEntry = true;
}
private long fill(final long pOffset, final long pNewOffset, final char pFill) throws IOException {
final long diff = pNewOffset - pOffset;
if (diff > 0) {
for (int i = 0; i < diff; i++) {
write(pFill);
}
}
return pNewOffset;
}
private long write(final String data) throws IOException {
final byte[] bytes = data.getBytes("ascii");
write(bytes);
return bytes.length;
}
private long writeEntryHeader(final ArArchiveEntry pEntry) throws IOException {
long offset = 0;
boolean mustAppendName = false;
final String n = pEntry.getName();
if (LONGFILE_ERROR == longFileMode && n.length() > 16) {
throw new IOException("filename too long, > 16 chars: " + n);
}
if (LONGFILE_BSD == longFileMode &&
(n.length() > 16 || n.indexOf(" ") > -1)) {
mustAppendName = true;
offset += write(ArArchiveInputStream.BSD_LONGNAME_PREFIX
+ String.valueOf(n.length()));
} else {
offset += write(n);
}
offset = fill(offset, 16, ' ');
final String m = "" + (pEntry.getLastModified());
if (m.length() > 12) {
throw new IOException("modified too long");
}
offset += write(m);
offset = fill(offset, 28, ' ');
final String u = "" + pEntry.getUserId();
if (u.length() > 6) {
throw new IOException("userid too long");
}
offset += write(u);
offset = fill(offset, 34, ' ');
final String g = "" + pEntry.getGroupId();
if (g.length() > 6) {
throw new IOException("groupid too long");
}
offset += write(g);
offset = fill(offset, 40, ' ');
final String fm = "" + Integer.toString(pEntry.getMode(), 8);
if (fm.length() > 8) {
throw new IOException("filemode too long");
}
offset += write(fm);
offset = fill(offset, 48, ' ');
final String s =
String.valueOf(pEntry.getLength()
+ (mustAppendName ? n.length() : 0));
if (s.length() > 10) {
throw new IOException("size too long");
}
offset += write(s);
offset = fill(offset, 58, ' ');
offset += write(ArArchiveEntry.TRAILER);
if (mustAppendName) {
offset += write(n);
}
return offset;
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len);
entryOffset += len;
}
/**
* Calls finish if necessary, and then closes the OutputStream
*/
@Override
public void close() throws IOException {
if (!finished) {
finish();
}
out.close();
prevEntry = null;
}
@Override
public void finish() throws IOException {
if (haveUnclosedEntry) {
throw new IOException("This archive contains unclosed entries.");
} else if (finished) {
throw new IOException("This archive has already been finished");
}
finished = true;
}
}

View file

@ -0,0 +1,3 @@
dependencies {
api project(':io-archive')
}

View file

@ -0,0 +1,4 @@
module org.xbib.io.archive.cpio {
exports org.xbib.io.archive.cpio;
requires org.xbib.io.archive;
}

View file

@ -0,0 +1,55 @@
package org.xbib.io.archive.cpio;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* Stream that tracks the number of bytes read.
*/
public class CountingOutputStream extends FilterOutputStream {
private long bytesWritten = 0;
public CountingOutputStream(final OutputStream out) {
super(out);
}
@Override
public void write(int b) throws IOException {
out.write(b);
count(1);
}
@Override
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len);
count(len);
}
/**
* Increments the counter of already written bytes.
* Doesn't increment if the EOF has been hit (written == -1)
*
* @param written the number of bytes written
*/
protected void count(long written) {
if (written != -1) {
bytesWritten += written;
}
}
/**
* Returns the current number of bytes written to this stream.
*
* @return the number of written bytes
*/
public long getBytesWritten() {
return bytesWritten;
}
}

View file

@ -0,0 +1,805 @@
package org.xbib.io.archive.cpio;
import org.xbib.io.archive.entry.ArchiveEntry;
import java.io.File;
import java.util.Date;
/**
* A cpio archive consists of a sequence of files. There are several types of
* headers defided in two categories of new and old format. The headers are
* recognized by magic numbers:
*
* <ul>
* <li>"070701" ASCII for new portable format</li>
* <li>"070702" ASCII for new portable format with CRC format</li>
* <li>"070707" ASCII for old ascii (also known as Portable ASCII, odc or old
* character format</li>
* <li>070707 binary for old binary</li>
* </ul>
* The old binary format is limited to 16 bits for user id, group
* id, device, and inode numbers. It is limited to 4 gigabyte file
* sizes.
* The old ASCII format is limited to 18 bits for the user id, group
* id, device, and inode numbers. It is limited to 8 gigabyte file
* sizes.
* The new ASCII format is limited to 4 gigabyte file sizes.
* CPIO 2.5 knows also about tar, but it is not recognized here.
* OLD FORMAT
* Each file has a 76 (ascii) / 26 (binary) byte header, a variable
* length, NUL terminated filename, and variable length file data. A
* header for a filename "TRAILER!!!" indicates the end of the
* archive.
* All the fields in the header are ISO 646 (approximately ASCII)
* strings of octal numbers, left padded, not NUL terminated.
* <pre>
* FIELDNAME NOTES
* c_magic The integer value octal 070707. This value can be used to deter-
* mine whether this archive is written with little-endian or big-
* endian integers.
* c_dev Device that contains a directory entry for this file
* c_ino I-node number that identifies the input file to the file system
* c_mode The mode specifies both the regular permissions and the file type.
* c_uid Numeric User ID of the owner of the input file
* c_gid Numeric Group ID of the owner of the input file
* c_nlink Number of links that are connected to the input file
* c_rdev For block special and character special entries, this field
* contains the associated device number. For all other entry types,
* it should be set to zero by writers and ignored by readers.
* c_mtime[2] Modification time of the file, indicated as the number of seconds
* since the start of the epoch, 00:00:00 UTC January 1, 1970. The
* four-byte integer is stored with the most-significant 16 bits
* first followed by the least-significant 16 bits. Each of the two
* 16 bit values are stored in machine-native byte order.
* c_namesize Length of the path name, including the terminating null byte
* c_filesize[2] Length of the file in bytes. This is the length of the data
* section that follows the header structure. Must be 0 for
* FIFOs and directories
*
* All fields are unsigned short fields with 16-bit integer values
* apart from c_mtime and c_filesize which are 32-bit integer values
* </pre>
* If necessary, the filename and file data are padded with a NUL byte to an even length
* Special files, directories, and the trailer are recorded with
* the h_filesize field equal to 0.
* In the ASCII version of this format, the 16-bit entries are represented as 6-byte octal numbers,
* and the 32-bit entries are represented as 11-byte octal numbers. No padding is added.
* NEW FORMAT
* Each file has a 110 byte header, a variable length, NUL
* terminated filename, and variable length file data. A header for a
* filename "TRAILER!!!" indicates the end of the archive. All the
* fields in the header are ISO 646 (approximately ASCII) strings of
* hexadecimal numbers, left padded, not NUL terminated.
* <pre>
* FIELDNAME NOTES
* c_magic[6] The string 070701 for new ASCII, the string 070702 for new ASCII with CRC
* c_ino[8]
* c_mode[8]
* c_uid[8]
* c_gid[8]
* c_nlink[8]
* c_mtim[8]
* c_filesize[8] must be 0 for FIFOs and directories
* c_maj[8]
* c_min[8]
* c_rmaj[8] only valid for chr and blk special files
* c_rmin[8] only valid for chr and blk special files
* c_namesize[8] count includes terminating NUL in pathname
* c_check[8] 0 for "new" portable format; for CRC format
* the sum of all the bytes in the file
* </pre>
* New ASCII Format The "new" ASCII format uses 8-byte hexadecimal
* fields for all numbers and separates device numbers into separate
* fields for major and minor numbers.
* The pathname is followed by NUL bytes so that the total size of
* the fixed header plus pathname is a multiple of four. Likewise, the
* file data is padded to a multiple of four bytes.
* This class uses mutable fields and is not considered to be
* threadsafe.
* Based on code from the jRPM project (http://jrpm.sourceforge.net)
* The MAGIC numbers and other constants are defined in {@link CpioConstants}
* N.B. does not handle the cpio "tar" format
*
* <a href="http://people.freebsd.org/~kientzle/libarchive/man/cpio.5.txt">CPIO man page</a>
*/
public class CpioArchiveEntry implements CpioConstants, ArchiveEntry {
/**
* See constructor documenation for possible values.
*/
private short fileFormat;
/**
* The number of bytes in each header record; depends on the file format
*/
private int headerSize;
/**
* The boundary to which the header and data elements are aligned: 0, 2 or 4 bytes
*/
private int alignmentBoundary;
// Header fields
private long chksum = 0;
/**
* Number of bytes in the file
*/
private long filesize = 0;
private long gid = 0;
private long inode = 0;
private long maj = 0;
private long min = 0;
private long mode = 0;
private long mtime = 0;
private String name;
private long nlink = 0;
private long rmaj = 0;
private long rmin = 0;
private long uid = 0;
public CpioArchiveEntry() {
}
/**
* Creates a CPIOArchiveEntry with a specified format.
*
* @param format The cpio format for this entry.
* Possible format values are:
* CpioConstants.FORMAT_NEW
* CpioConstants.FORMAT_NEW_CRC
* CpioConstants.FORMAT_OLD_BINARY
* CpioConstants.FORMAT_OLD_ASCII
*/
public CpioArchiveEntry(final short format) {
switch (format) {
case FORMAT_NEW:
this.headerSize = 110;
this.alignmentBoundary = 4;
break;
case FORMAT_NEW_CRC:
this.headerSize = 110;
this.alignmentBoundary = 4;
break;
case FORMAT_OLD_ASCII:
this.headerSize = 76;
this.alignmentBoundary = 0;
break;
case FORMAT_OLD_BINARY:
this.headerSize = 26;
this.alignmentBoundary = 2;
break;
default:
throw new IllegalArgumentException("Unknown header type");
}
this.fileFormat = format;
}
/**
* Creates a CPIOArchiveEntry with a specified name. The format of
* this entry will be the new format.
*
* @param name The name of this entry.
*/
public CpioArchiveEntry(final String name) {
this(FORMAT_NEW, name);
}
/**
* Creates a CPIOArchiveEntry with a specified name.
*
* @param format The cpio format for this entry.
* @param name The name of this entry.
* Possible format values are:
* CpioConstants.FORMAT_NEW
* CpioConstants.FORMAT_NEW_CRC
* CpioConstants.FORMAT_OLD_BINARY
* CpioConstants.FORMAT_OLD_ASCII
*/
public CpioArchiveEntry(final short format, final String name) {
this(format);
this.name = name;
}
/**
* Creates a CPIOArchiveEntry with a specified name. The format of
* this entry will be the new format.
*
* @param name The name of this entry.
* @param size The size of this entry
*/
public CpioArchiveEntry(final String name, final long size) {
this(name);
setEntrySize(size);
}
/**
* Creates a CPIOArchiveEntry with a specified name.
*
* @param format The cpio format for this entry.
* @param name The name of this entry.
* @param size The size of this entry
* Possible format values are:
* CpioConstants.FORMAT_NEW
* CpioConstants.FORMAT_NEW_CRC
* CpioConstants.FORMAT_OLD_BINARY
* CpioConstants.FORMAT_OLD_ASCII
*/
public CpioArchiveEntry(final short format, final String name,
final long size) {
this(format, name);
setEntrySize(size);
}
/**
* Creates a CPIOArchiveEntry with a specified name for a
* specified file. The format of this entry will be the new
* format.
*
* @param inputFile The file to gather information from.
* @param entryName The name of this entry.
*/
public CpioArchiveEntry(File inputFile, String entryName) {
this(FORMAT_NEW, inputFile, entryName);
}
/**
* Creates a CPIOArchiveEntry with a specified name for a
* specified file.
*
* @param format The cpio format for this entry.
* @param inputFile The file to gather information from.
* @param entryName The name of this entry.
* Possible format values are:
* CpioConstants.FORMAT_NEW
* CpioConstants.FORMAT_NEW_CRC
* CpioConstants.FORMAT_OLD_BINARY
* CpioConstants.FORMAT_OLD_ASCII
*/
public CpioArchiveEntry(final short format, File inputFile,
String entryName) {
this(format, entryName, inputFile.isFile() ? inputFile.length() : 0);
long mode = 0;
if (inputFile.isDirectory()) {
mode |= C_ISDIR;
} else if (inputFile.isFile()) {
mode |= C_ISREG;
} else {
throw new IllegalArgumentException("Cannot determine type of file "
+ inputFile.getName());
}
setMode(mode);
setTime(inputFile.lastModified() / 1000);
}
/**
* Check if the method is allowed for the defined format.
*/
private void checkNewFormat() {
if ((this.fileFormat & FORMAT_NEW_MASK) == 0) {
throw new UnsupportedOperationException();
}
}
/**
* Check if the method is allowed for the defined format.
*/
private void checkOldFormat() {
if ((this.fileFormat & FORMAT_OLD_MASK) == 0) {
throw new UnsupportedOperationException();
}
}
/**
* Get the checksum.
* Only supported for the new formats.
*
* @return Returns the checksum.
* @throws UnsupportedOperationException if the format is not a new format
*/
public long getChksum() {
checkNewFormat();
return this.chksum;
}
/**
* Get the device id.
*
* @return Returns the device id.
* @throws UnsupportedOperationException if this method is called for a CPIOArchiveEntry with a new
* format.
*/
public long getDevice() {
checkOldFormat();
return this.min;
}
/**
* Get the major device id.
*
* @return Returns the major device id.
* @throws UnsupportedOperationException if this method is called for a CPIOArchiveEntry with an old
* format.
*/
public long getDeviceMaj() {
checkNewFormat();
return this.maj;
}
/**
* Get the minor device id
*
* @return Returns the minor device id.
* @throws UnsupportedOperationException if format is not a new format
*/
public long getDeviceMin() {
checkNewFormat();
return this.min;
}
/**
* Get the filesize.
*
* @return Returns the filesize.
*/
public long getEntrySize() {
return this.filesize;
}
/**
* Get the format for this entry.
*
* @return Returns the format.
*/
public short getFormat() {
return this.fileFormat;
}
/**
* Get the group id.
*
* @return Returns the group id.
*/
public long getGID() {
return this.gid;
}
/**
* Get the header size for this CPIO format
*
* @return Returns the header size in bytes.
*/
public int getHeaderSize() {
return this.headerSize;
}
/**
* Get the alignment boundary for this CPIO format
*
* @return Returns the aligment boundary (0, 2, 4) in bytes
*/
public int getAlignmentBoundary() {
return this.alignmentBoundary;
}
/**
* Get the number of bytes needed to pad the header to the alignment boundary.
*
* @return the number of bytes needed to pad the header (0,1,2,3)
*/
public int getHeaderPadCount() {
if (this.alignmentBoundary == 0) {
return 0;
}
int size = this.headerSize + this.name.length() + 1; // Name has terminating null
int remain = size % this.alignmentBoundary;
if (remain > 0) {
return this.alignmentBoundary - remain;
}
return 0;
}
/**
* Get the number of bytes needed to pad the data to the alignment boundary.
*
* @return the number of bytes needed to pad the data (0,1,2,3)
*/
public int getDataPadCount() {
if (this.alignmentBoundary == 0) {
return 0;
}
long size = this.filesize;
int remain = (int) (size % this.alignmentBoundary);
if (remain > 0) {
return this.alignmentBoundary - remain;
}
return 0;
}
/**
* Set the inode.
*
* @return Returns the inode.
*/
public long getInode() {
return this.inode;
}
/**
* Get the mode of this entry (e.g. directory, regular file).
*
* @return Returns the mode.
*/
public long getMode() {
return mode == 0 && !CPIO_TRAILER.equals(name) ? C_ISREG : mode;
}
/**
* Get the name.
*
* @return Returns the name.
*/
public String getName() {
return this.name;
}
/**
* Get the number of links.
*
* @return Returns the number of links.
*/
public long getNumberOfLinks() {
return nlink == 0 ?
(isDirectory() ? 2 : 1)
: nlink;
}
/**
* Get the remote device id.
*
* @return Returns the remote device id.
* @throws UnsupportedOperationException if this method is called for a CPIOArchiveEntry with a new
* format.
*/
public long getRemoteDevice() {
checkOldFormat();
return this.rmin;
}
/**
* Get the remote major device id.
*
* @return Returns the remote major device id.
* @throws UnsupportedOperationException if this method is called for a CPIOArchiveEntry with an old
* format.
*/
public long getRemoteDeviceMaj() {
checkNewFormat();
return this.rmaj;
}
/**
* Get the remote minor device id.
*
* @return Returns the remote minor device id.
* @throws UnsupportedOperationException if this method is called for a CPIOArchiveEntry with an old
* format.
*/
public long getRemoteDeviceMin() {
checkNewFormat();
return this.rmin;
}
/**
* Get the time in seconds.
*
* @return Returns the time.
*/
public long getTime() {
return this.mtime;
}
public CpioArchiveEntry setLastModified(Date date) {
setTime(date.getTime() / 1000);
return this;
}
public Date getLastModified() {
return new Date(1000 * getTime());
}
/**
* Get the user id.
*
* @return Returns the user id.
*/
public long getUID() {
return this.uid;
}
/**
* Check if this entry represents a block device.
*
* @return TRUE if this entry is a block device.
*/
public boolean isBlockDevice() {
return (this.mode & S_IFMT) == C_ISBLK;
}
/**
* Check if this entry represents a character device.
*
* @return TRUE if this entry is a character device.
*/
public boolean isCharacterDevice() {
return (this.mode & S_IFMT) == C_ISCHR;
}
/**
* Check if this entry represents a directory.
*
* @return TRUE if this entry is a directory.
*/
public boolean isDirectory() {
return (this.mode & S_IFMT) == C_ISDIR;
}
/**
* Check if this entry represents a network device.
*
* @return TRUE if this entry is a network device.
*/
public boolean isNetwork() {
return (this.mode & S_IFMT) == C_ISNWK;
}
/**
* Check if this entry represents a pipe.
*
* @return TRUE if this entry is a pipe.
*/
public boolean isPipe() {
return (this.mode & S_IFMT) == C_ISFIFO;
}
/**
* Check if this entry represents a regular file.
*
* @return TRUE if this entry is a regular file.
*/
public boolean isRegularFile() {
return (this.mode & S_IFMT) == C_ISREG;
}
/**
* Check if this entry represents a socket.
*
* @return TRUE if this entry is a socket.
*/
public boolean isSocket() {
return (this.mode & S_IFMT) == C_ISSOCK;
}
/**
* Check if this entry represents a symbolic link.
*
* @return TRUE if this entry is a symbolic link.
*/
public boolean isSymbolicLink() {
return (this.mode & S_IFMT) == C_ISLNK;
}
/**
* Set the checksum. The checksum is calculated by adding all bytes of a
* file to transfer (crc += buf[pos] &amp; 0xFF).
*
* @param chksum The checksum to set.
*/
public void setChksum(final long chksum) {
checkNewFormat();
this.chksum = chksum;
}
/**
* Set the device id.
*
* @param device The device id to set.
* @throws UnsupportedOperationException if this method is called for a CPIOArchiveEntry with a new
* format.
*/
public void setDevice(final long device) {
checkOldFormat();
this.min = device;
}
/**
* Set major device id.
*
* @param maj The major device id to set.
*/
public void setDeviceMaj(final long maj) {
checkNewFormat();
this.maj = maj;
}
/**
* Set the minor device id
*
* @param min The minor device id to set.
*/
public void setDeviceMin(final long min) {
checkNewFormat();
this.min = min;
}
/**
* Set the filesize.
*
* @param size The filesize to set.
*/
public CpioArchiveEntry setEntrySize(final long size) {
if (size < 0 || size > 0xFFFFFFFFL) {
throw new IllegalArgumentException("invalid entry size <" + size
+ ">");
}
this.filesize = size;
return this;
}
/**
* Set the group id.
*
* @param gid The group id to set.
*/
public void setGID(final long gid) {
this.gid = gid;
}
/**
* Set the inode.
*
* @param inode The inode to set.
*/
public void setInode(final long inode) {
this.inode = inode;
}
/**
* Set the mode of this entry (e.g. directory, regular file).
*
* @param mode The mode to set.
*/
public void setMode(final long mode) {
final long maskedMode = mode & S_IFMT;
switch ((int) maskedMode) {
case C_ISDIR:
case C_ISLNK:
case C_ISREG:
case C_ISFIFO:
case C_ISCHR:
case C_ISBLK:
case C_ISSOCK:
case C_ISNWK:
break;
default:
throw new IllegalArgumentException(
"Unknown mode. "
+ "Full: " + Long.toHexString(mode)
+ " Masked: " + Long.toHexString(maskedMode));
}
this.mode = mode;
}
/**
* Set the name.
*
* @param name The name to set.
*/
public CpioArchiveEntry setName(final String name) {
this.name = name;
return this;
}
/**
* Set the number of links.
*
* @param nlink The number of links to set.
*/
public void setNumberOfLinks(final long nlink) {
this.nlink = nlink;
}
/**
* Set the remote device id.
*
* @param device The remote device id to set.
* @throws UnsupportedOperationException if this method is called for a CPIOArchiveEntry with a new
* format.
*/
public void setRemoteDevice(final long device) {
checkOldFormat();
this.rmin = device;
}
/**
* Set the remote major device id.
*
* @param rmaj The remote major device id to set.
* @throws UnsupportedOperationException if this method is called for a CPIOArchiveEntry with an old
* format.
*/
public void setRemoteDeviceMaj(final long rmaj) {
checkNewFormat();
this.rmaj = rmaj;
}
/**
* Set the remote minor device id.
*
* @param rmin The remote minor device id to set.
* @throws UnsupportedOperationException if this method is called for a CPIOArchiveEntry with an old
* format.
*/
public void setRemoteDeviceMin(final long rmin) {
checkNewFormat();
this.rmin = rmin;
}
/**
* Set the time in seconds.
*
* @param time The time to set.
*/
public void setTime(final long time) {
this.mtime = time;
}
/**
* Set the user id.
*
* @param uid The user id to set.
*/
public void setUID(final long uid) {
this.uid = uid;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
CpioArchiveEntry other = (CpioArchiveEntry) obj;
if (name == null) {
if (other.name != null) {
return false;
}
} else if (!name.equals(other.name)) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,387 @@
package org.xbib.io.archive.cpio;
import org.xbib.io.archive.entry.ArchiveEntry;
import org.xbib.io.archive.stream.ArchiveInputStream;
import org.xbib.io.archive.util.ArchiveUtils;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
/**
* CPIOArchiveInputStream is a stream for reading cpio streams. All formats of
* cpio are supported (old ascii, old binary, new portable format and the new
* portable format with crc).
* The stream can be read by extracting a cpio entry (containing all
* informations about a entry) and afterwards reading from the stream the file
* specified by the entry.
* <pre><code>
* CPIOArchiveInputStream cpioIn = new CPIOArchiveInputStream(
* new FileInputStream(new File(&quot;test.cpio&quot;)));
* CPIOArchiveEntry cpioEntry;
* while ((cpioEntry = cpioIn.getNextEntry()) != null) {
* System.out.println(cpioEntry.getName());
* int tmp;
* StringBuilder buf = new StringBuilder();
* while ((tmp = cpIn.read()) != -1) {
* buf.append((char) tmp);
* }
* System.out.println(buf.toString());
* }
* cpioIn.close();
* </code></pre>
* Note: This implementation should be compatible to cpio 2.5
*/
public class CpioArchiveInputStream extends ArchiveInputStream implements CpioConstants {
private boolean closed = false;
private CpioArchiveEntry entry;
private long entryBytesRead = 0;
private boolean entryEOF = false;
private final byte tmpbuf[] = new byte[4096];
private long crc = 0;
private final InputStream in;
/**
* Construct the cpio input stream
*
* @param in The cpio stream
*/
public CpioArchiveInputStream(final InputStream in) {
this.in = in;
}
/**
* Returns 0 after EOF has reached for the current entry data, otherwise
* always return 1.
* Programs should not count on this method to return the actual number of
* bytes that could be read without blocking.
*
* @return 1 before EOF and 0 after EOF has reached for current entry.
* @throws java.io.IOException if an I/O error has occurred or if a CPIO file error has
* occurred
*/
@Override
public int available() throws IOException {
ensureOpen();
if (this.entryEOF) {
return 0;
}
return 1;
}
/**
* Closes the CPIO input stream.
*
* @throws java.io.IOException if an I/O error has occurred
*/
@Override
public void close() throws IOException {
if (!this.closed) {
in.close();
this.closed = true;
}
}
/**
* Closes the current CPIO entry and positions the stream for reading the
* next entry.
*
* @throws java.io.IOException if an I/O error has occurred or if a CPIO file error has
* occurred
*/
private void closeEntry() throws IOException {
ensureOpen();
while (read(this.tmpbuf, 0, this.tmpbuf.length) != -1) {
// do nothing
}
this.entryEOF = true;
}
/**
* Check to make sure that this stream has not been closed
*
* @throws java.io.IOException if the stream is already closed
*/
private void ensureOpen() throws IOException {
if (this.closed) {
throw new IOException("stream closed");
}
}
/**
* Reads the next CPIO file entry and positions stream at the beginning of
* the entry data.
*
* @return the CPIOArchiveEntry just read
* @throws java.io.IOException if an I/O error has occurred or if a CPIO file error has
* occurred
*/
public CpioArchiveEntry getNextCPIOEntry() throws IOException {
ensureOpen();
if (this.entry != null) {
closeEntry();
}
byte magic[] = new byte[2];
readFully(magic, 0, magic.length);
if (CpioUtil.byteArray2long(magic, false) == MAGIC_OLD_BINARY) {
this.entry = readOldBinaryEntry(false);
} else if (CpioUtil.byteArray2long(magic, true) == MAGIC_OLD_BINARY) {
this.entry = readOldBinaryEntry(true);
} else {
byte more_magic[] = new byte[4];
readFully(more_magic, 0, more_magic.length);
byte tmp[] = new byte[6];
System.arraycopy(magic, 0, tmp, 0, magic.length);
System.arraycopy(more_magic, 0, tmp, magic.length,
more_magic.length);
String magicString = ArchiveUtils.toAsciiString(tmp);
if (magicString.equals(MAGIC_NEW)) {
this.entry = readNewEntry(false);
} else if (magicString.equals(MAGIC_NEW_CRC)) {
this.entry = readNewEntry(true);
} else if (magicString.equals(MAGIC_OLD_ASCII)) {
this.entry = readOldAsciiEntry();
} else {
throw new IOException("Unknown magic [" + magicString + "]");
}
}
this.entryBytesRead = 0;
this.entryEOF = false;
this.crc = 0;
if (this.entry.getName().equals(CPIO_TRAILER)) {
this.entryEOF = true;
return null;
}
return this.entry;
}
private void skip(int bytes) throws IOException {
final byte[] buff = new byte[4]; // Cannot be more than 3 bytes
if (bytes > 0) {
readFully(buff, 0, bytes);
}
}
/**
* Reads from the current CPIO entry into an array of bytes. Blocks until
* some input is available.
*
* @param b the buffer into which the data is read
* @param off the start offset of the data
* @param len the maximum number of bytes read
* @return the actual number of bytes read, or -1 if the end of the entry is
* reached
* @throws java.io.IOException if an I/O error has occurred or if a CPIO file error has
* occurred
*/
@Override
public int read(final byte[] b, final int off, final int len)
throws IOException {
ensureOpen();
if (off < 0 || len < 0 || off > b.length - len) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
if (this.entry == null || this.entryEOF) {
return -1;
}
if (this.entryBytesRead == this.entry.getEntrySize()) {
skip(entry.getDataPadCount());
this.entryEOF = true;
if (this.entry.getFormat() == FORMAT_NEW_CRC
&& this.crc != this.entry.getChksum()) {
throw new IOException("CRC Error");
}
return -1; // EOF for this entry
}
int tmplength = (int) Math.min(len, this.entry.getEntrySize()
- this.entryBytesRead);
if (tmplength < 0) {
return -1;
}
int tmpread = readFully(b, off, tmplength);
if (this.entry.getFormat() == FORMAT_NEW_CRC) {
for (int pos = 0; pos < tmpread; pos++) {
this.crc += b[pos] & 0xFF;
}
}
this.entryBytesRead += tmpread;
return tmpread;
}
private int readFully(final byte[] b, final int off, final int len)
throws IOException {
if (len < 0) {
throw new IndexOutOfBoundsException();
}
int n = 0;
while (n < len) {
int count = this.in.read(b, off + n, len - n);
if (count < 0) {
throw new EOFException();
}
n += count;
}
return n;
}
private long readBinaryLong(final int length, final boolean swapHalfWord)
throws IOException {
byte tmp[] = new byte[length];
readFully(tmp, 0, tmp.length);
return CpioUtil.byteArray2long(tmp, swapHalfWord);
}
private long readAsciiLong(final int length, final int radix)
throws IOException {
byte tmpBuffer[] = new byte[length];
readFully(tmpBuffer, 0, tmpBuffer.length);
return Long.parseLong(ArchiveUtils.toAsciiString(tmpBuffer), radix);
}
private CpioArchiveEntry readNewEntry(final boolean hasCrc)
throws IOException {
CpioArchiveEntry ret;
if (hasCrc) {
ret = new CpioArchiveEntry(FORMAT_NEW_CRC);
} else {
ret = new CpioArchiveEntry(FORMAT_NEW);
}
ret.setInode(readAsciiLong(8, 16));
long mode = readAsciiLong(8, 16);
if (mode != 0) { // mode is initialised to 0
ret.setMode(mode);
}
ret.setUID(readAsciiLong(8, 16));
ret.setGID(readAsciiLong(8, 16));
ret.setNumberOfLinks(readAsciiLong(8, 16));
ret.setTime(readAsciiLong(8, 16));
ret.setEntrySize(readAsciiLong(8, 16));
ret.setDeviceMaj(readAsciiLong(8, 16));
ret.setDeviceMin(readAsciiLong(8, 16));
ret.setRemoteDeviceMaj(readAsciiLong(8, 16));
ret.setRemoteDeviceMin(readAsciiLong(8, 16));
long namesize = readAsciiLong(8, 16);
ret.setChksum(readAsciiLong(8, 16));
String name = readCString((int) namesize);
ret.setName(name);
if (mode == 0 && !name.equals(CPIO_TRAILER)) {
throw new IOException("Mode 0 only allowed in the trailer. Found entry name: " + name);
}
skip(ret.getHeaderPadCount());
return ret;
}
private CpioArchiveEntry readOldAsciiEntry() throws IOException {
CpioArchiveEntry ret = new CpioArchiveEntry(FORMAT_OLD_ASCII);
ret.setDevice(readAsciiLong(6, 8));
ret.setInode(readAsciiLong(6, 8));
final long mode = readAsciiLong(6, 8);
if (mode != 0) {
ret.setMode(mode);
}
ret.setUID(readAsciiLong(6, 8));
ret.setGID(readAsciiLong(6, 8));
ret.setNumberOfLinks(readAsciiLong(6, 8));
ret.setRemoteDevice(readAsciiLong(6, 8));
ret.setTime(readAsciiLong(11, 8));
long namesize = readAsciiLong(6, 8);
ret.setEntrySize(readAsciiLong(11, 8));
final String name = readCString((int) namesize);
ret.setName(name);
if (mode == 0 && !name.equals(CPIO_TRAILER)) {
throw new IOException("Mode 0 only allowed in the trailer. Found entry: " + name);
}
return ret;
}
private CpioArchiveEntry readOldBinaryEntry(final boolean swapHalfWord)
throws IOException {
CpioArchiveEntry ret = new CpioArchiveEntry(FORMAT_OLD_BINARY);
ret.setDevice(readBinaryLong(2, swapHalfWord));
ret.setInode(readBinaryLong(2, swapHalfWord));
final long mode = readBinaryLong(2, swapHalfWord);
if (mode != 0) {
ret.setMode(mode);
}
ret.setUID(readBinaryLong(2, swapHalfWord));
ret.setGID(readBinaryLong(2, swapHalfWord));
ret.setNumberOfLinks(readBinaryLong(2, swapHalfWord));
ret.setRemoteDevice(readBinaryLong(2, swapHalfWord));
ret.setTime(readBinaryLong(4, swapHalfWord));
long namesize = readBinaryLong(2, swapHalfWord);
ret.setEntrySize(readBinaryLong(4, swapHalfWord));
final String name = readCString((int) namesize);
ret.setName(name);
if (mode == 0 && !name.equals(CPIO_TRAILER)) {
throw new IOException("Mode 0 only allowed in the trailer. Found entry: " + name);
}
skip(ret.getHeaderPadCount());
return ret;
}
private String readCString(final int length) throws IOException {
byte[] tmpBuffer = new byte[length];
readFully(tmpBuffer, 0, tmpBuffer.length);
return new String(tmpBuffer, 0, tmpBuffer.length - 1);
}
/**
* Skips specified number of bytes in the current CPIO entry.
*
* @param n the number of bytes to skip
* @return the actual number of bytes skipped
* @throws java.io.IOException if an I/O error has occurred
* @throws IllegalArgumentException if n &lt; 0
*/
@Override
public long skip(final long n) throws IOException {
if (n < 0) {
throw new IllegalArgumentException("negative skip length");
}
ensureOpen();
int max = (int) Math.min(n, Integer.MAX_VALUE);
int total = 0;
while (total < max) {
int len = max - total;
if (len > this.tmpbuf.length) {
len = this.tmpbuf.length;
}
len = read(this.tmpbuf, 0, len);
if (len == -1) {
this.entryEOF = true;
break;
}
total += len;
}
return total;
}
@Override
public ArchiveEntry getNextEntry() throws IOException {
return getNextCPIOEntry();
}
}

View file

@ -0,0 +1,430 @@
package org.xbib.io.archive.cpio;
import org.xbib.io.archive.stream.ArchiveOutputStream;
import org.xbib.io.archive.util.ArchiveUtils;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
/**
* CPIOArchiveOutputStream is a stream for writing CPIO streams. All formats of
* CPIO are supported (old ASCII, old binary, new portable format and the new
* portable format with CRC).
* An entry can be written by creating an instance of CpioArchiveEntry and fill
* it with the necessary values and put it into the CPIO stream. Afterwards
* write the contents of the file into the CPIO stream. Either close the stream
* by calling finish() or put a next entry into the cpio stream.
* <pre><code>
* CpioArchiveOutputStream out = new CpioArchiveOutputStream(
* new FileOutputStream(new File("test.cpio")));
* CpioArchiveEntry entry = new CpioArchiveEntry();
* entry.setName("testfile");
* String contents = &quot;12345&quot;;
* entry.setFileSize(contents.length());
* entry.setMode(CpioConstants.C_ISREG); // regular file
* ... set other attributes, e.g. time, number of links
* out.putArchiveEntry(entry);
* out.write(testContents.getBytes());
* out.close();
* </code></pre>
* Note: This implementation should be compatible to cpio 2.5
*/
public class CpioArchiveOutputStream extends ArchiveOutputStream<CpioArchiveEntry> implements CpioConstants {
private CpioArchiveEntry entry;
private boolean closed = false;
private boolean finished;
private final short entryFormat;
private final HashMap<String, CpioArchiveEntry> names = new HashMap<>();
private long crc = 0;
private long written;
private final CountingOutputStream out;
private final int blockSize;
private long nextArtificalDeviceAndInode = 1;
/**
* Construct the cpio output stream with a specified format and a
* blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE}.
*
* @param out The cpio stream
* @param format The format of the stream
*/
public CpioArchiveOutputStream(OutputStream out, final short format) {
this(out, format, BLOCK_SIZE);
}
/**
* Construct the cpio output stream with a specified format
*
* @param out The cpio stream
* @param format The format of the stream
* @param blockSize The block size of the archive.
*/
public CpioArchiveOutputStream(final OutputStream out, final short format,
final int blockSize) {
this.out = new CountingOutputStream(out);
switch (format) {
case FORMAT_NEW:
case FORMAT_NEW_CRC:
case FORMAT_OLD_ASCII:
case FORMAT_OLD_BINARY:
break;
default:
throw new IllegalArgumentException("Unknown format: " + format);
}
this.entryFormat = format;
this.blockSize = blockSize;
}
/**
* Construct the cpio output stream. The format for this CPIO stream is the
* "new" format
*
* @param out The cpio stream
*/
public CpioArchiveOutputStream(final OutputStream out) {
this(out, FORMAT_NEW);
}
/**
* Check to make sure that this stream has not been closed
*
* @throws java.io.IOException if the stream is already closed
*/
private void ensureOpen() throws IOException {
if (this.closed) {
throw new IOException("Stream closed");
}
}
@Override
public CpioArchiveEntry newArchiveEntry() {
return new CpioArchiveEntry();
}
/**
* Begins writing a new CPIO file entry and positions the stream to the
* start of the entry data. Closes the current entry if still active. The
* current time will be used if the entry has no set modification time and
* the default header format will be used if no other format is specified in
* the entry.
*
* @param entry the CPIO cpioEntry to be written
* @throws java.io.IOException if an I/O error has occurred or if a CPIO file error has
* occurred
* @throws ClassCastException if entry is not an instance of CpioArchiveEntry
*/
@Override
public void putArchiveEntry(CpioArchiveEntry entry) throws IOException {
if (finished) {
throw new IOException("Stream has already been finished");
}
ensureOpen();
if (this.entry != null) {
closeArchiveEntry(); // close previous entry
}
if (entry.getTime() == -1) {
entry.setTime(System.currentTimeMillis() / 1000);
}
final short format = entry.getFormat();
if (format != this.entryFormat) {
throw new IOException("Header format: " + format + " does not match existing format: " + this.entryFormat);
}
if (this.names.put(entry.getName(), entry) != null) {
throw new IOException("duplicate entry: " + entry.getName());
}
writeHeader(entry);
this.entry = entry;
this.written = 0;
}
private void writeHeader(final CpioArchiveEntry e) throws IOException {
switch (e.getFormat()) {
case FORMAT_NEW:
out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW));
writeNewEntry(e);
break;
case FORMAT_NEW_CRC:
out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW_CRC));
writeNewEntry(e);
break;
case FORMAT_OLD_ASCII:
out.write(ArchiveUtils.toAsciiBytes(MAGIC_OLD_ASCII));
writeOldAsciiEntry(e);
break;
case FORMAT_OLD_BINARY:
boolean swapHalfWord = true;
writeBinaryLong(MAGIC_OLD_BINARY, 2, swapHalfWord);
writeOldBinaryEntry(e, swapHalfWord);
break;
}
}
private void writeNewEntry(final CpioArchiveEntry entry) throws IOException {
long inode = entry.getInode();
long devMin = entry.getDeviceMin();
if (CPIO_TRAILER.equals(entry.getName())) {
inode = devMin = 0;
} else {
if (inode == 0 && devMin == 0) {
inode = nextArtificalDeviceAndInode & 0xFFFFFFFF;
devMin = (nextArtificalDeviceAndInode++ >> 32) & 0xFFFFFFFF;
} else {
nextArtificalDeviceAndInode =
Math.max(nextArtificalDeviceAndInode,
inode + 0x100000000L * devMin) + 1;
}
}
writeAsciiLong(inode, 8, 16);
writeAsciiLong(entry.getMode(), 8, 16);
writeAsciiLong(entry.getUID(), 8, 16);
writeAsciiLong(entry.getGID(), 8, 16);
writeAsciiLong(entry.getNumberOfLinks(), 8, 16);
writeAsciiLong(entry.getTime(), 8, 16);
writeAsciiLong(entry.getEntrySize(), 8, 16);
writeAsciiLong(entry.getDeviceMaj(), 8, 16);
writeAsciiLong(devMin, 8, 16);
writeAsciiLong(entry.getRemoteDeviceMaj(), 8, 16);
writeAsciiLong(entry.getRemoteDeviceMin(), 8, 16);
writeAsciiLong(entry.getName().length() + 1, 8, 16);
writeAsciiLong(entry.getChksum(), 8, 16);
writeCString(entry.getName());
pad(entry.getHeaderPadCount());
}
private void writeOldAsciiEntry(final CpioArchiveEntry entry)
throws IOException {
long inode = entry.getInode();
long device = entry.getDevice();
if (CPIO_TRAILER.equals(entry.getName())) {
inode = device = 0;
} else {
if (inode == 0 && device == 0) {
inode = nextArtificalDeviceAndInode & 0777777;
device = (nextArtificalDeviceAndInode++ >> 18) & 0777777;
} else {
nextArtificalDeviceAndInode =
Math.max(nextArtificalDeviceAndInode,
inode + 01000000 * device) + 1;
}
}
writeAsciiLong(device, 6, 8);
writeAsciiLong(inode, 6, 8);
writeAsciiLong(entry.getMode(), 6, 8);
writeAsciiLong(entry.getUID(), 6, 8);
writeAsciiLong(entry.getGID(), 6, 8);
writeAsciiLong(entry.getNumberOfLinks(), 6, 8);
writeAsciiLong(entry.getRemoteDevice(), 6, 8);
writeAsciiLong(entry.getTime(), 11, 8);
writeAsciiLong(entry.getName().length() + 1, 6, 8);
writeAsciiLong(entry.getEntrySize(), 11, 8);
writeCString(entry.getName());
}
private void writeOldBinaryEntry(final CpioArchiveEntry entry,
final boolean swapHalfWord) throws IOException {
long inode = entry.getInode();
long device = entry.getDevice();
if (CPIO_TRAILER.equals(entry.getName())) {
inode = device = 0;
} else {
if (inode == 0 && device == 0) {
inode = nextArtificalDeviceAndInode & 0xFFFF;
device = (nextArtificalDeviceAndInode++ >> 16) & 0xFFFF;
} else {
nextArtificalDeviceAndInode =
Math.max(nextArtificalDeviceAndInode,
inode + 0x10000 * device) + 1;
}
}
writeBinaryLong(device, 2, swapHalfWord);
writeBinaryLong(inode, 2, swapHalfWord);
writeBinaryLong(entry.getMode(), 2, swapHalfWord);
writeBinaryLong(entry.getUID(), 2, swapHalfWord);
writeBinaryLong(entry.getGID(), 2, swapHalfWord);
writeBinaryLong(entry.getNumberOfLinks(), 2, swapHalfWord);
writeBinaryLong(entry.getRemoteDevice(), 2, swapHalfWord);
writeBinaryLong(entry.getTime(), 4, swapHalfWord);
writeBinaryLong(entry.getName().length() + 1, 2, swapHalfWord);
writeBinaryLong(entry.getEntrySize(), 4, swapHalfWord);
writeCString(entry.getName());
pad(entry.getHeaderPadCount());
}
@Override
public void closeArchiveEntry() throws IOException {
if (finished) {
throw new IOException("Stream has already been finished");
}
ensureOpen();
if (entry == null) {
throw new IOException("Trying to close non-existent entry");
}
if (this.entry.getEntrySize() != this.written) {
throw new IOException("invalid entry size (expected "
+ this.entry.getEntrySize() + " but got " + this.written
+ " bytes)");
}
pad(this.entry.getDataPadCount());
if (this.entry.getFormat() == FORMAT_NEW_CRC
&& this.crc != this.entry.getChksum()) {
throw new IOException("CRC Error");
}
this.entry = null;
this.crc = 0;
this.written = 0;
}
/**
* Writes an array of bytes to the current CPIO entry data. This method will
* block until all the bytes are written.
*
* @param b the data to be written
* @param off the start offset in the data
* @param len the number of bytes that are written
* @throws java.io.IOException if an I/O error has occurred or if a CPIO file error has
* occurred
*/
@Override
public void write(final byte[] b, final int off, final int len)
throws IOException {
ensureOpen();
if (off < 0 || len < 0 || off > b.length - len) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
if (this.entry == null) {
throw new IOException("no current CPIO entry");
}
if (this.written + len > this.entry.getEntrySize()) {
throw new IOException("attempt to write past end of STORED entry");
}
out.write(b, off, len);
this.written += len;
if (this.entry.getFormat() == FORMAT_NEW_CRC) {
for (int pos = 0; pos < len; pos++) {
this.crc += b[pos] & 0xFF;
}
}
}
/**
* Finishes writing the contents of the CPIO output stream without closing
* the underlying stream. Use this method when applying multiple filters in
* succession to the same output stream.
*
* @throws IOException if an I/O exception has occurred or if a CPIO file error has occurred
*/
@Override
public void finish() throws IOException {
ensureOpen();
if (finished) {
throw new IOException("This archive has already been finished");
}
if (this.entry != null) {
throw new IOException("This archive contains unclosed entries.");
}
this.entry = new CpioArchiveEntry(this.entryFormat);
this.entry.setName(CPIO_TRAILER);
this.entry.setNumberOfLinks(1);
writeHeader(this.entry);
closeArchiveEntry();
int lengthOfLastBlock = (int) (out.getBytesWritten() % blockSize);
if (lengthOfLastBlock != 0) {
pad(blockSize - lengthOfLastBlock);
}
finished = true;
}
/**
* Closes the CPIO output stream as well as the stream being filtered.
*
* @throws java.io.IOException if an I/O error has occurred or if a CPIO file error has
* occurred
*/
@Override
public void close() throws IOException {
if (!finished) {
finish();
}
if (!this.closed) {
out.close();
this.closed = true;
}
}
private void pad(int count) throws IOException {
if (count > 0) {
byte buff[] = new byte[count];
out.write(buff);
}
}
private void writeBinaryLong(final long number, final int length,
final boolean swapHalfWord) throws IOException {
byte tmp[] = CpioUtil.long2byteArray(number, length, swapHalfWord);
out.write(tmp);
}
private void writeAsciiLong(final long number, final int length,
final int radix) throws IOException {
StringBuilder tmp = new StringBuilder();
String tmpStr;
if (radix == 16) {
tmp.append(Long.toHexString(number));
} else if (radix == 8) {
tmp.append(Long.toOctalString(number));
} else {
tmp.append(Long.toString(number));
}
if (tmp.length() <= length) {
long insertLength = length - tmp.length();
for (int pos = 0; pos < insertLength; pos++) {
tmp.insert(0, "0");
}
tmpStr = tmp.toString();
} else {
tmpStr = tmp.substring(tmp.length() - length);
}
byte[] b = ArchiveUtils.toAsciiBytes(tmpStr);
out.write(b);
}
/**
* Writes an ASCII string to the stream followed by \0
*
* @param str the String to write
* @throws java.io.IOException if the string couldn't be written
*/
private void writeCString(final String str) throws IOException {
byte[] b = ArchiveUtils.toAsciiBytes(str);
out.write(b);
out.write('\0');
}
}

View file

@ -0,0 +1,184 @@
package org.xbib.io.archive.cpio;
/**
* All constants needed by CPIO.
*/
public interface CpioConstants {
/**
* magic number of a cpio entry in the new format
*/
final String MAGIC_NEW = "070701";
/**
* magic number of a cpio entry in the new format with crc
*/
final String MAGIC_NEW_CRC = "070702";
/**
* magic number of a cpio entry in the old ascii format
*/
final String MAGIC_OLD_ASCII = "070707";
/**
* magic number of a cpio entry in the old binary format
*/
final int MAGIC_OLD_BINARY = 070707;
// These FORMAT_ constants are internal to the code
/**
* write/read a CPIOArchiveEntry in the new format
*/
final short FORMAT_NEW = 1;
/**
* write/read a CPIOArchiveEntry in the new format with crc
*/
final short FORMAT_NEW_CRC = 2;
/**
* write/read a CPIOArchiveEntry in the old ascii format
*/
final short FORMAT_OLD_ASCII = 4;
/**
* write/read a CPIOArchiveEntry in the old binary format
*/
final short FORMAT_OLD_BINARY = 8;
/**
* Mask for both new formats
*/
final short FORMAT_NEW_MASK = 3;
/**
* Mask for both old formats
*/
final short FORMAT_OLD_MASK = 12;
/*
* Constants for the MODE bits
*/
/**
* Mask for all file type bits.
*/
final int S_IFMT = 0170000;
// http://www.opengroup.org/onlinepubs/9699919799/basedefs/cpio.h.html
// has a list of the C_xxx constatnts
/**
* Defines a socket
*/
final int C_ISSOCK = 0140000;
/**
* Defines a symbolic link
*/
final int C_ISLNK = 0120000;
/**
* HP/UX network special (C_ISCTG)
*/
final int C_ISNWK = 0110000;
/**
* Defines a regular file
*/
final int C_ISREG = 0100000;
/**
* Defines a block device
*/
final int C_ISBLK = 0060000;
/**
* Defines a directory
*/
final int C_ISDIR = 0040000;
/**
* Defines a character device
*/
final int C_ISCHR = 0020000;
/**
* Defines a pipe
*/
final int C_ISFIFO = 0010000;
/**
* Set user ID
*/
final int C_ISUID = 0004000;
/**
* Set group ID
*/
final int C_ISGID = 0002000;
/**
* On directories, restricted deletion flag.
*/
final int C_ISVTX = 0001000;
/**
* Permits the owner of a file to read the file
*/
final int C_IRUSR = 0000400;
/**
* Permits the owner of a file to write to the file
*/
final int C_IWUSR = 0000200;
/**
* Permits the owner of a file to execute the file or to search the directory
*/
final int C_IXUSR = 0000100;
/**
* Permits a file's group to read the file
*/
final int C_IRGRP = 0000040;
/**
* Permits a file's group to write to the file
*/
final int C_IWGRP = 0000020;
/**
* Permits a file's group to execute the file or to search the directory
*/
final int C_IXGRP = 0000010;
/**
* Permits others to read the file
*/
final int C_IROTH = 0000004;
/**
* Permits others to write to the file
*/
final int C_IWOTH = 0000002;
/**
* Permits others to execute the file or to search the directory
*/
final int C_IXOTH = 0000001;
/**
* The special trailer marker
*/
final String CPIO_TRAILER = "TRAILER!!!";
/**
* The default block size.
*/
final int BLOCK_SIZE = 512;
}

View file

@ -0,0 +1,81 @@
package org.xbib.io.archive.cpio;
/**
* Package private utility class for Cpio
*/
class CpioUtil {
/**
* Converts a byte array to a long. Halfwords can be swapped by setting
* swapHalfWord=true.
*
* @param number An array of bytes containing a number
* @param swapHalfWord Swap halfwords ([0][1][2][3]->[1][0][3][2])
* @return The long value
* @throws UnsupportedOperationException if number length is not a multiple of 2
*/
static long byteArray2long(final byte[] number, final boolean swapHalfWord) {
if (number.length % 2 != 0) {
throw new UnsupportedOperationException();
}
long ret = 0;
int pos = 0;
byte tmp_number[] = new byte[number.length];
System.arraycopy(number, 0, tmp_number, 0, number.length);
if (!swapHalfWord) {
byte tmp = 0;
for (pos = 0; pos < tmp_number.length; pos++) {
tmp = tmp_number[pos];
tmp_number[pos++] = tmp_number[pos];
tmp_number[pos] = tmp;
}
}
ret = tmp_number[0] & 0xFF;
for (pos = 1; pos < tmp_number.length; pos++) {
ret <<= 8;
ret |= tmp_number[pos] & 0xFF;
}
return ret;
}
/**
* Converts a long number to a byte array
* Halfwords can be swapped by setting swapHalfWord=true.
*
* @param number the input long number to be converted
* @param length The length of the returned array
* @param swapHalfWord Swap halfwords ([0][1][2][3]->[1][0][3][2])
* @return The long value
* @throws UnsupportedOperationException if the length is not a positive multiple of two
*/
static byte[] long2byteArray(final long number, final int length,
final boolean swapHalfWord) {
byte[] ret = new byte[length];
int pos = 0;
long tmp_number = 0;
if (length % 2 != 0 || length < 2) {
throw new UnsupportedOperationException();
}
tmp_number = number;
for (pos = length - 1; pos >= 0; pos--) {
ret[pos] = (byte) (tmp_number & 0xFF);
tmp_number >>= 8;
}
if (!swapHalfWord) {
byte tmp = 0;
for (pos = 0; pos < length; pos++) {
tmp = ret[pos];
ret[pos++] = ret[pos];
ret[pos] = tmp;
}
}
return ret;
}
}

View file

@ -0,0 +1,3 @@
dependencies {
api project(':io-archive')
}

View file

@ -0,0 +1,4 @@
module org.xbib.io.archive.dump {
exports org.xbib.io.archive.dump;
requires org.xbib.io.archive;
}

View file

@ -0,0 +1,70 @@
package org.xbib.io.archive.dump;
/**
* Directory entry.
*/
class Dirent {
private int ino;
private int parentIno;
private int type;
private String name;
/**
* Constructor
*
* @param ino
* @param parentIno
* @param type
* @param name
*/
Dirent(int ino, int parentIno, int type, String name) {
this.ino = ino;
this.parentIno = parentIno;
this.type = type;
this.name = name;
}
/**
* Get ino.
*
* @return the i-node
*/
int getIno() {
return ino;
}
/**
* Get ino of parent directory.
*
* @return the parent i-node
*/
int getParentIno() {
return parentIno;
}
/**
* Get entry type.
*
* @return the entry type
*/
int getType() {
return type;
}
/**
* Get name of directory entry.
*
* @return the directory name
*/
String getName() {
return name;
}
/**
* @see Object#toString()
*/
@Override
public String toString() {
return String.format("[%d]: %s", ino, name);
}
}

View file

@ -0,0 +1,73 @@
package org.xbib.io.archive.dump;
/**
* Various constants associated with dump archives.
*/
public final class DumpArchiveConstants {
public static final int TP_SIZE = 1024;
public static final int NTREC = 10;
public static final int HIGH_DENSITY_NTREC = 32;
public static final int OFS_MAGIC = 60011;
public static final int NFS_MAGIC = 60012;
public static final int FS_UFS2_MAGIC = 0x19540119;
public static final int CHECKSUM = 84446;
public static final int LBLSIZE = 16;
public static final int NAMELEN = 64;
/* do not instantiate */
private DumpArchiveConstants() {
}
/**
* The type of tape segment.
*/
public enum SEGMENT_TYPE {
TAPE(1),
INODE(2),
BITS(3),
ADDR(4),
END(5),
CLRI(6);
int code;
SEGMENT_TYPE(int code) {
this.code = code;
}
public static SEGMENT_TYPE find(int code) {
for (SEGMENT_TYPE t : values()) {
if (t.code == code) {
return t;
}
}
return null;
}
}
/**
* The type of compression.
*/
public enum COMPRESSION_TYPE {
ZLIB(0),
BZLIB(1),
LZO(2);
int code;
COMPRESSION_TYPE(int code) {
this.code = code;
}
public static COMPRESSION_TYPE find(int code) {
for (COMPRESSION_TYPE t : values()) {
if (t.code == code) {
return t;
}
}
return null;
}
}
}

View file

@ -0,0 +1,797 @@
package org.xbib.io.archive.dump;
import org.xbib.io.archive.entry.ArchiveEntry;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;
/**
* This class represents an entry in a Dump archive. It consists
* of the entry's header, the entry's File and any extended attributes.
* DumpEntries that are created from the header bytes read from
* an archive are instantiated with the DumpArchiveEntry( byte[] )
* constructor. These entries will be used when extracting from
* or listing the contents of an archive. These entries have their
* header filled in using the header bytes. They also set the File
* to null, since they reference an archive entry not a file.
* DumpEntries can also be constructed from nothing but a name.
* This allows the programmer to construct the entry by hand, for
* instance when only an InputStream is available for writing to
* the archive, and the header information is constructed from
* other information. In this case the header fields are set to
* defaults and the File is set to null.
* The C structure for a Dump Entry's header is:
* <pre>
* #define TP_BSIZE 1024 // size of each file block
* #define NTREC 10 // number of blocks to write at once
* #define HIGHDENSITYTREC 32 // number of blocks to write on high-density tapes
* #define TP_NINDIR (TP_BSIZE/2) // number if indirect inodes in record
* #define TP_NINOS (TP_NINDIR / sizeof (int32_t))
* #define LBLSIZE 16
* #define NAMELEN 64
*
* #define OFS_MAGIC (int)60011 // old format magic value
* #define NFS_MAGIC (int)60012 // new format magic value
* #define FS_UFS2_MAGIC (int)0x19540119
* #define CHECKSUM (int)84446 // constant used in checksum algorithm
*
* struct s_spcl {
* int32_t c_type; // record type (see below)
* int32_t c_date; // date of this dump
* int32_t c_ddate; // date of previous dump
* int32_t c_volume; // dump volume number
* u_int32_t c_tapea; // logical block of this record
* dump_ino_t c_ino; // number of inode
* int32_t c_magic; // magic number (see above)
* int32_t c_checksum; // record checksum
* #ifdef __linux__
* struct new_bsd_inode c_dinode;
* #else
* #ifdef sunos
* struct new_bsd_inode c_dinode;
* #else
* struct dinode c_dinode; // ownership and mode of inode
* #endif
* #endif
* int32_t c_count; // number of valid c_addr entries
* union u_data c_data; // see above
* char c_label[LBLSIZE]; // dump label
* int32_t c_level; // level of this dump
* char c_filesys[NAMELEN]; // name of dumpped file system
* char c_dev[NAMELEN]; // name of dumpped device
* char c_host[NAMELEN]; // name of dumpped host
* int32_t c_flags; // additional information (see below)
* int32_t c_firstrec; // first record on volume
* int32_t c_ntrec; // blocksize on volume
* int32_t c_extattributes; // additional inode info (see below)
* int32_t c_spare[30]; // reserved for future uses
* } s_spcl;
*
* //
* // flag values
* //
* #define DR_NEWHEADER 0x0001 // new format tape header
* #define DR_NEWINODEFMT 0x0002 // new format inodes on tape
* #define DR_COMPRESSED 0x0080 // dump tape is compressed
* #define DR_METAONLY 0x0100 // only the metadata of the inode has been dumped
* #define DR_INODEINFO 0x0002 // [SIC] TS_END header contains c_inos information
* #define DR_EXTATTRIBUTES 0x8000
*
* //
* // extattributes inode info
* //
* #define EXT_REGULAR 0
* #define EXT_MACOSFNDRINFO 1
* #define EXT_MACOSRESFORK 2
* #define EXT_XATTR 3
*
* // used for EA on tape
* #define EXT2_GOOD_OLD_INODE_SIZE 128
* #define EXT2_XATTR_MAGIC 0xEA020000 // block EA
* #define EXT2_XATTR_MAGIC2 0xEA020001 // in inode EA
* </pre>
* The C structure for the inode (file) information is:
* <pre>
* struct bsdtimeval { // **** alpha-*-linux is deviant
* __u32 tv_sec;
* __u32 tv_usec;
* };
*
* #define NDADDR 12
* #define NIADDR 3
*
* //
* // This is the new (4.4) BSD inode structure
* // copied from the FreeBSD 2.0 ufs/ufs/dinode.h include file
* //
* struct new_bsd_inode {
* __u16 di_mode; // file type, standard Unix permissions
* __s16 di_nlink; // number of hard links to file.
* union {
* __u16 oldids[2];
* __u32 inumber;
* } di_u;
* u_quad_t di_size; // file size
* struct bsdtimeval di_atime; // time file was last accessed
* struct bsdtimeval di_mtime; // time file was last modified
* struct bsdtimeval di_ctime; // time file was created
* __u32 di_db[NDADDR];
* __u32 di_ib[NIADDR];
* __u32 di_flags; //
* __s32 di_blocks; // number of disk blocks
* __s32 di_gen; // generation number
* __u32 di_uid; // user id (see /etc/passwd)
* __u32 di_gid; // group id (see /etc/group)
* __s32 di_spare[2]; // unused
* };
* </pre>
* It is important to note that the header DOES NOT have the name of the
* file. It can't since hard links mean that you may have multiple filenames
* for a single physical file. You must read the contents of the directory
* entries to learn the mapping(s) from filename to inode.
* The C structure that indicates if a specific block is a real block
* that contains data or is a sparse block that is not persisted to the
* disk is:
* <pre>
* #define TP_BSIZE 1024
* #define TP_NINDIR (TP_BSIZE/2)
*
* union u_data {
* char s_addrs[TP_NINDIR]; // 1 =&gt; data; 0 =&gt; hole in inode
* int32_t s_inos[TP_NINOS]; // table of first inode on each volume
* } u_data;
* </pre>
*/
public class DumpArchiveEntry implements ArchiveEntry {
private String name;
private TYPE type = TYPE.UNKNOWN;
private int mode;
private Set<PERMISSION> permissions = Collections.emptySet();
private long size;
private long atime;
private long mtime;
private int uid;
private int gid;
/**
* Currently unused
*/
private DumpArchiveSummary summary = null;
// this information is available from standard index.
private TapeSegmentHeader header = new TapeSegmentHeader();
private String simpleName;
private String originalName;
// this information is available from QFA index
private int volume;
private long offset;
private int ino;
private int nlink;
private long ctime;
private int generation;
private boolean isDeleted;
/**
* Default constructor.
*/
public DumpArchiveEntry() {
}
/**
* Constructor taking only filename.
*
* @param name pathname
* @param simpleName actual filename.
*/
public DumpArchiveEntry(String name, String simpleName) {
setName(name);
this.simpleName = simpleName;
}
/**
* Constructor taking name, inode and type.
*
* @param name
* @param simpleName
* @param ino
* @param type
*/
protected DumpArchiveEntry(String name, String simpleName, int ino,
TYPE type) {
setType(type);
setName(name);
this.simpleName = simpleName;
this.ino = ino;
this.offset = 0;
}
/**
* Constructor taking tape buffer.
* @param buffer
* @param offset
*/
/**
* Returns the path of the entry.
*
* @return the path of the entry.
*/
public String getSimpleName() {
return simpleName;
}
/**
* Sets the path of the entry.
*/
protected void setSimpleName(String simpleName) {
this.simpleName = simpleName;
}
/**
* Returns the ino of the entry.
*/
public int getIno() {
return header.getIno();
}
/**
* Return the number of hard links to the entry.
*/
public int getNlink() {
return nlink;
}
/**
* Set the number of hard links.
*/
public void setNlink(int nlink) {
this.nlink = nlink;
}
/**
* Get file creation time.
*/
public Date getCreationTime() {
return new Date(ctime);
}
/**
* Set the file creation time.
*/
public void setCreationTime(Date ctime) {
this.ctime = ctime.getTime();
}
/**
* Return the generation of the file.
*/
public int getGeneration() {
return generation;
}
/**
* Set the generation of the file.
*/
public void setGeneration(int generation) {
this.generation = generation;
}
/**
* Has this file been deleted? (On valid on incremental dumps.)
*/
public boolean isDeleted() {
return isDeleted;
}
/**
* Set whether this file has been deleted.
*/
public void setDeleted(boolean isDeleted) {
this.isDeleted = isDeleted;
}
/**
* Return the offset within the archive
*/
public long getOffset() {
return offset;
}
/**
* Set the offset within the archive.
*/
public void setOffset(long offset) {
this.offset = offset;
}
/**
* Return the tape volume where this file is located.
*/
public int getVolume() {
return volume;
}
/**
* Set the tape volume.
*/
public void setVolume(int volume) {
this.volume = volume;
}
/**
* Return the type of the tape segment header.
*/
public DumpArchiveConstants.SEGMENT_TYPE getHeaderType() {
return header.getType();
}
/**
* Return the number of records in this segment.
*/
public int getHeaderCount() {
return header.getCount();
}
/**
* Return the number of sparse records in this segment.
*/
public int getHeaderHoles() {
return header.getHoles();
}
/**
* Is this a sparse record?
*/
public boolean isSparseRecord(int idx) {
return (header.getCdata(idx) & 0x01) == 0;
}
/**
* @see Object#hashCode()
*/
@Override
public int hashCode() {
return ino;
}
/**
* @see Object#equals(Object o)
*/
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o == null || !o.getClass().equals(getClass())) {
return false;
}
DumpArchiveEntry rhs = (DumpArchiveEntry) o;
if ((header == null) || (rhs.header == null)) {
return false;
}
if (ino != rhs.ino) {
return false;
}
if ((summary == null && rhs.summary != null)
|| (summary != null && !summary.equals(rhs.summary))) {
return false;
}
return true;
}
/**
* @see Object#toString()
*/
@Override
public String toString() {
return getName();
}
/**
* Populate the dump archive entry and tape segment header with
* the contents of the buffer.
*
* @param buffer
* @throws Exception
*/
static DumpArchiveEntry parse(byte[] buffer) {
DumpArchiveEntry entry = new DumpArchiveEntry();
TapeSegmentHeader header = entry.header;
header.type = DumpArchiveConstants.SEGMENT_TYPE.find(DumpArchiveUtil.convert32(
buffer, 0));
//header.dumpDate = new Date(1000L * DumpArchiveUtil.convert32(buffer, 4));
//header.previousDumpDate = new Date(1000L * DumpArchiveUtil.convert32(
// buffer, 8));
header.volume = DumpArchiveUtil.convert32(buffer, 12);
//header.tapea = DumpArchiveUtil.convert32(buffer, 16);
entry.ino = header.ino = DumpArchiveUtil.convert32(buffer, 20);
//header.magic = DumpArchiveUtil.convert32(buffer, 24);
//header.checksum = DumpArchiveUtil.convert32(buffer, 28);
int m = DumpArchiveUtil.convert16(buffer, 32);
// determine the type of the file.
entry.setType(TYPE.find((m >> 12) & 0x0F));
// determine the standard permissions
entry.setMode(m);
entry.nlink = DumpArchiveUtil.convert16(buffer, 34);
// inumber, oldids?
entry.setEntrySize(DumpArchiveUtil.convert64(buffer, 40));
long t = (1000L * DumpArchiveUtil.convert32(buffer, 48)) +
(DumpArchiveUtil.convert32(buffer, 52) / 1000);
entry.setAccessTime(new Date(t));
t = (1000L * DumpArchiveUtil.convert32(buffer, 56)) +
(DumpArchiveUtil.convert32(buffer, 60) / 1000);
entry.setLastModified(new Date(t));
t = (1000L * DumpArchiveUtil.convert32(buffer, 64)) +
(DumpArchiveUtil.convert32(buffer, 68) / 1000);
entry.ctime = t;
// db: 72-119 - direct blocks
// id: 120-131 - indirect blocks
//entry.flags = DumpArchiveUtil.convert32(buffer, 132);
//entry.blocks = DumpArchiveUtil.convert32(buffer, 136);
entry.generation = DumpArchiveUtil.convert32(buffer, 140);
entry.setUserId(DumpArchiveUtil.convert32(buffer, 144));
entry.setGroupId(DumpArchiveUtil.convert32(buffer, 148));
// two 32-bit spare values.
header.count = DumpArchiveUtil.convert32(buffer, 160);
header.holes = 0;
for (int i = 0; (i < 512) && (i < header.count); i++) {
if (buffer[164 + i] == 0) {
header.holes++;
}
}
System.arraycopy(buffer, 164, header.cdata, 0, 512);
entry.volume = header.getVolume();
//entry.isSummaryOnly = false;
return entry;
}
/**
* Update entry with information from next tape segment header.
*/
void update(byte[] buffer) {
header.volume = DumpArchiveUtil.convert32(buffer, 16);
header.count = DumpArchiveUtil.convert32(buffer, 160);
header.holes = 0;
for (int i = 0; (i < 512) && (i < header.count); i++) {
if (buffer[164 + i] == 0) {
header.holes++;
}
}
System.arraycopy(buffer, 164, header.cdata, 0, 512);
}
/**
* Archive entry as stored on tape. There is one TSH for (at most)
* every 512k in the file.
*/
static class TapeSegmentHeader {
private DumpArchiveConstants.SEGMENT_TYPE type;
private int volume;
private int ino;
private int count;
private int holes;
private byte[] cdata = new byte[512]; // map of any 'holes'
public DumpArchiveConstants.SEGMENT_TYPE getType() {
return type;
}
public int getVolume() {
return volume;
}
public int getIno() {
return ino;
}
void setIno(int ino) {
this.ino = ino;
}
public int getCount() {
return count;
}
public int getHoles() {
return holes;
}
public int getCdata(int idx) {
return cdata[idx];
}
}
/**
* Returns the name of the entry.
*
* @return the name of the entry.
*/
public String getName() {
return name;
}
/**
* Returns the unmodified name of the entry.
*
* @return the name of the entry.
*/
String getOriginalName() {
return originalName;
}
/**
* Sets the name of the entry.
*/
public DumpArchiveEntry setName(String name) {
this.originalName = name;
if (name != null) {
if (isDirectory() && !name.endsWith("/")) {
name += "/";
}
if (name.startsWith("./")) {
name = name.substring(2);
}
}
this.name = name;
return this;
}
public Date getLastModifiedDate() {
return new Date(mtime);
}
/**
* Is this a directory?
*/
public boolean isDirectory() {
return type == TYPE.DIRECTORY;
}
/**
* Is this a regular file?
*/
public boolean isFile() {
return type == TYPE.FILE;
}
/**
* Is this a network device?
*/
public boolean isSocket() {
return type == TYPE.SOCKET;
}
/**
* Is this a character device?
*/
public boolean isChrDev() {
return type == TYPE.CHRDEV;
}
/**
* Is this a block device?
*/
public boolean isBlkDev() {
return type == TYPE.BLKDEV;
}
/**
* Is this a fifo/pipe?
*/
public boolean isFifo() {
return type == TYPE.FIFO;
}
/**
* Get the type of the entry.
*/
public TYPE getType() {
return type;
}
/**
* Set the type of the entry.
*/
public void setType(TYPE type) {
this.type = type;
}
/**
* Return the access permissions on the entry.
*/
public int getMode() {
return mode;
}
/**
* Set the access permissions on the entry.
*/
public void setMode(int mode) {
this.mode = mode & 07777;
this.permissions = PERMISSION.find(mode);
}
/**
* Returns the permissions on the entry.
*/
public Set<PERMISSION> getPermissions() {
return permissions;
}
/**
* Returns the size of the entry as read from the archive.
*/
public long getEntrySize() {
return isDirectory() ? ArchiveEntry.SIZE_UNKNOWN : size;
}
/**
* Set the size of the entry.
*/
public DumpArchiveEntry setEntrySize(long size) {
this.size = size;
return this;
}
/**
* Set the time the file was last modified.
*/
public DumpArchiveEntry setLastModified(Date mtime) {
this.mtime = mtime.getTime();
return this;
}
public Date getLastModified() {
return new Date(mtime);
}
/**
* Returns the time the file was last accessed.
*/
public Date getAccessTime() {
return new Date(atime);
}
/**
* Set the time the file was last accessed.
*/
public void setAccessTime(Date atime) {
this.atime = atime.getTime();
}
/**
* Return the user id.
*/
public int getUserId() {
return uid;
}
/**
* Set the user id.
*/
public void setUserId(int uid) {
this.uid = uid;
}
/**
* Return the group id
*/
public int getGroupId() {
return gid;
}
/**
* Set the group id.
*/
public void setGroupId(int gid) {
this.gid = gid;
}
public enum TYPE {
WHITEOUT(14),
SOCKET(12),
LINK(10),
FILE(8),
BLKDEV(6),
DIRECTORY(4),
CHRDEV(2),
FIFO(1),
UNKNOWN(15);
private int code;
TYPE(int code) {
this.code = code;
}
public static TYPE find(int code) {
TYPE type = UNKNOWN;
for (TYPE t : TYPE.values()) {
if (code == t.code) {
type = t;
}
}
return type;
}
}
public enum PERMISSION {
SETUID(04000),
SETGUI(02000),
STICKY(01000),
USER_READ(00400),
USER_WRITE(00200),
USER_EXEC(00100),
GROUP_READ(00040),
GROUP_WRITE(00020),
GROUP_EXEC(00010),
WORLD_READ(00004),
WORLD_WRITE(00002),
WORLD_EXEC(00001);
private int code;
PERMISSION(int code) {
this.code = code;
}
public static Set<PERMISSION> find(int code) {
Set<PERMISSION> set = new HashSet<>();
for (PERMISSION p : PERMISSION.values()) {
if ((code & p.code) == p.code) {
set.add(p);
}
}
if (set.isEmpty()) {
return Collections.emptySet();
}
return EnumSet.copyOf(set);
}
}
}

View file

@ -0,0 +1,19 @@
package org.xbib.io.archive.dump;
import java.io.IOException;
/**
* Dump Archive Exception
*/
public class DumpArchiveException extends IOException {
public DumpArchiveException(String msg) {
super(msg);
}
public DumpArchiveException(String msg, Throwable cause) {
super(msg);
initCause(cause);
}
}

View file

@ -0,0 +1,490 @@
package org.xbib.io.archive.dump;
import org.xbib.io.archive.stream.ArchiveInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Stack;
/**
* The DumpArchiveInputStream reads a UNIX dump archive as an InputStream.
* Methods are provided to position at each successive entry in
* the archive, and the read each entry as a normal input stream
* using read().
*/
public class DumpArchiveInputStream extends ArchiveInputStream {
private DumpArchiveSummary summary;
private DumpArchiveEntry active;
private boolean isClosed;
private boolean hasHitEOF;
private long entrySize;
private long entryOffset;
private int readIdx;
private byte[] readBuf = new byte[DumpArchiveConstants.TP_SIZE];
private byte[] blockBuffer;
private int recordOffset;
private long filepos;
protected TapeInputStream raw;
// map of ino -> dirent entry. We can use this to reconstruct full paths.
private Map<Integer, Dirent> names = new HashMap<Integer, Dirent>();
// map of ino -> (directory) entry when we're missing one or more elements in the path.
private Map<Integer, DumpArchiveEntry> pending = new HashMap<Integer, DumpArchiveEntry>();
// queue of (directory) entries where we now have the full path.
private Queue<DumpArchiveEntry> queue;
/**
* Constructor.
*
* @param is
*/
public DumpArchiveInputStream(InputStream is) throws IOException {
this.raw = new TapeInputStream(is);
this.hasHitEOF = false;
// read header, verify it's a dump archive.
byte[] headerBytes = raw.readRecord();
if (!DumpArchiveUtil.verify(headerBytes)) {
throw new UnrecognizedFormatException();
}
// get summary information
summary = new DumpArchiveSummary(headerBytes);
// reset buffer with actual block size.
raw.resetBlockSize(summary.getNTRec(), summary.isCompressed());
// allocate our read buffer.
blockBuffer = new byte[4 * DumpArchiveConstants.TP_SIZE];
// skip past CLRI and BITS segments since we don't handle them yet.
readCLRI();
readBITS();
// put in a dummy record for the root node.
Dirent root = new Dirent(2, 2, 4, ".");
names.put(2, root);
// use priority based on queue to ensure parent directories are
// released first.
queue = new PriorityQueue<DumpArchiveEntry>(10,
new Comparator<DumpArchiveEntry>() {
public int compare(DumpArchiveEntry p, DumpArchiveEntry q) {
if ((p.getOriginalName() == null) || (q.getOriginalName() == null)) {
return Integer.MAX_VALUE;
}
return p.getOriginalName().compareTo(q.getOriginalName());
}
});
}
/**
* Return the archive summary information.
*/
public DumpArchiveSummary getSummary() {
return summary;
}
/**
* Read CLRI (deleted inode) segment.
*/
private void readCLRI() throws IOException {
byte[] readBuf = raw.readRecord();
if (!DumpArchiveUtil.verify(readBuf)) {
throw new InvalidFormatException();
}
active = DumpArchiveEntry.parse(readBuf);
if (DumpArchiveConstants.SEGMENT_TYPE.CLRI != active.getHeaderType()) {
throw new InvalidFormatException();
}
// we don't do anything with this yet.
if (raw.skip(DumpArchiveConstants.TP_SIZE * active.getHeaderCount())
== -1) {
throw new EOFException();
}
readIdx = active.getHeaderCount();
}
/**
* Read BITS segment.
*/
private void readBITS() throws IOException {
byte[] readBuf = raw.readRecord();
if (!DumpArchiveUtil.verify(readBuf)) {
throw new InvalidFormatException();
}
active = DumpArchiveEntry.parse(readBuf);
if (DumpArchiveConstants.SEGMENT_TYPE.BITS != active.getHeaderType()) {
throw new InvalidFormatException();
}
// we don't do anything with this yet.
if (raw.skip(DumpArchiveConstants.TP_SIZE * active.getHeaderCount())
== -1) {
throw new EOFException();
}
readIdx = active.getHeaderCount();
}
/**
* Read the next entry.
*/
public DumpArchiveEntry getNextDumpEntry() throws IOException {
return getNextEntry();
}
/**
* Read the next entry.
*/
@Override
public DumpArchiveEntry getNextEntry() throws IOException {
DumpArchiveEntry entry = null;
String path = null;
// is there anything in the queue?
if (!queue.isEmpty()) {
return queue.remove();
}
while (entry == null) {
if (hasHitEOF) {
return null;
}
// skip any remaining records in this segment for prior file.
// we might still have holes... easiest to do it
// block by block. We may want to revisit this if
// the unnecessary decompression time adds up.
while (readIdx < active.getHeaderCount()) {
if (!active.isSparseRecord(readIdx++)
&& raw.skip(DumpArchiveConstants.TP_SIZE) == -1) {
throw new EOFException();
}
}
readIdx = 0;
filepos = raw.getBytesRead();
byte[] headerBytes = raw.readRecord();
if (!DumpArchiveUtil.verify(headerBytes)) {
throw new InvalidFormatException();
}
active = DumpArchiveEntry.parse(headerBytes);
// skip any remaining segments for prior file.
while (DumpArchiveConstants.SEGMENT_TYPE.ADDR == active.getHeaderType()) {
if (raw.skip(DumpArchiveConstants.TP_SIZE
* (active.getHeaderCount()
- active.getHeaderHoles())) == -1) {
throw new EOFException();
}
filepos = raw.getBytesRead();
headerBytes = raw.readRecord();
if (!DumpArchiveUtil.verify(headerBytes)) {
throw new InvalidFormatException();
}
active = DumpArchiveEntry.parse(headerBytes);
}
// check if this is an end-of-volume marker.
if (DumpArchiveConstants.SEGMENT_TYPE.END == active.getHeaderType()) {
hasHitEOF = true;
isClosed = true;
raw.close();
return null;
}
entry = active;
if (entry.isDirectory()) {
readDirectoryEntry(active);
// now we create an empty InputStream.
entryOffset = 0;
entrySize = 0;
readIdx = active.getHeaderCount();
} else {
entryOffset = 0;
entrySize = active.getEntrySize();
readIdx = 0;
}
recordOffset = readBuf.length;
path = getPath(entry);
if (path == null) {
entry = null;
}
}
entry.setName(path);
entry.setSimpleName(names.get(entry.getIno()).getName());
entry.setOffset(filepos);
return entry;
}
/**
* Read directory entry.
*/
private void readDirectoryEntry(DumpArchiveEntry entry)
throws IOException {
long size = entry.getEntrySize();
boolean first = true;
while (first ||
(DumpArchiveConstants.SEGMENT_TYPE.ADDR == entry.getHeaderType())) {
// read the header that we just peeked at.
if (!first) {
raw.readRecord();
}
if (!names.containsKey(entry.getIno()) &&
(DumpArchiveConstants.SEGMENT_TYPE.INODE == entry.getHeaderType())) {
pending.put(entry.getIno(), entry);
}
int datalen = DumpArchiveConstants.TP_SIZE * entry.getHeaderCount();
if (blockBuffer.length < datalen) {
blockBuffer = new byte[datalen];
}
if (raw.read(blockBuffer, 0, datalen) != datalen) {
throw new EOFException();
}
int reclen = 0;
for (int i = 0; (i < (datalen - 8)) && (i < (size - 8));
i += reclen) {
int ino = DumpArchiveUtil.convert32(blockBuffer, i);
reclen = DumpArchiveUtil.convert16(blockBuffer, i + 4);
byte type = blockBuffer[i + 6];
String name = new String(blockBuffer, i + 8, blockBuffer[i + 7]); // TODO default charset?
if (".".equals(name) || "src/test".equals(name)) {
// do nothing...
continue;
}
Dirent d = new Dirent(ino, entry.getIno(), type, name);
names.put(Integer.valueOf(ino), d);
// check whether this allows us to fill anything in the pending list.
for (Map.Entry<Integer, DumpArchiveEntry> e : pending.entrySet()) {
String path = getPath(e.getValue());
if (path != null) {
e.getValue().setName(path);
e.getValue()
.setSimpleName(names.get(e.getKey()).getName());
queue.add(e.getValue());
}
}
// remove anything that we found. (We can't do it earlier
// because of concurrent modification exceptions.)
for (DumpArchiveEntry e : queue) {
pending.remove(Integer.valueOf(e.getIno()));
}
}
byte[] peekBytes = raw.peek();
if (!DumpArchiveUtil.verify(peekBytes)) {
throw new InvalidFormatException();
}
entry = DumpArchiveEntry.parse(peekBytes);
first = false;
size -= DumpArchiveConstants.TP_SIZE;
}
}
/**
* Get full path for specified archive entry, or null if there's a gap.
*
* @param entry
* @return full path for specified archive entry, or null if there's a gap.
*/
private String getPath(DumpArchiveEntry entry) {
// build the stack of elements. It's possible that we're
// still missing an intermediate value and if so we
Stack<String> elements = new Stack<String>();
Dirent dirent = null;
for (int i = entry.getIno(); ; i = dirent.getParentIno()) {
if (!names.containsKey(Integer.valueOf(i))) {
elements.clear();
break;
}
dirent = names.get(Integer.valueOf(i));
elements.push(dirent.getName());
if (dirent.getIno() == dirent.getParentIno()) {
break;
}
}
// if an element is missing defer the work and read next entry.
if (elements.isEmpty()) {
pending.put(Integer.valueOf(entry.getIno()), entry);
return null;
}
// generate full path from stack of elements.
StringBuilder sb = new StringBuilder(elements.pop());
while (!elements.isEmpty()) {
sb.append('/');
sb.append(elements.pop());
}
return sb.toString();
}
/**
* Reads bytes from the current dump archive entry.
* This method is aware of the boundaries of the current
* entry in the archive and will deal with them as if they
* were this stream's start and EOF.
*
* @param buf The buffer into which to place bytes read.
* @param off The offset at which to place bytes read.
* @param len The number of bytes to read.
* @return The number of bytes read, or -1 at EOF.
* @throws java.io.IOException on error
*/
@Override
public int read(byte[] buf, int off, int len) throws IOException {
int totalRead = 0;
if (isClosed || (entryOffset >= entrySize)) {
return -1;
}
if ((len + entryOffset) > entrySize) {
len = (int) (entrySize - entryOffset);
}
while (len > 0) {
int sz = (len > (readBuf.length - recordOffset))
? (readBuf.length - recordOffset) : len;
// copy any data we have
if ((recordOffset + sz) <= readBuf.length) {
System.arraycopy(readBuf, recordOffset, buf, off, sz);
totalRead += sz;
recordOffset += sz;
len -= sz;
off += sz;
}
// load next block if necessary.
if (len > 0) {
if (readIdx >= 512) {
byte[] headerBytes = raw.readRecord();
if (!DumpArchiveUtil.verify(headerBytes)) {
throw new InvalidFormatException();
}
active = DumpArchiveEntry.parse(headerBytes);
readIdx = 0;
}
if (!active.isSparseRecord(readIdx++)) {
int r = raw.read(readBuf, 0, readBuf.length);
if (r != readBuf.length) {
throw new EOFException();
}
} else {
Arrays.fill(readBuf, (byte) 0);
}
recordOffset = 0;
}
}
entryOffset += totalRead;
return totalRead;
}
/**
* Closes the stream for this entry.
*/
@Override
public void close() throws IOException {
if (!isClosed) {
isClosed = true;
raw.close();
}
}
/**
* Look at the first few bytes of the file to decide if it's a dump
* archive. With 32 bytes we can look at the magic value, with a full
* 1k we can verify the checksum.
*/
public static boolean matches(byte[] buffer, int length) {
// do we have enough of the header?
if (length < 32) {
return false;
}
// this is the best test
if (length >= DumpArchiveConstants.TP_SIZE) {
return DumpArchiveUtil.verify(buffer);
}
// this will work in a pinch.
return DumpArchiveConstants.NFS_MAGIC == DumpArchiveUtil.convert32(buffer,
24);
}
}

View file

@ -0,0 +1,333 @@
package org.xbib.io.archive.dump;
import java.util.Date;
/**
* This class represents identifying information about a Dump archive volume.
* It consists the archive's dump date, label, hostname, device name and possibly
* last mount point plus the volume's volume id andfirst record number.
* For the corresponding C structure see the header of {@link DumpArchiveEntry}.
*/
public class DumpArchiveSummary {
private long dumpDate;
private long previousDumpDate;
private int volume;
private String label;
private int level;
private String filesys;
private String devname;
private String hostname;
private int flags;
private int firstrec;
private int ntrec;
DumpArchiveSummary(byte[] buffer) {
dumpDate = 1000L * DumpArchiveUtil.convert32(buffer, 4);
previousDumpDate = 1000L * DumpArchiveUtil.convert32(buffer, 8);
volume = DumpArchiveUtil.convert32(buffer, 12);
label = new String(buffer, 676, DumpArchiveConstants.LBLSIZE).trim(); // TODO default charset?
level = DumpArchiveUtil.convert32(buffer, 692);
filesys = new String(buffer, 696, DumpArchiveConstants.NAMELEN).trim(); // TODO default charset?
devname = new String(buffer, 760, DumpArchiveConstants.NAMELEN).trim(); // TODO default charset?
hostname = new String(buffer, 824, DumpArchiveConstants.NAMELEN).trim(); // TODO default charset?
flags = DumpArchiveUtil.convert32(buffer, 888);
firstrec = DumpArchiveUtil.convert32(buffer, 892);
ntrec = DumpArchiveUtil.convert32(buffer, 896);
//extAttributes = DumpArchiveUtil.convert32(buffer, 900);
}
/**
* Get the date of this dump.
*
* @return the date of this dump.
*/
public Date getDumpDate() {
return new Date(dumpDate);
}
/**
* Set dump date.
*/
public void setDumpDate(Date dumpDate) {
this.dumpDate = dumpDate.getTime();
}
/**
* Get the date of the previous dump at this level higher.
*
* @return dumpdate may be null
*/
public Date getPreviousDumpDate() {
return new Date(previousDumpDate);
}
/**
* Set previous dump date.
*/
public void setPreviousDumpDate(Date previousDumpDate) {
this.previousDumpDate = previousDumpDate.getTime();
}
/**
* Get volume (tape) number.
*
* @return volume (tape) number.
*/
public int getVolume() {
return volume;
}
/**
* Set volume (tape) number.
*/
public void setVolume(int volume) {
this.volume = volume;
}
/**
* Get the level of this dump. This is a number between 0 and 9, inclusive,
* and a level 0 dump is a complete dump of the partition. For any other dump
* 'n' this dump contains all files that have changed since the last dump
* at this level or lower. This is used to support different levels of
* incremental backups.
*
* @return dump level
*/
public int getLevel() {
return level;
}
/**
* Set level.
*/
public void setLevel(int level) {
this.level = level;
}
/**
* Get dump label. This may be autogenerated or it may be specified
* bu the user.
*
* @return dump label
*/
public String getLabel() {
return label;
}
/**
* Set dump label.
*
* @param label
*/
public void setLabel(String label) {
this.label = label;
}
/**
* Get the last mountpoint, e.g., /home.
*
* @return last mountpoint
*/
public String getFilesystem() {
return filesys;
}
/**
* Set the last mountpoint.
*/
public void setFilesystem(String filesystem) {
this.filesys = filesystem;
}
/**
* Get the device name, e.g., /dev/sda3 or /dev/mapper/vg0-home.
*
* @return device name
*/
public String getDevname() {
return devname;
}
/**
* Set the device name.
*
* @param devname
*/
public void setDevname(String devname) {
this.devname = devname;
}
/**
* Get the hostname of the system where the dump was performed.
*
* @return hostname
*/
public String getHostname() {
return hostname;
}
/**
* Set the hostname.
*/
public void setHostname(String hostname) {
this.hostname = hostname;
}
/**
* Get the miscellaneous flags. See below.
*
* @return flags
*/
public int getFlags() {
return flags;
}
/**
* Set the miscellaneous flags.
*
* @param flags
*/
public void setFlags(int flags) {
this.flags = flags;
}
/**
* Get the inode of the first record on this volume.
*
* @return inode of the first record on this volume.
*/
public int getFirstRecord() {
return firstrec;
}
/**
* Set the inode of the first record.
*
* @param firstrec
*/
public void setFirstRecord(int firstrec) {
this.firstrec = firstrec;
}
/**
* Get the number of records per tape block. This is typically
* between 10 and 32.
*
* @return the number of records per tape block
*/
public int getNTRec() {
return ntrec;
}
/**
* Set the number of records per tape block.
*/
public void setNTRec(int ntrec) {
this.ntrec = ntrec;
}
/**
* Is this the new header format? (We do not currently support the
* old format.)
*
* @return true if using new header format
*/
public boolean isNewHeader() {
return (flags & 0x0001) == 0x0001;
}
/**
* Is this the new inode format? (We do not currently support the
* old format.)
*
* @return true if using new inode format
*/
public boolean isNewInode() {
return (flags & 0x0002) == 0x0002;
}
/**
* Is this volume compressed? N.B., individual blocks may or may not be compressed.
* The first block is never compressed.
*
* @return true if volume is compressed
*/
public boolean isCompressed() {
return (flags & 0x0080) == 0x0080;
}
/**
* Does this volume only contain metadata?
*
* @return true if volume only contains meta-data
*/
public boolean isMetaDataOnly() {
return (flags & 0x0100) == 0x0100;
}
/**
* Does this volume cotain extended attributes.
*
* @return true if volume cotains extended attributes.
*/
public boolean isExtendedAttributes() {
return (flags & 0x8000) == 0x8000;
}
/**
* @see Object#hashCode()
*/
@Override
public int hashCode() {
int hash = 17;
if (label != null) {
hash = label.hashCode();
}
hash += 31 * dumpDate;
if (hostname != null) {
hash = (31 * hostname.hashCode()) + 17;
}
if (devname != null) {
hash = (31 * devname.hashCode()) + 17;
}
return hash;
}
/**
* @see Object#equals(Object)
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || !o.getClass().equals(getClass())) {
return false;
}
DumpArchiveSummary rhs = (DumpArchiveSummary) o;
if (dumpDate != rhs.dumpDate) {
return false;
}
if ((getHostname() == null) ||
!getHostname().equals(rhs.getHostname())) {
return false;
}
if ((getDevname() == null) || !getDevname().equals(rhs.getDevname())) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,116 @@
package org.xbib.io.archive.dump;
/**
* Various utilities for dump archives.
*/
class DumpArchiveUtil {
/**
* Private constructor to prevent instantiation.
*/
private DumpArchiveUtil() {
}
/**
* Calculate checksum for buffer.
*
* @param buffer buffer containing tape segment header
* @return checksum
*/
public static int calculateChecksum(byte[] buffer) {
int calc = 0;
for (int i = 0; i < 256; i++) {
calc += DumpArchiveUtil.convert32(buffer, 4 * i);
}
return DumpArchiveConstants.CHECKSUM -
(calc - DumpArchiveUtil.convert32(buffer, 28));
}
/**
* Verify that the buffer contains a tape segment header.
*
* @param buffer
*/
public static final boolean verify(byte[] buffer) {
// verify magic. for now only accept NFS_MAGIC.
int magic = convert32(buffer, 24);
if (magic != DumpArchiveConstants.NFS_MAGIC) {
return false;
}
//verify checksum...
int checksum = convert32(buffer, 28);
if (checksum != calculateChecksum(buffer)) {
return false;
}
return true;
}
/**
* Get the ino associated with this buffer.
*
* @param buffer
*/
public static final int getIno(byte[] buffer) {
return convert32(buffer, 20);
}
/**
* Read 8-byte integer from buffer.
*
* @param buffer
* @param offset
* @return the 8-byte entry as a long
*/
public static final long convert64(byte[] buffer, int offset) {
long i = 0;
i += (((long) buffer[offset + 7]) << 56);
i += (((long) buffer[offset + 6] << 48) & 0x00FF000000000000L);
i += (((long) buffer[offset + 5] << 40) & 0x0000FF0000000000L);
i += (((long) buffer[offset + 4] << 32) & 0x000000FF00000000L);
i += (((long) buffer[offset + 3] << 24) & 0x00000000FF000000L);
i += (((long) buffer[offset + 2] << 16) & 0x0000000000FF0000L);
i += (((long) buffer[offset + 1] << 8) & 0x000000000000FF00L);
i += (buffer[offset] & 0x00000000000000FFL);
return i;
}
/**
* Read 4-byte integer from buffer.
*
* @param buffer
* @param offset
* @return the 4-byte entry as an int
*/
public static final int convert32(byte[] buffer, int offset) {
int i = 0;
i = buffer[offset + 3] << 24;
i += (buffer[offset + 2] << 16) & 0x00FF0000;
i += (buffer[offset + 1] << 8) & 0x0000FF00;
i += buffer[offset] & 0x000000FF;
return i;
}
/**
* Read 2-byte integer from buffer.
*
* @param buffer
* @param offset
* @return the 2-byte entry as an int
*/
public static final int convert16(byte[] buffer, int offset) {
int i = 0;
i += (buffer[offset + 1] << 8) & 0x0000FF00;
i += buffer[offset] & 0x000000FF;
return i;
}
}

View file

@ -0,0 +1,20 @@
package org.xbib.io.archive.dump;
/**
* Invalid Format Exception. There was an error decoding a
* tape segment header.
*/
public class InvalidFormatException extends DumpArchiveException {
protected long offset;
public InvalidFormatException() {
super("there was an error decoding a tape segment");
}
public long getOffset() {
return offset;
}
}

View file

@ -0,0 +1,15 @@
package org.xbib.io.archive.dump;
/**
* Short File Exception. There was an unexpected EOF when reading
* the input stream.
*/
public class ShortFileException extends DumpArchiveException {
private static final long serialVersionUID = 1L;
public ShortFileException() {
super("unexpected EOF");
}
}

View file

@ -0,0 +1,331 @@
package org.xbib.io.archive.dump;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
/**
* Filter stream that mimics a physical tape drive capable of compressing
* the data stream
*/
class TapeInputStream extends FilterInputStream {
private byte[] blockBuffer = new byte[DumpArchiveConstants.TP_SIZE];
private int currBlkIdx = -1;
private int blockSize = DumpArchiveConstants.TP_SIZE;
private int recordSize = DumpArchiveConstants.TP_SIZE;
private int readOffset = DumpArchiveConstants.TP_SIZE;
private boolean isCompressed = false;
private long bytesRead = 0;
/**
* Constructor
*/
public TapeInputStream(InputStream in) {
super(in);
}
/**
* Set the DumpArchive Buffer's block size. We need to sync the block size with the
* dump archive's actual block size since compression is handled at the
* block level.
*
* @param recsPerBlock records per block
* @param isCompressed true if the archive is compressed
* @throws java.io.IOException more than one block has been read
* @throws java.io.IOException there was an error reading additional blocks.
*/
public void resetBlockSize(int recsPerBlock, boolean isCompressed)
throws IOException {
this.isCompressed = isCompressed;
blockSize = recordSize * recsPerBlock;
// save first block in case we need it again
byte[] oldBuffer = blockBuffer;
// read rest of new block
blockBuffer = new byte[blockSize];
System.arraycopy(oldBuffer, 0, blockBuffer, 0, recordSize);
readFully(blockBuffer, recordSize, blockSize - recordSize);
this.currBlkIdx = 0;
this.readOffset = recordSize;
}
/**
* @see java.io.InputStream#available
*/
@Override
public int available() throws IOException {
if (readOffset < blockSize) {
return blockSize - readOffset;
}
return in.available();
}
/**
* @see java.io.InputStream#read()
*/
@Override
public int read() throws IOException {
throw new IllegalArgumentException(
"all reads must be multiple of record size (" + recordSize +
" bytes.");
}
/**
* <p>reads the full given length unless EOF is reached.</p>
*
* @param len length to read, must be a multiple of the stream's
* record size
*/
@Override
public int read(byte[] b, int off, int len) throws IOException {
if ((len % recordSize) != 0) {
throw new IllegalArgumentException(
"all reads must be multiple of record size (" + recordSize +
" bytes.");
}
int bytes = 0;
while (bytes < len) {
// we need to read from the underlying stream.
// this will reset readOffset value.
// return -1 if there's a problem.
if ((readOffset == blockSize) && !readBlock(true)) {
return -1;
}
int n = 0;
if ((readOffset + (len - bytes)) <= blockSize) {
// we can read entirely from the buffer.
n = len - bytes;
} else {
// copy what we can from the buffer.
n = blockSize - readOffset;
}
// copy data, increment counters.
System.arraycopy(blockBuffer, readOffset, b, off, n);
readOffset += n;
bytes += n;
off += n;
}
return bytes;
}
/**
* Skip bytes. Same as read but without the arraycopy.
* <p/>
* <p>skips the full given length unless EOF is reached.</p>
*
* @param len length to read, must be a multiple of the stream's
* record size
*/
@Override
public long skip(long len) throws IOException {
if ((len % recordSize) != 0) {
throw new IllegalArgumentException(
"all reads must be multiple of record size (" + recordSize +
" bytes.");
}
long bytes = 0;
while (bytes < len) {
// we need to read from the underlying stream.
// this will reset readOffset value. We do not perform
// any decompression if we won't eventually read the data.
// return -1 if there's a problem.
if ((readOffset == blockSize) &&
!readBlock((len - bytes) < blockSize)) {
return -1;
}
long n = 0;
if ((readOffset + (len - bytes)) <= blockSize) {
// we can read entirely from the buffer.
n = len - bytes;
} else {
// copy what we can from the buffer.
n = blockSize - readOffset;
}
// do not copy data but still increment counters.
readOffset += n;
bytes += n;
}
return bytes;
}
/**
* Close the input stream.
*
* @throws java.io.IOException on error
*/
@Override
public void close() throws IOException {
if (in != null && in != System.in) {
in.close();
}
}
/**
* Peek at the next record from the input stream and return the data.
*
* @return The record data.
* @throws java.io.IOException on error
*/
public byte[] peek() throws IOException {
// we need to read from the underlying stream. This
// isn't a problem since it would be the first step in
// any subsequent read() anyway.
if ((readOffset == blockSize) && !readBlock(true)) {
return null;
}
// copy data, increment counters.
byte[] b = new byte[recordSize];
System.arraycopy(blockBuffer, readOffset, b, 0, b.length);
return b;
}
/**
* Read a record from the input stream and return the data.
*
* @return The record data.
* @throws java.io.IOException on error
*/
public byte[] readRecord() throws IOException {
byte[] result = new byte[recordSize];
if (-1 == read(result, 0, result.length)) {
throw new ShortFileException();
}
return result;
}
/**
* Read next block. All decompression is handled here.
*
* @param decompress if false the buffer will not be decompressed.
* This is an optimization for longer seeks.
* @return false if End-Of-File, else true
*/
private boolean readBlock(boolean decompress) throws IOException {
boolean success = true;
if (in == null) {
throw new IOException("input buffer is closed");
}
if (!isCompressed || (currBlkIdx == -1)) {
// file is not compressed
success = readFully(blockBuffer, 0, blockSize);
bytesRead += blockSize;
} else {
if (!readFully(blockBuffer, 0, 4)) {
return false;
}
bytesRead += 4;
int h = DumpArchiveUtil.convert32(blockBuffer, 0);
boolean compressed = (h & 0x01) == 0x01;
if (!compressed) {
// file is compressed but this block is not.
success = readFully(blockBuffer, 0, blockSize);
bytesRead += blockSize;
} else {
// this block is compressed.
int flags = (h >> 1) & 0x07;
int length = (h >> 4) & 0x0FFFFFFF;
byte[] compBuffer = new byte[length];
success = readFully(compBuffer, 0, length);
bytesRead += length;
if (!decompress) {
// just in case someone reads the data.
Arrays.fill(blockBuffer, (byte) 0);
} else {
switch (DumpArchiveConstants.COMPRESSION_TYPE.find(flags &
0x03)) {
case ZLIB:
try {
Inflater inflator = new Inflater();
inflator.setInput(compBuffer, 0, compBuffer.length);
length = inflator.inflate(blockBuffer);
if (length != blockSize) {
throw new ShortFileException();
}
inflator.end();
} catch (DataFormatException e) {
throw new DumpArchiveException("bad data", e);
}
break;
case BZLIB:
throw new UnsupportedCompressionAlgorithmException(
"BZLIB2");
case LZO:
throw new UnsupportedCompressionAlgorithmException(
"LZO");
default:
throw new UnsupportedCompressionAlgorithmException();
}
}
}
}
currBlkIdx++;
readOffset = 0;
return success;
}
/**
* Read buffer
*/
private boolean readFully(byte[] b, int off, int len)
throws IOException {
int count = 0;
while (count < len) {
int n = in.read(b, off + count, len - count);
if (n == -1) {
throw new ShortFileException();
}
count += n;
}
return true;
}
/**
* Get number of bytes read.
*/
public long getBytesRead() {
return bytesRead;
}
}

View file

@ -0,0 +1,14 @@
package org.xbib.io.archive.dump;
/**
* Unrecognized Format Exception. This is either not a recognized dump archive or there's
* a bad tape segment header.
*/
public class UnrecognizedFormatException extends DumpArchiveException {
private static final long serialVersionUID = 1L;
public UnrecognizedFormatException() {
super("this is not a recognized format.");
}
}

View file

@ -0,0 +1,20 @@
package org.xbib.io.archive.dump;
/**
* Unsupported compression algorithm. The dump archive uses an unsupported
* compression algorithm (BZLIB2 or LZO).
*/
public class UnsupportedCompressionAlgorithmException
extends DumpArchiveException {
private static final long serialVersionUID = 1L;
public UnsupportedCompressionAlgorithmException() {
super("this file uses an unsupported compression algorithm.");
}
public UnsupportedCompressionAlgorithmException(String alg) {
super("this file uses an unsupported compression algorithm: " + alg +
".");
}
}

View file

@ -0,0 +1,4 @@
dependencies {
api project(':io-archive')
api project(':io-archive-zip')
}

View file

@ -0,0 +1,5 @@
module org.xbib.io.archive.jar {
exports org.xbib.io.archive.jar;
requires org.xbib.io.archive;
requires org.xbib.io.archive.zip;
}

View file

@ -0,0 +1,60 @@
package org.xbib.io.archive.jar;
import org.xbib.io.archive.zip.ZipArchiveEntry;
import java.security.cert.Certificate;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
public class JarArchiveEntry extends ZipArchiveEntry {
private Attributes manifestAttributes = null;
private Certificate[] certificates = null;
public JarArchiveEntry() {
super();
}
public JarArchiveEntry(ZipEntry entry) throws ZipException {
super(entry);
}
public JarArchiveEntry(String name) {
super(name);
}
public JarArchiveEntry(ZipArchiveEntry entry) throws ZipException {
super(entry);
}
public JarArchiveEntry(JarEntry entry) throws ZipException {
super(entry);
}
public Attributes getManifestAttributes() {
return manifestAttributes;
}
public Certificate[] getCertificates() {
if (certificates != null) {
Certificate[] certs = new Certificate[certificates.length];
System.arraycopy(certificates, 0, certs, 0, certs.length);
return certs;
}
return null;
}
@Override
public boolean equals(Object o) {
return super.equals(o);
}
@Override
public int hashCode() {
return super.hashCode();
}
}

View file

@ -0,0 +1,30 @@
package org.xbib.io.archive.jar;
import org.xbib.io.archive.entry.ArchiveEntry;
import org.xbib.io.archive.zip.ZipArchiveEntry;
import org.xbib.io.archive.zip.ZipArchiveInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Implements an input stream that can read entries from jar files.
*/
public class JarArchiveInputStream extends ZipArchiveInputStream {
public JarArchiveInputStream(final InputStream inputStream) {
super(inputStream);
}
public JarArchiveEntry getNextJarEntry() throws IOException {
ZipArchiveEntry entry = getNextZipEntry();
return entry == null ? null : new JarArchiveEntry(entry);
}
@Override
public ArchiveEntry getNextEntry() throws IOException {
return getNextJarEntry();
}
}

View file

@ -0,0 +1,31 @@
package org.xbib.io.archive.jar;
import org.xbib.io.archive.zip.JarMarker;
import org.xbib.io.archive.zip.ZipArchiveOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* Subclass that adds a special extra field to the very first entry
* which allows the created archive to be used as an executable jar on
* Solaris.
*/
public class JarArchiveOutputStream extends ZipArchiveOutputStream<JarArchiveEntry> {
private boolean jarMarkerAdded = false;
public JarArchiveOutputStream(final OutputStream out) {
super(out);
}
@Override
public void putArchiveEntry(JarArchiveEntry ze) throws IOException {
if (!jarMarkerAdded) {
ze.addAsFirstExtraField(JarMarker.getInstance());
jarMarkerAdded = true;
}
super.putArchiveEntry(ze);
}
}

View file

@ -0,0 +1,24 @@
package org.xbib.io.archive.jar;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
public class JarTest {
@Test
public void testJar() throws Exception {
InputStream in = getClass().getResourceAsStream("test.jar");
JarArchiveInputStream jarArchiveInputStream = new JarArchiveInputStream(in);
byte[] buffer = new byte[1024];
long total = 0L;
while ((jarArchiveInputStream.getNextEntry()) != null) {
int len = 0;
while ((len = jarArchiveInputStream.read(buffer)) > 0) {
total += len;
}
}
assertEquals(1813L, total);
jarArchiveInputStream.close();
}
}

View file

@ -0,0 +1,3 @@
dependencies {
api project(':io-archive')
}

View file

@ -0,0 +1,4 @@
module org.xbib.io.archive.tar {
exports org.xbib.io.archive.tar;
requires org.xbib.io.archive;
}

View file

@ -0,0 +1,842 @@
package org.xbib.io.archive.tar;
import org.xbib.io.archive.entry.ArchiveEntry;
import org.xbib.io.archive.util.ArchiveUtils;
import org.xbib.io.archive.entry.ArchiveEntryEncoding;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Date;
/**
* This class represents an entry in a Tar archive.
*/
public class TarArchiveEntry implements TarConstants, ArchiveEntry {
/**
* Maximum length of a user's name in the tar file
*/
private static final int MAX_NAMELEN = 31;
/**
* Default permissions bits for directories
*/
private static final int DEFAULT_DIR_MODE = 040755;
/**
* Default permissions bits for files
*/
private static final int DEFAULT_FILE_MODE = 0100644;
/**
* Convert millis to seconds
*/
private static final int MILLIS_PER_SECOND = 1000;
/**
* The entry's name.
*/
private String name;
/**
* The entry's permission mode.
*/
private int mode;
/**
* The entry's user id.
*/
private int userId;
/**
* The entry's group id.
*/
private int groupId;
/**
* The entry's size.
*/
private long size;
/**
* The entry's modification time.
*/
private long modTime;
/**
* The entry's link flag.
*/
private byte linkFlag;
/**
* The entry's link name.
*/
private String linkName;
/**
* The version of the format
*/
private String version;
/**
* The entry's user name.
*/
private String userName;
/**
* The entry's group name.
*/
private String groupName;
/**
* The entry's major device number.
*/
private int devMajor;
/**
* The entry's minor device number.
*/
private int devMinor;
/**
* If an extension sparse header follows.
*/
private boolean isExtended;
/**
* The entry's real size in case of a sparse file.
*/
private long realSize;
private boolean isDir;
/**
* Construct an empty entry and prepares the header values.
*/
public TarArchiveEntry() {
this.version = VERSION_POSIX;
this.name = "";
this.linkName = "";
this.linkFlag = LF_GNUTYPE_LONGNAME;
String user = System.getProperty("user.name", "");
if (user.length() > MAX_NAMELEN) {
user = user.substring(0, MAX_NAMELEN);
}
this.userName = user;
this.groupName = "";
this.userId = 0;
this.groupId = 0;
this.mode = DEFAULT_FILE_MODE;
}
/**
* Construct an entry with only a name. This allows the programmer
* to construct the entry's header "by hand". File is set to null.
*
* @param name the entry name
*/
public TarArchiveEntry(String name) {
this(name, false);
}
/**
* Construct an entry with only a name. This allows the programmer
* to construct the entry's header "by hand". File is set to null.
*
* @param name the entry name
* @param preserveLeadingSlashes whether to allow leading slashes
* in the name.
*/
public TarArchiveEntry(String name, boolean preserveLeadingSlashes) {
this();
name = ArchiveUtils.normalizeFileName(name, preserveLeadingSlashes);
this.name = name;
boolean isDir = name.endsWith("/");
this.mode = isDir ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE;
this.linkFlag = isDir ? LF_DIR : LF_NORMAL;
this.devMajor = 0;
this.devMinor = 0;
this.userId = 0;
this.groupId = 0;
this.size = 0;
this.modTime = (new Date()).getTime() / MILLIS_PER_SECOND;
this.linkName = "";
this.userName = "";
this.groupName = "";
}
/**
* Construct an entry with a name and a link flag.
*
* @param name the entry name
* @param linkFlag the entry link flag.
*/
public TarArchiveEntry(String name, byte linkFlag) {
this(name);
this.linkFlag = linkFlag;
if (linkFlag == LF_GNUTYPE_LONGNAME) {
version = VERSION_GNU_SPACE;
}
}
/**
* Construct an entry from an archive's header bytes. File is set
* to null.
*
* @param headerBuf The header bytes from a tar archive entry.
* @param encoding encoding to use for file names
* @throws IllegalArgumentException if any of the numeric fields have an invalid format
*/
public TarArchiveEntry(byte[] headerBuf, ArchiveEntryEncoding encoding) throws IOException {
this();
parseTarHeader(headerBuf, encoding);
}
/**
* Determine if the two entries are equal. Equality is determined
* by the header names being equal.
*
* @param it Entry to be checked for equality.
* @return True if the entries are equal.
*/
public boolean equals(TarArchiveEntry it) {
return getName().equals(it.getName());
}
/**
* Determine if the two entries are equal. Equality is determined
* by the header names being equal.
*
* @param it Entry to be checked for equality.
* @return True if the entries are equal.
*/
@Override
public boolean equals(Object it) {
return !(it == null || getClass() != it.getClass()) && equals((TarArchiveEntry) it);
}
/**
* Hashcodes are based on entry names.
*
* @return the entry hashcode
*/
@Override
public int hashCode() {
return getName().hashCode();
}
/**
* Get this entry's name.
*
* @return This entry's name.
*/
public String getName() {
return name;
}
/**
* Set this entry's name.
*
* @param name This entry's new name.
*/
public TarArchiveEntry setName(String name) {
this.name = ArchiveUtils.normalizeFileName(name, false);
this.isDir = name.endsWith("/");
this.mode = isDir ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE;
this.linkFlag = isDir ? LF_DIR : LF_NORMAL;
return this;
}
/**
* Set this entry's modification time
*
* @param date This entry's new modification time
*/
public TarArchiveEntry setLastModified(Date date) {
modTime = date.getTime() / MILLIS_PER_SECOND;
return this;
}
public Date getLastModified() {
return new Date(modTime * MILLIS_PER_SECOND);
}
@Override
public boolean isDirectory() {
return isDir;
}
/**
* Set this entry's file size.
*
* @param size This entry's new file size.
* @throws IllegalArgumentException if the size is &lt; 0.
*/
public TarArchiveEntry setEntrySize(long size) {
if (size < 0) {
throw new IllegalArgumentException("size is out of range: " + size);
}
this.size = size;
return this;
}
/**
* Get this entry's file size.
*
* @return This entry's file size.
*/
public long getEntrySize() {
return size;
}
/**
* Set the mode for this entry
*
* @param mode the mode for this entry
*/
public void setMode(int mode) {
this.mode = mode;
}
/**
* Get this entry's link name.
*
* @return This entry's link name.
*/
public String getLinkName() {
return linkName;
}
/**
* Set this entry's link name.
*
* @param link the link name to use.
*/
public void setLinkName(String link) {
this.linkName = link;
}
/**
* Get this entry's user id.
*
* @return This entry's user id.
*/
public int getUserId() {
return userId;
}
/**
* Set this entry's user id.
*
* @param userId This entry's new user id.
*/
public void setUserId(int userId) {
this.userId = userId;
}
/**
* Get this entry's group id.
*
* @return This entry's group id.
*/
public int getGroupId() {
return groupId;
}
/**
* Set this entry's group id.
*
* @param groupId This entry's new group id.
*/
public void setGroupId(int groupId) {
this.groupId = groupId;
}
/**
* Get this entry's user name.
*
* @return This entry's user name.
*/
public String getUserName() {
return userName;
}
/**
* Set this entry's user name.
*
* @param userName This entry's new user name.
*/
public void setUserName(String userName) {
this.userName = userName;
}
/**
* Get this entry's group name.
*
* @return This entry's group name.
*/
public String getGroupName() {
return groupName;
}
/**
* Set this entry's group name.
*
* @param groupName This entry's new group name.
*/
public void setGroupName(String groupName) {
this.groupName = groupName;
}
/**
* Get this entry's mode.
*
* @return This entry's mode.
*/
public int getMode() {
return mode;
}
/**
* Get this entry's major device number.
*
* @return This entry's major device number.
*/
public int getDevMajor() {
return devMajor;
}
/**
* Set this entry's major device number.
*
* @param devNo This entry's major device number.
* @throws IllegalArgumentException if the devNo is &lt; 0.
*/
public void setDevMajor(int devNo) {
if (devNo < 0) {
throw new IllegalArgumentException("Major device number is out of "
+ "range: " + devNo);
}
this.devMajor = devNo;
}
/**
* Get this entry's minor device number.
*
* @return This entry's minor device number.
*/
public int getDevMinor() {
return devMinor;
}
/**
* Set this entry's minor device number.
*
* @param devNo This entry's minor device number.
* @throws IllegalArgumentException if the devNo is &lt; 0.
*/
public void setDevMinor(int devNo) {
if (devNo < 0) {
throw new IllegalArgumentException("Minor device number is out of " + "range: " + devNo);
}
this.devMinor = devNo;
}
/**
* Indicates in case of a sparse file if an extension sparse header
* follows.
*
* @return true if an extension sparse header follows.
*/
public boolean isExtended() {
return isExtended;
}
/**
* Get this entry's real file size in case of a sparse file.
*
* @return This entry's real file size.
*/
public long getRealSize() {
return realSize;
}
/**
* Indicate if this entry is a GNU sparse block
*
* @return true if this is a sparse extension provided by GNU tar
*/
public boolean isGNUSparse() {
return linkFlag == LF_GNUTYPE_SPARSE;
}
/**
* Indicate if this entry is a GNU long name block
*
* @return true if this is a long name extension provided by GNU tar
*/
public boolean isGNULongNameEntry() {
return linkFlag == LF_GNUTYPE_LONGNAME && GNU_LONGLINK.equals(name);
}
/**
* Check if this is a Pax header.
*
* @return {@code true} if this is a Pax header.
*/
public boolean isPaxHeader() {
return linkFlag == LF_PAX_EXTENDED_HEADER_LC || linkFlag == LF_PAX_EXTENDED_HEADER_UC;
}
/**
* Check if this is a Pax header.
*
* @return {@code true} if this is a Pax header.
*/
public boolean isGlobalPaxHeader() {
return linkFlag == LF_PAX_GLOBAL_EXTENDED_HEADER;
}
/**
* Check if this is a symbolic link entry.
*/
public boolean isSymbolicLink() {
return linkFlag == LF_SYMLINK;
}
/**
* Check if this is a link entry.
*/
public boolean isLink() {
return linkFlag == LF_LINK;
}
/**
* Check if this is a character device entry.
*/
public boolean isCharacterDevice() {
return linkFlag == LF_CHR;
}
/**
* Check if this is a block device entry.
*/
public boolean isBlockDevice() {
return linkFlag == LF_BLK;
}
/**
* Check if this is a FIFO (pipe) entry.
*/
public boolean isFIFO() {
return linkFlag == LF_FIFO;
}
/**
* Parse an entry's header information from a header buffer.
*
* @param header The tar entry header buffer to get information from.
* @param encoding encoding to use for file names
* @throws IllegalArgumentException if any of the numeric fields
* have an invalid format
*/
public void parseTarHeader(byte[] header, ArchiveEntryEncoding encoding)
throws IOException {
parseTarHeader(header, encoding, false);
}
private void parseTarHeader(byte[] header, ArchiveEntryEncoding encoding, final boolean oldStyle)
throws IOException {
int offset = 0;
int type = evaluateType(header);
name = parseFileName(header);
offset += NAMELEN;
mode = (int) parseOctalOrBinary(header, offset, MODELEN);
offset += MODELEN;
userId = (int) parseOctalOrBinary(header, offset, UIDLEN);
offset += UIDLEN;
groupId = (int) parseOctalOrBinary(header, offset, GIDLEN);
offset += GIDLEN;
if (type == GNU_FORMAT) {
size = getSize(header, offset, SIZELEN);
} else {
size = parseOctalOrBinary(header, offset, SIZELEN);
}
offset += SIZELEN;
modTime = parseOctalOrBinary(header, offset, MODTIMELEN);
offset += MODTIMELEN;
offset += CHKSUMLEN;
linkFlag = header[offset++];
linkName = oldStyle ? parseName(header, offset, NAMELEN) : parseName(header, offset, NAMELEN, encoding);
offset += NAMELEN;
switch (type) {
case UNIX_FORMAT: {
offset += ATIMELEN_GNU;
offset += CTIMELEN_GNU;
offset += OFFSETLEN_GNU;
offset += LONGNAMESLEN_GNU;
offset += PAD2LEN_GNU;
offset += SPARSELEN_GNU;
isExtended = parseBoolean(header, offset);
offset += ISEXTENDEDLEN_GNU;
realSize = parseOctal(header, offset, REALSIZELEN_GNU);
offset += REALSIZELEN_GNU;
break;
}
case POSIX_FORMAT: {
parseName(header, offset, MAGICLEN); // magic
offset += MAGICLEN;
version = parseName(header, offset, VERSIONLEN);
offset += VERSIONLEN;
userName = oldStyle ? parseName(header, offset, UNAMELEN) : parseName(header, offset, UNAMELEN, encoding);
offset += UNAMELEN;
groupName = oldStyle ? parseName(header, offset, GNAMELEN) : parseName(header, offset, GNAMELEN, encoding);
offset += GNAMELEN;
devMajor = (int) parseOctalOrBinary(header, offset, DEVLEN);
offset += DEVLEN;
devMinor = (int) parseOctalOrBinary(header, offset, DEVLEN);
offset += DEVLEN;
}
}
}
/**
* Evaluate an entry's header format from a header buffer.
*
* @param header The tar entry header buffer to evaluate the format for.
* @return format type
*/
private int evaluateType(byte[] header) {
if (ArchiveUtils.matchAsciiBuffer(MAGIC_UNIX, header, MAGIC_OFFSET, MAGICLEN)) {
return UNIX_FORMAT;
}
if (ArchiveUtils.matchAsciiBuffer(MAGIC_POSIX, header, MAGIC_OFFSET, MAGICLEN)) {
return POSIX_FORMAT;
}
if (ArchiveUtils.matchAsciiBuffer(MAGIC_GNU, header, MAGIC_OFFSET, MAGICLEN)) {
return GNU_FORMAT;
}
return 0;
}
/**
* Parse an octal string from a buffer.
* <p>Leading spaces are ignored.
* The buffer must contain a trailing space or NUL,
* and may contain an additional trailing space or NUL.</p>
* <p>The input buffer is allowed to contain all NULs,
* in which case the method returns 0L
* (this allows for missing fields).</p>
* <p>To work-around some tar implementations that insert a
* leading NUL this method returns 0 if it detects a leading NUL.</p>
*
* @param buffer The buffer from which to parse.
* @param offset The offset into the buffer from which to parse.
* @param length The maximum number of bytes to parse - must be at least 2 bytes.
* @return The long value of the octal string.
* @throws IllegalArgumentException if the trailing space/NUL is missing or if a invalid byte is detected.
*/
private long parseOctal(final byte[] buffer, final int offset, final int length) {
long result = 0;
int end = offset + length;
int start = offset;
if (length < 2) {
throw new IllegalArgumentException("Length " + length + " must be at least 2");
}
if (buffer[start] == 0) {
return 0L;
}
while (start < end) {
if (buffer[start] == ' ') {
start++;
} else {
break;
}
}
byte trailer;
trailer = buffer[end - 1];
if (trailer == 0 || trailer == ' ') {
end--;
} else {
throw new IllegalArgumentException(exceptionMessage(buffer, offset, length, end - 1, trailer));
}
trailer = buffer[end - 1];
if (trailer == 0 || trailer == ' ') {
end--;
}
for (; start < end; start++) {
final byte currentByte = buffer[start];
if (currentByte < '0' || currentByte > '7') {
throw new IllegalArgumentException(
exceptionMessage(buffer, offset, length, start, currentByte));
}
result = (result << 3) + (currentByte - '0'); // convert from ASCII
}
return result;
}
/**
* Compute the value contained in a byte buffer. If the most
* significant bit of the first byte in the buffer is set, this
* bit is ignored and the rest of the buffer is interpreted as a
* binary number. Otherwise, the buffer is interpreted as an
* octal number as per the parseOctal function above.
*
* @param buffer The buffer from which to parse.
* @param offset The offset into the buffer from which to parse.
* @param length The maximum number of bytes to parse.
* @return The long value of the octal or binary string.
* @throws IllegalArgumentException if the trailing space/NUL is
* missing or an invalid byte is detected in an octal number, or
* if a binary number would exceed the size of a signed long
* 64-bit integer.
*/
private long parseOctalOrBinary(final byte[] buffer, final int offset, final int length) {
if ((buffer[offset] & 0x80) == 0) {
return parseOctal(buffer, offset, length);
}
final boolean negative = buffer[offset] == (byte) 0xff;
if (length < 9) {
return parseBinaryLong(buffer, offset, length, negative);
}
return parseBinaryBigInteger(buffer, offset, length, negative);
}
private long parseBinaryLong(final byte[] buffer, final int offset, final int length, final boolean negative) {
if (length >= 9) {
throw new IllegalArgumentException("At offset " + offset + ", "
+ length + " byte binary number"
+ " exceeds maximum signed long"
+ " value");
}
long val = 0;
for (int i = 1; i < length; i++) {
val = (val << 8) + (buffer[offset + i] & 0xff);
}
if (negative) {
// 2's complement
val--;
val ^= ((long) Math.pow(2, (length - 1) * 8) - 1);
}
return negative ? -val : val;
}
private long parseBinaryBigInteger(final byte[] buffer, final int offset, final int length, final boolean negative) {
byte[] remainder = new byte[length - 1];
System.arraycopy(buffer, offset + 1, remainder, 0, length - 1);
BigInteger val = new BigInteger(remainder);
if (negative) {
// 2's complement
val = val.add(BigInteger.valueOf(-1)).not();
}
if (val.bitLength() > 63) {
throw new IllegalArgumentException("At offset " + offset + ", "
+ length + " byte binary number"
+ " exceeds maximum signed long"
+ " value");
}
return negative ? -val.longValue() : val.longValue();
}
/**
* Parse a boolean byte from a buffer.
* Leading spaces and NUL are ignored.
* The buffer may contain trailing spaces or NULs.
*
* @param buffer The buffer from which to parse.
* @param offset The offset into the buffer from which to parse.
* @return The boolean value of the bytes.
* @throws IllegalArgumentException if an invalid byte is detected.
*/
private boolean parseBoolean(final byte[] buffer, final int offset) {
return buffer[offset] == 1;
}
private String exceptionMessage(byte[] buffer, final int offset, final int length, int current, final byte currentByte) {
String string = new String(buffer, offset, length); // TODO default charset?
string = string.replaceAll("\0", "{NUL}"); // Replace NULs to allow string to be printed
return "Invalid byte " + currentByte + " at offset " + (current - offset) + " in '" + string + "' len=" + length;
}
/**
* Parse an entry name from a buffer.
* Parsing stops when a NUL is found
* or the buffer length is reached.
*
* @param buffer The buffer from which to parse.
* @param offset The offset into the buffer from which to parse.
* @param length The maximum number of bytes to parse.
* @return The entry name.
*/
private String parseName(byte[] buffer, final int offset, final int length) {
try {
return parseName(buffer, offset, length, ArchiveUtils.DEFAULT_ENCODING);
} catch (IOException ex) {
try {
return parseName(buffer, offset, length, ArchiveUtils.FALLBACK_ENCODING);
} catch (IOException ex2) {
// impossible
throw new RuntimeException(ex2);
}
}
}
/**
* Parse an entry name from a buffer.
* Parsing stops when a NUL is found
* or the buffer length is reached.
*
* @param buffer The buffer from which to parse.
* @param offset The offset into the buffer from which to parse.
* @param length The maximum number of bytes to parse.
* @param encoding name of the encoding to use for file names
* @return The entry name.
*/
private String parseName(byte[] buffer, final int offset, final int length, final ArchiveEntryEncoding encoding) throws IOException {
int len = length;
for (; len > 0; len--) {
if (buffer[offset + len - 1] != 0) {
break;
}
}
if (len > 0) {
byte[] b = new byte[len];
System.arraycopy(buffer, offset, b, 0, len);
return encoding.decode(b);
}
return "";
}
private long getSize(byte[] header, int offset, int length) {
long test = parseOctal(header, offset, length);
if (test <= 0 && header[offset] == (byte) 128) {
byte[] last = new byte[length];
System.arraycopy(header, offset, last, 0, length);
last[0] = (byte) 0;
long rSize = new BigInteger(last).longValue();
last = null;
return rSize;
}
return test;
}
private String parseFileName(byte[] header) {
StringBuilder result = new StringBuilder(256);
// If header[345] is not equal to zero, then it is the "prefix"
// that 'ustar' defines. It must be prepended to the "normal"
// name field. We are responsible for the separating '/'.
if (header[345] != 0) {
for (int i = 345; i < 500 && header[i] != 0; ++i) {
result.append((char) header[i]);
}
result.append("/");
}
for (int i = 0; i < 100 && header[i] != 0; ++i) {
result.append((char) header[i]);
}
return result.toString();
}
}

View file

@ -0,0 +1,434 @@
package org.xbib.io.archive.tar;
import org.xbib.io.archive.stream.ArchiveInputStream;
import org.xbib.io.archive.entry.ArchiveEntryEncoding;
import org.xbib.io.archive.entry.ArchiveEntryEncodingHelper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
public class TarArchiveInputStream extends ArchiveInputStream<TarArchiveEntry> implements TarConstants {
private final ArchiveEntryEncoding encoding;
private final InputStream inStream;
private final int blockSize;
private final int recordSize;
private final int recsPerBlock;
private final byte[] blockBuffer;
private byte[] readBuf;
private boolean hasHitEOF;
private long entrySize;
private long entryOffset;
private TarArchiveEntry entry;
private int currRecIdx;
/**
* Constructor for TarInputStream.
*
* @param is the input stream to use
*/
public TarArchiveInputStream(InputStream is) {
this.encoding = ArchiveEntryEncodingHelper.getEncoding(null);
this.readBuf = null;
this.hasHitEOF = false;
this.inStream = is;
this.blockSize = DEFAULT_BLOCK_SIZE;
this.recordSize = DEFAULT_RECORD_SIZE;
this.recsPerBlock = this.blockSize / this.recordSize;
this.blockBuffer = new byte[this.blockSize];
this.currRecIdx = this.recsPerBlock;
}
/**
* Closes this stream
*
* @throws IOException on error
*/
@Override
public void close() throws IOException {
if (inStream != null) {
if (inStream != System.in) {
inStream.close();
}
}
}
/**
* Get the record size
*
* @return the record size.
*/
public int getRecordSize() {
return recordSize;
}
/**
* Get the available data that can be read from the current
* entry in the archive. This does not indicate how much data
* is left in the entire archive, only in the current entry.
* This value is determined from the entry's size header field
* and the amount of data already read from the current entry.
* Integer.MAX_VALUE is returen in case more than Integer.MAX_VALUE
* bytes are left in the current entry in the archive.
*
* @return The number of available bytes for the current entry.
* @throws IOException
*/
@Override
public int available() throws IOException {
if (entrySize - entryOffset > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
return (int) (entrySize - entryOffset);
}
/**
* Skip bytes in the input buffer. This skips bytes in the
* current entry's data, not the entire archive, and will
* stop at the end of the current entry's data if the number
* to skip extends beyond that point.
*
* @param numToSkip The number of bytes to skip.
* @return the number actually skipped
* @throws IOException on error
*/
@Override
public long skip(long numToSkip) throws IOException {
// REVIEW
// This is horribly inefficient, but it ensures that we
// properly skip over bytes
//
byte[] skipBuf = new byte[1024];
long skip = numToSkip;
while (skip > 0) {
int realSkip = (int) (skip > skipBuf.length ? skipBuf.length : skip);
int numRead = read(skipBuf, 0, realSkip);
if (numRead == -1) {
break;
}
skip -= numRead;
}
return (numToSkip - skip);
}
/**
* Since we do not support marking just yet, we do nothing.
*/
@Override
public void reset() {
}
/**
* Get the next entry in this tar archive. This will skip
* over any remaining data in the current entry, if there
* is one, and place the input stream at the header of the
* next entry, and read the header and instantiate a new
* TarEntry from the header bytes and return that entry.
* If there are no more entries in the archive, null will
* be returned to indicate that the end of the archive has
* been reached.
*
* @return The next TarEntry in the archive, or null.
* @throws IOException on error
*/
public synchronized TarArchiveEntry getNextTarEntry() throws IOException {
if (hasHitEOF) {
return null;
}
if (entry != null) {
long numToSkip = entrySize - entryOffset;
while (numToSkip > 0) {
long skipped = skip(numToSkip);
if (skipped <= 0) {
throw new RuntimeException("failed to skip current tar entry");
}
numToSkip -= skipped;
}
readBuf = null;
}
byte[] headerBuf = getRecord();
if (hasHitEOF) {
entry = null;
return null;
}
try {
this.entry = new TarArchiveEntry(headerBuf, encoding);
this.entryOffset = 0;
this.entrySize = this.entry.getEntrySize();
} catch (IllegalArgumentException e) {
throw new IOException("error detected parsing the header", e);
}
if (entry.isGNULongNameEntry()) {
StringBuilder longName = new StringBuilder();
byte[] buf = new byte[SMALL_BUFFER_SIZE];
int length;
while ((length = read(buf)) >= 0) {
longName.append(new String(buf, 0, length));
}
getNextEntry();
if (entry == null) {
return null;
}
if (longName.length() > 0 && longName.charAt(longName.length() - 1) == 0) {
longName.deleteCharAt(longName.length() - 1);
}
entry.setName(longName.toString());
}
if (entry.isPaxHeader()) {
paxHeaders();
}
return entry;
}
/**
* Get the next record in this tar archive. This will skip
* over any remaining data in the current entry, if there
* is one, and place the input stream at the header of the
* next entry.
* If there are no more entries in the archive, null will
* be returned to indicate that the end of the archive has
* been reached.
*
* @return The next header in the archive, or null.
* @throws IOException on error
*/
private byte[] getRecord() throws IOException {
if (hasHitEOF) {
return null;
}
byte[] headerBuf = readRecord();
if (headerBuf == null) {
hasHitEOF = true;
} else if (isEOFRecord(headerBuf)) {
hasHitEOF = true;
}
return hasHitEOF ? null : headerBuf;
}
/**
* Read a record from the input stream and return the data.
*
* @return The record data.
* @throws IOException on error
*/
private byte[] readRecord() throws IOException {
if (currRecIdx >= recsPerBlock && !readBlock()) {
return null;
}
byte[] result = new byte[recordSize];
System.arraycopy(blockBuffer, (currRecIdx * recordSize), result, 0, recordSize);
currRecIdx++;
return result;
}
private boolean readBlock() throws IOException {
currRecIdx = 0;
int offset = 0;
int bytesNeeded = blockSize;
while (bytesNeeded > 0) {
long numBytes = inStream.read(blockBuffer, offset, bytesNeeded);
if (numBytes == -1) {
if (offset == 0) {
return false;
}
Arrays.fill(blockBuffer, offset, offset + bytesNeeded, (byte) 0);
break;
}
offset += numBytes;
bytesNeeded -= numBytes;
}
return true;
}
/**
* Determine if an archive record indicate End of Archive. End of
* archive is indicated by a record that consists entirely of null bytes.
*
* @param record The record data to check.
* @return true if the record data is an End of Archive
*/
private boolean isEOFRecord(byte[] record) {
for (int i = 0, sz = getRecordSize(); i < sz; ++i) {
if (record[i] != 0) {
return false;
}
}
return true;
}
private void paxHeaders() throws IOException {
Map<String, String> headers = parsePaxHeaders(this);
getNextEntry(); // Get the actual file entry
applyPaxHeadersToCurrentEntry(headers);
}
private Map<String, String> parsePaxHeaders(InputStream i) throws IOException {
Map<String, String> headers = new HashMap<String, String>();
// Format is "length keyword=value\n";
while (true) { // get length
int ch;
int len = 0;
int read = 0;
while ((ch = i.read()) != -1) {
read++;
if (ch == ' ') { // End of length string
// Get keyword
ByteArrayOutputStream coll = new ByteArrayOutputStream();
while ((ch = i.read()) != -1) {
read++;
if (ch == '=') { // end of keyword
String keyword = coll.toString("UTF-8");
// Get rest of entry
byte[] rest = new byte[len - read];
int got = i.read(rest);
if (got != len - read) {
throw new IOException("Failed to read "
+ "Paxheader. Expected "
+ (len - read)
+ " bytes, read "
+ got);
}
// Drop trailing NL
String value = new String(rest, 0,
len - read - 1, Charset.forName("UTF-8"));
headers.put(keyword, value);
break;
}
coll.write((byte) ch);
}
break; // Processed single header
}
len *= 10;
len += ch - '0';
}
if (ch == -1) { // EOF
break;
}
}
return headers;
}
private void applyPaxHeadersToCurrentEntry(Map<String, String> headers) {
/*
* The following headers are defined for Pax.
* atime, ctime, charset: cannot use these without changing TarArchiveEntry fields
* mtime
* comment
* gid, gname
* linkpath
* size
* uid,uname
* SCHILY.devminor, SCHILY.devmajor: don't have setters/getters for those
*/
for (Entry<String, String> ent : headers.entrySet()) {
String key = ent.getKey();
String val = ent.getValue();
if ("path".equals(key)) {
entry.setName(val);
} else if ("linkpath".equals(key)) {
entry.setLinkName(val);
} else if ("gid".equals(key)) {
entry.setGroupId(Integer.parseInt(val));
} else if ("gname".equals(key)) {
entry.setGroupName(val);
} else if ("uid".equals(key)) {
entry.setUserId(Integer.parseInt(val));
} else if ("uname".equals(key)) {
entry.setUserName(val);
} else if ("size".equals(key)) {
entry.setEntrySize(Long.parseLong(val));
} else if ("mtime".equals(key)) {
long mtime = (long) (Double.parseDouble(val) * 1000);
entry.setLastModified(new Date(mtime));
} else if ("SCHILY.devminor".equals(key)) {
entry.setDevMinor(Integer.parseInt(val));
} else if ("SCHILY.devmajor".equals(key)) {
entry.setDevMajor(Integer.parseInt(val));
}
}
}
@Override
public TarArchiveEntry getNextEntry() throws IOException {
return getNextTarEntry();
}
/**
* Reads bytes from the current tar archive entry.
* This method is aware of the boundaries of the current
* entry in the archive and will deal with them as if they
* were this stream's start and EOF.
*
* @param buf The buffer into which to place bytes read.
* @param offset The offset at which to place bytes read.
* @param numToRead The number of bytes to read.
* @return The number of bytes read, or -1 at EOF
* @throws IOException on error
*/
@Override
public int read(byte[] buf, int offset, int numToRead) throws IOException {
int totalRead = 0;
if (entryOffset >= entrySize) {
return -1;
}
if ((numToRead + entryOffset) > entrySize) {
numToRead = (int) (entrySize - entryOffset);
}
if (readBuf != null) {
int sz = (numToRead > readBuf.length) ? readBuf.length : numToRead;
System.arraycopy(readBuf, 0, buf, offset, sz);
if (sz >= readBuf.length) {
readBuf = null;
} else {
int newLen = readBuf.length - sz;
byte[] newBuf = new byte[newLen];
System.arraycopy(readBuf, sz, newBuf, 0, newLen);
readBuf = newBuf;
}
totalRead += sz;
numToRead -= sz;
offset += sz;
}
while (numToRead > 0) {
byte[] rec = readRecord();
if (rec == null) {
throw new IOException("unexpected EOF with " + numToRead + " bytes unread");
}
int sz = numToRead;
int recLen = rec.length;
if (recLen > sz) {
System.arraycopy(rec, 0, buf, offset, sz);
readBuf = new byte[recLen - sz];
System.arraycopy(rec, sz, readBuf, 0, recLen - sz);
} else {
sz = recLen;
System.arraycopy(rec, 0, buf, offset, recLen);
}
totalRead += sz;
numToRead -= sz;
offset += sz;
}
entryOffset += totalRead;
return totalRead;
}
}

View file

@ -0,0 +1,859 @@
package org.xbib.io.archive.tar;
import org.xbib.io.archive.entry.ArchiveEntry;
import org.xbib.io.archive.entry.ArchiveEntryEncoding;
import org.xbib.io.archive.util.ArchiveUtils;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Date;
/**
* This class represents an entry in a Tar archive for output
*/
public class TarArchiveOutputEntry implements TarConstants, ArchiveEntry {
private static final int BYTE_MASK = 255;
/**
* Maximum length of a user's name in the tar file
*/
public static final int MAX_NAMELEN = 31;
/**
* Default permissions bits for directories
*/
public static final int DEFAULT_DIR_MODE = 040755;
/**
* Default permissions bits for files
*/
public static final int DEFAULT_FILE_MODE = 0100644;
/**
* Convert millis to seconds
*/
public static final int MILLIS_PER_SECOND = 1000;
/**
* The entry's name.
*/
private String name;
/**
* The entry's permission mode.
*/
private int mode;
/**
* The entry's user id.
*/
private int userId;
/**
* The entry's group id.
*/
private int groupId;
/**
* The entry's size.
*/
private long size;
/**
* The entry's modification time.
*/
private long modTime;
/**
* The entry's link flag.
*/
private byte linkFlag;
/**
* The entry's link name.
*/
private String linkName;
/**
* The entry's magic tag.
*/
private String magic;
/**
* The version of the format
*/
private String version;
/**
* The entry's user name.
*/
private String userName;
/**
* The entry's group name.
*/
private String groupName;
/**
* The entry's major device number.
*/
private int devMajor;
/**
* The entry's minor device number.
*/
private int devMinor;
/**
* If an extension sparse header follows.
*/
private boolean isExtended;
/**
* The entry's real size in case of a sparse file.
*/
private long realSize;
/**
* The entry's file reference
*/
private File file;
/**
* Construct an empty entry and prepares the header values.
*/
public TarArchiveOutputEntry() {
this.magic = MAGIC_POSIX;
this.version = VERSION_POSIX;
this.name = "";
this.linkName = "";
String user = System.getProperty("user.name", "");
if (user.length() > MAX_NAMELEN) {
user = user.substring(0, MAX_NAMELEN);
}
this.userId = 0;
this.groupId = 0;
this.userName = user;
this.groupName = "";
this.file = null;
this.mode = DEFAULT_FILE_MODE;
}
/**
* Construct an entry with only a name. This allows the programmer
* to construct the entry's header "by hand". File is set to null.
*
* @param name the entry name
*/
public TarArchiveOutputEntry(String name) {
this(name, false);
}
/**
* Construct an entry with only a name. This allows the programmer
* to construct the entry's header "by hand". File is set to null.
*
* @param name the entry name
* @param preserveLeadingSlashes whether to allow leading slashes
* in the name.
*/
public TarArchiveOutputEntry(String name, boolean preserveLeadingSlashes) {
this();
name = ArchiveUtils.normalizeFileName(name, preserveLeadingSlashes);
this.name = name;
boolean isDir = name.endsWith("/");
this.mode = isDir ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE;
this.linkFlag = isDir ? LF_DIR : LF_NORMAL;
this.devMajor = 0;
this.devMinor = 0;
this.userId = 0;
this.groupId = 0;
this.size = 0;
this.modTime = (new Date()).getTime() / MILLIS_PER_SECOND;
this.linkName = "";
this.userName = "";
this.groupName = "";
}
/**
* Construct an entry with a name and a link flag.
*
* @param name the entry name
* @param linkFlag the entry link flag.
*/
public TarArchiveOutputEntry(String name, byte linkFlag) {
this(name);
this.linkFlag = linkFlag;
if (linkFlag == LF_GNUTYPE_LONGNAME) {
magic = MAGIC_GNU;
version = VERSION_GNU_SPACE;
}
}
/**
* Construct an entry for a file. File is set to file, and the
* header is constructed from information from the file.
* The name is set from the normalized file path.
*
* @param file The file that the entry represents.
*/
public TarArchiveOutputEntry(File file) {
this(file, ArchiveUtils.normalizeFileName(file.getPath(), false));
}
/**
* Construct an entry for a file. File is set to file, and the
* header is constructed from information from the file.
*
* @param file The file that the entry represents.
* @param fileName the name to be used for the entry.
*/
public TarArchiveOutputEntry(File file, String fileName) {
this();
this.file = file;
this.linkName = "";
if (file.isDirectory()) {
this.mode = DEFAULT_DIR_MODE;
this.linkFlag = LF_DIR;
int nameLength = fileName.length();
if (nameLength == 0 || fileName.charAt(nameLength - 1) != '/') {
this.name = fileName + "/";
} else {
this.name = fileName;
}
this.size = 0;
} else {
this.mode = DEFAULT_FILE_MODE;
this.linkFlag = LF_NORMAL;
this.size = file.length();
this.name = fileName;
}
this.modTime = file.lastModified() / MILLIS_PER_SECOND;
this.devMajor = 0;
this.devMinor = 0;
}
/**
* Determine if the two entries are equal. Equality is determined
* by the header names being equal.
*
* @param it Entry to be checked for equality.
* @return True if the entries are equal.
*/
public boolean equals(TarArchiveOutputEntry it) {
return getName().equals(it.getName());
}
/**
* Determine if the two entries are equal. Equality is determined
* by the header names being equal.
*
* @param it Entry to be checked for equality.
* @return True if the entries are equal.
*/
@Override
public boolean equals(Object it) {
return !(it == null || getClass() != it.getClass()) && equals((TarArchiveOutputEntry) it);
}
/**
* Hashcodes are based on entry names.
*
* @return the entry hashcode
*/
@Override
public int hashCode() {
return getName().hashCode();
}
/**
* Determine if the given entry is a descendant of this entry.
* Descendancy is determined by the name of the descendant
* starting with this entry's name.
*
* @param desc Entry to be checked as a descendent of this.
* @return True if entry is a descendant of this.
*/
public boolean isDescendent(TarArchiveOutputEntry desc) {
return desc.getName().startsWith(getName());
}
/**
* Get this entry's name.
*
* @return This entry's name.
*/
public String getName() {
return name;
}
/**
* Set this entry's name.
*
* @param name This entry's new name.
*/
public TarArchiveOutputEntry setName(String name) {
this.name = ArchiveUtils.normalizeFileName(name, false);
boolean isDir = name.endsWith("/");
this.mode = isDir ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE;
this.linkFlag = isDir ? LF_DIR : LF_NORMAL;
return this;
}
/**
* Set the mode for this entry
*
* @param mode the mode for this entry
*/
public void setMode(int mode) {
this.mode = mode;
}
/**
* Get this entry's link name.
*
* @return This entry's link name.
*/
public String getLinkName() {
return linkName;
}
/**
* Set this entry's link name.
*
* @param link the link name to use.
*/
public void setLinkName(String link) {
this.linkName = link;
}
/**
* Get this entry's user id.
*
* @return This entry's user id.
*/
public int getUserId() {
return userId;
}
/**
* Set this entry's user id.
*
* @param userId This entry's new user id.
*/
public void setUserId(int userId) {
this.userId = userId;
}
/**
* Get this entry's group id.
*
* @return This entry's group id.
*/
public int getGroupId() {
return groupId;
}
/**
* Set this entry's group id.
*
* @param groupId This entry's new group id.
*/
public void setGroupId(int groupId) {
this.groupId = groupId;
}
/**
* Get this entry's user name.
*
* @return This entry's user name.
*/
public String getUserName() {
return userName;
}
/**
* Set this entry's user name.
*
* @param userName This entry's new user name.
*/
public void setUserName(String userName) {
this.userName = userName;
}
/**
* Get this entry's group name.
*
* @return This entry's group name.
*/
public String getGroupName() {
return groupName;
}
/**
* Set this entry's group name.
*
* @param groupName This entry's new group name.
*/
public void setGroupName(String groupName) {
this.groupName = groupName;
}
/**
* Convenience method to set this entry's group and user ids.
*
* @param userId This entry's new user id.
* @param groupId This entry's new group id.
*/
public void setIds(int userId, int groupId) {
setUserId(userId);
setGroupId(groupId);
}
/**
* Convenience method to set this entry's group and user names.
*
* @param userName This entry's new user name.
* @param groupName This entry's new group name.
*/
public void setNames(String userName, String groupName) {
setUserName(userName);
setGroupName(groupName);
}
/**
* Set this entry's modification time. The parameter passed
* to this method is in "Java time".
*
* @param date This entry's new modification time.
*/
public TarArchiveOutputEntry setLastModified(Date date) {
modTime = date.getTime() / MILLIS_PER_SECOND;
return this;
}
public Date getLastModified() {
return new Date(modTime * MILLIS_PER_SECOND);
}
/**
* Get this entry's file.
*
* @return This entry's file.
*/
public File getFile() {
return file;
}
/**
* Get this entry's mode.
*
* @return This entry's mode.
*/
public int getMode() {
return mode;
}
/**
* Get this entry's file size.
*
* @return This entry's file size.
*/
public long getEntrySize() {
return size;
}
/**
* Set this entry's file size.
*
* @param size This entry's new file size.
* @throws IllegalArgumentException if the size is &lt; 0.
*/
public TarArchiveOutputEntry setEntrySize(long size) {
if (size < 0) {
throw new IllegalArgumentException("Size is out of range: " + size);
}
this.size = size;
return this;
}
/**
* Get this entry's major device number.
*
* @return This entry's major device number.
*/
public int getDevMajor() {
return devMajor;
}
/**
* Set this entry's major device number.
*
* @param devNo This entry's major device number.
* @throws IllegalArgumentException if the devNo is &lt; 0.
*/
public void setDevMajor(int devNo) {
if (devNo < 0) {
throw new IllegalArgumentException("Major device number is out of " + "range: " + devNo);
}
this.devMajor = devNo;
}
/**
* Get this entry's minor device number.
*
* @return This entry's minor device number.
*/
public int getDevMinor() {
return devMinor;
}
/**
* Set this entry's minor device number.
*
* @param devNo This entry's minor device number.
* @throws IllegalArgumentException if the devNo is &lt; 0.
*/
public void setDevMinor(int devNo) {
if (devNo < 0) {
throw new IllegalArgumentException("Minor device number is out of "
+ "range: " + devNo);
}
this.devMinor = devNo;
}
/**
* Indicates in case of a sparse file if an extension sparse header
* follows.
*
* @return true if an extension sparse header follows.
*/
public boolean isExtended() {
return isExtended;
}
/**
* Get this entry's real file size in case of a sparse file.
*
* @return This entry's real file size.
*/
public long getRealSize() {
return realSize;
}
/**
* Indicate if this entry is a GNU sparse block
*
* @return true if this is a sparse extension provided by GNU tar
*/
public boolean isGNUSparse() {
return linkFlag == LF_GNUTYPE_SPARSE;
}
/**
* Indicate if this entry is a GNU long name block
*
* @return true if this is a long name extension provided by GNU tar
*/
public boolean isGNULongNameEntry() {
return linkFlag == LF_GNUTYPE_LONGNAME
&& name.equals(GNU_LONGLINK);
}
/**
* Check if this is a Pax header.
*
* @return {@code true} if this is a Pax header.
*/
public boolean isPaxHeader() {
return linkFlag == LF_PAX_EXTENDED_HEADER_LC
|| linkFlag == LF_PAX_EXTENDED_HEADER_UC;
}
/**
* Check if this is a Pax header.
*
* @return {@code true} if this is a Pax header.
*/
public boolean isGlobalPaxHeader() {
return linkFlag == LF_PAX_GLOBAL_EXTENDED_HEADER;
}
/**
* Return whether or not this entry represents a directory.
*
* @return True if this entry is a directory.
*/
public boolean isDirectory() {
if (file != null) {
return file.isDirectory();
}
return linkFlag == LF_DIR || getName().endsWith("/");
}
/**
* Check if this is a "normal file"
*/
public boolean isFile() {
if (file != null) {
return file.isFile();
}
return linkFlag == LF_OLDNORM || linkFlag == LF_NORMAL || !getName().endsWith("/");
}
/**
* Check if this is a symbolic link entry.
*/
public boolean isSymbolicLink() {
return linkFlag == LF_SYMLINK;
}
/**
* Check if this is a link entry.
*/
public boolean isLink() {
return linkFlag == LF_LINK;
}
/**
* Check if this is a character device entry.
*/
public boolean isCharacterDevice() {
return linkFlag == LF_CHR;
}
/**
* Check if this is a block device entry.
*/
public boolean isBlockDevice() {
return linkFlag == LF_BLK;
}
/**
* Check if this is a FIFO (pipe) entry.
*/
public boolean isFIFO() {
return linkFlag == LF_FIFO;
}
/**
* If this entry represents a file, and the file is a directory, return
* an array of TarEntries for this entry's children.
*
* @return An array of TarEntry's for this entry's children.
*/
public TarArchiveOutputEntry[] getDirectoryEntries() {
if (file == null || !file.isDirectory()) {
return new TarArchiveOutputEntry[0];
}
String[] list = file.list();
TarArchiveOutputEntry[] result = new TarArchiveOutputEntry[list.length];
for (int i = 0; i < list.length; ++i) {
result[i] = new TarArchiveOutputEntry(new File(file, list[i]));
}
return result;
}
/**
* Write an entry's header information to a header buffer.
*
* @param outbuf The tar entry header buffer to fill in.
* @param encoding encoding to use when writing the file name.
* @param starMode whether to use the star/GNU tar/BSD tar
* extension for numeric fields if their value doesn't fit in the
* maximum size of standard tar archives
*/
public void writeEntryHeader(byte[] outbuf, ArchiveEntryEncoding encoding, boolean starMode) throws IOException {
int offset = 0;
offset = ArchiveUtils.formatNameBytes(name, outbuf, offset, NAMELEN, encoding);
offset = writeEntryHeaderField(mode, outbuf, offset, MODELEN, starMode);
offset = writeEntryHeaderField(userId, outbuf, offset, UIDLEN, starMode);
offset = writeEntryHeaderField(groupId, outbuf, offset, GIDLEN, starMode);
offset = writeEntryHeaderField(size, outbuf, offset, SIZELEN, starMode);
offset = writeEntryHeaderField(modTime, outbuf, offset, MODTIMELEN, starMode);
int csOffset = offset;
for (int c = 0; c < CHKSUMLEN; ++c) {
outbuf[offset++] = (byte) ' ';
}
outbuf[offset++] = linkFlag;
offset = ArchiveUtils.formatNameBytes(linkName, outbuf, offset, NAMELEN, encoding);
offset = ArchiveUtils.formatNameBytes(magic, outbuf, offset, MAGICLEN);
offset = ArchiveUtils.formatNameBytes(version, outbuf, offset, VERSIONLEN);
offset = ArchiveUtils.formatNameBytes(userName, outbuf, offset, UNAMELEN, encoding);
offset = ArchiveUtils.formatNameBytes(groupName, outbuf, offset, GNAMELEN, encoding);
offset = writeEntryHeaderField(devMajor, outbuf, offset, DEVLEN, starMode);
offset = writeEntryHeaderField(devMinor, outbuf, offset, DEVLEN, starMode);
while (offset < outbuf.length) {
outbuf[offset++] = 0;
}
long chk = computeCheckSum(outbuf);
formatCheckSumOctalBytes(chk, outbuf, csOffset, CHKSUMLEN);
}
private int writeEntryHeaderField(long value, byte[] outbuf, int offset, int length, boolean starMode) {
if (!starMode && (value < 0
|| value >= (1l << (3 * (length - 1))))) {
// value doesn't fit into field when written as octal
// number, will be written to PAX header or causes an
// error
return formatLongOctalBytes(0, outbuf, offset, length);
}
return formatLongOctalOrBinaryBytes(value, outbuf, offset, length);
}
/**
* Fill buffer with unsigned octal number, padded with leading zeroes.
*
* @param value number to convert to octal - treated as unsigned
* @param buffer destination buffer
* @param offset starting offset in buffer
* @param length length of buffer to fill
* @throws IllegalArgumentException if the value will not fit in the buffer
*/
private void formatUnsignedOctalString(final long value, byte[] buffer, final int offset, final int length) {
int remaining = length;
remaining--;
if (value == 0) {
buffer[offset + remaining--] = (byte) '0';
} else {
long val = value;
for (; remaining >= 0 && val != 0; --remaining) {
buffer[offset + remaining] = (byte) ((byte) '0' + (byte) (val & 7));
val = val >>> 3;
}
if (val != 0) {
throw new IllegalArgumentException(value + "=" + Long.toOctalString(value) + " will not fit in octal number buffer of length " + length);
}
}
for (; remaining >= 0; --remaining) { // leading zeros
buffer[offset + remaining] = (byte) '0';
}
}
/**
* Write an octal long integer into a buffer.
* <p/>
* Uses {@link #formatUnsignedOctalString} to format
* the value as an octal string with leading zeros.
* The converted number is followed by a space.
*
* @param value The value to write as octal
* @param buf The destinationbuffer.
* @param offset The starting offset into the buffer.
* @param length The length of the buffer
* @return The updated offset
* @throws IllegalArgumentException if the value (and trailer) will not fit in the buffer
*/
private int formatLongOctalBytes(final long value, byte[] buf, final int offset, final int length) {
int idx = length - 1; // For space
formatUnsignedOctalString(value, buf, offset, idx);
buf[offset + idx] = (byte) ' '; // Trailing space
return offset + length;
}
/**
* Write an long integer into a buffer as an octal string if this
* will fit, or as a binary number otherwise.
* <p/>
* Uses {@link #formatUnsignedOctalString} to format
* the value as an octal string with leading zeros.
* The converted number is followed by a space.
*
* @param value The value to write into the buffer.
* @param buf The destination buffer.
* @param offset The starting offset into the buffer.
* @param length The length of the buffer.
* @return The updated offset.
* @throws IllegalArgumentException if the value (and trailer)
* will not fit in the buffer.
*/
private int formatLongOctalOrBinaryBytes(final long value, byte[] buf, final int offset, final int length) {
// Check whether we are dealing with UID/GID or SIZE field
final long maxAsOctalChar = length == UIDLEN ? MAXID : MAXSIZE;
final boolean negative = value < 0;
if (!negative && value <= maxAsOctalChar) { // OK to store as octal chars
return formatLongOctalBytes(value, buf, offset, length);
}
if (length < 9) {
formatLongBinary(value, buf, offset, length, negative);
}
formatBigIntegerBinary(value, buf, offset, length, negative);
buf[offset] = (byte) (negative ? 0xff : 0x80);
return offset + length;
}
private void formatLongBinary(final long value, byte[] buf, final int offset, final int length, final boolean negative) {
final int bits = (length - 1) * 8;
final long max = 1l << bits;
long val = Math.abs(value);
if (val >= max) {
throw new IllegalArgumentException("Value " + value +
" is too large for " + length + " byte field.");
}
if (negative) {
val ^= max - 1;
val |= 0xff << bits;
val++;
}
for (int i = offset + length - 1; i >= offset; i--) {
buf[i] = (byte) val;
val >>= 8;
}
}
private void formatBigIntegerBinary(final long value, byte[] buf,
final int offset,
final int length,
final boolean negative) {
BigInteger val = BigInteger.valueOf(value);
final byte[] b = val.toByteArray();
final int len = b.length;
final int off = offset + length - len;
System.arraycopy(b, 0, buf, off, len);
final byte fill = (byte) (negative ? 0xff : 0);
for (int i = offset + 1; i < off; i++) {
buf[i] = fill;
}
}
/**
* Writes an octal value into a buffer.
* <p/>
* Uses {@link #formatUnsignedOctalString} to format
* the value as an octal string with leading zeros.
* The converted number is followed by NUL and then space.
*
* @param value The value to convert
* @param buf The destination buffer
* @param offset The starting offset into the buffer.
* @param length The size of the buffer.
* @return The updated value of offset, i.e. offset+length
* @throws IllegalArgumentException if the value (and trailer) will not fit in the buffer
*/
private int formatCheckSumOctalBytes(final long value, byte[] buf, final int offset, final int length) {
int idx = length - 2;
formatUnsignedOctalString(value, buf, offset, idx);
buf[offset + idx++] = 0;
buf[offset + idx] = (byte) ' ';
return offset + length;
}
/**
* Compute the checksum of a tar entry header.
*
* @param buf The tar entry's header buffer.
* @return The computed checksum.
*/
private long computeCheckSum(final byte[] buf) {
long sum = 0;
for (byte aBuf : buf) {
sum += BYTE_MASK & aBuf;
}
return sum;
}
}

View file

@ -0,0 +1,573 @@
package org.xbib.io.archive.tar;
import org.xbib.io.archive.stream.ArchiveOutputStream;
import org.xbib.io.archive.entry.ArchiveEntryEncoding;
import org.xbib.io.archive.entry.ArchiveEntryEncodingHelper;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* The TarOutputStream writes a UNIX tar archive as an output stream
*/
public class TarArchiveOutputStream extends ArchiveOutputStream<TarArchiveOutputEntry> implements TarConstants {
private static final ArchiveEntryEncoding ASCII = ArchiveEntryEncodingHelper.getEncoding("ASCII");
private final ArchiveEntryEncoding encoding;
private final OutputStream outStream;
private final int blockSize;
private final int recordSize;
private final int recsPerBlock;
private final byte[] blockBuffer;
private final byte[] recordBuf;
private final byte[] assemBuf;
private int currRecIdx;
private long currSize;
private String currName;
private long currBytes;
private int assemLen;
private int longFileMode = LONGFILE_GNU;
private int bigNumberMode = BIGNUMBER_ERROR;
private boolean closed = false;
/**
* Indicates if putArchiveEntry has been called without closeArchiveEntry
*/
private boolean haveUnclosedEntry = false;
/**
* indicates if this archive is finished
*/
private boolean finished = false;
private boolean addPaxHeadersForNonAsciiNames = false;
/**
* Constructor for TarInputStream.
*
* @param os the output stream to use
*/
public TarArchiveOutputStream(OutputStream os) {
this(os, DEFAULT_BLOCK_SIZE, DEFAULT_RECORD_SIZE);
}
/**
* Constructor for TarInputStream.
*
* @param os the output stream to use
* @param encoding name of the encoding to use for file names
*/
public TarArchiveOutputStream(OutputStream os, String encoding) {
this(os, DEFAULT_BLOCK_SIZE, DEFAULT_RECORD_SIZE, encoding);
}
/**
* Constructor for TarInputStream.
*
* @param os the output stream to use
* @param blockSize the block size to use
*/
public TarArchiveOutputStream(OutputStream os, int blockSize) {
this(os, blockSize, DEFAULT_RECORD_SIZE);
}
/**
* Constructor for TarInputStream.
*
* @param os the output stream to use
* @param blockSize the block size to use
* @param encoding name of the encoding to use for file names
*/
public TarArchiveOutputStream(OutputStream os, int blockSize, String encoding) {
this(os, blockSize, DEFAULT_RECORD_SIZE, encoding);
}
/**
* Constructor for TarInputStream.
*
* @param os the output stream to use
* @param blockSize the block size to use
* @param recordSize the record size to use
*/
public TarArchiveOutputStream(OutputStream os, int blockSize, int recordSize) {
this(os, blockSize, recordSize, null);
}
/**
* Constructor for TarInputStream.
*
* @param os the output stream to use
* @param blockSize the block size to use
* @param recordSize the record size to use
* @param encoding name of the encoding to use for file names
*/
public TarArchiveOutputStream(OutputStream os, int blockSize, int recordSize, String encoding) {
this.encoding = ArchiveEntryEncodingHelper.getEncoding(encoding);
this.assemLen = 0;
this.assemBuf = new byte[recordSize];
this.recordBuf = new byte[recordSize];
this.outStream = os;
this.blockSize = blockSize;
this.recordSize = recordSize;
this.recsPerBlock = (this.blockSize / this.recordSize);
this.blockBuffer = new byte[this.blockSize];
this.currRecIdx = 0;
}
/**
* Set the long file mode.
* This can be LONGFILE_ERROR(0), LONGFILE_TRUNCATE(1) or LONGFILE_GNU(2).
* This specifies the treatment of long file names (names &gt;= NAMELEN).
* Default is LONGFILE_GNU.
*
* @param longFileMode the mode to use
*/
public void setLongFileMode(int longFileMode) {
this.longFileMode = longFileMode;
}
/**
* Set the big number mode.
* This can be BIGNUMBER_ERROR(0), BIGNUMBER_POSIX(1) or BIGNUMBER_STAR(2).
* This specifies the treatment of big files (sizes &gt; MAXSIZE) and other numeric values to big to fit into a traditional tar header.
* Default is BIGNUMBER_ERROR.
*
* @param bigNumberMode the mode to use
*/
public void setBigNumberMode(int bigNumberMode) {
this.bigNumberMode = bigNumberMode;
}
/**
* Whether to add a PAX extension header for non-ASCII file names.
*/
public void setAddPaxHeadersForNonAsciiNames(boolean b) {
addPaxHeadersForNonAsciiNames = b;
}
/**
* Ends the TAR archive without closing the underlying OutputStream.
* An archive consists of a series of file entries terminated by an
* end-of-archive entry, which consists of two 512 blocks of zero bytes.
* POSIX.1 requires two EOF records, like some other implementations.
*
* @throws IOException on error
*/
@Override
public void finish() throws IOException {
if (finished) {
throw new IOException("This archive has already been finished");
}
if (haveUnclosedEntry) {
throw new IOException("This archives contains unclosed entries.");
}
writeEOFRecord();
writeEOFRecord();
flushBlock();
finished = true;
}
/**
* Closes the underlying OutputStream.
*
* @throws IOException on error
*/
@Override
public void close() throws IOException {
if (!finished) {
finish();
}
if (!closed) {
if (outStream != null) {
flushBlock();
if (outStream != System.out && outStream != System.err) {
outStream.close();
}
}
closed = true;
}
}
private void flushBlock() throws IOException {
if (outStream == null) {
throw new IOException("writing to an input buffer");
}
if (currRecIdx > 0) {
writeBlock();
}
}
private void writeBlock() throws IOException {
if (outStream == null) {
throw new IOException("writing to an input buffer");
}
outStream.write(blockBuffer, 0, blockSize);
outStream.flush();
currRecIdx = 0;
Arrays.fill(blockBuffer, (byte) 0);
}
/**
* Get the record size being used by this stream's TarBuffer.
*
* @return The TarBuffer record size.
*/
public int getRecordSize() {
return recordSize;
}
@Override
public TarArchiveOutputEntry newArchiveEntry() {
return new TarArchiveOutputEntry();
}
/**
* Put an entry on the output stream. This writes the entry's
* header record and positions the output stream for writing
* the contents of the entry. Once this method is called, the
* stream is ready for calls to write() to write the entry's
* contents. Once the contents are written, closeArchiveEntry()
* <B>MUST</B> be called to ensure that all buffered data
* is completely written to the output stream.
*
* @param archiveEntry The TarEntry to be written to the archive.
* @throws IOException on error
* @throws ClassCastException if archiveEntry is not an instance of TarArchiveEntry
*/
@Override
public void putArchiveEntry(TarArchiveOutputEntry archiveEntry) throws IOException {
if (finished) {
throw new IOException("Stream has already been finished");
}
Map<String, String> paxHeaders = new HashMap<String, String>();
final String entryName = archiveEntry.getName();
final byte[] nameBytes = encoding.encode(entryName).array();
boolean paxHeaderContainsPath = false;
if (nameBytes.length >= NAMELEN) {
if (longFileMode == LONGFILE_POSIX) {
paxHeaders.put("path", entryName);
paxHeaderContainsPath = true;
} else if (longFileMode == LONGFILE_GNU) {
// create a TarEntry for the LongLink, the contents
// of which are the entry's name
TarArchiveOutputEntry longLinkEntry = new TarArchiveOutputEntry(GNU_LONGLINK, LF_GNUTYPE_LONGNAME);
longLinkEntry.setEntrySize(nameBytes.length + 1); // +1 for NUL
putArchiveEntry(longLinkEntry);
write(nameBytes);
write(0); // NUL terminator
closeArchiveEntry();
} else if (longFileMode != LONGFILE_TRUNCATE) {
throw new RuntimeException("file name '" + entryName
+ "' is too long ( > "
+ NAMELEN + " bytes)");
}
}
if (bigNumberMode == BIGNUMBER_POSIX) {
addPaxHeadersForBigNumbers(paxHeaders, archiveEntry);
} else if (bigNumberMode != BIGNUMBER_STAR) {
failForBigNumbers(archiveEntry);
}
if (addPaxHeadersForNonAsciiNames && !paxHeaderContainsPath
&& !ASCII.canEncode(entryName)) {
paxHeaders.put("path", entryName);
}
if (addPaxHeadersForNonAsciiNames
&& (archiveEntry.isLink() || archiveEntry.isSymbolicLink())
&& !ASCII.canEncode(archiveEntry.getLinkName())) {
paxHeaders.put("linkpath", archiveEntry.getLinkName());
}
if (paxHeaders.size() > 0) {
writePaxHeaders(entryName, paxHeaders);
}
archiveEntry.writeEntryHeader(recordBuf, encoding, bigNumberMode == BIGNUMBER_STAR);
writeRecord(recordBuf);
currBytes = 0;
if (archiveEntry.isDirectory()) {
currSize = 0;
} else {
currSize = archiveEntry.getEntrySize();
}
currName = entryName;
haveUnclosedEntry = true;
}
/**
* Close an entry. This method MUST be called for all file
* entries that contain data. The reason is that we must
* buffer data written to the stream in order to satisfy
* the buffer's record based writes. Thus, there may be
* data fragments still being assembled that must be written
* to the output stream before this entry is closed and the
* next entry written.
*
* @throws IOException on error
*/
@Override
public void closeArchiveEntry() throws IOException {
if (finished) {
throw new IOException("stream has already been finished");
}
if (!haveUnclosedEntry) {
throw new IOException("no current entry to close");
}
if (assemLen > 0) {
for (int i = assemLen; i < assemBuf.length; ++i) {
assemBuf[i] = 0;
}
writeRecord(assemBuf);
currBytes += assemLen;
assemLen = 0;
}
if (currBytes < currSize) {
throw new IOException("entry '" + currName + "' closed at '"
+ currBytes
+ "' before the '" + currSize
+ "' bytes specified in the header were written");
}
haveUnclosedEntry = false;
}
/**
* Writes bytes to the current tar archive entry. This method
* is aware of the current entry and will throw an exception if
* you attempt to write bytes past the length specified for the
* current entry. The method is also (painfully) aware of the
* record buffering required by TarBuffer, and manages buffers
* that are not a multiple of recordsize in length, including
* assembling records from small buffers.
*
* @param wBuf The buffer to write to the archive.
* @param wOffset The offset in the buffer from which to get bytes.
* @param numToWrite The number of bytes to write.
* @throws IOException on error
*/
@Override
public void write(byte[] wBuf, int wOffset, int numToWrite) throws IOException {
if ((currBytes + numToWrite) > currSize) {
throw new IOException("request to write '" + numToWrite
+ "' bytes exceeds size in header of '"
+ currSize + "' bytes for entry '"
+ currName + "'");
//
// We have to deal with assembly!!!
// The programmer can be writing little 32 byte chunks for all
// we know, and we must assemble complete records for writing.
// REVIEW Maybe this should be in TarBuffer? Could that help to
// eliminate some of the buffer copying.
//
}
if (assemLen > 0) {
if ((assemLen + numToWrite) >= recordBuf.length) {
int aLen = recordBuf.length - assemLen;
System.arraycopy(assemBuf, 0, recordBuf, 0,
assemLen);
System.arraycopy(wBuf, wOffset, recordBuf,
assemLen, aLen);
writeRecord(recordBuf);
currBytes += recordBuf.length;
wOffset += aLen;
numToWrite -= aLen;
assemLen = 0;
} else {
System.arraycopy(wBuf, wOffset, assemBuf, assemLen,
numToWrite);
wOffset += numToWrite;
assemLen += numToWrite;
numToWrite = 0;
}
}
//
// When we get here we have EITHER:
// o An empty "assemble" buffer.
// o No bytes to write (numToWrite == 0)
//
while (numToWrite > 0) {
if (numToWrite < recordBuf.length) {
System.arraycopy(wBuf, wOffset, assemBuf, assemLen,
numToWrite);
assemLen += numToWrite;
break;
}
writeRecord(wBuf, wOffset);
int num = recordBuf.length;
currBytes += num;
numToWrite -= num;
wOffset += num;
}
}
/**
* Writes a PAX extended header with the given map as contents.
*/
void writePaxHeaders(String entryName, Map<String, String> headers) throws IOException {
String name = "./PaxHeaders.X/" + stripTo7Bits(entryName);
if (name.length() >= NAMELEN) {
name = name.substring(0, NAMELEN - 1);
}
TarArchiveOutputEntry pex = new TarArchiveOutputEntry(name, LF_PAX_EXTENDED_HEADER_LC);
StringWriter w = new StringWriter();
for (Map.Entry<String, String> h : headers.entrySet()) {
String key = h.getKey();
String value = h.getValue();
int len = key.length() + value.length()
+ 3 /* blank, equals and newline */
+ 2 /* guess 9 < actual length < 100 */;
String line = len + " " + key + "=" + value + "\n";
int actualLength = line.getBytes(Charset.forName("UTF-8")).length;
while (len != actualLength) {
// Adjust for cases where length < 10 or > 100
// or where UTF-8 encoding isn't a single octet
// per character.
// Must be in loop as size may go from 99 to 100 in
// first pass so we'd need a second.
len = actualLength;
line = len + " " + key + "=" + value + "\n";
actualLength = line.getBytes(Charset.forName("UTF-8")).length;
}
w.write(line);
}
byte[] data = w.toString().getBytes(Charset.forName("UTF-8"));
pex.setEntrySize(data.length);
putArchiveEntry(pex);
write(data);
closeArchiveEntry();
}
private String stripTo7Bits(String name) {
final int length = name.length();
StringBuilder result = new StringBuilder(length);
for (int i = 0; i < length; i++) {
char stripped = (char) (name.charAt(i) & 0x7F);
if (stripped != 0) { // would be read as Trailing null
result.append(stripped);
}
}
return result.toString();
}
/**
* Write an EOF (end of archive) record to the tar archive.
* An EOF record consists of a record of all zeros.
*/
private void writeEOFRecord() throws IOException {
for (int i = 0; i < recordBuf.length; ++i) {
recordBuf[i] = 0;
}
writeRecord(recordBuf);
}
@Override
public void flush() throws IOException {
outStream.flush();
}
private void addPaxHeadersForBigNumbers(Map<String, String> paxHeaders,
TarArchiveOutputEntry entry) {
addPaxHeaderForBigNumber(paxHeaders, "size", entry.getEntrySize(), MAXSIZE);
addPaxHeaderForBigNumber(paxHeaders, "gid", entry.getGroupId(), MAXID);
addPaxHeaderForBigNumber(paxHeaders, "mtime", entry.getLastModified().getTime() / 1000, MAXSIZE);
addPaxHeaderForBigNumber(paxHeaders, "uid", entry.getUserId(), MAXID);
// star extensions by J\u00f6rg Schilling
addPaxHeaderForBigNumber(paxHeaders, "SCHILY.devmajor", entry.getDevMajor(), MAXID);
addPaxHeaderForBigNumber(paxHeaders, "SCHILY.devminor", entry.getDevMinor(), MAXID);
// there is no PAX header for file mode
failForBigNumber("mode", entry.getMode(), MAXID);
}
private void addPaxHeaderForBigNumber(Map<String, String> paxHeaders,
String header, long value,
long maxValue) {
if (value < 0 || value > maxValue) {
paxHeaders.put(header, String.valueOf(value));
}
}
private void failForBigNumbers(TarArchiveOutputEntry entry) {
failForBigNumber("entry size", entry.getEntrySize(), MAXSIZE);
failForBigNumber("group id", entry.getGroupId(), MAXID);
failForBigNumber("last modification time", entry.getLastModified().getTime() / 1000, MAXSIZE);
failForBigNumber("user id", entry.getUserId(), MAXID);
failForBigNumber("mode", entry.getMode(), MAXID);
failForBigNumber("major device number", entry.getDevMajor(), MAXID);
failForBigNumber("minor device number", entry.getDevMinor(), MAXID);
}
private void failForBigNumber(String field, long value, long maxValue) {
if (value < 0 || value > maxValue) {
throw new RuntimeException(field + " '" + value
+ "' is too big ( > "
+ maxValue + " )");
}
}
private void writeRecord(byte[] record) throws IOException {
if (outStream == null) {
throw new IOException("Output buffer is closed");
}
if (record.length != recordSize) {
throw new IOException("record to write has length '"
+ record.length
+ "' which is not the record size of '"
+ recordSize + "'");
}
if (currRecIdx >= recsPerBlock) {
writeBlock();
}
System.arraycopy(record, 0, blockBuffer, (currRecIdx * recordSize), recordSize);
currRecIdx++;
}
private void writeRecord(byte[] buf, int offset) throws IOException {
if (outStream == null) {
throw new IOException("Output buffer is closed");
}
if ((offset + recordSize) > buf.length) {
throw new IOException("record has length '" + buf.length
+ "' with offset '" + offset
+ "' which is less than the record size of '"
+ recordSize + "'");
}
if (currRecIdx >= recsPerBlock) {
writeBlock();
}
System.arraycopy(buf, offset, blockBuffer, (currRecIdx * recordSize), recordSize);
currRecIdx++;
}
}

View file

@ -0,0 +1,299 @@
package org.xbib.io.archive.tar;
/**
* This interface contains all the definitions used in the package.
* For tar formats (FORMAT_OLDGNU, FORMAT_POSIX, etc.) see GNU tar
* <I>tar.h</I> type <I>enum archive_format</I>
*/
public interface TarConstants {
int GNU_FORMAT = 0;
int USTAR_FORMAT = 1;
int UNIX_FORMAT = 2;
/**
* Pure Posix format.
*/
int POSIX_FORMAT = 3;
/**
* The length of the name field in a header buffer.
*/
int NAMELEN = 100;
/**
* The length of the mode field in a header buffer.
*/
int MODELEN = 8;
/**
* The length of the user id field in a header buffer.
*/
int UIDLEN = 8;
/**
* The length of the group id field in a header buffer.
*/
int GIDLEN = 8;
/**
* The maximum value of gid/uid in a tar archive which can
* be expressed in octal char notation (that's 7 sevens, octal).
*/
long MAXID = 07777777L;
/**
* The length of the checksum field in a header buffer.
*/
int CHKSUMLEN = 8;
/**
* The length of the size field in a header buffer.
* Includes the trailing space or NUL.
*/
int SIZELEN = 12;
/**
* The maximum size of a file in a tar archive
* which can be expressed in octal char notation (that's 11 sevens, octal).
*/
long MAXSIZE = 077777777777L;
/**
* Offset of start of magic field within header record
*/
int MAGIC_OFFSET = 257;
/**
* The length of the magic field in a header buffer.
*/
int MAGICLEN = 6;
/**
* Offset of start of magic field within header record
*/
int VERSION_OFFSET = 263;
/**
* Previously this was regarded as part of "magic" field, but it is separate.
*/
int VERSIONLEN = 2;
/**
* The length of the modification time field in a header buffer.
*/
int MODTIMELEN = 12;
/**
* The length of the user name field in a header buffer.
*/
int UNAMELEN = 32;
/**
* The length of the group name field in a header buffer.
*/
int GNAMELEN = 32;
/**
* The length of each of the device fields (major and minor) in a header buffer.
*/
int DEVLEN = 8;
/**
* Length of the prefix field.
*/
int PREFIXLEN = 155;
/**
* The length of the access time field in an old GNU header buffer.
*/
int ATIMELEN_GNU = 12;
/**
* The length of the created time field in an old GNU header buffer.
*/
int CTIMELEN_GNU = 12;
/**
* The length of the multivolume start offset field in an old GNU header buffer.
*/
int OFFSETLEN_GNU = 12;
/**
* The length of the long names field in an old GNU header buffer.
*/
int LONGNAMESLEN_GNU = 4;
/**
* The length of the padding field in an old GNU header buffer.
*/
int PAD2LEN_GNU = 1;
/**
* The sum of the length of all sparse headers in an old GNU header buffer.
*/
int SPARSELEN_GNU = 96;
/**
* The length of the is extension field in an old GNU header buffer.
*/
int ISEXTENDEDLEN_GNU = 1;
/**
* The length of the real size field in an old GNU header buffer.
*/
int REALSIZELEN_GNU = 12;
/**
* LF_ constants represent the "link flag" of an entry, or more commonly,
* the "entry type". This is the "old way" of indicating a normal file.
*/
byte LF_OLDNORM = 0;
/**
* Normal file type.
*/
byte LF_NORMAL = (byte) '0';
/**
* Link file type.
*/
byte LF_LINK = (byte) '1';
/**
* Symbolic link file type.
*/
byte LF_SYMLINK = (byte) '2';
/**
* Character device file type.
*/
byte LF_CHR = (byte) '3';
/**
* Block device file type.
*/
byte LF_BLK = (byte) '4';
/**
* Directory file type.
*/
byte LF_DIR = (byte) '5';
/**
* FIFO (pipe) file type.
*/
byte LF_FIFO = (byte) '6';
/**
* Contiguous file type.
*/
byte LF_CONTIG = (byte) '7';
/**
* Identifies the *next* file on the tape as having a long name.
*/
byte LF_GNUTYPE_LONGNAME = (byte) 'L';
/**
* Sparse file type.
*/
byte LF_GNUTYPE_SPARSE = (byte) 'S';
// See "http://www.opengroup.org/onlinepubs/009695399/utilities/pax.html#tag_04_100_13_02"
/**
* Identifies the entry as a Pax extended header.
*/
byte LF_PAX_EXTENDED_HEADER_LC = (byte) 'x';
/**
* Identifies the entry as a Pax extended header (SunOS tar -E).
*/
byte LF_PAX_EXTENDED_HEADER_UC = (byte) 'X';
/**
* Identifies the entry as a Pax global extended header.
*/
byte LF_PAX_GLOBAL_EXTENDED_HEADER = (byte) 'g';
String MAGIC_UNIX = "\0\0\0\0\0";
/**
* The magic tag representing a POSIX tar archive.
*/
String MAGIC_POSIX = "ustar\0";
String VERSION_POSIX = "00";
/**
* The magic tag representing a GNU tar archive.
*/
String MAGIC_GNU = "ustar ";
// Appear to be two possible GNU versions
String VERSION_GNU_SPACE = " \0";
String VERSION_GNU_ZERO = "0\0";
/**
* The magic tag representing an Ant tar archive.
*/
String MAGIC_ANT = "ustar\0";
/**
* The "version" representing an Ant tar archive.
*/
// Does not appear to have a version, however Ant does write 8 bytes,
// so assume the version is 2 nulls
String VERSION_ANT = "\0\0";
/**
* The name of the GNU tar entry which contains a long name.
*/
String GNU_LONGLINK = "././@LongLink";
/**
* Fail if a long file name is required in the archive.
*/
int LONGFILE_ERROR = 0;
/**
* Long paths will be truncated in the archive.
*/
int LONGFILE_TRUNCATE = 1;
/**
* GNU tar extensions are used to store long file names in the archive.
*/
int LONGFILE_GNU = 2;
/**
* POSIX/PAX extensions are used to store long file names in the archive.
*/
int LONGFILE_POSIX = 3;
/**
* Fail if a big number (e.g. size &gt; 8GiB) is required in the archive.
*/
int BIGNUMBER_ERROR = 0;
/**
* star/GNU tar/BSD tar extensions are used to store big number in the archive.
*/
int BIGNUMBER_STAR = 1;
/**
* POSIX/PAX extensions are used to store big numbers in the archive.
*/
int BIGNUMBER_POSIX = 2;
/**
* Default record size
*/
int DEFAULT_RECORD_SIZE = 512;
/**
* Default block size
*/
int DEFAULT_BLOCK_SIZE = DEFAULT_RECORD_SIZE * 20;
int SMALL_BUFFER_SIZE = 256;
}

View file

@ -0,0 +1,26 @@
package org.xbib.io.archive.tar;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.IOException;
import java.io.InputStream;
import org.junit.jupiter.api.Test;
public class TarTest {
@Test
public void testTar() throws IOException {
InputStream in = getClass().getResourceAsStream("test.tar");
TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(in);
byte[] buffer = new byte[1024];
long total = 0L;
while (tarArchiveInputStream.getNextEntry() != null) {
int len = 0;
while ((len = tarArchiveInputStream.read(buffer)) > 0) {
total += len;
}
}
assertEquals(1889L, total);
tarArchiveInputStream.close();
}
}

View file

@ -0,0 +1,3 @@
dependencies {
api project(':io-archive')
}

View file

@ -0,0 +1,4 @@
module org.xbib.io.archive.zip {
exports org.xbib.io.archive.zip;
requires org.xbib.io.archive;
}

View file

@ -0,0 +1,165 @@
package org.xbib.io.archive.zip;
import java.nio.charset.Charset;
import java.util.zip.CRC32;
import java.util.zip.ZipException;
/**
* A common base class for Unicode extra information extra fields.
*/
public abstract class AbstractUnicodeExtraField implements ZipExtraField {
private long nameCRC32;
private byte[] unicodeName;
private byte[] data;
protected AbstractUnicodeExtraField() {
}
/**
* Assemble as unicode extension from the name/comment and
* encoding of the orginal zip entry.
*
* @param text The file name or comment.
* @param bytes The encoded of the filename or comment in the zip
* file.
* @param off The offset of the encoded filename or comment in
* <code>bytes</code>.
* @param len The length of the encoded filename or commentin
* <code>bytes</code>.
*/
protected AbstractUnicodeExtraField(String text, byte[] bytes, int off, int len) {
CRC32 crc32 = new CRC32();
crc32.update(bytes, off, len);
nameCRC32 = crc32.getValue();
unicodeName = text.getBytes(Charset.forName("UTF-8"));
}
/**
* Assemble as unicode extension from the name/comment and
* encoding of the orginal zip entry.
*
* @param text The file name or comment.
* @param bytes The encoded of the filename or comment in the zip
* file.
*/
protected AbstractUnicodeExtraField(String text, byte[] bytes) {
this(text, bytes, 0, bytes.length);
}
private void assembleData() {
if (unicodeName == null) {
return;
}
data = new byte[5 + unicodeName.length];
// version 1
data[0] = 0x01;
System.arraycopy(ZipLong.getBytes(nameCRC32), 0, data, 1, 4);
System.arraycopy(unicodeName, 0, data, 5, unicodeName.length);
}
/**
* @return The CRC32 checksum of the filename or comment as
* encoded in the central directory of the zip file.
*/
public long getNameCRC32() {
return nameCRC32;
}
/**
* @param nameCRC32 The CRC32 checksum of the filename as encoded
* in the central directory of the zip file to set.
*/
public void setNameCRC32(long nameCRC32) {
this.nameCRC32 = nameCRC32;
data = null;
}
/**
* @return The utf-8 encoded name.
*/
public byte[] getUnicodeName() {
byte[] b = null;
if (unicodeName != null) {
b = new byte[unicodeName.length];
System.arraycopy(unicodeName, 0, b, 0, b.length);
}
return b;
}
/**
* @param unicodeName The utf-8 encoded name to set.
*/
public void setUnicodeName(byte[] unicodeName) {
if (unicodeName != null) {
this.unicodeName = new byte[unicodeName.length];
System.arraycopy(unicodeName, 0, this.unicodeName, 0,
unicodeName.length);
} else {
this.unicodeName = null;
}
data = null;
}
public byte[] getCentralDirectoryData() {
if (data == null) {
this.assembleData();
}
byte[] b = null;
if (data != null) {
b = new byte[data.length];
System.arraycopy(data, 0, b, 0, b.length);
}
return b;
}
public ZipShort getCentralDirectoryLength() {
if (data == null) {
assembleData();
}
return new ZipShort(data.length);
}
public byte[] getLocalFileDataData() {
return getCentralDirectoryData();
}
public ZipShort getLocalFileDataLength() {
return getCentralDirectoryLength();
}
public void parseFromLocalFileData(byte[] buffer, int offset, int length)
throws ZipException {
if (length < 5) {
throw new ZipException("UniCode path extra data must have at least 5 bytes.");
}
int version = buffer[offset];
if (version != 0x01) {
throw new ZipException("Unsupported version [" + version
+ "] for UniCode path extra data.");
}
nameCRC32 = ZipLong.getValue(buffer, offset + 1);
unicodeName = new byte[length - 5];
System.arraycopy(buffer, offset + 5, unicodeName, 0, length - 5);
data = null;
}
/**
* Doesn't do anything special since this class always uses the
* same data in central directory and local file data.
*/
public void parseFromCentralDirectoryData(byte[] buffer, int offset,
int length)
throws ZipException {
parseFromLocalFileData(buffer, offset, length);
}
}

View file

@ -0,0 +1,318 @@
package org.xbib.io.archive.zip;
import java.util.zip.CRC32;
import java.util.zip.ZipException;
/**
* Adds Unix file permission and UID/GID fields as well as symbolic
* link handling.
* This class uses the ASi extra field in the format:
* <pre>
* Value Size Description
* ----- ---- -----------
* (Unix3) 0x756e Short tag for this extra block type
* TSize Short total data size for this block
* CRC Long CRC-32 of the remaining data
* Mode Short file permissions
* SizDev Long symlink'd size OR major/minor dev num
* UID Short user ID
* GID Short group ID
* (var.) variable symbolic link filename
* </pre>
* taken from appnote.iz (Info-ZIP note, 981119) found at
* <a href="ftp://ftp.uu.net/pub/archiving/zip/doc/">ftp://ftp.uu.net/pub/archiving/zip/doc/</a>
* Short is two bytes and Long is four bytes in big endian byte and
* word order, device numbers are currently not supported.
* Since the documentation this class is based upon doesn't mention
* the character encoding of the file name at all, it is assumed that
* it uses the current platform's default encoding.
*/
public class AsiExtraField implements ZipExtraField, UnixStat, Cloneable {
private static final ZipShort HEADER_ID = new ZipShort(0x756E);
private static final int WORD = 4;
/**
* Standard Unix stat(2) file mode.
*/
private int mode = 0;
/**
* User ID.
*/
private int uid = 0;
/**
* Group ID.
*/
private int gid = 0;
/**
* File this entry points to, if it is a symbolic link.
* <p/>
* <p>empty string - if entry is not a symbolic link.</p>
*/
private String link = "";
/**
* Is this an entry for a directory?
*/
private boolean dirFlag = false;
/**
* Instance used to calculate checksums.
*/
private CRC32 crc = new CRC32();
/**
* Constructor for AsiExtraField.
*/
public AsiExtraField() {
}
/**
* The Header-ID.
*
* @return the value for the header id for this extrafield
*/
public ZipShort getHeaderId() {
return HEADER_ID;
}
/**
* Length of the extra field in the local file data - without
* Header-ID or length specifier.
*
* @return a <code>ZipShort</code> for the length of the data of this extra field
*/
public ZipShort getLocalFileDataLength() {
return new ZipShort(WORD // CRC
+ 2 // Mode
+ WORD // SizDev
+ 2 // UID
+ 2 // GID
+ getLinkedFile().getBytes().length);
// Uses default charset - see class Javadoc
}
/**
* Delegate to local file data.
*
* @return the centralDirectory length
*/
public ZipShort getCentralDirectoryLength() {
return getLocalFileDataLength();
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return get the data
*/
public byte[] getLocalFileDataData() {
// CRC will be added later
byte[] data = new byte[getLocalFileDataLength().getValue() - WORD];
System.arraycopy(ZipShort.getBytes(getMode()), 0, data, 0, 2);
byte[] linkArray = getLinkedFile().getBytes(); // Uses default charset - see class Javadoc
// CheckStyle:MagicNumber OFF
System.arraycopy(ZipLong.getBytes(linkArray.length),
0, data, 2, WORD);
System.arraycopy(ZipShort.getBytes(getUserId()),
0, data, 6, 2);
System.arraycopy(ZipShort.getBytes(getGroupId()),
0, data, 8, 2);
System.arraycopy(linkArray, 0, data, 10, linkArray.length);
crc.reset();
crc.update(data);
long checksum = crc.getValue();
byte[] result = new byte[data.length + WORD];
System.arraycopy(ZipLong.getBytes(checksum), 0, result, 0, WORD);
System.arraycopy(data, 0, result, WORD, data.length);
return result;
}
/**
* Delegate to local file data.
*
* @return the local file data
*/
public byte[] getCentralDirectoryData() {
return getLocalFileDataData();
}
/**
* Set the user id.
*
* @param uid the user id
*/
public void setUserId(int uid) {
this.uid = uid;
}
/**
* Get the user id.
*
* @return the user id
*/
public int getUserId() {
return uid;
}
/**
* Set the group id.
*
* @param gid the group id
*/
public void setGroupId(int gid) {
this.gid = gid;
}
/**
* Get the group id.
*
* @return the group id
*/
public int getGroupId() {
return gid;
}
/**
* Indicate that this entry is a symbolic link to the given filename.
*
* @param name Name of the file this entry links to, empty String
* if it is not a symbolic link.
*/
public void setLinkedFile(String name) {
link = name;
mode = getMode(mode);
}
/**
* Name of linked file
*
* @return name of the file this entry links to if it is a
* symbolic link, the empty string otherwise.
*/
public String getLinkedFile() {
return link;
}
/**
* Is this entry a symbolic link?
*
* @return true if this is a symbolic link
*/
public boolean isLink() {
return getLinkedFile().length() != 0;
}
/**
* File mode of this file.
*
* @param mode the file mode
*/
public void setMode(int mode) {
this.mode = getMode(mode);
}
/**
* File mode of this file.
*
* @return the file mode
*/
public int getMode() {
return mode;
}
/**
* Indicate whether this entry is a directory.
*
* @param dirFlag if true, this entry is a directory
*/
public void setDirectory(boolean dirFlag) {
this.dirFlag = dirFlag;
mode = getMode(mode);
}
/**
* Is this entry a directory?
*
* @return true if this entry is a directory
*/
public boolean isDirectory() {
return dirFlag && !isLink();
}
/**
* Populate data from this array as if it was in local file data.
*
* @param data an array of bytes
* @param offset the start offset
* @param length the number of bytes in the array from offset
* @throws java.util.zip.ZipException on error
*/
public void parseFromLocalFileData(byte[] data, int offset, int length)
throws ZipException {
long givenChecksum = ZipLong.getValue(data, offset);
byte[] tmp = new byte[length - WORD];
System.arraycopy(data, offset + WORD, tmp, 0, length - WORD);
crc.reset();
crc.update(tmp);
long realChecksum = crc.getValue();
if (givenChecksum != realChecksum) {
throw new ZipException("bad CRC checksum "
+ Long.toHexString(givenChecksum)
+ " instead of "
+ Long.toHexString(realChecksum));
}
int newMode = ZipShort.getValue(tmp, 0);
byte[] linkArray = new byte[(int) ZipLong.getValue(tmp, 2)];
uid = ZipShort.getValue(tmp, 6);
gid = ZipShort.getValue(tmp, 8);
if (linkArray.length == 0) {
link = "";
} else {
System.arraycopy(tmp, 10, linkArray, 0, linkArray.length);
link = new String(linkArray); // Uses default charset - see class Javadoc
}
setDirectory((newMode & DIR_FLAG) != 0);
setMode(newMode);
}
/**
* Doesn't do anything special since this class always uses the
* same data in central directory and local file data.
*/
public void parseFromCentralDirectoryData(byte[] buffer, int offset,
int length)
throws ZipException {
parseFromLocalFileData(buffer, offset, length);
}
/**
* Get the file mode for given permissions with the correct file type.
*
* @param mode the mode
* @return the type with the mode
*/
protected int getMode(int mode) {
int type = FILE_FLAG;
if (isLink()) {
type = LINK_FLAG;
} else if (isDirectory()) {
type = DIR_FLAG;
}
return type | (mode & PERM_MASK);
}
@Override
public Object clone() throws CloneNotSupportedException {
AsiExtraField cloned = (AsiExtraField) super.clone();
cloned.crc = new CRC32();
return cloned;
}
}

View file

@ -0,0 +1,290 @@
package org.xbib.io.archive.zip;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipException;
/**
* ZipExtraField related methods
*/
public class ExtraFieldUtils {
private static final int WORD = 4;
/**
* Static registry of known extra fields.
*/
private static final Map<ZipShort, Class<?>> implementations;
static {
implementations = new HashMap<ZipShort, Class<?>>();
register(AsiExtraField.class);
register(JarMarker.class);
register(UnicodePathExtraField.class);
register(UnicodeCommentExtraField.class);
register(Zip64ExtendedInformationExtraField.class);
}
/**
* Register a ZipExtraField implementation.
* The given class must have a no-arg constructor and implement
* the {@link ZipExtraField ZipExtraField interface}.
*
* @param c the class to register
*/
public static void register(Class<?> c) {
try {
ZipExtraField ze = (ZipExtraField) c.newInstance();
implementations.put(ze.getHeaderId(), c);
} catch (ClassCastException cc) {
throw new RuntimeException(c + " doesn\'t implement ZipExtraField");
} catch (InstantiationException ie) {
throw new RuntimeException(c + " is not a concrete class");
} catch (IllegalAccessException ie) {
throw new RuntimeException(c + "\'s no-arg constructor is not public");
}
}
/**
* Create an instance of the approriate ExtraField, falls back to
* {@link UnrecognizedExtraField UnrecognizedExtraField}.
*
* @param headerId the header identifier
* @return an instance of the appropiate ExtraField
* @throws InstantiationException if unable to instantiate the class
* @throws IllegalAccessException if not allowed to instatiate the class
*/
public static ZipExtraField createExtraField(ZipShort headerId)
throws InstantiationException, IllegalAccessException {
Class<?> c = implementations.get(headerId);
if (c != null) {
return (ZipExtraField) c.newInstance();
}
UnrecognizedExtraField u = new UnrecognizedExtraField();
u.setHeaderId(headerId);
return u;
}
/**
* Split the array into ExtraFields and populate them with the
* given data as local file data, throwing an exception if the
* data cannot be parsed.
*
* @param data an array of bytes as it appears in local file data
* @return an array of ExtraFields
* @throws java.util.zip.ZipException on error
*/
public static ZipExtraField[] parse(byte[] data) throws ZipException {
return parse(data, true, UnparseableExtraField.THROW);
}
/**
* Split the array into ExtraFields and populate them with the
* given data, throwing an exception if the data cannot be parsed.
*
* @param data an array of bytes
* @param local whether data originates from the local file data
* or the central directory
* @return an array of ExtraFields
* @throws java.util.zip.ZipException on error
*/
public static ZipExtraField[] parse(byte[] data, boolean local)
throws ZipException {
return parse(data, local, UnparseableExtraField.THROW);
}
/**
* Split the array into ExtraFields and populate them with the
* given data.
*
* @param data an array of bytes
* @param local whether data originates from the local file data
* or the central directory
* @param onUnparseableData what to do if the extra field data
* cannot be parsed.
* @return an array of ExtraFields
* @throws java.util.zip.ZipException on error
*/
public static ZipExtraField[] parse(byte[] data, boolean local,
UnparseableExtraField onUnparseableData)
throws ZipException {
List<ZipExtraField> v = new ArrayList<ZipExtraField>();
int start = 0;
LOOP:
while (start <= data.length - WORD) {
ZipShort headerId = new ZipShort(data, start);
int length = (new ZipShort(data, start + 2)).getValue();
if (start + WORD + length > data.length) {
switch (onUnparseableData.getKey()) {
case UnparseableExtraField.THROW_KEY:
throw new ZipException("bad extra field starting at "
+ start + ". Block length of "
+ length + " bytes exceeds remaining"
+ " data of "
+ (data.length - start - WORD)
+ " bytes.");
case UnparseableExtraField.READ_KEY:
UnparseableExtraFieldData field =
new UnparseableExtraFieldData();
if (local) {
field.parseFromLocalFileData(data, start,
data.length - start);
} else {
field.parseFromCentralDirectoryData(data, start,
data.length - start);
}
v.add(field);
//$FALL-THROUGH$
case UnparseableExtraField.SKIP_KEY:
// since we cannot parse the data we must assume
// the extra field consumes the whole rest of the
// available data
break LOOP;
default:
throw new ZipException("unknown UnparseableExtraField key: "
+ onUnparseableData.getKey());
}
}
try {
ZipExtraField ze = createExtraField(headerId);
if (local) {
ze.parseFromLocalFileData(data, start + WORD, length);
} else {
ze.parseFromCentralDirectoryData(data, start + WORD,
length);
}
v.add(ze);
} catch (InstantiationException | IllegalAccessException ie) {
throw new ZipException(ie.getMessage());
}
start += (length + WORD);
}
ZipExtraField[] result = new ZipExtraField[v.size()];
return v.toArray(result);
}
/**
* Merges the local file data fields of the given ZipExtraFields.
*
* @param data an array of ExtraFiles
* @return an array of bytes
*/
public static byte[] mergeLocalFileDataData(ZipExtraField[] data) {
final boolean lastIsUnparseableHolder = data.length > 0
&& data[data.length - 1] instanceof UnparseableExtraFieldData;
int regularExtraFieldCount =
lastIsUnparseableHolder ? data.length - 1 : data.length;
int sum = WORD * regularExtraFieldCount;
for (ZipExtraField element : data) {
sum += element.getLocalFileDataLength().getValue();
}
byte[] result = new byte[sum];
int start = 0;
for (int i = 0; i < regularExtraFieldCount; i++) {
System.arraycopy(data[i].getHeaderId().getBytes(),
0, result, start, 2);
System.arraycopy(data[i].getLocalFileDataLength().getBytes(),
0, result, start + 2, 2);
byte[] local = data[i].getLocalFileDataData();
System.arraycopy(local, 0, result, start + WORD, local.length);
start += (local.length + WORD);
}
if (lastIsUnparseableHolder) {
byte[] local = data[data.length - 1].getLocalFileDataData();
System.arraycopy(local, 0, result, start, local.length);
}
return result;
}
/**
* Merges the central directory fields of the given ZipExtraFields.
*
* @param data an array of ExtraFields
* @return an array of bytes
*/
public static byte[] mergeCentralDirectoryData(ZipExtraField[] data) {
final boolean lastIsUnparseableHolder = data.length > 0
&& data[data.length - 1] instanceof UnparseableExtraFieldData;
int regularExtraFieldCount =
lastIsUnparseableHolder ? data.length - 1 : data.length;
int sum = WORD * regularExtraFieldCount;
for (ZipExtraField element : data) {
sum += element.getCentralDirectoryLength().getValue();
}
byte[] result = new byte[sum];
int start = 0;
for (int i = 0; i < regularExtraFieldCount; i++) {
System.arraycopy(data[i].getHeaderId().getBytes(),
0, result, start, 2);
System.arraycopy(data[i].getCentralDirectoryLength().getBytes(),
0, result, start + 2, 2);
byte[] local = data[i].getCentralDirectoryData();
System.arraycopy(local, 0, result, start + WORD, local.length);
start += (local.length + WORD);
}
if (lastIsUnparseableHolder) {
byte[] local = data[data.length - 1].getCentralDirectoryData();
System.arraycopy(local, 0, result, start, local.length);
}
return result;
}
/**
* "enum" for the possible actions to take if the extra field
* cannot be parsed.
*/
public static final class UnparseableExtraField {
/**
* Key for "throw an exception" action.
*/
public static final int THROW_KEY = 0;
/**
* Key for "skip" action.
*/
public static final int SKIP_KEY = 1;
/**
* Key for "read" action.
*/
public static final int READ_KEY = 2;
/**
* Throw an exception if field cannot be parsed.
*/
public static final UnparseableExtraField THROW
= new UnparseableExtraField(THROW_KEY);
/**
* Skip the extra field entirely and don't make its data
* available - effectively removing the extra field data.
*/
public static final UnparseableExtraField SKIP
= new UnparseableExtraField(SKIP_KEY);
/**
* Read the extra field data into an instance of {@link
* UnparseableExtraFieldData UnparseableExtraFieldData}.
*/
public static final UnparseableExtraField READ
= new UnparseableExtraField(READ_KEY);
private final int key;
private UnparseableExtraField(int k) {
key = k;
}
/**
* Key of the action to take.
*/
public int getKey() {
return key;
}
}
}

View file

@ -0,0 +1,152 @@
package org.xbib.io.archive.zip;
/**
* Parser/encoder for the "general purpose bit" field in ZIP's local
* file and central directory headers.
*/
public final class GeneralPurposeBit {
/**
* Indicates that the file is encrypted.
*/
private static final int ENCRYPTION_FLAG = 1;
/**
* Indicates that a data descriptor stored after the file contents
* will hold CRC and size information.
*/
private static final int DATA_DESCRIPTOR_FLAG = 1 << 3;
/**
* Indicates strong encryption.
*/
private static final int STRONG_ENCRYPTION_FLAG = 1 << 6;
/**
* Indicates that filenames are written in utf-8.
*/
protected static final int UFT8_NAMES_FLAG = 1 << 11;
private boolean languageEncodingFlag = false;
private boolean dataDescriptorFlag = false;
private boolean encryptionFlag = false;
private boolean strongEncryptionFlag = false;
public GeneralPurposeBit() {
}
/**
* whether the current entry uses UTF8 for file name and comment.
*/
public boolean usesUTF8ForNames() {
return languageEncodingFlag;
}
/**
* whether the current entry will use UTF8 for file name and comment.
*/
public void useUTF8ForNames(boolean b) {
languageEncodingFlag = b;
}
/**
* whether the current entry uses the data descriptor to store CRC
* and size information
*/
public boolean usesDataDescriptor() {
return dataDescriptorFlag;
}
/**
* whether the current entry will use the data descriptor to store
* CRC and size information
*/
public void useDataDescriptor(boolean b) {
dataDescriptorFlag = b;
}
/**
* whether the current entry is encrypted
*/
public boolean usesEncryption() {
return encryptionFlag;
}
/**
* whether the current entry will be encrypted
*/
public void useEncryption(boolean b) {
encryptionFlag = b;
}
/**
* whether the current entry is encrypted using strong encryption
*/
public boolean usesStrongEncryption() {
return encryptionFlag && strongEncryptionFlag;
}
/**
* whether the current entry will be encrypted using strong encryption
*/
public void useStrongEncryption(boolean b) {
strongEncryptionFlag = b;
if (b) {
useEncryption(true);
}
}
/**
* Encodes the set bits in a form suitable for ZIP archives.
*/
public byte[] encode() {
return
ZipShort.getBytes((dataDescriptorFlag ? DATA_DESCRIPTOR_FLAG : 0)
|
(languageEncodingFlag ? UFT8_NAMES_FLAG : 0)
|
(encryptionFlag ? ENCRYPTION_FLAG : 0)
|
(strongEncryptionFlag ? STRONG_ENCRYPTION_FLAG : 0)
);
}
/**
* Parses the supported flags from the given archive data.
*
* @param data local file header or a central directory entry.
* @param offset offset at which the general purpose bit starts
*/
public static GeneralPurposeBit parse(final byte[] data, final int offset) {
final int generalPurposeFlag = ZipShort.getValue(data, offset);
GeneralPurposeBit b = new GeneralPurposeBit();
b.useDataDescriptor((generalPurposeFlag & DATA_DESCRIPTOR_FLAG) != 0);
b.useUTF8ForNames((generalPurposeFlag & UFT8_NAMES_FLAG) != 0);
b.useStrongEncryption((generalPurposeFlag & STRONG_ENCRYPTION_FLAG)
!= 0);
b.useEncryption((generalPurposeFlag & ENCRYPTION_FLAG) != 0);
return b;
}
@Override
public int hashCode() {
return 3 * (7 * (13 * (17 * (encryptionFlag ? 1 : 0)
+ (strongEncryptionFlag ? 1 : 0))
+ (languageEncodingFlag ? 1 : 0))
+ (dataDescriptorFlag ? 1 : 0));
}
@Override
public boolean equals(Object o) {
if (!(o instanceof GeneralPurposeBit)) {
return false;
}
GeneralPurposeBit g = (GeneralPurposeBit) o;
return g.encryptionFlag == encryptionFlag
&& g.strongEncryptionFlag == strongEncryptionFlag
&& g.languageEncodingFlag == languageEncodingFlag
&& g.dataDescriptorFlag == dataDescriptorFlag;
}
}

View file

@ -0,0 +1,102 @@
package org.xbib.io.archive.zip;
import java.util.zip.ZipException;
/**
* If this extra field is added as the very first extra field of the
* archive, Solaris will consider it an executable jar file.
*/
public final class JarMarker implements ZipExtraField {
private static final ZipShort ID = new ZipShort(0xCAFE);
private static final ZipShort NULL = new ZipShort(0);
private static final byte[] NO_BYTES = new byte[0];
private static final JarMarker DEFAULT = new JarMarker();
public JarMarker() {
}
/**
* Since JarMarker is stateless we can always use the same instance.
*
* @return the DEFAULT jarmaker.
*/
public static JarMarker getInstance() {
return DEFAULT;
}
/**
* The Header-ID.
*
* @return the header id
*/
public ZipShort getHeaderId() {
return ID;
}
/**
* Length of the extra field in the local file data - without
* Header-ID or length specifier.
*
* @return 0
*/
public ZipShort getLocalFileDataLength() {
return NULL;
}
/**
* Length of the extra field in the central directory - without
* Header-ID or length specifier.
*
* @return 0
*/
public ZipShort getCentralDirectoryLength() {
return NULL;
}
/**
* The actual data to put into local file data - without Header-ID
* or length specifier.
*
* @return the data
*/
public byte[] getLocalFileDataData() {
return NO_BYTES;
}
/**
* The actual data to put central directory - without Header-ID or
* length specifier.
*
* @return the data
*/
public byte[] getCentralDirectoryData() {
return NO_BYTES;
}
/**
* Populate data from this array as if it was in local file data.
*
* @param data an array of bytes
* @param offset the start offset
* @param length the number of bytes in the array from offset
* @throws java.util.zip.ZipException on error
*/
public void parseFromLocalFileData(byte[] data, int offset, int length)
throws ZipException {
if (length != 0) {
throw new ZipException("JarMarker doesn't expect any data");
}
}
/**
* Doesn't do anything special since this class always uses the
* same data in central directory and local file data.
*/
public void parseFromCentralDirectoryData(byte[] buffer, int offset,
int length)
throws ZipException {
parseFromLocalFileData(buffer, offset, length);
}
}

View file

@ -0,0 +1,42 @@
package org.xbib.io.archive.zip;
/**
* Info-ZIP Unicode Comment Extra Field (0x6375):
* Stores the UTF-8 version of the file comment as stored in the
* central directory header.
* <pre>
* Value Size Description
* ----- ---- -----------
* (UCom) 0x6375 Short tag for this extra block type ("uc")
* TSize Short total data size for this block
* Version 1 byte version of this extra field, currently 1
* ComCRC32 4 bytes Comment Field CRC32 Checksum
* UnicodeCom Variable UTF-8 version of the entry comment
* </pre>
*/
public class UnicodeCommentExtraField extends AbstractUnicodeExtraField {
public static final ZipShort UCOM_ID = new ZipShort(0x6375);
public UnicodeCommentExtraField() {
}
/**
* Assemble as unicode comment extension from the name given as
* text as well as the encoded bytes actually written to the archive.
*
* @param text The file name
* @param bytes the bytes actually written to the archive
* @param off The offset of the encoded comment in <code>bytes</code>.
* @param len The length of the encoded comment or comment in
* <code>bytes</code>.
*/
public UnicodeCommentExtraField(String text, byte[] bytes, int off, int len) {
super(text, bytes, off, len);
}
public ZipShort getHeaderId() {
return UCOM_ID;
}
}

View file

@ -0,0 +1,52 @@
package org.xbib.io.archive.zip;
/**
* Info-ZIP Unicode Path Extra Field (0x7075):
* Stores the UTF-8 version of the file name field as stored in the
* local header and central directory header.
* <pre>
* Value Size Description
* ----- ---- -----------
* (UPath) 0x7075 Short tag for this extra block type ("up")
* TSize Short total data size for this block
* Version 1 byte version of this extra field, currently 1
* NameCRC32 4 bytes File Name Field CRC32 Checksum
* UnicodeName Variable UTF-8 version of the entry File Name
* </pre>
*/
public class UnicodePathExtraField extends AbstractUnicodeExtraField {
public static final ZipShort UPATH_ID = new ZipShort(0x7075);
public UnicodePathExtraField() {
}
/**
* Assemble as unicode path extension from the name given as
* text as well as the encoded bytes actually written to the archive.
*
* @param text The file name
* @param bytes the bytes actually written to the archive
* @param off The offset of the encoded filename in <code>bytes</code>.
* @param len The length of the encoded filename or comment in
* <code>bytes</code>.
*/
public UnicodePathExtraField(String text, byte[] bytes, int off, int len) {
super(text, bytes, off, len);
}
/**
* Assemble as unicode path extension from the name given as
* text as well as the encoded bytes actually written to the archive.
*
* @param name The file name
* @param bytes the bytes actually written to the archive
*/
public UnicodePathExtraField(String name, byte[] bytes) {
super(name, bytes);
}
public ZipShort getHeaderId() {
return UPATH_ID;
}
}

View file

@ -0,0 +1,25 @@
package org.xbib.io.archive.zip;
/**
* Constants from stat.h on Unix systems.
*/
public interface UnixStat {
/**
* Bits used for permissions (and sticky bit)
*/
int PERM_MASK = 07777;
/**
* Indicates symbolic links.
*/
int LINK_FLAG = 0120000;
/**
* Indicates plain files.
*/
int FILE_FLAG = 0100000;
/**
* Indicates directories.
*/
int DIR_FLAG = 040000;
}

View file

@ -0,0 +1,91 @@
package org.xbib.io.archive.zip;
/**
* Wrapper for extra field data that doesn't conform to the recommended format of header-tag + size + data.
* The header-id is artificial (and not listed as a known ID in
* <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">APPNOTE.TXT</a>
* Since it isn't used anywhere except to satisfy the
* ZipExtraField contract it shouldn't matter anyway.
*/
public final class UnparseableExtraFieldData implements ZipExtraField {
private static final ZipShort HEADER_ID = new ZipShort(0xACC1);
private byte[] localFileData;
private byte[] centralDirectoryData;
/**
* The Header-ID.
*
* @return a completely arbitrary value that should be ignored.
*/
public ZipShort getHeaderId() {
return HEADER_ID;
}
/**
* Length of the complete extra field in the local file data.
*
* @return The LocalFileDataLength value
*/
public ZipShort getLocalFileDataLength() {
return new ZipShort(localFileData == null ? 0 : localFileData.length);
}
/**
* Length of the complete extra field in the central directory.
*
* @return The CentralDirectoryLength value
*/
public ZipShort getCentralDirectoryLength() {
return centralDirectoryData == null
? getLocalFileDataLength()
: new ZipShort(centralDirectoryData.length);
}
/**
* The actual data to put into local file data.
*
* @return The LocalFileDataData value
*/
public byte[] getLocalFileDataData() {
return ZipUtil.copy(localFileData);
}
/**
* The actual data to put into central directory.
*
* @return The CentralDirectoryData value
*/
public byte[] getCentralDirectoryData() {
return centralDirectoryData == null
? getLocalFileDataData() : ZipUtil.copy(centralDirectoryData);
}
/**
* Populate data from this array as if it was in local file data.
*
* @param buffer the buffer to read data from
* @param offset offset into buffer to read data
* @param length the length of data
*/
public void parseFromLocalFileData(byte[] buffer, int offset, int length) {
localFileData = new byte[length];
System.arraycopy(buffer, offset, localFileData, 0, length);
}
/**
* Populate data from this array as if it was in central directory data.
*
* @param buffer the buffer to read data from
* @param offset offset into buffer to read data
* @param length the length of data
*/
public void parseFromCentralDirectoryData(byte[] buffer, int offset,
int length) {
centralDirectoryData = new byte[length];
System.arraycopy(buffer, offset, centralDirectoryData, 0, length);
if (localFileData == null) {
parseFromLocalFileData(buffer, offset, length);
}
}
}

View file

@ -0,0 +1,135 @@
package org.xbib.io.archive.zip;
/**
* Simple placeholder for all those extra fields we don't want to deal
* with. Assumes local file data and central directory entries are
* identical - unless told the opposite.
*/
public class UnrecognizedExtraField implements ZipExtraField {
/**
* The Header-ID.
*/
private ZipShort headerId;
/**
* Set the header id.
*
* @param headerId the header id to use
*/
public void setHeaderId(ZipShort headerId) {
this.headerId = headerId;
}
/**
* Get the header id.
*
* @return the header id
*/
public ZipShort getHeaderId() {
return headerId;
}
/**
* Extra field data in local file data - without
* Header-ID or length specifier.
*/
private byte[] localData;
/**
* Set the extra field data in the local file data -
* without Header-ID or length specifier.
*
* @param data the field data to use
*/
public void setLocalFileDataData(byte[] data) {
localData = ZipUtil.copy(data);
}
/**
* Get the length of the local data.
*
* @return the length of the local data
*/
public ZipShort getLocalFileDataLength() {
return new ZipShort(localData.length);
}
/**
* Get the local data.
*
* @return the local data
*/
public byte[] getLocalFileDataData() {
return ZipUtil.copy(localData);
}
/**
* Extra field data in central directory - without
* Header-ID or length specifier.
*/
private byte[] centralData;
/**
* Set the extra field data in central directory.
*
* @param data the data to use
*/
public void setCentralDirectoryData(byte[] data) {
centralData = ZipUtil.copy(data);
}
/**
* Get the central data length.
* If there is no central data, get the local file data length.
*
* @return the central data length
*/
public ZipShort getCentralDirectoryLength() {
if (centralData != null) {
return new ZipShort(centralData.length);
}
return getLocalFileDataLength();
}
/**
* Get the central data.
*
* @return the central data if present, else return the local file data
*/
public byte[] getCentralDirectoryData() {
if (centralData != null) {
return ZipUtil.copy(centralData);
}
return getLocalFileDataData();
}
/**
* @param data the array of bytes.
* @param offset the source location in the data array.
* @param length the number of bytes to use in the data array.
* @see ZipExtraField#parseFromLocalFileData(byte[], int, int)
*/
public void parseFromLocalFileData(byte[] data, int offset, int length) {
byte[] tmp = new byte[length];
System.arraycopy(data, offset, tmp, 0, length);
setLocalFileDataData(tmp);
}
/**
* @param data the array of bytes.
* @param offset the source location in the data array.
* @param length the number of bytes to use in the data array.
* @see ZipExtraField#parseFromCentralDirectoryData(byte[], int, int)
*/
public void parseFromCentralDirectoryData(byte[] data, int offset,
int length) {
byte[] tmp = new byte[length];
System.arraycopy(data, offset, tmp, 0, length);
setCentralDirectoryData(tmp);
if (localData == null) {
setLocalFileDataData(tmp);
}
}
}

View file

@ -0,0 +1,72 @@
package org.xbib.io.archive.zip;
import java.util.zip.ZipException;
/**
* Exception thrown when attempting to read or write data for a zip
* entry that uses ZIP features not supported by this library.
*/
public class UnsupportedZipFeatureException extends ZipException {
private final Feature reason;
private final ZipArchiveEntry entry;
/**
* Creates an exception.
*
* @param reason the feature that is not supported
* @param entry the entry using the feature
*/
public UnsupportedZipFeatureException(Feature reason,
ZipArchiveEntry entry) {
super("unsupported feature " + reason + " used in entry "
+ entry.getName());
this.reason = reason;
this.entry = entry;
}
/**
* The unsupported feature that has been used.
*/
public Feature getFeature() {
return reason;
}
/**
* The entry using the unsupported feature.
*/
public ZipArchiveEntry getEntry() {
return entry;
}
/**
* ZIP Features that may or may not be supported.
*/
public static class Feature {
/**
* The entry is encrypted.
*/
public static final Feature ENCRYPTION = new Feature("encryption");
/**
* The entry used an unsupported compression method.
*/
public static final Feature METHOD = new Feature("compression method");
/**
* The entry uses a data descriptor.
*/
public static final Feature DATA_DESCRIPTOR = new Feature("data descriptor");
private final String name;
private Feature(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
}

View file

@ -0,0 +1,315 @@
package org.xbib.io.archive.zip;
import java.util.zip.ZipException;
import static org.xbib.io.archive.zip.ZipConstants.DWORD;
import static org.xbib.io.archive.zip.ZipConstants.WORD;
/**
* Holds size and other extended information for entries that use Zip64
* features.
* From <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">PKWARE's APPNOTE.TXT</a>
* <pre>
* Zip64 Extended Information Extra Field (0x0001):
*
* The following is the layout of the zip64 extended
* information "extra" block. If one of the size or
* offset fields in the Local or Central directory
* record is too small to hold the required data,
* a Zip64 extended information record is created.
* The order of the fields in the zip64 extended
* information record is fixed, but the fields will
* only appear if the corresponding Local or Central
* directory record field is set to 0xFFFF or 0xFFFFFFFF.
*
* Note: all fields stored in Intel low-byte/high-byte order.
*
* Value Size Description
* ----- ---- -----------
* (ZIP64) 0x0001 2 bytes Tag for this "extra" block type
* Size 2 bytes Size of this "extra" block
* Original
* Size 8 bytes Original uncompressed file size
* Compressed
* Size 8 bytes Size of compressed data
* Relative Header
* Offset 8 bytes Offset of local header record
* Disk Start
* Number 4 bytes Number of the disk on which
* this file starts
*
* This entry in the Local header must include BOTH original
* and compressed file size fields. If encrypting the
* central directory and bit 13 of the general purpose bit
* flag is set indicating masking, the value stored in the
* Local Header for the original file size will be zero.
* </pre>
* Currently this code doesn't support encrypting the
* central directory so the not about masking doesn't apply.
* The implementation relies on data being read from the local file
* header and assumes that both size values are always present.
*/
public class Zip64ExtendedInformationExtraField implements ZipExtraField {
static final ZipShort HEADER_ID = new ZipShort(0x0001);
private static final String LFH_MUST_HAVE_BOTH_SIZES_MSG =
"Zip64 extended information must contain"
+ " both size values in the local file header.";
private ZipEightByteInteger size, compressedSize, relativeHeaderOffset;
private ZipLong diskStart;
/**
* Stored in {@link #parseFromCentralDirectoryData
* parseFromCentralDirectoryData} so it can be reused when ZipFile
* calls {@link #reparseCentralDirectoryData
* reparseCentralDirectoryData}.
*/
private byte[] rawCentralDirectoryData;
public Zip64ExtendedInformationExtraField() {
}
/**
* Creates an extra field based on the original and compressed size.
*
* @param size the entry's original size
* @param compressedSize the entry's compressed size
* @throws IllegalArgumentException if size or compressedSize is null
*/
public Zip64ExtendedInformationExtraField(ZipEightByteInteger size,
ZipEightByteInteger compressedSize) {
this(size, compressedSize, null, null);
}
/**
* Creates an extra field based on all four possible values.
*
* @param size the entry's original size
* @param compressedSize the entry's compressed size
* @throws IllegalArgumentException if size or compressedSize is null
*/
public Zip64ExtendedInformationExtraField(ZipEightByteInteger size,
ZipEightByteInteger compressedSize,
ZipEightByteInteger relativeHeaderOffset,
ZipLong diskStart) {
this.size = size;
this.compressedSize = compressedSize;
this.relativeHeaderOffset = relativeHeaderOffset;
this.diskStart = diskStart;
}
public ZipShort getHeaderId() {
return HEADER_ID;
}
public ZipShort getLocalFileDataLength() {
return new ZipShort(size != null ? 2 * DWORD : 0);
}
public ZipShort getCentralDirectoryLength() {
return new ZipShort((size != null ? DWORD : 0)
+ (compressedSize != null ? DWORD : 0)
+ (relativeHeaderOffset != null ? DWORD : 0)
+ (diskStart != null ? WORD : 0));
}
public byte[] getLocalFileDataData() {
if (size != null || compressedSize != null) {
if (size == null || compressedSize == null) {
throw new IllegalArgumentException(LFH_MUST_HAVE_BOTH_SIZES_MSG);
}
byte[] data = new byte[2 * DWORD];
addSizes(data);
return data;
}
return new byte[0];
}
public byte[] getCentralDirectoryData() {
byte[] data = new byte[getCentralDirectoryLength().getValue()];
int off = addSizes(data);
if (relativeHeaderOffset != null) {
System.arraycopy(relativeHeaderOffset.getBytes(), 0, data, off, DWORD);
off += DWORD;
}
if (diskStart != null) {
System.arraycopy(diskStart.getBytes(), 0, data, off, WORD);
off += WORD;
}
return data;
}
public void parseFromLocalFileData(byte[] buffer, int offset, int length)
throws ZipException {
if (length == 0) {
// no local file data at all, may happen if an archive
// only holds a ZIP64 extended information extra field
// inside the central directory but not inside the local
// file header
return;
}
if (length < 2 * DWORD) {
throw new ZipException(LFH_MUST_HAVE_BOTH_SIZES_MSG);
}
size = new ZipEightByteInteger(buffer, offset);
offset += DWORD;
compressedSize = new ZipEightByteInteger(buffer, offset);
offset += DWORD;
int remaining = length - 2 * DWORD;
if (remaining >= DWORD) {
relativeHeaderOffset = new ZipEightByteInteger(buffer, offset);
offset += DWORD;
remaining -= DWORD;
}
if (remaining >= WORD) {
diskStart = new ZipLong(buffer, offset);
offset += WORD;
remaining -= WORD;
}
}
public void parseFromCentralDirectoryData(byte[] buffer, int offset,
int length)
throws ZipException {
// store for processing in reparseCentralDirectoryData
rawCentralDirectoryData = new byte[length];
System.arraycopy(buffer, offset, rawCentralDirectoryData, 0, length);
// if there is no size information in here, we are screwed and
// can only hope things will get resolved by LFH data later
// But there are some cases that can be detected
// * all data is there
// * length == 24 -> both sizes and offset
// * length % 8 == 4 -> at least we can identify the diskStart field
if (length >= 3 * DWORD + WORD) {
parseFromLocalFileData(buffer, offset, length);
} else if (length == 3 * DWORD) {
size = new ZipEightByteInteger(buffer, offset);
offset += DWORD;
compressedSize = new ZipEightByteInteger(buffer, offset);
offset += DWORD;
relativeHeaderOffset = new ZipEightByteInteger(buffer, offset);
} else if (length % DWORD == WORD) {
diskStart = new ZipLong(buffer, offset + length - WORD);
}
}
/**
* Parses the raw bytes read from the central directory extra
* field with knowledge which fields are expected to be there.
* All four fields inside the zip64 extended information extra
* field are optional and only present if their corresponding
* entry inside the central directory contains the correct magic
* value.
*/
public void reparseCentralDirectoryData(boolean hasUncompressedSize,
boolean hasCompressedSize,
boolean hasRelativeHeaderOffset,
boolean hasDiskStart)
throws ZipException {
if (rawCentralDirectoryData != null) {
int expectedLength = (hasUncompressedSize ? DWORD : 0)
+ (hasCompressedSize ? DWORD : 0)
+ (hasRelativeHeaderOffset ? DWORD : 0)
+ (hasDiskStart ? WORD : 0);
if (rawCentralDirectoryData.length != expectedLength) {
throw new ZipException("central directory zip64 extended"
+ " information extra field's length"
+ " doesn't match central directory"
+ " data. Expected length "
+ expectedLength + " but is "
+ rawCentralDirectoryData.length);
}
int offset = 0;
if (hasUncompressedSize) {
size = new ZipEightByteInteger(rawCentralDirectoryData, offset);
offset += DWORD;
}
if (hasCompressedSize) {
compressedSize = new ZipEightByteInteger(rawCentralDirectoryData,
offset);
offset += DWORD;
}
if (hasRelativeHeaderOffset) {
relativeHeaderOffset =
new ZipEightByteInteger(rawCentralDirectoryData, offset);
offset += DWORD;
}
if (hasDiskStart) {
diskStart = new ZipLong(rawCentralDirectoryData, offset);
offset += WORD;
}
}
}
/**
* The uncompressed size stored in this extra field.
*/
public ZipEightByteInteger getSize() {
return size;
}
/**
* The uncompressed size stored in this extra field.
*/
public void setSize(ZipEightByteInteger size) {
this.size = size;
}
/**
* The compressed size stored in this extra field.
*/
public ZipEightByteInteger getCompressedSize() {
return compressedSize;
}
/**
* The uncompressed size stored in this extra field.
*/
public void setCompressedSize(ZipEightByteInteger compressedSize) {
this.compressedSize = compressedSize;
}
/**
* The relative header offset stored in this extra field.
*/
public ZipEightByteInteger getRelativeHeaderOffset() {
return relativeHeaderOffset;
}
/**
* The relative header offset stored in this extra field.
*/
public void setRelativeHeaderOffset(ZipEightByteInteger rho) {
relativeHeaderOffset = rho;
}
/**
* The disk start number stored in this extra field.
*/
public ZipLong getDiskStartNumber() {
return diskStart;
}
/**
* The disk start number stored in this extra field.
*/
public void setDiskStartNumber(ZipLong ds) {
diskStart = ds;
}
private int addSizes(byte[] data) {
int off = 0;
if (size != null) {
System.arraycopy(size.getBytes(), 0, data, 0, DWORD);
off += DWORD;
}
if (compressedSize != null) {
System.arraycopy(compressedSize.getBytes(), 0, data, off, DWORD);
off += DWORD;
}
return off;
}
}

View file

@ -0,0 +1,26 @@
package org.xbib.io.archive.zip;
/**
* The different modes {@link ZipArchiveOutputStream} can operate in.
*
* @see ZipArchiveOutputStream#setUseZip64
*/
public enum Zip64Mode {
/**
* Use Zip64 extensions for all entries, even if it is clear it is
* not required.
*/
Always,
/**
* Don't use Zip64 extensions for any entries.
* This will cause a {@link Zip64RequiredException} to be
* thrown if {@link ZipArchiveOutputStream} detects it needs Zip64
* support.
*/
Never,
/**
* Use Zip64 extensions for all entries where they are required,
* don't use them for entries that clearly don't require them.
*/
AsNeeded
}

View file

@ -0,0 +1,29 @@
package org.xbib.io.archive.zip;
import java.util.zip.ZipException;
/**
* Exception thrown when attempting to write data that requires Zip64
* support to an archive and {@link ZipArchiveOutputStream#setUseZip64
* UseZip64} has been set to {@link Zip64Mode#Never Never}.
*/
public class Zip64RequiredException extends ZipException {
/**
* Helper to format "entry too big" messages.
*/
static String getEntryTooBigMessage(ZipArchiveEntry ze) {
return ze.getName() + "'s size exceeds the limit of 4GByte.";
}
static final String ARCHIVE_TOO_BIG_MESSAGE =
"archive's size exceeds the limit of 4GByte.";
static final String TOO_MANY_ENTRIES_MESSAGE =
"archive contains more than 65535 entries.";
public Zip64RequiredException(String reason) {
super(reason);
}
}

View file

@ -0,0 +1,673 @@
package org.xbib.io.archive.zip;
import org.xbib.io.archive.entry.ArchiveEntry;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.zip.ZipException;
/**
* Extension that adds better handling of extra fields and provides
* access to the internal and external file attributes.
* The extra data is expected to follow the recommendation of
* <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">APPNOTE.txt</a>
* <ul>
* <li>the extra byte array consists of a sequence of extra fields</li>
* <li>each extra fields starts by a two byte header id followed by
* a two byte sequence holding the length of the remainder of
* data.</li>
* </ul>
* Any extra data that cannot be parsed by the rules above will be
* consumed as "unparseable" extra data and treated differently by the
* methods of this class.
*/
public class ZipArchiveEntry extends java.util.zip.ZipEntry implements ArchiveEntry {
public static final int PLATFORM_UNIX = 3;
public static final int PLATFORM_FAT = 0;
private static final int SHORT_MASK = 0xFFFF;
private static final int SHORT_SHIFT = 16;
/**
* The {@link java.util.zip.ZipEntry} base class only supports
* the compression methods STORED and DEFLATED. We override the
* field so that any compression methods can be used.
* The default value -1 means that the method has not been specified.
* <a href="https://issues.apache.org/jira/browse/COMPRESS-93">COMPRESS-93</a>
*/
private int method = -1;
/**
* The {@link java.util.zip.ZipEntry#setSize} method in the base
* class throws an IllegalArgumentException if the size is bigger
* than 2GB for Java versions < 7. Need to keep our own size
* information for Zip64 support.
*/
private long size = ArchiveEntry.SIZE_UNKNOWN;
private int internalAttributes = 0;
private int platform = PLATFORM_FAT;
private long externalAttributes = 0;
private LinkedHashMap<ZipShort, ZipExtraField> extraFields = null;
private UnparseableExtraFieldData unparseableExtra = null;
private String name = null;
private byte[] rawName = null;
private GeneralPurposeBit gpb = new GeneralPurposeBit();
public ZipArchiveEntry() {
this("");
}
/**
* Creates a new zip entry with the specified name.
* Assumes the entry represents a directory if and only if the
* name ends with a forward slash "/".
*
* @param name the name of the entry
*/
public ZipArchiveEntry(String name) {
super(name);
setName(name);
}
/**
* Creates a new zip entry with fields taken from the specified zip entry.
* Assumes the entry represents a directory if and only if the
* name ends with a forward slash "/".
*
* @param entry the entry to get fields from
* @throws java.util.zip.ZipException on error
*/
public ZipArchiveEntry(java.util.zip.ZipEntry entry) throws ZipException {
super(entry);
setName(entry.getName());
byte[] extra = entry.getExtra();
if (extra != null) {
setExtraFields(ExtraFieldUtils.parse(extra, true,
ExtraFieldUtils
.UnparseableExtraField.READ));
} else {
// initializes extra data to an empty byte array
setExtra();
}
setMethod(entry.getMethod());
setEntrySize(entry.getSize());
}
/**
* Creates a new zip entry with fields taken from the specified zip entry.
* Assumes the entry represents a directory if and only if the
* name ends with a forward slash "/".
*
* @param entry the entry to get fields from
* @throws java.util.zip.ZipException on error
*/
public ZipArchiveEntry(ZipArchiveEntry entry) throws ZipException {
this((java.util.zip.ZipEntry) entry);
setInternalAttributes(entry.getInternalAttributes());
setExternalAttributes(entry.getExternalAttributes());
setExtraFields(entry.getExtraFields(true));
}
/**
* Creates a new zip entry taking some information from the given
* file and using the provided name.
* The name will be adjusted to end with a forward slash "/" if
* the file is a directory. If the file is not a directory a
* potential trailing forward slash will be stripped from the
* entry name.
*/
public ZipArchiveEntry(File inputFile, String entryName) {
this(inputFile.isDirectory() && !entryName.endsWith("/") ?
entryName + "/" : entryName);
if (inputFile.isFile()) {
setSize(inputFile.length());
}
setTime(inputFile.lastModified());
// TODO are there any other fields we can set here?
}
/**
* Overwrite clone.
*
* @return a cloned copy of this ZipArchiveEntry
*/
@Override
public Object clone() {
ZipArchiveEntry e = (ZipArchiveEntry) super.clone();
e.setInternalAttributes(getInternalAttributes());
e.setExternalAttributes(getExternalAttributes());
e.setExtraFields(getExtraFields(true));
return e;
}
/**
* Returns the compression method of this entry, or -1 if the
* compression method has not been specified.
*
* @return compression method
*/
@Override
public int getMethod() {
return method;
}
/**
* Sets the compression method of this entry.
*
* @param method compression method
*/
@Override
public void setMethod(int method) {
if (method < 0) {
throw new IllegalArgumentException(
"ZIP compression method can not be negative: " + method);
}
this.method = method;
}
/**
* Retrieves the internal file attributes.
*
* @return the internal file attributes
*/
public int getInternalAttributes() {
return internalAttributes;
}
/**
* Sets the internal file attributes.
*
* @param value an <code>int</code> value
*/
public void setInternalAttributes(int value) {
internalAttributes = value;
}
/**
* Retrieves the external file attributes.
*
* @return the external file attributes
*/
public long getExternalAttributes() {
return externalAttributes;
}
/**
* Sets the external file attributes.
*
* @param value an <code>long</code> value
*/
public void setExternalAttributes(long value) {
externalAttributes = value;
}
/**
* Sets Unix permissions in a way that is understood by Info-Zip's
* unzip command.
*
* @param mode an <code>int</code> value
*/
public void setUnixMode(int mode) {
// CheckStyle:MagicNumberCheck OFF - no point
setExternalAttributes((mode << SHORT_SHIFT)
// MS-DOS read-only attribute
| ((mode & 0200) == 0 ? 1 : 0)
// MS-DOS directory flag
| (isDirectory() ? 0x10 : 0));
// CheckStyle:MagicNumberCheck ON
platform = PLATFORM_UNIX;
}
/**
* Unix permission.
*
* @return the unix permissions
*/
public int getUnixMode() {
return platform != PLATFORM_UNIX ? 0 :
(int) ((getExternalAttributes() >> SHORT_SHIFT) & SHORT_MASK);
}
/**
* Platform specification to put into the &quot;version made
* by&quot; part of the central file header.
*
* @return PLATFORM_FAT unless {@link #setUnixMode setUnixMode}
* has been called, in which case PLATORM_UNIX will be returned.
*/
public int getPlatform() {
return platform;
}
/**
* Set the platform (UNIX or FAT).
*
* @param platform an <code>int</code> value - 0 is FAT, 3 is UNIX
*/
protected void setPlatform(int platform) {
this.platform = platform;
}
/**
* Replaces all currently attached extra fields with the new array.
*
* @param fields an array of extra fields
*/
public void setExtraFields(ZipExtraField[] fields) {
extraFields = new LinkedHashMap<ZipShort, ZipExtraField>();
for (ZipExtraField field : fields) {
if (field instanceof UnparseableExtraFieldData) {
unparseableExtra = (UnparseableExtraFieldData) field;
} else {
extraFields.put(field.getHeaderId(), field);
}
}
setExtra();
}
/**
* Retrieves all extra fields that have been parsed successfully.
*
* @return an array of the extra fields
*/
public ZipExtraField[] getExtraFields() {
return getExtraFields(false);
}
/**
* Retrieves extra fields.
*
* @param includeUnparseable whether to also return unparseable
* extra fields as {@link UnparseableExtraFieldData} if such data
* exists.
* @return an array of the extra fields
*/
public ZipExtraField[] getExtraFields(boolean includeUnparseable) {
if (extraFields == null) {
return !includeUnparseable || unparseableExtra == null
? new ZipExtraField[0]
: new ZipExtraField[]{unparseableExtra};
}
List<ZipExtraField> result =
new ArrayList<ZipExtraField>(extraFields.values());
if (includeUnparseable && unparseableExtra != null) {
result.add(unparseableExtra);
}
return result.toArray(new ZipExtraField[0]);
}
/**
* Adds an extra field - replacing an already present extra field
* of the same type.
* If no extra field of the same type exists, the field will be
* added as last field.
*
* @param ze an extra field
*/
public void addExtraField(ZipExtraField ze) {
if (ze instanceof UnparseableExtraFieldData) {
unparseableExtra = (UnparseableExtraFieldData) ze;
} else {
if (extraFields == null) {
extraFields = new LinkedHashMap<ZipShort, ZipExtraField>();
}
extraFields.put(ze.getHeaderId(), ze);
}
setExtra();
}
/**
* Adds an extra field - replacing an already present extra field
* of the same type.
* The new extra field will be the first one.
*
* @param ze an extra field
*/
public void addAsFirstExtraField(ZipExtraField ze) {
if (ze instanceof UnparseableExtraFieldData) {
unparseableExtra = (UnparseableExtraFieldData) ze;
} else {
LinkedHashMap<ZipShort, ZipExtraField> copy = extraFields;
extraFields = new LinkedHashMap<ZipShort, ZipExtraField>();
extraFields.put(ze.getHeaderId(), ze);
if (copy != null) {
copy.remove(ze.getHeaderId());
extraFields.putAll(copy);
}
}
setExtra();
}
/**
* Remove an extra field.
*
* @param type the type of extra field to remove
*/
public void removeExtraField(ZipShort type) {
if (extraFields == null) {
throw new java.util.NoSuchElementException();
}
if (extraFields.remove(type) == null) {
throw new java.util.NoSuchElementException();
}
setExtra();
}
/**
* Removes unparseable extra field data.
*/
public void removeUnparseableExtraFieldData() {
if (unparseableExtra == null) {
throw new java.util.NoSuchElementException();
}
unparseableExtra = null;
setExtra();
}
/**
* Looks up an extra field by its header id.
*
* @return null if no such field exists.
*/
public ZipExtraField getExtraField(ZipShort type) {
if (extraFields != null) {
return extraFields.get(type);
}
return null;
}
/**
* Looks up extra field data that couldn't be parsed correctly.
*
* @return null if no such field exists.
*/
public UnparseableExtraFieldData getUnparseableExtraFieldData() {
return unparseableExtra;
}
/**
* Parses the given bytes as extra field data and consumes any
* unparseable data as an {@link UnparseableExtraFieldData}
* instance.
*
* @param extra an array of bytes to be parsed into extra fields
* @throws RuntimeException if the bytes cannot be parsed
* @throws RuntimeException on error
*/
@Override
public void setExtra(byte[] extra) throws RuntimeException {
try {
ZipExtraField[] local =
ExtraFieldUtils.parse(extra, true,
ExtraFieldUtils.UnparseableExtraField.READ);
mergeExtraFields(local, true);
} catch (ZipException e) {
throw new RuntimeException("Error parsing extra fields for entry: "
+ getName() + " - " + e.getMessage(), e);
}
}
/**
* Unfortunately {@link java.util.zip.ZipOutputStream
* java.util.zip.ZipOutputStream} seems to access the extra data
* directly, so overriding getExtra doesn't help - we need to
* modify super's data directly.
*/
protected void setExtra() {
super.setExtra(ExtraFieldUtils.mergeLocalFileDataData(getExtraFields(true)));
}
/**
* Sets the central directory part of extra fields.
*/
public void setCentralDirectoryExtra(byte[] b) {
try {
ZipExtraField[] central =
ExtraFieldUtils.parse(b, false,
ExtraFieldUtils.UnparseableExtraField.READ);
mergeExtraFields(central, false);
} catch (ZipException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
/**
* Retrieves the extra data for the local file data.
*
* @return the extra data for local file
*/
public byte[] getLocalFileDataExtra() {
byte[] extra = getExtra();
return extra != null ? extra : new byte[0];
}
/**
* Retrieves the extra data for the central directory.
*
* @return the central directory extra data
*/
public byte[] getCentralDirectoryExtra() {
return ExtraFieldUtils.mergeCentralDirectoryData(getExtraFields(true));
}
/**
* Get the name of the entry.
*
* @return the entry name
*/
@Override
public String getName() {
return name == null ? super.getName() : name;
}
/**
* Is this entry a directory?
*
* @return true if the entry is a directory
*/
@Override
public boolean isDirectory() {
return getName().endsWith("/");
}
/**
* Set the name of the entry.
*
* @param name the name to use
*/
public ZipArchiveEntry setName(String name) {
if (name != null && getPlatform() == PLATFORM_FAT
&& name.indexOf("/") == -1) {
name = name.replace('\\', '/');
}
this.name = name;
return this;
}
/**
* Gets the uncompressed size of the entry data.
*
* @return the entry size
*/
@Override
public long getEntrySize() {
return size;
}
/**
* Sets the uncompressed size of the entry data.
*
* @param size the uncompressed size in bytes
* @throws IllegalArgumentException if the specified size is less
* than 0
*/
@Override
public ZipArchiveEntry setEntrySize(long size) {
if (size < 0) {
throw new IllegalArgumentException("invalid entry size");
}
this.size = size;
return this;
}
/**
* Sets the name using the raw bytes and the string created from
* it by guessing or using the configured encoding.
*
* @param name the name to use created from the raw bytes using
* the guessed or configured encoding
* @param rawName the bytes originally read as name from the
* archive
*/
protected void setName(String name, byte[] rawName) {
setName(name);
this.rawName = rawName;
}
/**
* Returns the raw bytes that made up the name before it has been
* converted using the configured or guessed encoding.
* This method will return null if this instance has not been
* read from an archive.
*/
public byte[] getRawName() {
if (rawName != null) {
byte[] b = new byte[rawName.length];
System.arraycopy(rawName, 0, b, 0, rawName.length);
return b;
}
return null;
}
/**
* Get the hashCode of the entry.
* This uses the name as the hashcode.
*
* @return a hashcode.
*/
@Override
public int hashCode() {
// this method has severe consequences on performance. We cannot rely
// on the super.hashCode() method since super.getName() always return
// the empty string in the current implemention (there's no setter)
// so it is basically draining the performance of a hashmap lookup
return getName().hashCode();
}
/**
* The "general purpose bit" field.
*/
public GeneralPurposeBit getGeneralPurposeBit() {
return gpb;
}
/**
* The "general purpose bit" field.
*/
public void setGeneralPurposeBit(GeneralPurposeBit b) {
gpb = b;
}
/**
* If there are no extra fields, use the given fields as new extra
* data - otherwise merge the fields assuming the existing fields
* and the new fields stem from different locations inside the
* archive.
*
* @param f the extra fields to merge
* @param local whether the new fields originate from local data
*/
private void mergeExtraFields(ZipExtraField[] f, boolean local)
throws ZipException {
if (extraFields == null) {
setExtraFields(f);
} else {
for (ZipExtraField element : f) {
ZipExtraField existing;
if (element instanceof UnparseableExtraFieldData) {
existing = unparseableExtra;
} else {
existing = getExtraField(element.getHeaderId());
}
if (existing == null) {
addExtraField(element);
} else {
if (local) {
byte[] b = element.getLocalFileDataData();
existing.parseFromLocalFileData(b, 0, b.length);
} else {
byte[] b = element.getCentralDirectoryData();
existing.parseFromCentralDirectoryData(b, 0, b.length);
}
}
}
setExtra();
}
}
public ZipArchiveEntry setLastModified(Date date) {
setTime(date.getTime());
return this;
}
public Date getLastModified() {
return new Date(getTime());
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ZipArchiveEntry other = (ZipArchiveEntry) obj;
String myName = getName();
String otherName = other.getName();
if (myName == null) {
if (otherName != null) {
return false;
}
} else if (!myName.equals(otherName)) {
return false;
}
String myComment = getComment();
String otherComment = other.getComment();
if (myComment == null) {
if (otherComment != null) {
return false;
}
} else if (!myComment.equals(otherComment)) {
return false;
}
return getTime() == other.getTime()
&& getInternalAttributes() == other.getInternalAttributes()
&& getPlatform() == other.getPlatform()
&& getExternalAttributes() == other.getExternalAttributes()
&& getMethod() == other.getMethod()
&& getEntrySize() == other.getEntrySize()
&& getCrc() == other.getCrc()
&& getCompressedSize() == other.getCompressedSize()
&& Arrays.equals(getCentralDirectoryExtra(),
other.getCentralDirectoryExtra())
&& Arrays.equals(getLocalFileDataExtra(),
other.getLocalFileDataExtra())
&& gpb.equals(other.gpb);
}
}

View file

@ -0,0 +1,748 @@
package org.xbib.io.archive.zip;
import org.xbib.io.archive.entry.ArchiveEntry;
import org.xbib.io.archive.stream.ArchiveInputStream;
import org.xbib.io.archive.entry.ArchiveEntryEncoding;
import org.xbib.io.archive.entry.ArchiveEntryEncodingHelper;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.util.zip.CRC32;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import static org.xbib.io.archive.zip.ZipConstants.DWORD;
import static org.xbib.io.archive.zip.ZipConstants.SHORT;
import static org.xbib.io.archive.zip.ZipConstants.WORD;
import static org.xbib.io.archive.zip.ZipConstants.ZIP64_MAGIC;
/**
* Implements an input stream that can read Zip archives.
* Note that {@link ZipArchiveEntry#getSize()} may return -1 if the
* DEFLATE algorithm is used, as the size information is not available
* from the header.
* The {@link ZipFile} class is preferred when reading from files.
* This code transparently supports Zip64
* extensions and thus individual entries and archives larger than 4
* GB or with more than 65536 entries.
*
* @see ZipFile
*/
public class ZipArchiveInputStream extends ArchiveInputStream {
/**
* The zip encoding to use for filenames and the file comment.
*/
private final ArchiveEntryEncoding archiveEntryEncoding;
/**
* Whether to look for and use Unicode extra fields.
*/
private final boolean useUnicodeExtraFields;
/**
* Wrapped stream, will always be a PushbackInputStream.
*/
private final InputStream in;
/**
* Inflater used for all deflated entries.
*/
private final Inflater inf = new Inflater(true);
/**
* Calculates checkusms for all entries.
*/
private final CRC32 crc = new CRC32();
/**
* Buffer used to read from the wrapped stream.
*/
private final Buffer buf = new Buffer();
/**
* The entry that is currently being read.
*/
private CurrentEntry current = null;
/**
* Whether the stream has been closed.
*/
private boolean closed = false;
/**
* Whether the stream has reached the central directory - and thus
* found all entries.
*/
private boolean hitCentralDirectory = false;
/**
* When reading a stored entry that uses the data descriptor this
* stream has to read the full entry and caches it. This is the
* cache.
*/
private ByteArrayInputStream lastStoredEntry = null;
/**
* Whether the stream will try to read STORED entries that use a
* data descriptor.
*/
private boolean allowStoredEntriesWithDataDescriptor = false;
private static final int LFH_LEN = 30;
/*
local file header signature 4 bytes (0x04034b50)
version needed to extract 2 bytes
general purpose bit flag 2 bytes
compression method 2 bytes
last mod file time 2 bytes
last mod file date 2 bytes
crc-32 4 bytes
compressed size 4 bytes
uncompressed size 4 bytes
file name length 2 bytes
extra field length 2 bytes
*/
private static final long TWO_EXP_32 = ZIP64_MAGIC + 1;
public ZipArchiveInputStream(InputStream inputStream) {
this(inputStream, ArchiveEntryEncodingHelper.UTF8, true);
}
/**
* @param encoding the encoding to use for file names, use null
* for the platform's default encoding
* @param useUnicodeExtraFields whether to use InfoZIP Unicode
* Extra Fields (if present) to set the file names.
*/
public ZipArchiveInputStream(InputStream inputStream,
String encoding,
boolean useUnicodeExtraFields) {
this(inputStream, encoding, useUnicodeExtraFields, false);
}
/**
* @param encoding the encoding to use for file names, use null
* for the platform's default encoding
* @param useUnicodeExtraFields whether to use InfoZIP Unicode
* Extra Fields (if present) to set the file names.
* @param allowStoredEntriesWithDataDescriptor whether the stream
* will try to read STORED entries that use a data descriptor
*/
public ZipArchiveInputStream(InputStream inputStream,
String encoding,
boolean useUnicodeExtraFields,
boolean allowStoredEntriesWithDataDescriptor) {
archiveEntryEncoding = ArchiveEntryEncodingHelper.getEncoding(encoding);
this.useUnicodeExtraFields = useUnicodeExtraFields;
in = new PushbackInputStream(inputStream, buf.buf.length);
this.allowStoredEntriesWithDataDescriptor =
allowStoredEntriesWithDataDescriptor;
}
public org.xbib.io.archive.zip.ZipArchiveEntry getNextZipEntry() throws IOException {
if (closed || hitCentralDirectory) {
return null;
}
if (current != null) {
closeEntry();
}
byte[] lfh = new byte[LFH_LEN];
try {
readFully(lfh);
} catch (EOFException e) {
return null;
}
ZipLong sig = new ZipLong(lfh);
if (sig.equals(ZipLong.CFH_SIG)) {
hitCentralDirectory = true;
return null;
}
if (!sig.equals(ZipLong.LFH_SIG)) {
return null;
}
int off = WORD;
current = new CurrentEntry();
int versionMadeBy = ZipShort.getValue(lfh, off);
off += SHORT;
current.entry.setPlatform((versionMadeBy >> ZipFile.BYTE_SHIFT)
& ZipFile.NIBLET_MASK);
final GeneralPurposeBit gpFlag = GeneralPurposeBit.parse(lfh, off);
final boolean hasUTF8Flag = gpFlag.usesUTF8ForNames();
final ArchiveEntryEncoding entryEncoding =
hasUTF8Flag ? ArchiveEntryEncodingHelper.UTF8_ENCODING : archiveEntryEncoding;
current.hasDataDescriptor = gpFlag.usesDataDescriptor();
current.entry.setGeneralPurposeBit(gpFlag);
off += SHORT;
current.entry.setMethod(ZipShort.getValue(lfh, off));
off += SHORT;
long time = ZipUtil.dosToJavaTime(ZipLong.getValue(lfh, off));
current.entry.setTime(time);
off += WORD;
ZipLong size = null, cSize = null;
if (!current.hasDataDescriptor) {
current.entry.setCrc(ZipLong.getValue(lfh, off));
off += WORD;
cSize = new ZipLong(lfh, off);
off += WORD;
size = new ZipLong(lfh, off);
off += WORD;
} else {
off += 3 * WORD;
}
int fileNameLen = ZipShort.getValue(lfh, off);
off += SHORT;
int extraLen = ZipShort.getValue(lfh, off);
off += SHORT;
byte[] fileName = new byte[fileNameLen];
readFully(fileName);
current.entry.setName(entryEncoding.decode(fileName), fileName);
byte[] extraData = new byte[extraLen];
readFully(extraData);
current.entry.setExtra(extraData);
if (!hasUTF8Flag && useUnicodeExtraFields) {
ZipUtil.setNameAndCommentFromExtraFields(current.entry, fileName,
null);
}
processZip64Extra(size, cSize);
return current.entry;
}
/**
* Records whether a Zip64 extra is present and sets the size
* information from it if sizes are 0xFFFFFFFF and the entry
* doesn't use a data descriptor.
*/
private void processZip64Extra(ZipLong size, ZipLong cSize) {
Zip64ExtendedInformationExtraField z64 =
(Zip64ExtendedInformationExtraField)
current.entry.getExtraField(Zip64ExtendedInformationExtraField
.HEADER_ID);
current.usesZip64 = z64 != null;
if (!current.hasDataDescriptor) {
if (current.usesZip64 && (cSize.equals(ZipLong.ZIP64_MAGIC)
|| size.equals(ZipLong.ZIP64_MAGIC))
) {
current.entry.setCompressedSize(z64.getCompressedSize() // z64 cannot be null here
.getLongValue());
current.entry.setSize(z64.getSize().getLongValue());
} else {
current.entry.setCompressedSize(cSize.getValue());
current.entry.setSize(size.getValue());
}
}
}
@Override
public ArchiveEntry getNextEntry() throws IOException {
return getNextZipEntry();
}
@Override
public int read(byte[] buffer, int start, int length) throws IOException {
if (closed) {
throw new IOException("The stream is closed");
}
if (inf.finished() || current == null) {
return -1;
}
// avoid int overflow, check null buffer
if (start <= buffer.length && length >= 0 && start >= 0
&& buffer.length - start >= length) {
ZipUtil.checkRequestedFeatures(current.entry);
if (!supportsDataDescriptorFor(current.entry)) {
throw new UnsupportedZipFeatureException(UnsupportedZipFeatureException
.Feature
.DATA_DESCRIPTOR,
current.entry);
}
if (current.entry.getMethod() == ZipArchiveOutputStream.STORED) {
return readStored(buffer, start, length);
}
return readDeflated(buffer, start, length);
}
throw new ArrayIndexOutOfBoundsException();
}
/**
* Implementation of read for STORED entries.
*/
private int readStored(byte[] buffer, int start, int length)
throws IOException {
if (current.hasDataDescriptor) {
if (lastStoredEntry == null) {
readStoredEntry();
}
return lastStoredEntry.read(buffer, start, length);
}
long csize = current.entry.getSize();
if (current.bytesRead >= csize) {
return -1;
}
if (buf.offsetInBuffer >= buf.lengthOfLastRead) {
buf.offsetInBuffer = 0;
if ((buf.lengthOfLastRead = in.read(buf.buf)) == -1) {
return -1;
}
current.bytesReadFromStream += buf.lengthOfLastRead;
}
int toRead = length > buf.lengthOfLastRead
? buf.lengthOfLastRead - buf.offsetInBuffer
: length;
if ((csize - current.bytesRead) < toRead) {
// if it is smaller than toRead then it fits into an int
toRead = (int) (csize - current.bytesRead);
}
System.arraycopy(buf.buf, buf.offsetInBuffer, buffer, start, toRead);
buf.offsetInBuffer += toRead;
current.bytesRead += toRead;
crc.update(buffer, start, toRead);
return toRead;
}
/**
* Implementation of read for DEFLATED entries.
*/
private int readDeflated(byte[] buffer, int start, int length)
throws IOException {
if (inf.needsInput()) {
fill();
if (buf.lengthOfLastRead > 0) {
current.bytesReadFromStream += buf.lengthOfLastRead;
}
}
int read = 0;
try {
read = inf.inflate(buffer, start, length);
} catch (DataFormatException e) {
throw new ZipException(e.getMessage());
}
if (read == 0) {
if (inf.finished()) {
return -1;
} else if (buf.lengthOfLastRead == -1) {
throw new IOException("Truncated ZIP file");
}
}
crc.update(buffer, start, read);
return read;
}
@Override
public void close() throws IOException {
if (!closed) {
closed = true;
in.close();
inf.end();
}
}
/**
* Skips over and discards value bytes of data from this input
* stream.
* This implementation may end up skipping over some smaller
* number of bytes, possibly 0, if and only if it reaches the end
* of the underlying stream.
* The actual number of bytes skipped is returned.
*
* @param value the number of bytes to be skipped.
* @return the actual number of bytes skipped.
* @throws java.io.IOException - if an I/O error occurs.
* @throws IllegalArgumentException - if value is negative.
*/
@Override
public long skip(long value) throws IOException {
if (value >= 0) {
long skipped = 0;
byte[] b = new byte[1024];
while (skipped < value) {
long rem = value - skipped;
int x = read(b, 0, (int) (b.length > rem ? rem : b.length));
if (x == -1) {
return skipped;
}
skipped += x;
}
return skipped;
}
throw new IllegalArgumentException();
}
/**
* Closes the current ZIP archive entry and positions the underlying
* stream to the beginning of the next entry. All per-entry variables
* and data structures are cleared.
* If the compressed size of this entry is included in the entry header,
* then any outstanding bytes are simply skipped from the underlying
* stream without uncompressing them. This allows an entry to be safely
* closed even if the compression method is unsupported.
* In case we don't know the compressed size of this entry or have
* already buffered too much data from the underlying stream to support
* uncompression, then the uncompression process is completed and the
* end position of the stream is adjusted based on the result of that
* process.
*
* @throws java.io.IOException if an error occurs
*/
private void closeEntry() throws IOException {
if (closed) {
throw new IOException("The stream is closed");
}
if (current == null) {
return;
}
// Ensure all entry bytes are read
if (current.bytesReadFromStream <= current.entry.getCompressedSize()
&& !current.hasDataDescriptor) {
drainCurrentEntryData();
} else {
skip(Long.MAX_VALUE);
long inB =
current.entry.getMethod() == ZipArchiveOutputStream.DEFLATED
? getBytesInflated() : current.bytesRead;
// this is at most a single read() operation and can't
// exceed the range of int
int diff = (int) (current.bytesReadFromStream - inB);
// Pushback any required bytes
if (diff > 0) {
pushback(buf.buf, buf.lengthOfLastRead - diff, diff);
}
}
if (lastStoredEntry == null && current.hasDataDescriptor) {
readDataDescriptor();
}
inf.reset();
buf.reset();
crc.reset();
current = null;
lastStoredEntry = null;
}
/**
* Read all data of the current entry from the underlying stream
* that hasn't been read, yet.
*/
private void drainCurrentEntryData() throws IOException {
long remaining = current.entry.getCompressedSize()
- current.bytesReadFromStream;
while (remaining > 0) {
long n = in.read(buf.buf, 0, (int) Math.min(buf.buf.length,
remaining));
if (n < 0) {
throw new EOFException(
"Truncated ZIP entry: " + current.entry.getName());
} else {
remaining -= n;
}
}
}
/**
* Get the number of bytes Inflater has actually processed.
* for Java &lt; Java7 the getBytes* methods in
* Inflater/Deflater seem to return unsigned ints rather than
* longs that start over with 0 at 2^32.
* The stream knows how many bytes it has read, but not how
* many the Inflater actually consumed - it should be between the
* total number of bytes read for the entry and the total number
* minus the last read operation. Here we just try to make the
* value close enough to the bytes we've read by assuming the
* number of bytes consumed must be smaller than (or equal to) the
* number of bytes read but not smaller by more than 2^32.
*/
private long getBytesInflated() {
long inB = inf.getBytesRead();
if (current.bytesReadFromStream >= TWO_EXP_32) {
while (inB + TWO_EXP_32 <= current.bytesReadFromStream) {
inB += TWO_EXP_32;
}
}
return inB;
}
private void fill() throws IOException {
if (closed) {
throw new IOException("The stream is closed");
}
if ((buf.lengthOfLastRead = in.read(buf.buf)) > 0) {
inf.setInput(buf.buf, 0, buf.lengthOfLastRead);
}
}
private void readFully(byte[] b) throws IOException {
int count = 0, x = 0;
while (count != b.length) {
count += x = in.read(b, count, b.length - count);
if (x == -1) {
throw new EOFException();
}
}
}
private void readDataDescriptor() throws IOException {
byte[] b = new byte[WORD];
readFully(b);
ZipLong val = new ZipLong(b);
if (ZipLong.DD_SIG.equals(val)) {
// data descriptor with signature, skip sig
readFully(b);
val = new ZipLong(b);
}
current.entry.setCrc(val.getValue());
// if there is a ZIP64 extra field, sizes are eight bytes
// each, otherwise four bytes each. Unfortunately some
// implementations - namely Java7 - use eight bytes without
// using a ZIP64 extra field -
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7073588
// just read 16 bytes and check whether bytes nine to twelve
// look like one of the signatures of what could follow a data
// descriptor (ignoring archive decryption headers for now).
// If so, push back eight bytes and assume sizes are four
// bytes, otherwise sizes are eight bytes each.
b = new byte[2 * DWORD];
readFully(b);
ZipLong potentialSig = new ZipLong(b, DWORD);
if (potentialSig.equals(ZipLong.CFH_SIG)
|| potentialSig.equals(ZipLong.LFH_SIG)) {
pushback(b, DWORD, DWORD);
current.entry.setCompressedSize(ZipLong.getValue(b));
current.entry.setSize(ZipLong.getValue(b, WORD));
} else {
current.entry
.setCompressedSize(ZipEightByteInteger.getLongValue(b));
current.entry.setSize(ZipEightByteInteger.getLongValue(b, DWORD));
}
}
/**
* Whether this entry requires a data descriptor this library can work with.
*
* @return true if allowStoredEntriesWithDataDescriptor is true,
* the entry doesn't require any data descriptor or the method is
* DEFLATED.
*/
private boolean supportsDataDescriptorFor(ZipArchiveEntry entry) {
return allowStoredEntriesWithDataDescriptor ||
!entry.getGeneralPurposeBit().usesDataDescriptor()
|| entry.getMethod() == ZipEntry.DEFLATED;
}
/**
* Caches a stored entry that uses the data descriptor.
* <ul>
* <li>Reads a stored entry until the signature of a local file
* header, central directory header or data descriptor has been
* found.</li>
* <li>Stores all entry data in lastStoredEntry.
* <li>Rewinds the stream to position at the data
* descriptor.</li>
* <li>reads the data descriptor</li>
* </ul>
* After calling this method the entry should know its size,
* the entry's data is cached and the stream is positioned at the
* next local file or central directory header.
*/
private void readStoredEntry() throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int off = 0;
boolean done = false;
// length of DD without signature
int ddLen = current.usesZip64 ? WORD + 2 * DWORD : 3 * WORD;
while (!done) {
int r = in.read(buf.buf, off,
ZipArchiveOutputStream.BUFFER_SIZE - off);
if (r <= 0) {
// read the whole archive without ever finding a
// central directory
throw new IOException("Truncated ZIP file");
}
if (r + off < 4) {
// buf is too small to check for a signature, loop
off += r;
continue;
}
done = bufferContainsSignature(bos, off, r, ddLen);
if (!done) {
off = cacheBytesRead(bos, off, r, ddLen);
}
}
byte[] b = bos.toByteArray();
lastStoredEntry = new ByteArrayInputStream(b);
}
private static final byte[] LFH = ZipLong.LFH_SIG.getBytes();
private static final byte[] CFH = ZipLong.CFH_SIG.getBytes();
private static final byte[] DD = ZipLong.DD_SIG.getBytes();
/**
* Checks whether the current buffer contains the signature of a
* &quot;data decsriptor&quot;, &quot;local file header&quot; or
* &quot;central directory entry&quot;.
* If it contains such a signature, reads the data descriptor
* and positions the stream right after the data descriptor.
*/
private boolean bufferContainsSignature(ByteArrayOutputStream bos,
int offset, int lastRead,
int expectedDDLen) throws IOException {
boolean done = false;
int readTooMuch = 0;
for (int i = 0; !done && i < lastRead - 4; i++) {
if (buf.buf[i] == LFH[0] && buf.buf[i + 1] == LFH[1]) {
if ((buf.buf[i + 2] == LFH[2] && buf.buf[i + 3] == LFH[3])
|| (buf.buf[i] == CFH[2] && buf.buf[i + 3] == CFH[3])) {
// found a LFH or CFH:
readTooMuch = offset + lastRead - i - expectedDDLen;
done = true;
} else if (buf.buf[i + 2] == DD[2] && buf.buf[i + 3] == DD[3]) {
// found DD:
readTooMuch = offset + lastRead - i;
done = true;
}
if (done) {
// * push back bytes read in excess as well as the data
// descriptor
// * copy the remaining bytes to cache
// * read data descriptor
pushback(buf.buf, offset + lastRead - readTooMuch,
readTooMuch);
bos.write(buf.buf, 0, i);
readDataDescriptor();
}
}
}
return done;
}
/**
* If the last read bytes could hold a data descriptor and an
* incomplete signature then save the last bytes to the front of
* the buffer and cache everything in front of the potential data
* descriptor into the given ByteArrayOutputStream.
* Data descriptor plus incomplete signature (3 bytes in the
* worst case) can be 20 bytes max.
*/
private int cacheBytesRead(ByteArrayOutputStream bos, int offset,
int lastRead, int expecteDDLen) {
final int cacheable = offset + lastRead - expecteDDLen - 3;
if (cacheable > 0) {
bos.write(buf.buf, 0, cacheable);
System.arraycopy(buf.buf, cacheable, buf.buf, 0,
expecteDDLen + 3);
offset = expecteDDLen + 3;
} else {
offset += lastRead;
}
return offset;
}
private void pushback(byte[] buf, int offset, int length)
throws IOException {
((PushbackInputStream) in).unread(buf, offset, length);
pushedBackBytes(length);
}
/**
* Decrements the counter of already read bytes.
*
* @param pushedBack the number of bytes pushed back.
*/
protected void pushedBackBytes(long pushedBack) {
}
/**
* Structure collecting information for the entry that is
* currently being read.
*/
private static final class CurrentEntry {
/**
* Current ZIP entry.
*/
private final ZipArchiveEntry entry = new ZipArchiveEntry();
/**
* Does the entry use a data descriptor?
*/
private boolean hasDataDescriptor;
/**
* Does the entry have a ZIP64 extended information extra field.
*/
private boolean usesZip64;
/**
* Number of bytes of entry content read by the client if the
* entry is STORED.
*/
private long bytesRead;
/**
* Number of bytes of entry content read so from the stream.
* This may be more than the actual entry's length as some
* stuff gets buffered up and needs to be pushed back when the
* end of the entry has been reached.
*/
private long bytesReadFromStream;
}
/**
* Contains a temporary buffer used to read from the wrapped
* stream together with some information needed for internal
* housekeeping.
*/
private static final class Buffer {
/**
* Buffer used as temporary buffer when reading from the stream.
*/
private final byte[] buf = new byte[ZipArchiveOutputStream.BUFFER_SIZE];
/**
* {@link #buf buf} may contain data the client hasnt read, yet,
* this is the first byte that hasn't been read so far.
*/
private int offsetInBuffer = 0;
/**
* Number of bytes read from the wrapped stream into {@link #buf
* buf} with the last read operation.
*/
private int lengthOfLastRead = 0;
/**
* Reset internal housekeeping.
*/
private void reset() {
offsetInBuffer = lengthOfLastRead = 0;
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,56 @@
package org.xbib.io.archive.zip;
/**
* Various constants used throughout the package.
*/
interface ZipConstants {
/**
* Masks last eight bits
*/
int BYTE_MASK = 0xFF;
/**
* length of a ZipShort in bytes
*/
int SHORT = 2;
/**
* length of a ZipLong in bytes
*/
int WORD = 4;
/**
* length of a ZipEightByteInteger in bytes
*/
int DWORD = 8;
/**
* Initial ZIP specification version
*/
int INITIAL_VERSION = 10;
/**
* ZIP specification version that introduced data descriptor method
*/
int DATA_DESCRIPTOR_MIN_VERSION = 20;
/**
* ZIP specification version that introduced ZIP64
*/
int ZIP64_MIN_VERSION = 45;
/**
* Value stored in two-byte size and similar fields if ZIP64
* extensions are used.
*/
int ZIP64_MAGIC_SHORT = 0xFFFF;
/**
* Value stored in four-byte size and similar fields if ZIP64
* extensions are used.
*/
long ZIP64_MAGIC = 0xFFFFFFFFL;
}

View file

@ -0,0 +1,226 @@
package org.xbib.io.archive.zip;
import java.math.BigInteger;
import static org.xbib.io.archive.zip.ZipConstants.BYTE_MASK;
/**
* Utility class that represents an eight byte integer with conversion
* rules for the big endian byte order of ZIP files.
*/
public final class ZipEightByteInteger {
private static final int BYTE_1 = 1;
private static final int BYTE_1_MASK = 0xFF00;
private static final int BYTE_1_SHIFT = 8;
private static final int BYTE_2 = 2;
private static final int BYTE_2_MASK = 0xFF0000;
private static final int BYTE_2_SHIFT = 16;
private static final int BYTE_3 = 3;
private static final long BYTE_3_MASK = 0xFF000000L;
private static final int BYTE_3_SHIFT = 24;
private static final int BYTE_4 = 4;
private static final long BYTE_4_MASK = 0xFF00000000L;
private static final int BYTE_4_SHIFT = 32;
private static final int BYTE_5 = 5;
private static final long BYTE_5_MASK = 0xFF0000000000L;
private static final int BYTE_5_SHIFT = 40;
private static final int BYTE_6 = 6;
private static final long BYTE_6_MASK = 0xFF000000000000L;
private static final int BYTE_6_SHIFT = 48;
private static final int BYTE_7 = 7;
private static final long BYTE_7_MASK = 0x7F00000000000000L;
private static final int BYTE_7_SHIFT = 56;
private static final int LEFTMOST_BIT_SHIFT = 63;
private static final byte LEFTMOST_BIT = (byte) 0x80;
private final BigInteger value;
public static final ZipEightByteInteger ZERO = new ZipEightByteInteger(0);
/**
* Create instance from a number.
*
* @param value the long to store as a ZipEightByteInteger
*/
public ZipEightByteInteger(long value) {
this(BigInteger.valueOf(value));
}
/**
* Create instance from a number.
*
* @param value the BigInteger to store as a ZipEightByteInteger
*/
public ZipEightByteInteger(BigInteger value) {
this.value = value;
}
/**
* Create instance from bytes.
*
* @param bytes the bytes to store as a ZipEightByteInteger
*/
public ZipEightByteInteger(byte[] bytes) {
this(bytes, 0);
}
/**
* Create instance from the eight bytes starting at offset.
*
* @param bytes the bytes to store as a ZipEightByteInteger
* @param offset the offset to start
*/
public ZipEightByteInteger(byte[] bytes, int offset) {
value = ZipEightByteInteger.getValue(bytes, offset);
}
/**
* Get value as eight bytes in big endian byte order.
*
* @return value as eight bytes in big endian order
*/
public byte[] getBytes() {
return ZipEightByteInteger.getBytes(value);
}
/**
* Get value as Java long.
*
* @return value as a long
*/
public long getLongValue() {
return value.longValue();
}
/**
* Get value as Java long.
*
* @return value as a long
*/
public BigInteger getValue() {
return value;
}
/**
* Get value as eight bytes in big endian byte order.
*
* @param value the value to convert
* @return value as eight bytes in big endian byte order
*/
public static byte[] getBytes(long value) {
return getBytes(BigInteger.valueOf(value));
}
/**
* Get value as eight bytes in big endian byte order.
*
* @param value the value to convert
* @return value as eight bytes in big endian byte order
*/
public static byte[] getBytes(BigInteger value) {
byte[] result = new byte[8];
long val = value.longValue();
result[0] = (byte) ((val & BYTE_MASK));
result[BYTE_1] = (byte) ((val & BYTE_1_MASK) >> BYTE_1_SHIFT);
result[BYTE_2] = (byte) ((val & BYTE_2_MASK) >> BYTE_2_SHIFT);
result[BYTE_3] = (byte) ((val & BYTE_3_MASK) >> BYTE_3_SHIFT);
result[BYTE_4] = (byte) ((val & BYTE_4_MASK) >> BYTE_4_SHIFT);
result[BYTE_5] = (byte) ((val & BYTE_5_MASK) >> BYTE_5_SHIFT);
result[BYTE_6] = (byte) ((val & BYTE_6_MASK) >> BYTE_6_SHIFT);
result[BYTE_7] = (byte) ((val & BYTE_7_MASK) >> BYTE_7_SHIFT);
if (value.testBit(LEFTMOST_BIT_SHIFT)) {
result[BYTE_7] |= LEFTMOST_BIT;
}
return result;
}
/**
* Helper method to get the value as a Java long from eight bytes
* starting at given array offset
*
* @param bytes the array of bytes
* @param offset the offset to start
* @return the corresponding Java long value
*/
public static long getLongValue(byte[] bytes, int offset) {
return getValue(bytes, offset).longValue();
}
/**
* Helper method to get the value as a Java BigInteger from eight
* bytes starting at given array offset
*
* @param bytes the array of bytes
* @param offset the offset to start
* @return the corresponding Java BigInteger value
*/
public static BigInteger getValue(byte[] bytes, int offset) {
long value = ((long) bytes[offset + BYTE_7] << BYTE_7_SHIFT) & BYTE_7_MASK;
value += ((long) bytes[offset + BYTE_6] << BYTE_6_SHIFT) & BYTE_6_MASK;
value += ((long) bytes[offset + BYTE_5] << BYTE_5_SHIFT) & BYTE_5_MASK;
value += ((long) bytes[offset + BYTE_4] << BYTE_4_SHIFT) & BYTE_4_MASK;
value += ((long) bytes[offset + BYTE_3] << BYTE_3_SHIFT) & BYTE_3_MASK;
value += ((long) bytes[offset + BYTE_2] << BYTE_2_SHIFT) & BYTE_2_MASK;
value += ((long) bytes[offset + BYTE_1] << BYTE_1_SHIFT) & BYTE_1_MASK;
value += ((long) bytes[offset] & BYTE_MASK);
BigInteger val = BigInteger.valueOf(value);
return (bytes[offset + BYTE_7] & LEFTMOST_BIT) == LEFTMOST_BIT
? val.setBit(LEFTMOST_BIT_SHIFT) : val;
}
/**
* Helper method to get the value as a Java long from an eight-byte array
*
* @param bytes the array of bytes
* @return the corresponding Java long value
*/
public static long getLongValue(byte[] bytes) {
return getLongValue(bytes, 0);
}
/**
* Helper method to get the value as a Java long from an eight-byte array
*
* @param bytes the array of bytes
* @return the corresponding Java BigInteger value
*/
public static BigInteger getValue(byte[] bytes) {
return getValue(bytes, 0);
}
/**
* Override to make two instances with same value equal.
*
* @param o an object to compare
* @return true if the objects are equal
*/
@Override
public boolean equals(Object o) {
return !(o == null || !(o instanceof ZipEightByteInteger))
&& value.equals(((ZipEightByteInteger) o).getValue());
}
/**
* Override to make two instances with same value equal.
*
* @return the hashCode of the value stored in the ZipEightByteInteger
*/
@Override
public int hashCode() {
return value.hashCode();
}
@Override
public String toString() {
return "ZipEightByteInteger value: " + value;
}
}

View file

@ -0,0 +1,73 @@
package org.xbib.io.archive.zip;
import java.util.zip.ZipException;
/**
* General format of extra field data.
* Extra fields usually appear twice per file, once in the local file data and
* once in the central directory. Usually they are the same, but they don't have
* to be. {@link java.util.zip.ZipOutputStream java.util.zip.ZipOutputStream}
* will only use the local file data in both places.
*/
public interface ZipExtraField {
/**
* The Header-ID.
*
* @return The HeaderId value
*/
ZipShort getHeaderId();
/**
* Length of the extra field in the local file data - without Header-ID or
* length specifier.
*
* @return The LocalFileDataLength value
*/
ZipShort getLocalFileDataLength();
/**
* Length of the extra field in the central directory - without Header-ID or
* length specifier.
*
* @return The CentralDirectoryLength value
*/
ZipShort getCentralDirectoryLength();
/**
* The actual data to put into local file data - without Header-ID or length
* specifier.
*
* @return The LocalFileDataData value
*/
byte[] getLocalFileDataData();
/**
* The actual data to put into central directory - without Header-ID or
* length specifier.
*
* @return The CentralDirectoryData value
*/
byte[] getCentralDirectoryData();
/**
* Populate data from this array as if it was in local file data.
*
* @param buffer the buffer to read data from
* @param offset offset into buffer to read data
* @param length the length of data
* @throws java.util.zip.ZipException on error
*/
void parseFromLocalFileData(byte[] buffer, int offset, int length)
throws ZipException;
/**
* Populate data from this array as if it was in central directory data.
*
* @param buffer the buffer to read data from
* @param offset offset into buffer to read data
* @param length the length of data
* @throws java.util.zip.ZipException on error
*/
void parseFromCentralDirectoryData(byte[] buffer, int offset, int length)
throws ZipException;
}

View file

@ -0,0 +1,941 @@
package org.xbib.io.archive.zip;
import org.xbib.io.archive.entry.ArchiveEntryEncoding;
import org.xbib.io.archive.entry.ArchiveEntryEncodingHelper;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import static org.xbib.io.archive.zip.ZipConstants.DWORD;
import static org.xbib.io.archive.zip.ZipConstants.SHORT;
import static org.xbib.io.archive.zip.ZipConstants.WORD;
import static org.xbib.io.archive.zip.ZipConstants.ZIP64_MAGIC;
import static org.xbib.io.archive.zip.ZipConstants.ZIP64_MAGIC_SHORT;
/**
* Replacement for <code>java.util.ZipFile</code>.
* This class adds support for file name encodings other than UTF-8
* (which is required to work on ZIP files created by native zip tools
* and is able to skip a preamble like the one found in self
* extracting archives. Furthermore it returns instances of
* <code>org.apache.commons.compress.archivers.zip.ZipArchiveEntry</code>
* instead of <code>java.util.zip.ZipEntry</code>.
* It doesn't extend <code>java.util.zip.ZipFile</code> as it would
* have to reimplement all methods anyway. Like
* <code>java.util.ZipFile</code>, it uses RandomAccessFile under the
* covers and supports compressed and uncompressed entries. This code
* also transparently supports Zip64
* extensions and thus individual entries and archives larger than 4
* GB or with more than 65536 entries.
* The method signatures mimic the ones of
* <code>java.util.zip.ZipFile</code>, with a couple of exceptions:
* <ul>
* <li>There is no getName method.</li>
* <li>entries has been renamed to getEntries.</li>
* <li>getEntries and getEntry return
* <code>org.apache.commons.compress.archivers.zip.ZipArchiveEntry</code>
* instances.</li>
* <li>close is allowed to throw IOException.</li>
* </ul>
*/
public class ZipFile {
private static final int HASH_SIZE = 509;
static final int NIBLET_MASK = 0x0f;
static final int BYTE_SHIFT = 8;
private static final int POS_0 = 0;
private static final int POS_1 = 1;
private static final int POS_2 = 2;
private static final int POS_3 = 3;
/**
* Maps ZipArchiveEntrys to two longs, recording the offsets of
* the local file headers and the start of entry data.
*/
private final Map<ZipArchiveEntry, OffsetEntry> entries =
new LinkedHashMap<ZipArchiveEntry, OffsetEntry>(HASH_SIZE);
/**
* Maps String to ZipArchiveEntrys, name -> actual entry.
*/
private final Map<String, ZipArchiveEntry> nameMap =
new HashMap<String, ZipArchiveEntry>(HASH_SIZE);
private static final class OffsetEntry {
private long headerOffset = -1;
private long dataOffset = -1;
}
/**
* The encoding to use for filenames and the file comment.
* <p/>
* <p>For a list of possible values see <a
* href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>.
* Defaults to UTF-8.</p>
*/
private final String encoding;
/**
* The zip encoding to use for filenames and the file comment.
*/
private final ArchiveEntryEncoding archiveEntryEncoding;
/**
* File name of actual source.
*/
private final String archiveName;
/**
* The actual data source.
*/
private final RandomAccessFile archive;
/**
* Whether to look for and use Unicode extra fields.
*/
private final boolean useUnicodeExtraFields;
/**
* Whether the file is closed.
*/
private boolean closed;
/**
* Opens the given file for reading, assuming "UTF8" for file names.
*
* @param f the archive.
* @throws java.io.IOException if an error occurs while reading the file.
*/
public ZipFile(File f) throws IOException {
this(f, ArchiveEntryEncodingHelper.UTF8);
}
/**
* Opens the given file for reading, assuming "UTF8".
*
* @param name name of the archive.
* @throws java.io.IOException if an error occurs while reading the file.
*/
public ZipFile(String name) throws IOException {
this(new File(name), ArchiveEntryEncodingHelper.UTF8);
}
/**
* Opens the given file for reading, assuming the specified
* encoding for file names, scanning unicode extra fields.
*
* @param name name of the archive.
* @param encoding the encoding to use for file names, use null
* for the platform's default encoding
* @throws java.io.IOException if an error occurs while reading the file.
*/
public ZipFile(String name, String encoding) throws IOException {
this(new File(name), encoding, true);
}
/**
* Opens the given file for reading, assuming the specified
* encoding for file names and scanning for unicode extra fields.
*
* @param f the archive.
* @param encoding the encoding to use for file names, use null
* for the platform's default encoding
* @throws java.io.IOException if an error occurs while reading the file.
*/
public ZipFile(File f, String encoding) throws IOException {
this(f, encoding, true);
}
/**
* Opens the given file for reading, assuming the specified
* encoding for file names.
*
* @param f the archive.
* @param encoding the encoding to use for file names, use null
* for the platform's default encoding
* @param useUnicodeExtraFields whether to use InfoZIP Unicode
* Extra Fields (if present) to set the file names.
* @throws java.io.IOException if an error occurs while reading the file.
*/
public ZipFile(File f, String encoding, boolean useUnicodeExtraFields)
throws IOException {
this.archiveName = f.getAbsolutePath();
this.encoding = encoding;
this.archiveEntryEncoding = ArchiveEntryEncodingHelper.getEncoding(encoding);
this.useUnicodeExtraFields = useUnicodeExtraFields;
archive = new RandomAccessFile(f, "r");
boolean success = false;
try {
Map<ZipArchiveEntry, NameAndComment> entriesWithoutUTF8Flag = populateFromCentralDirectory();
resolveLocalFileHeaderData(entriesWithoutUTF8Flag);
success = true;
} finally {
if (!success) {
try {
closed = true;
archive.close();
} catch (IOException e2) {
// swallow, throw the original exception instead
}
}
}
}
/**
* The encoding to use for filenames and the file comment.
*
* @return null if using the platform's default character encoding.
*/
public String getEncoding() {
return encoding;
}
/**
* Closes the archive.
*
* @throws java.io.IOException if an error occurs closing the archive.
*/
public void close() throws IOException {
// this flag is only written here and read in finalize() which
// can never be run in parallel.
// no synchronization needed.
closed = true;
archive.close();
}
/**
* close a zipfile quietly; throw no io fault, do nothing
* on a null parameter
*
* @param zipfile file to close, can be null
*/
public static void closeQuietly(ZipFile zipfile) {
if (zipfile != null) {
try {
zipfile.close();
} catch (IOException e) { // NOPMD
//ignore, that's why the method is called "quietly"
}
}
}
/**
* Returns all entries.
* Entries will be returned in the same order they appear
* within the archive's central directory.
*
* @return all entries as {@link ZipArchiveEntry} instances
*/
public Enumeration<ZipArchiveEntry> getEntries() {
return Collections.enumeration(entries.keySet());
}
/**
* Returns all entries in physical order.
* Entries will be returned in the same order their contents
* appear within the archive.
*
* @return all entries as {@link ZipArchiveEntry} instances
*/
public Enumeration<ZipArchiveEntry> getEntriesInPhysicalOrder() {
ZipArchiveEntry[] allEntries =
entries.keySet().toArray(new ZipArchiveEntry[0]);
Arrays.sort(allEntries, OFFSET_COMPARATOR);
return Collections.enumeration(Arrays.asList(allEntries));
}
/**
* Returns a named entry - or {@code null} if no entry by
* that name exists.
*
* @param name name of the entry.
* @return the ZipArchiveEntry corresponding to the given name - or
* {@code null} if not present.
*/
public ZipArchiveEntry getEntry(String name) {
return nameMap.get(name);
}
/**
* Whether this class is able to read the given entry.
* May return false if it is set up to use encryption or a
* compression method that hasn't been implemented yet.
*/
public boolean canReadEntryData(ZipArchiveEntry ze) {
return ZipUtil.canHandleEntryData(ze);
}
/**
* Returns an InputStream for reading the contents of the given entry.
*
* @param ze the entry to get the stream for.
* @return a stream to read the entry from.
* @throws java.io.IOException if unable to create an input stream from the zipenty
* @throws java.util.zip.ZipException if the zipentry uses an unsupported feature
*/
public InputStream getInputStream(ZipArchiveEntry ze)
throws IOException, ZipException {
OffsetEntry offsetEntry = entries.get(ze);
if (offsetEntry == null) {
return null;
}
ZipUtil.checkRequestedFeatures(ze);
long start = offsetEntry.dataOffset;
BoundedInputStream bis =
new BoundedInputStream(start, ze.getCompressedSize());
switch (ze.getMethod()) {
case ZipEntry.STORED:
return bis;
case ZipEntry.DEFLATED:
bis.addDummy();
final Inflater inflater = new Inflater(true);
return new InflaterInputStream(bis, inflater) {
@Override
public void close() throws IOException {
super.close();
inflater.end();
}
};
default:
throw new ZipException("Found unsupported compression method "
+ ze.getMethod());
}
}
/**
* Ensures that the close method of this zipfile is called when
* there are no more references to it.
*
* @see #close()
*/
@Override
protected void finalize() throws Throwable {
try {
if (!closed) {
close();
}
} finally {
super.finalize();
}
}
/**
* Length of a "central directory" entry structure without file
* name, extra fields or comment.
*/
private static final int CFH_LEN =
/* version made by */ SHORT
/* version needed to extract */ + SHORT
/* general purpose bit flag */ + SHORT
/* compression method */ + SHORT
/* last mod file time */ + SHORT
/* last mod file date */ + SHORT
/* crc-32 */ + WORD
/* compressed size */ + WORD
/* uncompressed size */ + WORD
/* filename length */ + SHORT
/* extra field length */ + SHORT
/* file comment length */ + SHORT
/* disk number start */ + SHORT
/* internal file attributes */ + SHORT
/* external file attributes */ + WORD
/* relative offset of local header */ + WORD;
private static final long CFH_SIG =
ZipLong.getValue(ZipArchiveOutputStream.CFH_SIG);
/**
* Reads the central directory of the given archive and populates
* the internal tables with ZipArchiveEntry instances.
* The ZipArchiveEntrys will know all data that can be obtained from
* the central directory alone, but not the data that requires the
* local file header or additional data to be read.
*
* @return a map of zipentries that didn't have the language
* encoding flag set when read.
*/
private Map<ZipArchiveEntry, NameAndComment> populateFromCentralDirectory()
throws IOException {
HashMap<ZipArchiveEntry, NameAndComment> noUTF8Flag =
new HashMap<ZipArchiveEntry, NameAndComment>();
positionAtCentralDirectory();
byte[] signatureBytes = new byte[WORD];
archive.readFully(signatureBytes);
long sig = ZipLong.getValue(signatureBytes);
if (sig != CFH_SIG && startsWithLocalFileHeader()) {
throw new IOException("central directory is empty, can't expand"
+ " corrupt archive.");
}
while (sig == CFH_SIG) {
readCentralDirectoryEntry(noUTF8Flag);
archive.readFully(signatureBytes);
sig = ZipLong.getValue(signatureBytes);
}
return noUTF8Flag;
}
/**
* Reads an individual entry of the central directory, creats an
* ZipArchiveEntry from it and adds it to the global maps.
*
* @param noUTF8Flag map used to collect entries that don't have
* their UTF-8 flag set and whose name will be set by data read
* from the local file header later. The current entry may be
* added to this map.
*/
private void
readCentralDirectoryEntry(Map<ZipArchiveEntry, NameAndComment> noUTF8Flag)
throws IOException {
byte[] cfh = new byte[CFH_LEN];
archive.readFully(cfh);
int off = 0;
ZipArchiveEntry ze = new ZipArchiveEntry();
int versionMadeBy = ZipShort.getValue(cfh, off);
off += SHORT;
ze.setPlatform((versionMadeBy >> BYTE_SHIFT) & NIBLET_MASK);
off += SHORT; // skip version info
final GeneralPurposeBit gpFlag = GeneralPurposeBit.parse(cfh, off);
final boolean hasUTF8Flag = gpFlag.usesUTF8ForNames();
final ArchiveEntryEncoding entryEncoding =
hasUTF8Flag ? ArchiveEntryEncodingHelper.UTF8_ENCODING : archiveEntryEncoding;
ze.setGeneralPurposeBit(gpFlag);
off += SHORT;
ze.setMethod(ZipShort.getValue(cfh, off));
off += SHORT;
long time = ZipUtil.dosToJavaTime(ZipLong.getValue(cfh, off));
ze.setTime(time);
off += WORD;
ze.setCrc(ZipLong.getValue(cfh, off));
off += WORD;
ze.setCompressedSize(ZipLong.getValue(cfh, off));
off += WORD;
ze.setSize(ZipLong.getValue(cfh, off));
off += WORD;
int fileNameLen = ZipShort.getValue(cfh, off);
off += SHORT;
int extraLen = ZipShort.getValue(cfh, off);
off += SHORT;
int commentLen = ZipShort.getValue(cfh, off);
off += SHORT;
int diskStart = ZipShort.getValue(cfh, off);
off += SHORT;
ze.setInternalAttributes(ZipShort.getValue(cfh, off));
off += SHORT;
ze.setExternalAttributes(ZipLong.getValue(cfh, off));
off += WORD;
byte[] fileName = new byte[fileNameLen];
archive.readFully(fileName);
ze.setName(entryEncoding.decode(fileName), fileName);
// LFH offset,
OffsetEntry offset = new OffsetEntry();
offset.headerOffset = ZipLong.getValue(cfh, off);
// data offset will be filled later
entries.put(ze, offset);
nameMap.put(ze.getName(), ze);
byte[] cdExtraData = new byte[extraLen];
archive.readFully(cdExtraData);
ze.setCentralDirectoryExtra(cdExtraData);
setSizesAndOffsetFromZip64Extra(ze, offset, diskStart);
byte[] comment = new byte[commentLen];
archive.readFully(comment);
ze.setComment(entryEncoding.decode(comment));
if (!hasUTF8Flag && useUnicodeExtraFields) {
noUTF8Flag.put(ze, new NameAndComment(fileName, comment));
}
}
/**
* If the entry holds a Zip64 extended information extra field,
* read sizes from there if the entry's sizes are set to
* 0xFFFFFFFFF, do the same for the offset of the local file
* header.
* <p/>
* <p>Ensures the Zip64 extra either knows both compressed and
* uncompressed size or neither of both as the internal logic in
* ExtraFieldUtils forces the field to create local header data
* even if they are never used - and here a field with only one
* size would be invalid.</p>
*/
private void setSizesAndOffsetFromZip64Extra(ZipArchiveEntry ze,
OffsetEntry offset,
int diskStart)
throws IOException {
Zip64ExtendedInformationExtraField z64 =
(Zip64ExtendedInformationExtraField)
ze.getExtraField(Zip64ExtendedInformationExtraField.HEADER_ID);
if (z64 != null) {
boolean hasUncompressedSize = ze.getSize() == ZIP64_MAGIC;
boolean hasCompressedSize = ze.getCompressedSize() == ZIP64_MAGIC;
boolean hasRelativeHeaderOffset =
offset.headerOffset == ZIP64_MAGIC;
z64.reparseCentralDirectoryData(hasUncompressedSize,
hasCompressedSize,
hasRelativeHeaderOffset,
diskStart == ZIP64_MAGIC_SHORT);
if (hasUncompressedSize) {
ze.setSize(z64.getSize().getLongValue());
} else if (hasCompressedSize) {
z64.setSize(new ZipEightByteInteger(ze.getSize()));
}
if (hasCompressedSize) {
ze.setCompressedSize(z64.getCompressedSize().getLongValue());
} else if (hasUncompressedSize) {
z64.setCompressedSize(new ZipEightByteInteger(ze.getCompressedSize()));
}
if (hasRelativeHeaderOffset) {
offset.headerOffset =
z64.getRelativeHeaderOffset().getLongValue();
}
}
}
/**
* Length of the "End of central directory record" - which is
* supposed to be the last structure of the archive - without file
* comment.
*/
private static final int MIN_EOCD_SIZE =
/* end of central dir signature */ WORD
/* number of this disk */ + SHORT
/* number of the disk with the */
/* start of the central directory */ + SHORT
/* total number of entries in */
/* the central dir on this disk */ + SHORT
/* total number of entries in */
/* the central dir */ + SHORT
/* size of the central directory */ + WORD
/* offset of start of central */
/* directory with respect to */
/* the starting disk number */ + WORD
/* zipfile comment length */ + SHORT;
/**
* Maximum length of the "End of central directory record" with a
* file comment.
*/
private static final int MAX_EOCD_SIZE = MIN_EOCD_SIZE
/* maximum length of zipfile comment */ + ZIP64_MAGIC_SHORT;
/**
* Offset of the field that holds the location of the first
* central directory entry inside the "End of central directory
* record" relative to the start of the "End of central directory
* record".
*/
private static final int CFD_LOCATOR_OFFSET =
/* end of central dir signature */ WORD
/* number of this disk */ + SHORT
/* number of the disk with the */
/* start of the central directory */ + SHORT
/* total number of entries in */
/* the central dir on this disk */ + SHORT
/* total number of entries in */
/* the central dir */ + SHORT
/* size of the central directory */ + WORD;
/**
* Length of the "Zip64 end of central directory locator" - which
* should be right in front of the "end of central directory
* record" if one is present at all.
*/
private static final int ZIP64_EOCDL_LENGTH =
/* zip64 end of central dir locator sig */ WORD
/* number of the disk with the start */
/* start of the zip64 end of */
/* central directory */ + WORD
/* relative offset of the zip64 */
/* end of central directory record */ + DWORD
/* total number of disks */ + WORD;
/**
* Offset of the field that holds the location of the "Zip64 end
* of central directory record" inside the "Zip64 end of central
* directory locator" relative to the start of the "Zip64 end of
* central directory locator".
*/
private static final int ZIP64_EOCDL_LOCATOR_OFFSET =
/* zip64 end of central dir locator sig */ WORD
/* number of the disk with the start */
/* start of the zip64 end of */
/* central directory */ + WORD;
/**
* Offset of the field that holds the location of the first
* central directory entry inside the "Zip64 end of central
* directory record" relative to the start of the "Zip64 end of
* central directory record".
*/
private static final int ZIP64_EOCD_CFD_LOCATOR_OFFSET =
/* zip64 end of central dir */
/* signature */ WORD
/* size of zip64 end of central */
/* directory record */ + DWORD
/* version made by */ + SHORT
/* version needed to extract */ + SHORT
/* number of this disk */ + WORD
/* number of the disk with the */
/* start of the central directory */ + WORD
/* total number of entries in the */
/* central directory on this disk */ + DWORD
/* total number of entries in the */
/* central directory */ + DWORD
/* size of the central directory */ + DWORD;
/**
* Searches for either the &quot;Zip64 end of central directory
* locator&quot; or the &quot;End of central dir record&quot;, parses
* it and positions the stream at the first central directory
* record.
*/
private void positionAtCentralDirectory()
throws IOException {
boolean found = tryToLocateSignature(MIN_EOCD_SIZE + ZIP64_EOCDL_LENGTH,
MAX_EOCD_SIZE + ZIP64_EOCDL_LENGTH,
ZipArchiveOutputStream
.ZIP64_EOCD_LOC_SIG);
if (!found) {
// not a ZIP64 archive
positionAtCentralDirectory32();
} else {
positionAtCentralDirectory64();
}
}
/**
* Parses the &quot;Zip64 end of central directory locator&quot;,
* finds the &quot;Zip64 end of central directory record&quot; using the
* parsed information, parses that and positions the stream at the
* first central directory record.
*/
private void positionAtCentralDirectory64()
throws IOException {
skipBytes(ZIP64_EOCDL_LOCATOR_OFFSET);
byte[] zip64EocdOffset = new byte[DWORD];
archive.readFully(zip64EocdOffset);
archive.seek(ZipEightByteInteger.getLongValue(zip64EocdOffset));
byte[] sig = new byte[WORD];
archive.readFully(sig);
if (sig[POS_0] != ZipArchiveOutputStream.ZIP64_EOCD_SIG[POS_0]
|| sig[POS_1] != ZipArchiveOutputStream.ZIP64_EOCD_SIG[POS_1]
|| sig[POS_2] != ZipArchiveOutputStream.ZIP64_EOCD_SIG[POS_2]
|| sig[POS_3] != ZipArchiveOutputStream.ZIP64_EOCD_SIG[POS_3]
) {
throw new ZipException("archive's ZIP64 end of central "
+ "directory locator is corrupt.");
}
skipBytes(ZIP64_EOCD_CFD_LOCATOR_OFFSET
- WORD /* signature has already been read */);
byte[] cfdOffset = new byte[DWORD];
archive.readFully(cfdOffset);
archive.seek(ZipEightByteInteger.getLongValue(cfdOffset));
}
/**
* Searches for the &quot;End of central dir record&quot;, parses
* it and positions the stream at the first central directory
* record.
*/
private void positionAtCentralDirectory32()
throws IOException {
boolean found = tryToLocateSignature(MIN_EOCD_SIZE, MAX_EOCD_SIZE,
ZipArchiveOutputStream.EOCD_SIG);
if (!found) {
throw new ZipException("archive is not a ZIP archive");
}
skipBytes(CFD_LOCATOR_OFFSET);
byte[] cfdOffset = new byte[WORD];
archive.readFully(cfdOffset);
archive.seek(ZipLong.getValue(cfdOffset));
}
/**
* Searches the archive backwards from minDistance to maxDistance
* for the given signature, positions the RandomaccessFile right
* at the signature if it has been found.
*/
private boolean tryToLocateSignature(long minDistanceFromEnd,
long maxDistanceFromEnd,
byte[] sig) throws IOException {
boolean found = false;
long off = archive.length() - minDistanceFromEnd;
final long stopSearching =
Math.max(0L, archive.length() - maxDistanceFromEnd);
if (off >= 0) {
for (; off >= stopSearching; off--) {
archive.seek(off);
int curr = archive.read();
if (curr == -1) {
break;
}
if (curr == sig[POS_0]) {
curr = archive.read();
if (curr == sig[POS_1]) {
curr = archive.read();
if (curr == sig[POS_2]) {
curr = archive.read();
if (curr == sig[POS_3]) {
found = true;
break;
}
}
}
}
}
}
if (found) {
archive.seek(off);
}
return found;
}
/**
* Skips the given number of bytes or throws an EOFException if
* skipping failed.
*/
private void skipBytes(final int count) throws IOException {
int totalSkipped = 0;
while (totalSkipped < count) {
int skippedNow = archive.skipBytes(count - totalSkipped);
if (skippedNow <= 0) {
throw new EOFException();
}
totalSkipped += skippedNow;
}
}
/**
* Number of bytes in local file header up to the &quot;length of
* filename&quot; entry.
*/
private static final long LFH_OFFSET_FOR_FILENAME_LENGTH =
/* local file header signature */ WORD
/* version needed to extract */ + SHORT
/* general purpose bit flag */ + SHORT
/* compression method */ + SHORT
/* last mod file time */ + SHORT
/* last mod file date */ + SHORT
/* crc-32 */ + WORD
/* compressed size */ + WORD
/* uncompressed size */ + WORD;
/**
* Walks through all recorded entries and adds the data available
* from the local file header.
* <p/>
* <p>Also records the offsets for the data to read from the
* entries.</p>
*/
private void resolveLocalFileHeaderData(Map<ZipArchiveEntry, NameAndComment>
entriesWithoutUTF8Flag)
throws IOException {
// changing the name of a ZipArchiveEntry is going to change
// the hashcode - see COMPRESS-164
// Map needs to be reconstructed in order to keep central
// directory order
Map<ZipArchiveEntry, OffsetEntry> origMap =
new LinkedHashMap<ZipArchiveEntry, OffsetEntry>(entries);
entries.clear();
for (Map.Entry<ZipArchiveEntry, OffsetEntry> ent : origMap.entrySet()) {
ZipArchiveEntry ze = ent.getKey();
OffsetEntry offsetEntry = ent.getValue();
long offset = offsetEntry.headerOffset;
archive.seek(offset + LFH_OFFSET_FOR_FILENAME_LENGTH);
byte[] b = new byte[SHORT];
archive.readFully(b);
int fileNameLen = ZipShort.getValue(b);
archive.readFully(b);
int extraFieldLen = ZipShort.getValue(b);
int lenToSkip = fileNameLen;
while (lenToSkip > 0) {
int skipped = archive.skipBytes(lenToSkip);
if (skipped <= 0) {
throw new IOException("failed to skip file name in"
+ " local file header");
}
lenToSkip -= skipped;
}
byte[] localExtraData = new byte[extraFieldLen];
archive.readFully(localExtraData);
ze.setExtra(localExtraData);
offsetEntry.dataOffset = offset + LFH_OFFSET_FOR_FILENAME_LENGTH
+ SHORT + SHORT + fileNameLen + extraFieldLen;
if (entriesWithoutUTF8Flag.containsKey(ze)) {
String orig = ze.getName();
NameAndComment nc = entriesWithoutUTF8Flag.get(ze);
ZipUtil.setNameAndCommentFromExtraFields(ze, nc.name,
nc.comment);
if (!orig.equals(ze.getName())) {
nameMap.remove(orig);
nameMap.put(ze.getName(), ze);
}
}
entries.put(ze, offsetEntry);
}
}
/**
* Checks whether the archive starts with a LFH. If it doesn't,
* it may be an empty archive.
*/
private boolean startsWithLocalFileHeader() throws IOException {
archive.seek(0);
final byte[] start = new byte[WORD];
archive.readFully(start);
for (int i = 0; i < start.length; i++) {
if (start[i] != ZipArchiveOutputStream.LFH_SIG[i]) {
return false;
}
}
return true;
}
/**
* InputStream that delegates requests to the underlying
* RandomAccessFile, making sure that only bytes from a certain
* range can be read.
*/
private class BoundedInputStream extends InputStream {
private long remaining;
private long loc;
private boolean addDummyByte = false;
BoundedInputStream(long start, long remaining) {
this.remaining = remaining;
loc = start;
}
@Override
public int read() throws IOException {
if (remaining-- <= 0) {
if (addDummyByte) {
addDummyByte = false;
return 0;
}
return -1;
}
synchronized (archive) {
archive.seek(loc++);
return archive.read();
}
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (remaining <= 0) {
if (addDummyByte) {
addDummyByte = false;
b[off] = 0;
return 1;
}
return -1;
}
if (len <= 0) {
return 0;
}
if (len > remaining) {
len = (int) remaining;
}
int ret = -1;
synchronized (archive) {
archive.seek(loc);
ret = archive.read(b, off, len);
}
if (ret > 0) {
loc += ret;
remaining -= ret;
}
return ret;
}
/**
* Inflater needs an extra dummy byte for nowrap - see
* Inflater's javadocs.
*/
void addDummy() {
addDummyByte = true;
}
}
private static final class NameAndComment {
private final byte[] name;
private final byte[] comment;
private NameAndComment(byte[] name, byte[] comment) {
this.name = name;
this.comment = comment;
}
}
/**
* Compares two ZipArchiveEntries based on their offset within the archive.
* <p/>
* <p>Won't return any meaningful results if one of the entries
* isn't part of the archive at all.</p>
*/
private final Comparator<ZipArchiveEntry> OFFSET_COMPARATOR =
new Comparator<ZipArchiveEntry>() {
public int compare(ZipArchiveEntry e1, ZipArchiveEntry e2) {
if (e1 == e2) {
return 0;
}
OffsetEntry off1 = entries.get(e1);
OffsetEntry off2 = entries.get(e2);
if (off1 == null) {
return 1;
}
if (off2 == null) {
return -1;
}
long val = (off1.headerOffset - off2.headerOffset);
return val == 0 ? 0 : val < 0 ? -1 : +1;
}
};
}

View file

@ -0,0 +1,172 @@
package org.xbib.io.archive.zip;
import static org.xbib.io.archive.zip.ZipConstants.BYTE_MASK;
import static org.xbib.io.archive.zip.ZipConstants.WORD;
/**
* Utility class that represents a four byte integer with conversion
* rules for the big endian byte order of ZIP files.
*/
public final class ZipLong implements Cloneable {
private static final int BYTE_1 = 1;
private static final int BYTE_1_MASK = 0xFF00;
private static final int BYTE_1_SHIFT = 8;
private static final int BYTE_2 = 2;
private static final int BYTE_2_MASK = 0xFF0000;
private static final int BYTE_2_SHIFT = 16;
private static final int BYTE_3 = 3;
private static final long BYTE_3_MASK = 0xFF000000L;
private static final int BYTE_3_SHIFT = 24;
private final long value;
/**
* Central File Header Signature
*/
public static final ZipLong CFH_SIG = new ZipLong(0X02014B50L);
/**
* Local File Header Signature
*/
public static final ZipLong LFH_SIG = new ZipLong(0X04034B50L);
/**
* Data Descriptor signature
*/
public static final ZipLong DD_SIG = new ZipLong(0X08074B50L);
/**
* Value stored in size and similar fields if ZIP64 extensions are
* used.
*/
static final ZipLong ZIP64_MAGIC = new ZipLong(ZipConstants.ZIP64_MAGIC);
/**
* Create instance from a number.
*
* @param value the long to store as a ZipLong
*/
public ZipLong(long value) {
this.value = value;
}
/**
* Create instance from bytes.
*
* @param bytes the bytes to store as a ZipLong
*/
public ZipLong(byte[] bytes) {
this(bytes, 0);
}
/**
* Create instance from the four bytes starting at offset.
*
* @param bytes the bytes to store as a ZipLong
* @param offset the offset to start
*/
public ZipLong(byte[] bytes, int offset) {
value = ZipLong.getValue(bytes, offset);
}
/**
* Get value as four bytes in big endian byte order.
*
* @return value as four bytes in big endian order
*/
public byte[] getBytes() {
return ZipLong.getBytes(value);
}
/**
* Get value as Java long.
*
* @return value as a long
*/
public long getValue() {
return value;
}
/**
* Get value as four bytes in big endian byte order.
*
* @param value the value to convert
* @return value as four bytes in big endian byte order
*/
public static byte[] getBytes(long value) {
byte[] result = new byte[WORD];
result[0] = (byte) ((value & BYTE_MASK));
result[BYTE_1] = (byte) ((value & BYTE_1_MASK) >> BYTE_1_SHIFT);
result[BYTE_2] = (byte) ((value & BYTE_2_MASK) >> BYTE_2_SHIFT);
result[BYTE_3] = (byte) ((value & BYTE_3_MASK) >> BYTE_3_SHIFT);
return result;
}
/**
* Helper method to get the value as a Java long from four bytes starting at given array offset
*
* @param bytes the array of bytes
* @param offset the offset to start
* @return the corresponding Java long value
*/
public static long getValue(byte[] bytes, int offset) {
long value = (bytes[offset + BYTE_3] << BYTE_3_SHIFT) & BYTE_3_MASK;
value += (bytes[offset + BYTE_2] << BYTE_2_SHIFT) & BYTE_2_MASK;
value += (bytes[offset + BYTE_1] << BYTE_1_SHIFT) & BYTE_1_MASK;
value += (bytes[offset] & BYTE_MASK);
return value;
}
/**
* Helper method to get the value as a Java long from a four-byte array
*
* @param bytes the array of bytes
* @return the corresponding Java long value
*/
public static long getValue(byte[] bytes) {
return getValue(bytes, 0);
}
/**
* Override to make two instances with same value equal.
*
* @param o an object to compare
* @return true if the objects are equal
*/
@Override
public boolean equals(Object o) {
if (o == null || !(o instanceof ZipLong)) {
return false;
}
return value == ((ZipLong) o).getValue();
}
/**
* Override to make two instances with same value equal.
*
* @return the value stored in the ZipLong
*/
@Override
public int hashCode() {
return (int) value;
}
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException cnfe) {
// impossible
throw new RuntimeException(cnfe);
}
}
@Override
public String toString() {
return "ZipLong value: " + value;
}
}

View file

@ -0,0 +1,139 @@
package org.xbib.io.archive.zip;
import static org.xbib.io.archive.zip.ZipConstants.BYTE_MASK;
/**
* Utility class that represents a two byte integer with conversion
* rules for the big endian byte order of ZIP files.
*/
public final class ZipShort implements Cloneable {
private static final int BYTE_1_MASK = 0xFF00;
private static final int BYTE_1_SHIFT = 8;
private final int value;
/**
* Create instance from a number.
*
* @param value the int to store as a ZipShort
*/
public ZipShort(int value) {
this.value = value;
}
/**
* Create instance from bytes.
*
* @param bytes the bytes to store as a ZipShort
*/
public ZipShort(byte[] bytes) {
this(bytes, 0);
}
/**
* Create instance from the two bytes starting at offset.
*
* @param bytes the bytes to store as a ZipShort
* @param offset the offset to start
*/
public ZipShort(byte[] bytes, int offset) {
value = ZipShort.getValue(bytes, offset);
}
/**
* Get value as two bytes in big endian byte order.
*
* @return the value as a a two byte array in big endian byte order
*/
public byte[] getBytes() {
byte[] result = new byte[2];
result[0] = (byte) (value & BYTE_MASK);
result[1] = (byte) ((value & BYTE_1_MASK) >> BYTE_1_SHIFT);
return result;
}
/**
* Get value as Java int.
*
* @return value as a Java int
*/
public int getValue() {
return value;
}
/**
* Get value as two bytes in big endian byte order.
*
* @param value the Java int to convert to bytes
* @return the converted int as a byte array in big endian byte order
*/
public static byte[] getBytes(int value) {
byte[] result = new byte[2];
result[0] = (byte) (value & BYTE_MASK);
result[1] = (byte) ((value & BYTE_1_MASK) >> BYTE_1_SHIFT);
return result;
}
/**
* Helper method to get the value as a java int from two bytes starting at given array offset
*
* @param bytes the array of bytes
* @param offset the offset to start
* @return the corresponding java int value
*/
public static int getValue(byte[] bytes, int offset) {
int value = (bytes[offset + 1] << BYTE_1_SHIFT) & BYTE_1_MASK;
value += (bytes[offset] & BYTE_MASK);
return value;
}
/**
* Helper method to get the value as a java int from a two-byte array
*
* @param bytes the array of bytes
* @return the corresponding java int value
*/
public static int getValue(byte[] bytes) {
return getValue(bytes, 0);
}
/**
* Override to make two instances with same value equal.
*
* @param o an object to compare
* @return true if the objects are equal
*/
@Override
public boolean equals(Object o) {
if (o == null || !(o instanceof ZipShort)) {
return false;
}
return value == ((ZipShort) o).getValue();
}
/**
* Override to make two instances with same value equal.
*
* @return the value stored in the ZipShort
*/
@Override
public int hashCode() {
return value;
}
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException cnfe) {
// impossible
throw new RuntimeException(cnfe);
}
}
@Override
public String toString() {
return "ZipShort value: " + value;
}
}

View file

@ -0,0 +1,172 @@
package org.xbib.io.archive.zip;
import org.xbib.io.archive.entry.ArchiveEntryEncodingHelper;
import java.io.IOException;
import java.util.Calendar;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
/**
* Utility class for handling DOS and Java time conversions.
*/
public abstract class ZipUtil {
/**
* Smallest date/time ZIP can handle.
*/
private static final byte[] DOS_TIME_MIN = ZipLong.getBytes(0x00002100L);
/**
* Convert a Date object to a DOS date/time field.
* Stolen from InfoZip's <code>fileio.c</code>
*
* @param t number of milliseconds since the epoch
* @return the date as a byte array
*/
public static byte[] toDosTime(long t) {
Calendar c = Calendar.getInstance();
c.setTimeInMillis(t);
int year = c.get(Calendar.YEAR);
if (year < 1980) {
return copy(DOS_TIME_MIN); // stop callers from changing the array
}
int month = c.get(Calendar.MONTH) + 1;
long value = ((year - 1980) << 25)
| (month << 21)
| (c.get(Calendar.DAY_OF_MONTH) << 16)
| (c.get(Calendar.HOUR_OF_DAY) << 11)
| (c.get(Calendar.MINUTE) << 5)
| (c.get(Calendar.SECOND) >> 1);
return ZipLong.getBytes(value);
}
/**
* Converts DOS time to Java time (number of milliseconds since
* epoch).
*/
public static long dosToJavaTime(long dosTime) {
Calendar cal = Calendar.getInstance();
// CheckStyle:MagicNumberCheck OFF - no point
cal.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980);
cal.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1);
cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f);
cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f);
cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f);
cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e);
// CheckStyle:MagicNumberCheck ON
return cal.getTime().getTime();
}
/**
* If the entry has Unicode*ExtraFields and the CRCs of the
* names/comments match those of the extra fields, transfer the
* known Unicode values from the extra field.
*/
static void setNameAndCommentFromExtraFields(ZipArchiveEntry ze,
byte[] originalNameBytes,
byte[] commentBytes) {
UnicodePathExtraField name = (UnicodePathExtraField)
ze.getExtraField(UnicodePathExtraField.UPATH_ID);
String originalName = ze.getName();
String newName = getUnicodeStringIfOriginalMatches(name,
originalNameBytes);
if (newName != null && !originalName.equals(newName)) {
ze.setName(newName);
}
if (commentBytes != null && commentBytes.length > 0) {
UnicodeCommentExtraField cmt = (UnicodeCommentExtraField)
ze.getExtraField(UnicodeCommentExtraField.UCOM_ID);
String newComment =
getUnicodeStringIfOriginalMatches(cmt, commentBytes);
if (newComment != null) {
ze.setComment(newComment);
}
}
}
/**
* If the stored CRC matches the one of the given name, return the
* Unicode name of the given field.
* If the field is null or the CRCs don't match, return null
* instead.
*/
private static String getUnicodeStringIfOriginalMatches(AbstractUnicodeExtraField f,
byte[] orig) {
if (f != null) {
CRC32 crc32 = new CRC32();
crc32.update(orig);
long origCRC32 = crc32.getValue();
if (origCRC32 == f.getNameCRC32()) {
try {
return ArchiveEntryEncodingHelper.UTF8_ENCODING.decode(f.getUnicodeName());
} catch (IOException ex) {
return null;
}
}
}
return null;
}
/**
* Create a copy of the given array - or return null if the
* argument is null.
*/
static byte[] copy(byte[] from) {
if (from != null) {
byte[] to = new byte[from.length];
System.arraycopy(from, 0, to, 0, to.length);
return to;
}
return null;
}
/**
* Whether this library is able to read or write the given entry.
*/
static boolean canHandleEntryData(ZipArchiveEntry entry) {
return supportsEncryptionOf(entry) && supportsMethodOf(entry);
}
/**
* Whether this library supports the encryption used by the given
* entry.
*
* @return true if the entry isn't encrypted at all
*/
private static boolean supportsEncryptionOf(ZipArchiveEntry entry) {
return !entry.getGeneralPurposeBit().usesEncryption();
}
/**
* Whether this library supports the compression method used by
* the given entry.
*
* @return true if the compression method is STORED or DEFLATED
*/
private static boolean supportsMethodOf(ZipArchiveEntry entry) {
return entry.getMethod() == ZipEntry.STORED
|| entry.getMethod() == ZipEntry.DEFLATED;
}
/**
* Checks whether the entry requires features not (yet) supported
* by the library and throws an exception if it does.
*/
static void checkRequestedFeatures(ZipArchiveEntry ze)
throws UnsupportedZipFeatureException {
if (!supportsEncryptionOf(ze)) {
throw
new UnsupportedZipFeatureException(UnsupportedZipFeatureException
.Feature.ENCRYPTION, ze);
}
if (!supportsMethodOf(ze)) {
throw
new UnsupportedZipFeatureException(UnsupportedZipFeatureException
.Feature.METHOD, ze);
}
}
}

View file

@ -0,0 +1,24 @@
package org.xbib.io.archive.zip;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
public class ZipTest {
@Test
public void testZip() throws Exception {
InputStream in = getClass().getResourceAsStream("test.zip");
ZipArchiveInputStream z = new ZipArchiveInputStream(in);
byte[] buffer = new byte[1024];
long total = 0L;
while ((z.getNextEntry()) != null) {
int len = 0;
while ((len = z.read(buffer)) > 0) {
total += len;
}
}
assertEquals(1813L, total);
z.close();
}
}

View file

@ -0,0 +1,5 @@
module org.xbib.io.archive {
exports org.xbib.io.archive.entry;
exports org.xbib.io.archive.stream;
exports org.xbib.io.archive.util;
}

View file

@ -0,0 +1,44 @@
package org.xbib.io.archive.entry;
import java.util.Date;
/**
* Represents an entry of an archive.
*/
public interface ArchiveEntry {
/**
* Special value indicating that the size is unknown
*/
long SIZE_UNKNOWN = -1;
ArchiveEntry setName(String name);
/**
* The name of the entry in the archive. May refer to a file or directory or other item
*/
String getName();
/**
* Set the entry size in bytes
*/
ArchiveEntry setEntrySize(long size);
/**
* The size of the entry. May be -1 (SIZE_UNKNOWN) if the size is unknown
*/
long getEntrySize();
ArchiveEntry setLastModified(Date date);
/**
* The last modified date of the entry.
*/
Date getLastModified();
/**
* True if the entry refers to a directory
*/
boolean isDirectory();
}

View file

@ -0,0 +1,61 @@
package org.xbib.io.archive.entry;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* An interface for encoders that do a pretty encoding of archive
* filenames.
* There are mostly two implementations, one that uses java.nio
* {@link java.nio.charset.Charset Charset} and one implementation,
* which copes with simple 8 bit charsets, because java-1.4 did not
* support Cp437 in java.nio.
* The main reason for defining an own encoding layer comes from
* the problems with {@link String#getBytes(String)
* String.getBytes}, which encodes unknown characters as ASCII
* quotation marks ('?'). Quotation marks are per definition an
* invalid filename on some operating systems like Windows, which
* leads to ignored ZIP entries. All implementations should
* implement this interface in a
* reentrant way.
*/
public interface ArchiveEntryEncoding {
/**
* Check, whether the given string may be losslessly encoded using this
* encoding.
*
* @param name A filename or ZIP comment.
* @return Whether the given name may be encoded with out any losses.
*/
boolean canEncode(String name);
/**
* Encode a filename or a comment to a byte array suitable for
* storing it to a zip entry.
* Examples for CP 437 (in pseudo-notation, right hand side is
* C-style notation):
* <pre>
* encode("\u20AC_for_Dollar.txt") = "%U20AC_for_Dollar.txt"
* encode("\u00D6lf\u00E4sser.txt") = "\231lf\204sser.txt"
* </pre>
*
* @param name A filename or ZIP comment.
* @return A byte buffer with a backing array containing the
* encoded name. Unmappable characters or malformed
* character sequences are mapped to a sequence of utf-16
* words encoded in the format <code>%Uxxxx</code>. It is
* assumed, that the byte buffer is positioned at the
* beginning of the encoded result, the byte buffer has a
* backing array and the limit of the byte buffer points
* to the end of the encoded result.
* @throws java.io.IOException
*/
ByteBuffer encode(String name) throws IOException;
/**
* @param data The byte values to decode.
* @return The decoded string.
* @throws java.io.IOException
*/
String decode(byte[] data) throws IOException;
}

View file

@ -0,0 +1,222 @@
package org.xbib.io.archive.entry;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.HashMap;
import java.util.Map;
/**
* Static helper functions for encoding filenames in archives.
*/
public abstract class ArchiveEntryEncodingHelper {
/**
* A class, which holds the high characters of a simple encoding
* and lazily instantiates a {@link Simple8BitArchiveEntryEncoding} instance in a
* thread-safe manner.
*/
private static class SimpleEncodingHolder {
private final char[] highChars;
private Simple8BitArchiveEntryEncoding encoding;
/**
* Instantiate a simple encoding holder.
*
* @param highChars The characters for byte codes 128 to 255.
* @see Simple8BitArchiveEntryEncoding#Simple8BitArchiveEntryEncoding(char[])
*/
SimpleEncodingHolder(char[] highChars) {
this.highChars = highChars;
}
/**
* @return The associated {@link Simple8BitArchiveEntryEncoding}, which
* is instantiated if not done so far.
*/
public synchronized Simple8BitArchiveEntryEncoding getEncoding() {
if (this.encoding == null) {
this.encoding = new Simple8BitArchiveEntryEncoding(this.highChars);
}
return this.encoding;
}
}
private static final Map<String, SimpleEncodingHolder> simpleEncodings;
static {
simpleEncodings = new HashMap<String, SimpleEncodingHolder>();
char[] cp437_high_chars =
new char[]{0x00c7, 0x00fc, 0x00e9, 0x00e2, 0x00e4, 0x00e0,
0x00e5, 0x00e7, 0x00ea, 0x00eb, 0x00e8, 0x00ef,
0x00ee, 0x00ec, 0x00c4, 0x00c5, 0x00c9, 0x00e6,
0x00c6, 0x00f4, 0x00f6, 0x00f2, 0x00fb, 0x00f9,
0x00ff, 0x00d6, 0x00dc, 0x00a2, 0x00a3, 0x00a5,
0x20a7, 0x0192, 0x00e1, 0x00ed, 0x00f3, 0x00fa,
0x00f1, 0x00d1, 0x00aa, 0x00ba, 0x00bf, 0x2310,
0x00ac, 0x00bd, 0x00bc, 0x00a1, 0x00ab, 0x00bb,
0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561,
0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557,
0x255d, 0x255c, 0x255b, 0x2510, 0x2514, 0x2534,
0x252c, 0x251c, 0x2500, 0x253c, 0x255e, 0x255f,
0x255a, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550,
0x256c, 0x2567, 0x2568, 0x2564, 0x2565, 0x2559,
0x2558, 0x2552, 0x2553, 0x256b, 0x256a, 0x2518,
0x250c, 0x2588, 0x2584, 0x258c, 0x2590, 0x2580,
0x03b1, 0x00df, 0x0393, 0x03c0, 0x03a3, 0x03c3,
0x00b5, 0x03c4, 0x03a6, 0x0398, 0x03a9, 0x03b4,
0x221e, 0x03c6, 0x03b5, 0x2229, 0x2261, 0x00b1,
0x2265, 0x2264, 0x2320, 0x2321, 0x00f7, 0x2248,
0x00b0, 0x2219, 0x00b7, 0x221a, 0x207f, 0x00b2,
0x25a0, 0x00a0};
SimpleEncodingHolder cp437 = new SimpleEncodingHolder(cp437_high_chars);
simpleEncodings.put("CP437", cp437);
simpleEncodings.put("Cp437", cp437);
simpleEncodings.put("cp437", cp437);
simpleEncodings.put("IBM437", cp437);
simpleEncodings.put("ibm437", cp437);
char[] cp850_high_chars =
new char[]{0x00c7, 0x00fc, 0x00e9, 0x00e2, 0x00e4, 0x00e0,
0x00e5, 0x00e7, 0x00ea, 0x00eb, 0x00e8, 0x00ef,
0x00ee, 0x00ec, 0x00c4, 0x00c5, 0x00c9, 0x00e6,
0x00c6, 0x00f4, 0x00f6, 0x00f2, 0x00fb, 0x00f9,
0x00ff, 0x00d6, 0x00dc, 0x00f8, 0x00a3, 0x00d8,
0x00d7, 0x0192, 0x00e1, 0x00ed, 0x00f3, 0x00fa,
0x00f1, 0x00d1, 0x00aa, 0x00ba, 0x00bf, 0x00ae,
0x00ac, 0x00bd, 0x00bc, 0x00a1, 0x00ab, 0x00bb,
0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x00c1,
0x00c2, 0x00c0, 0x00a9, 0x2563, 0x2551, 0x2557,
0x255d, 0x00a2, 0x00a5, 0x2510, 0x2514, 0x2534,
0x252c, 0x251c, 0x2500, 0x253c, 0x00e3, 0x00c3,
0x255a, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550,
0x256c, 0x00a4, 0x00f0, 0x00d0, 0x00ca, 0x00cb,
0x00c8, 0x0131, 0x00cd, 0x00ce, 0x00cf, 0x2518,
0x250c, 0x2588, 0x2584, 0x00a6, 0x00cc, 0x2580,
0x00d3, 0x00df, 0x00d4, 0x00d2, 0x00f5, 0x00d5,
0x00b5, 0x00fe, 0x00de, 0x00da, 0x00db, 0x00d9,
0x00fd, 0x00dd, 0x00af, 0x00b4, 0x00ad, 0x00b1,
0x2017, 0x00be, 0x00b6, 0x00a7, 0x00f7, 0x00b8,
0x00b0, 0x00a8, 0x00b7, 0x00b9, 0x00b3, 0x00b2,
0x25a0, 0x00a0};
SimpleEncodingHolder cp850 = new SimpleEncodingHolder(cp850_high_chars);
simpleEncodings.put("CP850", cp850);
simpleEncodings.put("Cp850", cp850);
simpleEncodings.put("cp850", cp850);
simpleEncodings.put("IBM850", cp850);
simpleEncodings.put("ibm850", cp850);
}
/**
* Grow a byte buffer, so it has a minimal capacity or at least
* the double capacity of the original buffer
*
* @param b The original buffer.
* @param newCapacity The minimal requested new capacity.
* @return A byte buffer <code>r</code> with
* <code>r.capacity() = max(b.capacity()*2,newCapacity)</code> and
* all the data contained in <code>b</code> copied to the beginning
* of <code>r</code>.
*/
static ByteBuffer growBuffer(ByteBuffer b, int newCapacity) {
b.limit(b.position());
b.rewind();
int c2 = b.capacity() * 2;
ByteBuffer on = ByteBuffer.allocate(c2 < newCapacity ? newCapacity : c2);
on.put(b);
return on;
}
/**
* The hexadecimal digits <code>0,...,9,A,...,F</code> encoded as
* ASCII bytes.
*/
private static final byte[] HEX_DIGITS =
new byte[]{
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41,
0x42, 0x43, 0x44, 0x45, 0x46
};
/**
* Append <code>%Uxxxx</code> to the given byte buffer.
* The caller must assure, that <code>bb.remaining()&gt;=6</code>.
*
* @param bb The byte buffer to write to.
* @param c The character to write.
*/
public static void appendSurrogate(ByteBuffer bb, char c) {
bb.put((byte) '%');
bb.put((byte) 'U');
bb.put(HEX_DIGITS[(c >> 12) & 0x0f]);
bb.put(HEX_DIGITS[(c >> 8) & 0x0f]);
bb.put(HEX_DIGITS[(c >> 4) & 0x0f]);
bb.put(HEX_DIGITS[c & 0x0f]);
}
/**
* name of the encoding UTF-8
*/
public static final String UTF8 = "UTF8";
/**
* variant name of the encoding UTF-8 used for comparisions.
*/
private static final String UTF_DASH_8 = "UTF_8";
/**
* name of the encoding UTF-8
*/
public static final ArchiveEntryEncoding UTF8_ENCODING = new FallbackArchiveEntryEncoding(UTF8);
/**
* Instantiates an encoding.
*
* @param name The name of the encoding. Specify {@code null} for
* the platform's default encoding.
* @return An encoding for the given encoding name.
*/
public static ArchiveEntryEncoding getEncoding(String name) {
// fallback encoding is good enough for utf-8.
if (isUTF8(name)) {
return UTF8_ENCODING;
}
if (name == null) {
return new FallbackArchiveEntryEncoding();
}
SimpleEncodingHolder h = simpleEncodings.get(name);
if (h != null) {
return h.getEncoding();
}
try {
Charset cs = Charset.forName(name);
return new NioArchiveEntryEncoding(cs);
} catch (UnsupportedCharsetException e) {
return new FallbackArchiveEntryEncoding(name);
}
}
/**
* Whether a given encoding - or the platform's default encoding
* if the parameter is null - is UTF-8.
*/
public static boolean isUTF8(String encoding) {
if (encoding == null) {
// check platform's default encoding
encoding = System.getProperty("file.encoding");
}
return UTF8.equalsIgnoreCase(encoding)
|| UTF_DASH_8.equalsIgnoreCase(encoding);
}
}

View file

@ -0,0 +1,62 @@
package org.xbib.io.archive.entry;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* A fallback ZipEncoding, which uses a java.io means to encode names.
* <p/>
* <p>This implementation is not suitable for encodings other than
* utf-8, because java.io encodes unmappable character as question
* marks leading to unreadable ZIP entries on some operating
* systems.</p>
* <p/>
* <p>Furthermore this implementation is unable to tell whether a
* given name can be safely encoded or not.</p>
* <p/>
* <p>This implementation acts as a last resort implementation, when
* neither {@link Simple8BitArchiveEntryEncoding} nor {@link NioArchiveEntryEncoding} is
* available.</p>
*/
class FallbackArchiveEntryEncoding implements ArchiveEntryEncoding {
private final String charset;
/**
* Construct a fallback zip encoding, which uses the platform's
* default charset.
*/
public FallbackArchiveEntryEncoding() {
this.charset = null;
}
/**
* Construct a fallback zip encoding, which uses the given charset.
*
* @param charset The name of the charset or {@code null} for
* the platform's default character set.
*/
public FallbackArchiveEntryEncoding(String charset) {
this.charset = charset;
}
public boolean canEncode(String name) {
return true;
}
public ByteBuffer encode(String name) throws IOException {
if (this.charset == null) { // i.e. use default charset, see no-args constructor
return ByteBuffer.wrap(name.getBytes());
} else {
return ByteBuffer.wrap(name.getBytes(this.charset));
}
}
public String decode(byte[] data) throws IOException {
if (this.charset == null) { // i.e. use default charset, see no-args constructor
return new String(data);
} else {
return new String(data, this.charset);
}
}
}

View file

@ -0,0 +1,92 @@
package org.xbib.io.archive.entry;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
/**
* A ZipEncoding, which uses a java.nio {@link
* java.nio.charset.Charset Charset} to encode names.
* <p/>
* <p>This implementation works for all cases under java-1.5 or
* later. However, in java-1.4, some charsets don't have a java.nio
* implementation, most notably the default ZIP encoding Cp437.</p>
* <p/>
* <p>The methods of this class are reentrant.</p>
*/
class NioArchiveEntryEncoding implements ArchiveEntryEncoding {
private final Charset charset;
/**
* Construct an NIO based zip encoding, which wraps the given
* charset.
*
* @param charset The NIO charset to wrap.
*/
public NioArchiveEntryEncoding(Charset charset) {
this.charset = charset;
}
public boolean canEncode(String name) {
CharsetEncoder enc = this.charset.newEncoder();
enc.onMalformedInput(CodingErrorAction.REPORT);
enc.onUnmappableCharacter(CodingErrorAction.REPORT);
return enc.canEncode(name);
}
public ByteBuffer encode(String name) {
CharsetEncoder enc = this.charset.newEncoder();
enc.onMalformedInput(CodingErrorAction.REPORT);
enc.onUnmappableCharacter(CodingErrorAction.REPORT);
CharBuffer cb = CharBuffer.wrap(name);
ByteBuffer out = ByteBuffer.allocate(name.length()
+ (name.length() + 1) / 2);
while (cb.remaining() > 0) {
CoderResult res = enc.encode(cb, out, true);
if (res.isUnmappable() || res.isMalformed()) {
// write the unmappable characters in utf-16
// pseudo-URL encoding style to ByteBuffer.
if (res.length() * 6 > out.remaining()) {
out = ArchiveEntryEncodingHelper.growBuffer(out, out.position()
+ res.length() * 6);
}
for (int i = 0; i < res.length(); ++i) {
ArchiveEntryEncodingHelper.appendSurrogate(out, cb.get());
}
} else if (res.isOverflow()) {
out = ArchiveEntryEncodingHelper.growBuffer(out, 0);
} else if (res.isUnderflow()) {
enc.flush(out);
break;
}
}
out.limit(out.position());
out.rewind();
return out;
}
public String decode(byte[] data) throws IOException {
return this.charset.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(data)).toString();
}
}

View file

@ -0,0 +1,212 @@
package org.xbib.io.archive.entry;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* This ZipEncoding implementation implements a simple 8bit character
* set, which mets the following restrictions:
* <p/>
* <ul>
* <li>Characters 0x0000 to 0x007f are encoded as the corresponding
* byte values 0x00 to 0x7f.</li>
* <li>All byte codes from 0x80 to 0xff are mapped to a unique unicode
* character in the range 0x0080 to 0x7fff. (No support for
* UTF-16 surrogates)
* </ul>
* <p/>
* <p>These restrictions most notably apply to the most prominent
* omissions of java-1.4's {@link java.nio.charset.Charset Charset}
* implementation, Cp437 and Cp850.</p>
* <p/>
* <p>The methods of this class are reentrant.</p>
*/
class Simple8BitArchiveEntryEncoding implements ArchiveEntryEncoding {
/**
* A character entity, which is put to the reverse mapping table
* of a simple encoding.
*/
private static final class Simple8BitChar implements Comparable<Simple8BitChar> {
public final char unicode;
public final byte code;
Simple8BitChar(byte code, char unicode) {
this.code = code;
this.unicode = unicode;
}
public int compareTo(Simple8BitChar a) {
return this.unicode - a.unicode;
}
@Override
public String toString() {
return "0x" + Integer.toHexString(0xffff & unicode)
+ "->0x" + Integer.toHexString(0xff & code);
}
@Override
public boolean equals(Object o) {
if (o instanceof Simple8BitChar) {
Simple8BitChar other = (Simple8BitChar) o;
return unicode == other.unicode && code == other.code;
}
return false;
}
@Override
public int hashCode() {
return unicode;
}
}
/**
* The characters for byte values of 128 to 255 stored as an array of
* 128 chars.
*/
private final char[] highChars;
/**
* A list of {@link Simple8BitArchiveEntryEncoding.Simple8BitChar} objects sorted by the unicode
* field. This list is used to binary search reverse mapping of
* unicode characters with a character code greater than 127.
*/
private final List<Simple8BitChar> reverseMapping;
/**
* @param highChars The characters for byte values of 128 to 255
* stored as an array of 128 chars.
*/
public Simple8BitArchiveEntryEncoding(char[] highChars) {
this.highChars = highChars.clone();
List<Simple8BitChar> temp = new ArrayList<Simple8BitChar>(this.highChars.length);
byte code = 127;
for (char highChar : this.highChars) {
temp.add(new Simple8BitChar(++code, highChar));
}
Collections.sort(temp);
this.reverseMapping = Collections.unmodifiableList(temp);
}
/**
* Return the character code for a given encoded byte.
*
* @param b The byte to decode.
* @return The associated character value.
*/
public char decodeByte(byte b) {
// code 0-127
if (b >= 0) {
return (char) b;
}
// byte is signed, so 128 == -128 and 255 == -1
return this.highChars[128 + b];
}
/**
* @param c The character to encode.
* @return Whether the given unicode character is covered by this encoding.
*/
public boolean canEncodeChar(char c) {
if (c >= 0 && c < 128) {
return true;
}
Simple8BitChar r = this.encodeHighChar(c);
return r != null;
}
/**
* Pushes the encoded form of the given character to the given byte buffer.
*
* @param bb The byte buffer to write to.
* @param c The character to encode.
* @return Whether the given unicode character is covered by this encoding.
* If {@code false} is returned, nothing is pushed to the
* byte buffer.
*/
public boolean pushEncodedChar(ByteBuffer bb, char c) {
if (c >= 0 && c < 128) {
bb.put((byte) c);
return true;
}
Simple8BitChar r = this.encodeHighChar(c);
if (r == null) {
return false;
}
bb.put(r.code);
return true;
}
/**
* @param c A unicode character in the range from 0x0080 to 0x7f00
* @return A Simple8BitChar, if this character is covered by this encoding.
* A {@code null} value is returned, if this character is not
* covered by this encoding.
*/
private Simple8BitChar encodeHighChar(char c) {
// for performance an simplicity, yet another reincarnation of
// binary search...
int i0 = 0;
int i1 = this.reverseMapping.size();
while (i1 > i0) {
int i = i0 + (i1 - i0) / 2;
Simple8BitChar m = this.reverseMapping.get(i);
if (m.unicode == c) {
return m;
}
if (m.unicode < c) {
i0 = i + 1;
} else {
i1 = i;
}
}
if (i0 >= this.reverseMapping.size()) {
return null;
}
Simple8BitChar r = this.reverseMapping.get(i0);
if (r.unicode != c) {
return null;
}
return r;
}
public boolean canEncode(String name) {
for (int i = 0; i < name.length(); ++i) {
char c = name.charAt(i);
if (!this.canEncodeChar(c)) {
return false;
}
}
return true;
}
public ByteBuffer encode(String name) {
ByteBuffer out = ByteBuffer.allocate(name.length() + 6 + (name.length() + 1) / 2);
for (int i = 0; i < name.length(); ++i) {
char c = name.charAt(i);
if (out.remaining() < 6) {
out = ArchiveEntryEncodingHelper.growBuffer(out, out.position() + 6);
}
if (!this.pushEncodedChar(out, c)) {
ArchiveEntryEncodingHelper.appendSurrogate(out, c);
}
}
out.limit(out.position());
out.rewind();
return out;
}
public String decode(byte[] data) throws IOException {
char[] ret = new char[data.length];
for (int i = 0; i < data.length; ++i) {
ret[i] = this.decodeByte(data[i]);
}
return new String(ret);
}
}

View file

@ -0,0 +1,43 @@
package org.xbib.io.archive.stream;
import org.xbib.io.archive.entry.ArchiveEntry;
import java.io.IOException;
import java.io.InputStream;
/**
* Archive input streams must override the
* {@link #read(byte[], int, int)} - or {@link #read()} -
* method so that reading from the stream generates EOF for the end of
* data in each entry as well as at the end of the file proper.
* The {@link #getNextEntry()} method is used to reset the input stream
* ready for reading the data from the next entry.
*/
public abstract class ArchiveInputStream<E extends ArchiveEntry> extends InputStream {
/**
* Returns the next archive entry in this stream.
*
* @return the next entry,
* or {@code null} if there are no more entries
* @throws java.io.IOException if the next entry could not be read
*/
public abstract E getNextEntry() throws IOException;
/**
* Reads a byte of data. This method will block until enough input is
* available.
* Simply calls the {@link #read(byte[], int, int)} method.
* MUST be overridden if the {@link #read(byte[], int, int)} method
* is not overridden; may be overridden otherwise.
*
* @return the byte read, or -1 if end of input is reached
* @throws IOException if an I/O error has occurred
*/
@Override
public int read() throws IOException {
byte[] b = new byte[1];
int num = read(b, 0, 1);
return num == -1 ? -1 : b[0] & 0xFF;
}
}

View file

@ -0,0 +1,78 @@
package org.xbib.io.archive.stream;
import org.xbib.io.archive.entry.ArchiveEntry;
import java.io.IOException;
import java.io.OutputStream;
/**
* Archive output stream implementations are expected to override the
* {@link #write(byte[], int, int)} method to improve performance.
* They should also override {@link #close()} to ensure that any necessary
* trailers are added.
* The normal sequence of calls for working with ArchiveOutputStreams is:
* + create ArchiveOutputStream object
* + write SFX header (optional, Zip only)
* + repeat as needed:
* - putArchiveEntry() (writes entry header)
* - write() (writes entry data)
* - closeArchiveEntry() (closes entry)
* + finish() (ends the addition of entries)
* + write additional data if format supports it (optional)
* + close()
*/
public abstract class ArchiveOutputStream<E extends ArchiveEntry> extends OutputStream {
/**
* Temporary buffer used for the {@link #write(int)} method
*/
private final byte[] oneByte = new byte[1];
static final int BYTE_MASK = 0xFF;
public abstract E newArchiveEntry() throws IOException;
/**
* Writes the headers for an archive entry to the output stream.
* The caller must then write the content to the stream and call
* {@link #closeArchiveEntry()} to complete the process.
*
* @param entry describes the entry
* @throws java.io.IOException
*/
public abstract void putArchiveEntry(E entry) throws IOException;
/**
* Closes the archive entry, writing any trailer information that may
* be required.
*
* @throws java.io.IOException
*/
public abstract void closeArchiveEntry() throws IOException;
/**
* Finishes the addition of entries to this stream, without closing it.
* Additional data can be written, if the format supports it.
*
* The finish() method throws an Exception if the user forgets to close the entry
* .
*
* @throws IOException
*/
public abstract void finish() throws IOException;
/**
* Writes a byte to the current archive entry.
* This method simply calls write( byte[], 0, 1 ).
* MUST be overridden if the {@link #write(byte[], int, int)} method
* is not overridden; may be overridden otherwise.
*
* @param b The byte to be written.
* @throws java.io.IOException on error
*/
@Override
public void write(int b) throws IOException {
oneByte[0] = (byte) (b & BYTE_MASK);
write(oneByte, 0, 1);
}
}

View file

@ -0,0 +1,277 @@
package org.xbib.io.archive.util;
import org.xbib.io.archive.entry.ArchiveEntry;
import org.xbib.io.archive.entry.ArchiveEntryEncoding;
import org.xbib.io.archive.entry.ArchiveEntryEncodingHelper;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Locale;
/**
* Generic Archive utilities
*/
public class ArchiveUtils {
/**
* Private constructor to prevent instantiation of this utility class.
*/
private ArchiveUtils() {
}
/**
* Strips Windows' drive letter as well as any leading slashes,
* turns path separators into forward slahes.
*/
public static String normalizeFileName(String fileName, boolean preserveLeadingSlashes) {
String osname = System.getProperty("os.name").toLowerCase(Locale.ENGLISH);
if (osname.startsWith("windows")) {
if (fileName.length() > 2) {
char ch1 = fileName.charAt(0);
char ch2 = fileName.charAt(1);
if (ch2 == ':' && ((ch1 >= 'a' && ch1 <= 'z') || (ch1 >= 'A' && ch1 <= 'Z'))) {
fileName = fileName.substring(2);
}
}
} else if (osname.contains("netware")) {
int colon = fileName.indexOf(':');
if (colon != -1) {
fileName = fileName.substring(colon + 1);
}
}
fileName = fileName.replace(File.separatorChar, '/');
// No absolute pathnames. Windows paths can start with "\\NetworkDrive\", so we loop on starting /'s.
while (!preserveLeadingSlashes && fileName.startsWith("/")) {
fileName = fileName.substring(1);
}
return fileName;
}
public static final ArchiveEntryEncoding DEFAULT_ENCODING = ArchiveEntryEncodingHelper.getEncoding(null);
public static final ArchiveEntryEncoding FALLBACK_ENCODING = new ArchiveEntryEncoding() {
public boolean canEncode(String name) {
return true;
}
public ByteBuffer encode(String name) {
final int length = name.length();
byte[] buf = new byte[length];
for (int i = 0; i < length; ++i) {
buf[i] = (byte) name.charAt(i);
}
return ByteBuffer.wrap(buf);
}
public String decode(byte[] buffer) {
final int length = buffer.length;
StringBuilder result = new StringBuilder(length);
for (byte b : buffer) {
if (b == 0) {
break;
}
result.append((char) (b & 0xFF));
}
return result.toString();
}
};
/**
* Copy a name into a buffer.
* Copies characters from the name into the buffer
* starting at the specified offset.
* If the buffer is longer than the name, the buffer
* is filled with trailing NULs.
* If the name is longer than the buffer,
* the output is truncated.
*
* @param name The header name from which to copy the characters.
* @param buf The buffer where the name is to be stored.
* @param offset The starting offset into the buffer
* @param length The maximum number of header bytes to copy.
* @return The updated offset, i.e. offset + length
*/
public static int formatNameBytes(String name, byte[] buf, final int offset, final int length) {
try {
return formatNameBytes(name, buf, offset, length, DEFAULT_ENCODING);
} catch (IOException ex) {
try {
return formatNameBytes(name, buf, offset, length, ArchiveUtils.FALLBACK_ENCODING);
} catch (IOException ex2) {
// impossible
throw new RuntimeException(ex2);
}
}
}
/**
* Copy a name into a buffer.
* Copies characters from the name into the buffer
* starting at the specified offset.
* If the buffer is longer than the name, the buffer
* is filled with trailing NULs.
* If the name is longer than the buffer,
* the output is truncated.
*
* @param name The header name from which to copy the characters.
* @param buf The buffer where the name is to be stored.
* @param offset The starting offset into the buffer
* @param length The maximum number of header bytes to copy.
* @param encoding name of the encoding to use for file names
* @return The updated offset, i.e. offset + length
*/
public static int formatNameBytes(String name, byte[] buf, final int offset,
final int length,
final ArchiveEntryEncoding encoding)
throws IOException {
int len = name.length();
ByteBuffer b = encoding.encode(name);
while (b.limit() > length && len > 0) {
b = encoding.encode(name.substring(0, --len));
}
final int limit = b.limit();
System.arraycopy(b.array(), b.arrayOffset(), buf, offset, limit);
// Pad any remaining output bytes with NUL
for (int i = limit; i < length; ++i) {
buf[offset + i] = 0;
}
return offset + length;
}
/**
* Generates a string containing the name, isDirectory setting and size of an entry.
* For example:
* 2000 main.c
* 100 testfiles
*
* @return the representation of the entry
*/
public static String toString(ArchiveEntry entry) {
StringBuilder sb = new StringBuilder();
sb.append(entry.isDirectory() ? 'd' : '-');// c.f. "ls -l" output
String size = Long.toString((entry.getEntrySize()));
sb.append(' ');
// Pad output to 7 places, leading spaces
for (int i = 7; i > size.length(); i--) {
sb.append(' ');
}
sb.append(size);
sb.append(' ').append(entry.getName());
return sb.toString();
}
/**
* Check if buffer contents matches Ascii String.
*
* @param expected
* @param buffer
* @param offset
* @param length
* @return {@code true} if buffer is the same as the expected string
*/
public static boolean matchAsciiBuffer(
String expected, byte[] buffer, int offset, int length) {
byte[] buffer1;
try {
buffer1 = expected.getBytes("ASCII");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); // Should not happen
}
return isEqual(buffer1, 0, buffer1.length, buffer, offset, length, false);
}
/**
* Convert a string to Ascii bytes.
* Used for comparing "magic" strings which need to be independent of the default Locale.
*
* @param inputString
* @return the bytes
*/
public static byte[] toAsciiBytes(String inputString) {
try {
return inputString.getBytes("ASCII");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); // Should never happen
}
}
/**
* Convert an input byte array to a String using the ASCII character set.
*
* @param inputBytes
* @return the bytes, interpreted as an Ascii string
*/
public static String toAsciiString(final byte[] inputBytes) {
try {
return new String(inputBytes, "ASCII");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); // Should never happen
}
}
/**
* Convert an input byte array to a String using the ASCII character set.
*
* @param inputBytes input byte array
* @param offset offset within array
* @param length length of array
* @return the bytes, interpreted as an Ascii string
*/
public static String toAsciiString(final byte[] inputBytes, int offset, int length) {
try {
return new String(inputBytes, offset, length, "ASCII");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); // Should never happen
}
}
/**
* Compare byte buffers, optionally ignoring trailing nulls
*
* @param buffer1
* @param offset1
* @param length1
* @param buffer2
* @param offset2
* @param length2
* @param ignoreTrailingNulls
* @return {@code true} if buffer1 and buffer2 have same contents, having regard to trailing nulls
*/
public static boolean isEqual(
final byte[] buffer1, final int offset1, final int length1,
final byte[] buffer2, final int offset2, final int length2,
boolean ignoreTrailingNulls) {
int minLen = length1 < length2 ? length1 : length2;
for (int i = 0; i < minLen; i++) {
if (buffer1[offset1 + i] != buffer2[offset2 + i]) {
return false;
}
}
if (length1 == length2) {
return true;
}
if (ignoreTrailingNulls) {
if (length1 > length2) {
for (int i = length2; i < length1; i++) {
if (buffer1[offset1 + i] != 0) {
return false;
}
}
} else {
for (int i = length1; i < length2; i++) {
if (buffer2[offset2 + i] != 0) {
return false;
}
}
}
return true;
}
return false;
}
}

13
io-codec/build.gradle Normal file
View file

@ -0,0 +1,13 @@
dependencies {
api project(':io-archive')
implementation project(':io-compress-bzip2')
implementation project(':io-compress-lzf')
implementation project(':io-compress-xz')
implementation project(':io-compress-zlib')
implementation project(':io-archive-ar')
implementation project(':io-archive-cpio')
implementation project(':io-archive-dump')
implementation project(':io-archive-jar')
implementation project(':io-archive-tar')
implementation project(':io-archive-zip')
}

View file

@ -0,0 +1,20 @@
module org.xbib.io.codec {
exports org.xbib.io.codec;
exports org.xbib.io.codec.ar;
exports org.xbib.io.codec.cpio;
exports org.xbib.io.codec.file;
exports org.xbib.io.codec.jar;
exports org.xbib.io.codec.tar;
exports org.xbib.io.codec.zip;
requires org.xbib.io.compress.bzip;
requires org.xbib.io.compress.lzf;
requires org.xbib.io.compress.xz;
requires org.xbib.io.compress.zlib;
requires org.xbib.io.archive;
requires org.xbib.io.archive.ar;
requires org.xbib.io.archive.cpio;
requires org.xbib.io.archive.dump;
requires org.xbib.io.archive.jar;
requires org.xbib.io.archive.tar;
requires org.xbib.io.archive.zip;
}

View file

@ -0,0 +1,52 @@
package org.xbib.io.codec;
import org.xbib.io.archive.stream.ArchiveInputStream;
import org.xbib.io.archive.stream.ArchiveOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* An archive codec defines the session and the input or output stream that are
* used for reading or writing to an archive.
*
* @param <S> the archive session type
* @param <I> the archive input stream type
* @param <O> the archive output type
*/
public interface ArchiveCodec<S extends ArchiveSession, I extends ArchiveInputStream, O extends ArchiveOutputStream> {
/**
* Returns the name of this archive codec ("cpio", "tar", "zip")
*
* @return the name
*/
String getName();
/**
* Creates a new archive session with a progress watcher.
*
* @param watcher the progress watcher
* @return the new archive session
*/
S newSession(BytesProgressWatcher watcher);
/**
* Creates a new archive input stream
*
* @param in the input stream for the archive input stream
* @return the archive input stream
* @throws IOException if archive input stream can not be created
*/
I createArchiveInputStream(InputStream in) throws IOException;
/**
* Creates a new archive output stream
*
* @param out the output stream for the archive output stream
* @return the archive output stream
* @throws IOException if archive output stream can not be created
*/
O createArchiveOutputStream(OutputStream out) throws IOException;
}

View file

@ -0,0 +1,250 @@
/*
* Licensed to Jörg Prante and xbib under one or more contributor
* license agreements. See the NOTICE.txt file distributed with this work
* for additional information regarding copyright ownership.
*
* Copyright (C) 2012 Jörg Prante and xbib
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, see http://www.gnu.org/licenses
* or write to the Free Software Foundation, Inc., 51 Franklin Street,
* Fifth Floor, Boston, MA 02110-1301 USA.
*
* The interactive user interfaces in modified source and object code
* versions of this program must display Appropriate Legal Notices,
* as required under Section 5 of the GNU Affero General Public License.
*
* In accordance with Section 7(b) of the GNU Affero General Public
* License, these Appropriate Legal Notices must retain the display of the
* "Powered by xbib" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by xbib".
*/
package org.xbib.io.codec;
import org.xbib.io.archive.entry.ArchiveEntry;
import org.xbib.io.archive.stream.ArchiveInputStream;
import org.xbib.io.archive.stream.ArchiveOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.util.Date;
import java.util.Set;
/**
* Archive session
*/
public abstract class ArchiveSession<I extends ArchiveInputStream, O extends ArchiveOutputStream>
implements Session<StringPacket> {
private final static StreamCodecService codecFactory = StreamCodecService.getInstance();
private final static int DEFAULT_INPUT_BUFSIZE = 65536;
protected int bufferSize = DEFAULT_INPUT_BUFSIZE;
private boolean isOpen;
private Path path;
private OpenOption option;
protected ArchiveSession() {
}
public ArchiveSession setPath(Path path, OpenOption option) {
this.path = path;
this.option = option;
return this;
}
public ArchiveSession setBufferSize(int bufferSize) {
this.bufferSize = bufferSize;
return this;
}
@Override
public synchronized void open(Session.Mode mode) throws IOException {
if (isOpen) {
return;
}
switch (mode) {
case READ: {
InputStream in = newInputStream(path, option);
open(in);
this.isOpen = getInputStream() != null;
break;
}
case WRITE: {
OutputStream out = newOutputStream(path, option);
open(out);
this.isOpen = getOutputStream() != null;
break;
}
}
}
@Override
public StringPacket newPacket() {
return new StringPacket();
}
@Override
public synchronized StringPacket read() throws IOException {
if (!isOpen()) {
throw new IOException("not open");
}
if (getInputStream() == null) {
throw new IOException("no input stream found");
}
ArchiveEntry entry = getInputStream().getNextEntry();
if (entry == null) {
return null;
}
StringPacket packet = newPacket();
String name = entry.getName();
packet.name(name);
int size = (int)entry.getEntrySize();
byte[] b = new byte[size];
getInputStream().read(b, 0, size);
packet.packet(new String(b));
return packet;
}
@Override
public synchronized void write(StringPacket packet) throws IOException {
if (!isOpen()) {
throw new IOException("not open");
}
if (getOutputStream() == null) {
throw new IOException("no output stream found");
}
if (packet == null || packet.toString() == null) {
throw new IOException("no packet to write");
}
byte[] buf = packet.toString().getBytes();
if (buf.length > 0) {
String name = packet.name();
ArchiveEntry entry = getOutputStream().newArchiveEntry();
entry.setName(name);
entry.setLastModified(new Date());
entry.setEntrySize(buf.length);
getOutputStream().putArchiveEntry(entry);
getOutputStream().write(buf);
getOutputStream().closeArchiveEntry();
}
}
@Override
public synchronized void close() throws IOException {
if (!isOpen) {
return;
}
if (getOutputStream() != null) {
getOutputStream().close();
}
if (getInputStream() != null) {
getInputStream().close();
}
this.isOpen = false;
}
@Override
public boolean isOpen() {
return isOpen;
}
public boolean canOpen(URI uri) {
return canOpen(uri, getSuffix(), true);
}
public static boolean canOpen(URI uri, String suffix, boolean withCodecs) {
final String scheme = uri.getScheme();
final String part = uri.getSchemeSpecificPart();
if (scheme.equals(suffix) ||
(scheme.equals("file") && part.endsWith("." + suffix.toLowerCase())) ||
(scheme.equals("file") && part.endsWith("." + suffix.toUpperCase()))) {
return true;
}
if (withCodecs) {
Set<String> codecs = StreamCodecService.getCodecs();
for (String codec : codecs) {
String s = "." + suffix + "." + codec;
if (part.endsWith(s) || part.endsWith(s.toLowerCase()) || part.endsWith(s.toUpperCase())) {
return true;
}
}
}
return false;
}
protected abstract String getSuffix();
protected abstract void open(InputStream in) throws IOException;
protected abstract void open(OutputStream in) throws IOException;
protected abstract I getInputStream();
protected abstract O getOutputStream();
/**
* Helper method for creating the FileInputStream
*
* @param path the path
* @return an InputStream
* @throws java.io.IOException if existence or access rights do not suffice
*/
public static InputStream newInputStream(Path path, OpenOption option) throws IOException {
if (path == null) {
throw new IOException("no path given");
}
String part = path.toUri().getSchemeSpecificPart();
if (Files.isReadable(path) && Files.isRegularFile(path)) {
InputStream in = Files.newInputStream(path, option);
Set<String> codecs = StreamCodecService.getCodecs();
for (String codec : codecs) {
String s = "." + codec;
if (part.endsWith(s.toLowerCase()) || part.endsWith(s.toUpperCase())) {
in = StreamCodecService.getInstance().getCodec(codec).decode(in);
}
}
return in;
} else {
throw new IOException("can't open for input, check existence or access rights: " + path);
}
}
/**
* Helper method for creating the FileOutputStream. Creates the directory if
* it does not exist.
*
* @throws java.io.IOException if existence or access rights do not suffice
*/
public static OutputStream newOutputStream(Path path, OpenOption option) throws IOException {
String part = path.toUri().getSchemeSpecificPart();
OutputStream out = Files.newOutputStream(path, option);
Set<String> codecs = StreamCodecService.getCodecs();
for (String codec : codecs) {
String s = "." + codec;
if (part.endsWith(s.toLowerCase()) || part.endsWith(s.toUpperCase())) {
out = StreamCodecService.getInstance().getCodec(codec).encode(out);
}
}
return out;
}
}

View file

@ -0,0 +1,315 @@
package org.xbib.io.codec;
import java.util.Map;
import java.util.TreeMap;
/**
* Utility class that tracks the number of bytes transferred from a source, and
* uses this information to calculate transfer rates and estimate end times. The
* watcher stores the number of bytes that will be transferred, the number of
* bytes that have been transferred in the current session and the time this has
* taken, and the number of bytes and time taken overal (eg for transfers that
* have been restarted).
*/
public class BytesProgressWatcher {
/**
* The number of seconds worth of historical byte transfer information that
* will be stored and used to calculate the recent transfer rate.
*/
private static final int SECONDS_OF_HISTORY = 5;
private boolean isStarted = false;
private long bytesToTransfer = 0;
private long startTimeAllTransfersMS = -1;
private long totalBytesInAllTransfers = 0;
private long startTimeCurrentTransferMS = -1;
private long totalBytesInCurrentTransfer = 0;
private long endTimeCurrentTransferMS = -1;
private Map<Long, Long> historyOfBytesBySecond = new TreeMap<Long, Long>();
private long earliestHistorySecond = Long.MAX_VALUE;
/**
* Construct a watcher for a transfer that will involve a given number of
* bytes.
*
* @param bytesToTransfer the number of bytes that will be transferred, eg
* the size of a file being uploaded.
*/
public BytesProgressWatcher(long bytesToTransfer) {
this.bytesToTransfer = bytesToTransfer;
}
/**
* @return the count of bytes that will be transferred by the object watched
* by this class.
*/
public synchronized long getBytesToTransfer() {
return bytesToTransfer;
}
/**
* Resets the byte count and timer variables for a watcher. This method is
* called automatically when a transfer is started (ie the first bytes are
* registered in the method {@link #updateBytesTransferred(long)}), or when
* a transfer is restarted (eg due to transmission errors).
*/
public synchronized void resetWatcher() {
startTimeCurrentTransferMS = System.currentTimeMillis();
if (startTimeAllTransfersMS == -1) {
startTimeAllTransfersMS = startTimeCurrentTransferMS;
}
endTimeCurrentTransferMS = -1;
totalBytesInCurrentTransfer = 0;
isStarted = true;
}
/**
* Notifies this watcher that bytes have been transferred.
*
* @param byteCount the number of bytes that have been transferred.
*/
public synchronized void updateBytesTransferred(long byteCount) {
// Start the monitor when we are notified of the first bytes transferred.
if (!isStarted) {
resetWatcher();
}
// Store the total byte count for the current transfer, and for all transfers.
totalBytesInCurrentTransfer += byteCount;
totalBytesInAllTransfers += byteCount;
// Recognise when all the expected bytes have been transferred and mark the end time.
if (totalBytesInCurrentTransfer >= bytesToTransfer) {
endTimeCurrentTransferMS = System.currentTimeMillis();
}
// Keep historical records of the byte counts transferred in a given second.
Long currentSecond = System.currentTimeMillis() / 1000;
Long bytesInSecond = historyOfBytesBySecond.get(currentSecond);
if (bytesInSecond != null) {
historyOfBytesBySecond.put(currentSecond, byteCount + bytesInSecond);
} else {
historyOfBytesBySecond.put(currentSecond, byteCount);
}
// Remember the earliest second value for which we have historical info.
if (currentSecond < earliestHistorySecond) {
earliestHistorySecond = currentSecond;
}
// Remove any history records we are no longer interested in.
long removeHistoryBeforeSecond = currentSecond - SECONDS_OF_HISTORY;
for (long sec = earliestHistorySecond; sec < removeHistoryBeforeSecond; sec++) {
historyOfBytesBySecond.remove(sec);
}
earliestHistorySecond = removeHistoryBeforeSecond;
}
/**
* @return the number of bytes that have so far been transferred in the most
* recent transfer session.
*/
public synchronized long getBytesTransferred() {
return totalBytesInCurrentTransfer;
}
/**
* @return the number of bytes that are remaining to be transferred.
*/
public synchronized long getBytesRemaining() {
return bytesToTransfer - totalBytesInCurrentTransfer;
}
/**
* @return an estimate of the time (in seconds) it will take for the
* transfer to completed, based on the number of bytes remaining to transfer
* and the overall bytes/second rate.
*/
public synchronized long getRemainingTime() {
BytesProgressWatcher[] progressWatchers = new BytesProgressWatcher[1];
progressWatchers[0] = this;
long bytesRemaining = bytesToTransfer - totalBytesInCurrentTransfer;
double remainingSecs =
(double) bytesRemaining / calculateOverallBytesPerSecond(progressWatchers);
return Math.round(remainingSecs);
}
/**
* @return the byte rate (per second) based on the historical information
* for the last
* {@link #SECONDS_OF_HISTORY} seconds before the current time.
*/
public synchronized double getRecentByteRatePerSecond() {
if (!isStarted) {
return 0;
}
long currentSecond = System.currentTimeMillis() / 1000;
long startSecond = 1 + (currentSecond - SECONDS_OF_HISTORY);
long endSecond = (endTimeCurrentTransferMS != -1
? endTimeCurrentTransferMS / 1000
: currentSecond);
if (currentSecond - SECONDS_OF_HISTORY > endSecond) {
// This item finished too long ago, ignore it now.
historyOfBytesBySecond.clear();
return 0;
}
// Count the number of bytes transferred from SECONDS_OF_HISTORY ago to the second before now.
long sumOfBytes = 0;
long numberOfSecondsInHistory = 0;
for (long sec = startSecond; sec <= endSecond; sec++) {
numberOfSecondsInHistory++;
Long bytesInSecond = historyOfBytesBySecond.get(sec);
if (bytesInSecond != null) {
sumOfBytes += bytesInSecond;
}
}
return (numberOfSecondsInHistory == 0 ? 0 : (double) sumOfBytes / numberOfSecondsInHistory);
}
/**
* @return the number of milliseconds time elapsed for a transfer. The value
* returned is the time elapsed so far if the transfer is ongoing, the total
* time taken for the transfer if it is complete, or 0 if the transfer has
* not yet started.
*/
public synchronized long getElapsedTimeMS() {
if (!isStarted) {
return 0;
}
if (endTimeCurrentTransferMS != -1) {
// Transfer is complete, report the time it took.
return endTimeCurrentTransferMS - startTimeCurrentTransferMS;
} else {
return System.currentTimeMillis() - startTimeCurrentTransferMS;
}
}
/**
* @return the number of bytes that have been transferred over all sessions,
* including any sessions that have been restarted.
*/
public synchronized long getTotalBytesInAllTransfers() {
return totalBytesInAllTransfers;
}
protected synchronized boolean isStarted() {
return isStarted;
}
/**
* @return the time (in milliseconds) when the first bytes were transferred,
* regardless of how many times the transfer was reset.
*/
public synchronized long getHistoricStartTimeMS() {
return startTimeAllTransfersMS;
}
/**
* @param progressWatchers all the watchers involved in the same byte
* transfer operation.
* @return the total number of bytes to transfer.
*/
public static long sumBytesToTransfer(BytesProgressWatcher[] progressWatchers) {
long sumOfBytes = 0;
for (BytesProgressWatcher progressWatcher : progressWatchers) {
sumOfBytes += progressWatcher.getBytesToTransfer();
}
return sumOfBytes;
}
/**
* @param progressWatchers all the watchers involved in the same byte
* transfer operation.
* @return the total number of bytes already transferred.
*/
public static long sumBytesTransferred(BytesProgressWatcher[] progressWatchers) {
long sumOfBytes = 0;
for (BytesProgressWatcher progressWatcher : progressWatchers) {
sumOfBytes += progressWatcher.getBytesTransferred();
}
return sumOfBytes;
}
/**
* @param progressWatchers all the watchers involved in the same byte
* transfer operation.
* @return an estimate of the time (in seconds) it will take for the
* transfer to completed, based on the number of bytes remaining to transfer
* and the overall bytes/second rate.
*/
public static long calculateRemainingTime(BytesProgressWatcher[] progressWatchers) {
long bytesRemaining = sumBytesToTransfer(progressWatchers)
- sumBytesTransferred(progressWatchers);
double bytesPerSecond = calculateOverallBytesPerSecond(progressWatchers);
if (Math.abs(bytesPerSecond) < 0.001d) {
// No transfer has occurred yet.
return 0;
}
double remainingSecs =
(double) bytesRemaining / bytesPerSecond;
return Math.round(remainingSecs);
}
/**
* @param progressWatchers all the watchers involved in the same byte
* transfer operation.
* @return the overall rate of bytes/second over all transfers for all
* watchers.
*/
public static double calculateOverallBytesPerSecond(BytesProgressWatcher[] progressWatchers) {
long initialStartTime = Long.MAX_VALUE; // The oldest start time of any monitor.
long bytesTotal = 0;
for (BytesProgressWatcher progressWatcher : progressWatchers) {
// Ignore any watchers that have not yet started.
if (!progressWatcher.isStarted()) {
continue;
}
// Add up all the bytes transferred by all started watchers.
bytesTotal += progressWatcher.getTotalBytesInAllTransfers();
// Find the earliest starting time of any monitor.
if (progressWatcher.getHistoricStartTimeMS() < initialStartTime) {
initialStartTime = progressWatcher.getHistoricStartTimeMS();
}
}
// Determine how much time has elapsed since the earliest watcher start time.
long elapsedTimeSecs = (System.currentTimeMillis() - initialStartTime) / 1000;
// Calculate the overall rate of bytes/second over all transfers for all watchers.
return elapsedTimeSecs == 0 ? bytesTotal : (double) bytesTotal / elapsedTimeSecs;
}
/**
* @param progressWatchers all the watchers involved in the same byte
* transfer operation.
* @return the rate of bytes/second that has been achieved recently (ie
* within the last
* {@link #SECONDS_OF_HISTORY} seconds).
*/
public static long calculateRecentByteRatePerSecond(BytesProgressWatcher[] progressWatchers) {
double sumOfRates = 0;
for (BytesProgressWatcher progressWatcher : progressWatchers) {
if (progressWatcher.isStarted()) {
sumOfRates += progressWatcher.getRecentByteRatePerSecond();
}
}
return Math.round(sumOfRates);
}
}

View file

@ -0,0 +1,19 @@
package org.xbib.io.codec;
import java.io.Closeable;
import java.io.IOException;
/**
* A Connection is an access to a resource via a scheme or a protocol.
* Each connection can serve multiple sessions in parallel.
*/
public interface Connection<S extends Session<StringPacket>> extends Closeable {
/**
* Create a new session on this connection
*
* @return the session
* @throws java.io.IOException if the session can not be created
*/
S createSession() throws IOException;
}

View file

@ -0,0 +1,8 @@
package org.xbib.io.codec;
import java.net.URLStreamHandler;
public abstract class CustomURLStreamHandler extends URLStreamHandler {
public abstract String getName();
}

View file

@ -0,0 +1,67 @@
/*
* Licensed to Jörg Prante and xbib under one or more contributor
* license agreements. See the NOTICE.txt file distributed with this work
* for additional information regarding copyright ownership.
*
* Copyright (C) 2012 Jörg Prante and xbib
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, see http://www.gnu.org/licenses
* or write to the Free Software Foundation, Inc., 51 Franklin Street,
* Fifth Floor, Boston, MA 02110-1301 USA.
*
* The interactive user interfaces in modified source and object code
* versions of this program must display Appropriate Legal Notices,
* as required under Section 5 of the GNU Affero General Public License.
*
* In accordance with Section 7(b) of the GNU Affero General Public
* License, these Appropriate Legal Notices must retain the display of the
* "Powered by xbib" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by xbib".
*/
package org.xbib.io.codec;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.ServiceLoader;
public class CustomURLStreamHandlerFactory implements URLStreamHandlerFactory {
private final static Map<String, CustomURLStreamHandler> protocolHandlers = new HashMap<>();
private final static CustomURLStreamHandlerFactory factory = new CustomURLStreamHandlerFactory();
public CustomURLStreamHandlerFactory() {
ServiceLoader<CustomURLStreamHandler> serviceLoader = ServiceLoader.load(CustomURLStreamHandler.class);
for (CustomURLStreamHandler handler : serviceLoader) {
if (!protocolHandlers.containsKey(handler.getName())) {
protocolHandlers.put(handler.getName(), handler);
}
}
}
public static CustomURLStreamHandlerFactory getFactory() {
return factory;
}
public void addHandler(String protocol, CustomURLStreamHandler urlHandler) {
protocolHandlers.put(protocol, urlHandler);
}
@Override
public URLStreamHandler createURLStreamHandler(String protocol) {
return protocolHandlers.get(protocol);
}
}

Some files were not shown because too many files have changed in this diff Show more