Skip to content

Commit

Permalink
COMPRESS-400 It should be possible for users to create and access ext…
Browse files Browse the repository at this point in the history
…ra PAX headers to tar archives

Add extra header map to tar archive entry.
Move PAX header processing code from TarArchiveInputStream to TarAchiveEntry.
Use same code for processing user supplied extra headers - thus setting "size "changes the value of getSize().
Add any extra PAX headers to output map when putting entry in TarArchiveOutputStream.
Add simple tests for getting/setting xattr, setting "size", and round tripping.

This PR uses COMPRESS-399 as a base.  To make it easier to cherry-pick, this commit does not bump the minor
package version;  this will cause the baseline verify stage in 399 to  break the build, since the API has changed in a backwards compatible fashion

A separate commit increases the package minor version.

Signed-off-by: Simon Spero <sesuncedu@gmail.com>
  • Loading branch information
sesuncedu committed Jun 5, 2017
1 parent 27b6577 commit 1d9b3c8
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 56 deletions.
Expand Up @@ -20,10 +20,11 @@

import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipEncoding;
import org.apache.commons.compress.utils.ArchiveUtils;
Expand Down Expand Up @@ -133,7 +134,7 @@
* char prefix[131]; // offset 345
* char atime[12]; // offset 476
* char ctime[12]; // offset 488
* char mfill[8]; // offset 500
* char mfill[8]; // offset 500
* char xmagic[4]; // offset 508 "tar"
* };
* </pre>
Expand Down Expand Up @@ -207,6 +208,9 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants {
/** The entry's file reference */
private final File file;

/** Extra, user supplied pax headers */
private final Map<String,String> extraPaxHeaders = new HashMap<>();

/** Maximum length of a user's name in the tar file */
public static final int MAX_NAMELEN = 31;

Expand All @@ -219,6 +223,9 @@ public class TarArchiveEntry implements ArchiveEntry, TarConstants {
/** Convert millis to seconds */
public static final int MILLIS_PER_SECOND = 1000;

/** The prefix to use to mark record extended attributes */
private static final String XATTR_PREFIX = "SCHILY.xattr.";

/**
* Construct an empty entry and prepares the header values.
*/
Expand Down Expand Up @@ -942,6 +949,171 @@ public boolean isSparse() {
return isGNUSparse() || isStarSparse();
}

/**
* get extra PAX Headers
* @return read-only map containing any extra PAX Headers
* @since 1.15
*/
public Map<String, String> getExtraPaxHeaders() {
return Collections.unmodifiableMap(extraPaxHeaders);
}

/**
* clear all extra PAX headers.
* @since 1.15
*/
public void clearExtraPaxHeaders() {
extraPaxHeaders.clear();
}

/**
* add a PAX header to this entry. If the header corresponds to an existing field in the entry,
* that field will be set; otherwise the header will be added to the extraPaxHeaders Map
* @param name The full name of the header to set.
* @param value value of header.
* @since 1.15
*/
public void addPaxHeader(String name,String value) {
processPaxHeader(name,value);
}

/**
* get named extra PAX header
* @param name The full name of an extended PAX header to retrieve
* @return The value of the header, if any.
* @since 1.15
*/
public String getExtraPaxHeader(String name) {
return extraPaxHeaders.get(name);
}

/**
* add a Posix eXtended attribute to the entry. This will be stored as a PAX header, using the
* SCHILY.xattr. prefix. No further encoding is performed on the name or value.
* @param name The name of the extended attribute to set - for example security.selinux or
* user.maven.groupId
* @param value
* @since 1.15
*/
public void addXattr(String name, String value) {
extraPaxHeaders.put(XATTR_PREFIX + name, value);
}

/**
* get named extended Attribute
* @param name The name of the extended attribute to set - for example security.selinux or
* user.maven.groupId
* @return the value of the attribute, if any.
* @since 1.15
*/
public String getXattr(String name) {
return extraPaxHeaders.get(XATTR_PREFIX + name);

}

/**
* Update the entry using a map of pax headers.
* @param headers
* @since 1.15
*/
void updateEntryFromPaxHeaders(Map<String, String> headers) {
for (final Map.Entry<String, String> ent : headers.entrySet()) {
final String key = ent.getKey();
final String val = ent.getValue();
processPaxHeader(key, val, headers);
}
}

/**
* process one pax header, using the entries extraPaxHeaders map as source for extra headers
* used when handling entries for sparse files.
* @param key
* @param val
* @since 1.15
*/
private void processPaxHeader(String key, String val) {
processPaxHeader(key,val,extraPaxHeaders);
}

/**
* Process one pax header, using the supplied map as source for extra headers to be used when handling
* entries for sparse files
*
* @param key the header name.
* @param val the header value.
* @param headers map of headers used for dealing with sparse file.
* @since 1.15
*/
private void processPaxHeader(String key, String val, 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
*
* GNU sparse files use additional members, we use
* GNU.sparse.size to detect the 0.0 and 0.1 versions and
* GNU.sparse.realsize for 1.0.
*
* star files use additional members of which we use
* SCHILY.filetype in order to detect star sparse files.
*
* If called from addExtraPaxHeader, these additional headers must be already present .
*/
switch (key) {
case "path":
setName(val);
break;
case "linkpath":
setLinkName(val);
break;
case "gid":
setGroupId(Long.parseLong(val));
break;
case "gname":
setGroupName(val);
break;
case "uid":
setUserId(Long.parseLong(val));
break;
case "uname":
setUserName(val);
break;
case "size":
setSize(Long.parseLong(val));
break;
case "mtime":
setModTime((long) (Double.parseDouble(val) * 1000));
break;
case "SCHILY.devminor":
setDevMinor(Integer.parseInt(val));
break;
case "SCHILY.devmajor":
setDevMajor(Integer.parseInt(val));
break;
case "GNU.sparse.size":
fillGNUSparse0xData(headers);
break;
case "GNU.sparse.realsize":
fillGNUSparse1xData(headers);
break;
case "SCHILY.filetype":
if ("sparse".equals(val)) {
fillStarSparseData(headers);
}
break;
default:
extraPaxHeaders.put(key,val);
}
}



/**
* If this entry represents a file, and the file is a directory, return
* an array of TarEntries for this entry's children.
Expand Down
Expand Up @@ -28,7 +28,6 @@
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
Expand Down Expand Up @@ -504,55 +503,8 @@ Map<String, String> parsePaxHeaders(final InputStream i)
}

private void applyPaxHeadersToCurrentEntry(final 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
*
* GNU sparse files use additional members, we use
* GNU.sparse.size to detect the 0.0 and 0.1 versions and
* GNU.sparse.realsize for 1.0.
*
* star files use additional members of which we use
* SCHILY.filetype in order to detect star sparse files.
*/
for (final Entry<String, String> ent : headers.entrySet()){
final String key = ent.getKey();
final String val = ent.getValue();
if ("path".equals(key)){
currEntry.setName(val);
} else if ("linkpath".equals(key)){
currEntry.setLinkName(val);
} else if ("gid".equals(key)){
currEntry.setGroupId(Long.parseLong(val));
} else if ("gname".equals(key)){
currEntry.setGroupName(val);
} else if ("uid".equals(key)){
currEntry.setUserId(Long.parseLong(val));
} else if ("uname".equals(key)){
currEntry.setUserName(val);
} else if ("size".equals(key)){
currEntry.setSize(Long.parseLong(val));
} else if ("mtime".equals(key)){
currEntry.setModTime((long) (Double.parseDouble(val) * 1000));
} else if ("SCHILY.devminor".equals(key)){
currEntry.setDevMinor(Integer.parseInt(val));
} else if ("SCHILY.devmajor".equals(key)){
currEntry.setDevMajor(Integer.parseInt(val));
} else if ("GNU.sparse.size".equals(key)) {
currEntry.fillGNUSparse0xData(headers);
} else if ("GNU.sparse.realsize".equals(key)) {
currEntry.fillGNUSparse1xData(headers);
} else if ("SCHILY.filetype".equals(key) && "sparse".equals(val)) {
currEntry.fillStarSparseData(headers);
}
}
currEntry.updateEntryFromPaxHeaders(headers);

}

/**
Expand Down
Expand Up @@ -301,6 +301,7 @@ && handleLongName(entry, linkName, paxHeaders, "linkpath",
&& !ASCII.canEncode(linkName)) {
paxHeaders.put("linkpath", linkName);
}
paxHeaders.putAll(entry.getExtraPaxHeaders());

if (paxHeaders.size() > 0) {
writePaxHeaders(entry, entryName, paxHeaders);
Expand Down
Expand Up @@ -18,16 +18,21 @@

package org.apache.commons.compress.archivers.tar;

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

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Locale;

import org.apache.commons.compress.AbstractTestCase;
import org.junit.Test;

public class TarArchiveEntryTest implements TarConstants {

Expand Down Expand Up @@ -121,6 +126,43 @@ public void testMaxFileSize(){
t.setSize(0100000000000L);
}

@Test public void testExtraPaxHeaders() throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
TarArchiveOutputStream tos = new TarArchiveOutputStream(bos);

TarArchiveEntry entry = new TarArchiveEntry("./weasels");
entry.addPaxHeader("APACHE.mustelida","true");
entry.addXattr("user.org.apache.weasels","maximum weasels");
entry.addPaxHeader("size","1");
assertEquals("extra header count",2,entry.getExtraPaxHeaders().size());
assertEquals("APACHE.mustelida","true",
entry.getExtraPaxHeader("APACHE.mustelida"));
assertEquals("SCHILY.xattr.user.org.apache.weasels","maximum weasels",
entry.getExtraPaxHeader("SCHILY.xattr.user.org.apache.weasels"));
assertEquals("size",entry.getSize(),1);

tos.putArchiveEntry(entry);
tos.write('W');
tos.closeArchiveEntry();
tos.close();

TarArchiveInputStream tis = new TarArchiveInputStream(new ByteArrayInputStream(bos.toByteArray()));
entry = tis.getNextTarEntry();
assertNotNull("couldn't get entry",entry);

assertEquals("extra header count",2,entry.getExtraPaxHeaders().size());
assertEquals("APACHE.mustelida","true",
entry.getExtraPaxHeader("APACHE.mustelida"));
assertEquals("user.org.apache.weasels","maximum weasels",
entry.getXattr("user.org.apache.weasels"));

assertEquals('W',tis.read());
assertTrue("should be at end of entry",tis.read() <0);

assertNull("should be at end of file",tis.getNextTarEntry());
tis.close();
}

@Test
public void testLinkFlagConstructor() {
final TarArchiveEntry t = new TarArchiveEntry("/foo", LF_GNUTYPE_LONGNAME);
Expand Down

0 comments on commit 1d9b3c8

Please sign in to comment.