Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-72540] FilePath.copyRecursiveTo() copying now also if local and remote have incompatible character sets at binary level #8860

Merged
merged 2 commits into from Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 25 additions & 14 deletions core/src/main/java/hudson/FilePath.java
Expand Up @@ -87,6 +87,7 @@
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.CopyOption;
import java.nio.file.FileSystemException;
import java.nio.file.FileSystems;
Expand Down Expand Up @@ -2848,8 +2849,8 @@
// local -> remote copy
final Pipe pipe = Pipe.createLocalToRemote();

Future<Void> future = target.actAsync(new ReadFromTar(target, pipe, description, compression));
Future<Integer> future2 = actAsync(new WriteToTar(scanner, pipe, compression));
Future<Void> future = target.actAsync(new ReadFromTar(target, pipe, description, compression, StandardCharsets.UTF_8));
Future<Integer> future2 = actAsync(new WriteToTar(scanner, pipe, compression, StandardCharsets.UTF_8));
try {
// JENKINS-9540 in case the reading side failed, report that error first
future.get();
Expand All @@ -2861,9 +2862,9 @@
// remote -> local copy
final Pipe pipe = Pipe.createRemoteToLocal();

Future<Integer> future = actAsync(new CopyRecursiveRemoteToLocal(pipe, scanner, compression));
Future<Integer> future = actAsync(new CopyRecursiveRemoteToLocal(pipe, scanner, compression, StandardCharsets.UTF_8));
try {
readFromTar(remote + '/' + description, new File(target.remote), compression.extract(pipe.getIn()));
readFromTar(remote + '/' + description, new File(target.remote), compression.extract(pipe.getIn()), StandardCharsets.UTF_8);
} catch (IOException e) { // BuildException or IOException
try {
future.get(3, TimeUnit.SECONDS);
Expand Down Expand Up @@ -2976,20 +2977,22 @@
private final String description;
private final TarCompression compression;
private final FilePath target;
private final String filenamesEncoding;

ReadFromTar(FilePath target, Pipe pipe, String description, @NonNull TarCompression compression) {
ReadFromTar(FilePath target, Pipe pipe, String description, @NonNull TarCompression compression, Charset filenamesEncoding) {
this.target = target;
this.pipe = pipe;
this.description = description;
this.compression = compression;
this.filenamesEncoding = filenamesEncoding.name();
}

private static final long serialVersionUID = 1L;

@Override
public Void invoke(File f, VirtualChannel channel) throws IOException {
try (InputStream in = pipe.getIn()) {
readFromTar(target.remote + '/' + description, f, compression.extract(in));
readFromTar(target.remote + '/' + description, f, compression.extract(in), Charset.forName(filenamesEncoding));

Check warning on line 2995 in core/src/main/java/hudson/FilePath.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 2995 is not covered by tests
return null;
}
}
Expand All @@ -2999,18 +3002,20 @@
private final DirScanner scanner;
private final Pipe pipe;
private final TarCompression compression;
private final String filenamesEncoding;

WriteToTar(DirScanner scanner, Pipe pipe, @NonNull TarCompression compression) {
WriteToTar(DirScanner scanner, Pipe pipe, @NonNull TarCompression compression, Charset filenamesEncoding) {
this.scanner = scanner;
this.pipe = pipe;
this.compression = compression;
this.filenamesEncoding = filenamesEncoding.name();
}

private static final long serialVersionUID = 1L;

@Override
public Integer invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
return writeToTar(f, scanner, compression.compress(pipe.getOut()));
return writeToTar(f, scanner, compression.compress(pipe.getOut()), Charset.forName(filenamesEncoding));

Check warning on line 3018 in core/src/main/java/hudson/FilePath.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 3018 is not covered by tests
}
}

Expand All @@ -3019,17 +3024,19 @@
private final Pipe pipe;
private final DirScanner scanner;
private final TarCompression compression;
private final String filenamesEncoding;

CopyRecursiveRemoteToLocal(Pipe pipe, DirScanner scanner, @NonNull TarCompression compression) {
CopyRecursiveRemoteToLocal(Pipe pipe, DirScanner scanner, @NonNull TarCompression compression, Charset filenamesEncoding) {
this.pipe = pipe;
this.scanner = scanner;
this.compression = compression;
this.filenamesEncoding = filenamesEncoding.name();
}

@Override
public Integer invoke(File f, VirtualChannel channel) throws IOException {
try (OutputStream out = pipe.getOut()) {
return writeToTar(f, scanner, compression.compress(out));
return writeToTar(f, scanner, compression.compress(out), Charset.forName(filenamesEncoding));

Check warning on line 3039 in core/src/main/java/hudson/FilePath.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 3039 is not covered by tests
}
}
}
Expand Down Expand Up @@ -3061,22 +3068,26 @@
* @return
* number of files/directories that are written.
*/
private static Integer writeToTar(File baseDir, DirScanner scanner, OutputStream out) throws IOException {
Archiver tw = ArchiverFactory.TAR.create(out);
private static Integer writeToTar(File baseDir, DirScanner scanner, OutputStream out, Charset filenamesEncoding) throws IOException {
Archiver tw = ArchiverFactory.TAR.create(out, filenamesEncoding);
try (tw) {
scanner.scan(baseDir, tw);
}
return tw.countEntries();
}

private static void readFromTar(String name, File baseDir, InputStream in) throws IOException {
readFromTar(name, baseDir, in, Charset.defaultCharset());
}

/**
* Reads from a tar stream and stores obtained files to the base dir.
* Supports large files > 10 GB since 1.627 when this was migrated to use commons-compress.
*/
private static void readFromTar(String name, File baseDir, InputStream in) throws IOException {
private static void readFromTar(String name, File baseDir, InputStream in, Charset filenamesEncoding) throws IOException {

// TarInputStream t = new TarInputStream(in);
try (TarArchiveInputStream t = new TarArchiveInputStream(in)) {
try (TarArchiveInputStream t = new TarArchiveInputStream(in, filenamesEncoding.name())) {
TarArchiveEntry te;
while ((te = t.getNextTarEntry()) != null) {
File f = new File(baseDir, te.getName());
Expand Down
23 changes: 18 additions & 5 deletions core/src/main/java/hudson/util/io/ArchiverFactory.java
Expand Up @@ -30,6 +30,7 @@
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.nio.file.OpenOption;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
Expand All @@ -43,9 +44,21 @@
public abstract class ArchiverFactory implements Serializable {
/**
* Creates an archiver on top of the given stream.
* File names in the archive are encoded with default character set.
*/
@NonNull
public abstract Archiver create(OutputStream out) throws IOException;
public Archiver create(OutputStream out) throws IOException {
return create(out, Charset.defaultCharset());
}

/**
* Creates an archiver on top of the given stream.
*
* @param filenamesEncoding the encoding to be used in the archive for file names
* @since TODO
*/
@NonNull
public abstract Archiver create(OutputStream out, Charset filenamesEncoding) throws IOException;

/**
* Uncompressed tar format.
Expand Down Expand Up @@ -85,8 +98,8 @@ private TarArchiverFactory(TarCompression method) {

@NonNull
@Override
public Archiver create(OutputStream out) throws IOException {
return new TarArchiver(method.compress(out));
public Archiver create(OutputStream out, Charset filenamesEncoding) throws IOException {
return new TarArchiver(method.compress(out), filenamesEncoding);
}

private static final long serialVersionUID = 1L;
Expand All @@ -108,8 +121,8 @@ private static final class ZipArchiverFactory extends ArchiverFactory {

@NonNull
@Override
public Archiver create(OutputStream out) {
return new ZipArchiver(out, prefix, openOptions);
public Archiver create(OutputStream out, Charset filenamesEncoding) {
return new ZipArchiver(out, prefix, filenamesEncoding, openOptions);
}

private static final long serialVersionUID = 1L;
Expand Down
5 changes: 3 additions & 2 deletions core/src/main/java/hudson/util/io/TarArchiver.java
Expand Up @@ -32,6 +32,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.attribute.BasicFileAttributes;
Expand All @@ -49,8 +50,8 @@ final class TarArchiver extends Archiver {
private final byte[] buf = new byte[8192];
private final TarArchiveOutputStream tar;

TarArchiver(OutputStream out) {
tar = new TarArchiveOutputStream(out);
TarArchiver(OutputStream out, Charset filenamesEncoding) {
tar = new TarArchiveOutputStream(out, filenamesEncoding.name());
tar.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR);
tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
}
Expand Down
7 changes: 4 additions & 3 deletions core/src/main/java/hudson/util/io/ZipArchiver.java
Expand Up @@ -32,6 +32,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.OpenOption;
Expand All @@ -55,12 +56,12 @@ final class ZipArchiver extends Archiver {
private final String prefix;

ZipArchiver(OutputStream out) {
this(out, "");
this(out, "", Charset.defaultCharset());
}

// Restriction added for clarity, it's a package class, you should not use it outside of Jenkins core
@Restricted(NoExternalUse.class)
ZipArchiver(OutputStream out, String prefix, OpenOption... openOptions) {
ZipArchiver(OutputStream out, String prefix, Charset filenamesEncoding, OpenOption... openOptions) {
this.openOptions = openOptions;
if (StringUtils.isBlank(prefix)) {
this.prefix = "";
Expand All @@ -69,7 +70,7 @@ final class ZipArchiver extends Archiver {
}

zip = new ZipOutputStream(out);
zip.setEncoding(System.getProperty("file.encoding"));
zip.setEncoding(filenamesEncoding.name());
zip.setUseZip64(Zip64Mode.AsNeeded);
}

Expand Down