initial commit
This commit is contained in:
commit
9945b77f55
259 changed files with 38619 additions and 0 deletions
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
/.idea
|
||||
/target
|
||||
/.settings
|
||||
/.classpath
|
||||
/.project
|
||||
/.gradle
|
||||
build
|
||||
out
|
||||
logs
|
||||
*~
|
||||
*.iml
|
||||
.DS_Store
|
33
build.gradle
Normal file
33
build.gradle
Normal 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
5
gradle.properties
Normal file
|
@ -0,0 +1,5 @@
|
|||
group = org.xbib
|
||||
name = archive
|
||||
version = 1.0.0
|
||||
|
||||
gradle.wrapper.version = 6.4.1
|
35
gradle/compile/java.gradle
Normal file
35
gradle/compile/java.gradle
Normal 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
|
||||
}
|
55
gradle/documentation/asciidoc.gradle
Normal file
55
gradle/documentation/asciidoc.gradle
Normal 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
13
gradle/ide/idea.gradle
Normal 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")
|
||||
}
|
64
gradle/publishing/publication.gradle
Normal file
64
gradle/publishing/publication.gradle
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
11
gradle/publishing/sonatype.gradle
Normal file
11
gradle/publishing/sonatype.gradle
Normal 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
27
gradle/test/junit5.gradle
Normal 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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
185
gradlew
vendored
Executable 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
104
gradlew.bat
vendored
Normal 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
|
3
io-archive-ar/build.gradle
Normal file
3
io-archive-ar/build.gradle
Normal file
|
@ -0,0 +1,3 @@
|
|||
dependencies {
|
||||
api project(':io-archive')
|
||||
}
|
4
io-archive-ar/src/main/java/module-info.java
Normal file
4
io-archive-ar/src/main/java/module-info.java
Normal file
|
@ -0,0 +1,4 @@
|
|||
module org.xbib.io.archive.ar {
|
||||
exports org.xbib.io.archive.ar;
|
||||
requires org.xbib.io.archive;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 >= 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;
|
||||
}
|
||||
}
|
3
io-archive-cpio/build.gradle
Normal file
3
io-archive-cpio/build.gradle
Normal file
|
@ -0,0 +1,3 @@
|
|||
dependencies {
|
||||
api project(':io-archive')
|
||||
}
|
4
io-archive-cpio/src/main/java/module-info.java
Normal file
4
io-archive-cpio/src/main/java/module-info.java
Normal file
|
@ -0,0 +1,4 @@
|
|||
module org.xbib.io.archive.cpio {
|
||||
exports org.xbib.io.archive.cpio;
|
||||
requires org.xbib.io.archive;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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] & 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;
|
||||
}
|
||||
}
|
|
@ -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("test.cpio")));
|
||||
* 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 < 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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 = "12345";
|
||||
* 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');
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
3
io-archive-dump/build.gradle
Normal file
3
io-archive-dump/build.gradle
Normal file
|
@ -0,0 +1,3 @@
|
|||
dependencies {
|
||||
api project(':io-archive')
|
||||
}
|
4
io-archive-dump/src/main/java/module-info.java
Normal file
4
io-archive-dump/src/main/java/module-info.java
Normal file
|
@ -0,0 +1,4 @@
|
|||
module org.xbib.io.archive.dump {
|
||||
exports org.xbib.io.archive.dump;
|
||||
requires org.xbib.io.archive;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 => data; 0 => 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
}
|
|
@ -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 +
|
||||
".");
|
||||
}
|
||||
}
|
4
io-archive-jar/build.gradle
Normal file
4
io-archive-jar/build.gradle
Normal file
|
@ -0,0 +1,4 @@
|
|||
dependencies {
|
||||
api project(':io-archive')
|
||||
api project(':io-archive-zip')
|
||||
}
|
5
io-archive-jar/src/main/java/module-info.java
Normal file
5
io-archive-jar/src/main/java/module-info.java
Normal 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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
Binary file not shown.
3
io-archive-tar/build.gradle
Normal file
3
io-archive-tar/build.gradle
Normal file
|
@ -0,0 +1,3 @@
|
|||
dependencies {
|
||||
api project(':io-archive')
|
||||
}
|
4
io-archive-tar/src/main/java/module-info.java
Normal file
4
io-archive-tar/src/main/java/module-info.java
Normal file
|
@ -0,0 +1,4 @@
|
|||
module org.xbib.io.archive.tar {
|
||||
exports org.xbib.io.archive.tar;
|
||||
requires org.xbib.io.archive;
|
||||
}
|
|
@ -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 < 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 < 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 < 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 < 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 < 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 < 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 >= 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 > 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++;
|
||||
}
|
||||
}
|
|
@ -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 > 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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
3
io-archive-zip/build.gradle
Normal file
3
io-archive-zip/build.gradle
Normal file
|
@ -0,0 +1,3 @@
|
|||
dependencies {
|
||||
api project(':io-archive')
|
||||
}
|
4
io-archive-zip/src/main/java/module-info.java
Normal file
4
io-archive-zip/src/main/java/module-info.java
Normal file
|
@ -0,0 +1,4 @@
|
|||
module org.xbib.io.archive.zip {
|
||||
exports org.xbib.io.archive.zip;
|
||||
requires org.xbib.io.archive;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 "version made
|
||||
* by" 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);
|
||||
}
|
||||
}
|
|
@ -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 < 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
|
||||
* "data decsriptor", "local file header" or
|
||||
* "central directory entry".
|
||||
* 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
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 "Zip64 end of central directory
|
||||
* locator" or the "End of central dir record", 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 "Zip64 end of central directory locator",
|
||||
* finds the "Zip64 end of central directory record" 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 "End of central dir record", 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 "length of
|
||||
* filename" 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;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
5
io-archive/src/main/java/module-info.java
Normal file
5
io-archive/src/main/java/module-info.java
Normal 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;
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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()>=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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
13
io-codec/build.gradle
Normal 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')
|
||||
}
|
20
io-codec/src/main/java/module-info.java
Normal file
20
io-codec/src/main/java/module-info.java
Normal 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;
|
||||
}
|
52
io-codec/src/main/java/org/xbib/io/codec/ArchiveCodec.java
Normal file
52
io-codec/src/main/java/org/xbib/io/codec/ArchiveCodec.java
Normal 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;
|
||||
|
||||
}
|
250
io-codec/src/main/java/org/xbib/io/codec/ArchiveSession.java
Normal file
250
io-codec/src/main/java/org/xbib/io/codec/ArchiveSession.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
19
io-codec/src/main/java/org/xbib/io/codec/Connection.java
Normal file
19
io-codec/src/main/java/org/xbib/io/codec/Connection.java
Normal 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;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.xbib.io.codec;
|
||||
|
||||
import java.net.URLStreamHandler;
|
||||
|
||||
public abstract class CustomURLStreamHandler extends URLStreamHandler {
|
||||
|
||||
public abstract String getName();
|
||||
}
|
|
@ -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
Loading…
Reference in a new issue