diff --git a/java/google/registry/rde/RydeEncoder.java b/java/google/registry/rde/RydeEncoder.java index 34c5d9804..31e142808 100644 --- a/java/google/registry/rde/RydeEncoder.java +++ b/java/google/registry/rde/RydeEncoder.java @@ -19,6 +19,7 @@ import static google.registry.rde.RydeCompression.openCompressor; import static google.registry.rde.RydeEncryption.RYDE_USE_INTEGRITY_PACKET; import static google.registry.rde.RydeEncryption.openEncryptor; import static google.registry.rde.RydeFileEncoding.openPgpFileWriter; +import static google.registry.rde.RydeTar.openTarWriter; import com.google.common.collect.ImmutableList; import com.google.common.io.Closer; @@ -76,8 +77,7 @@ public final class RydeEncoder extends FilterOutputStream { kompressor = closer.register(openCompressor(encryptLayer)); fileLayer = closer.register(openPgpFileWriter(kompressor, filenamePrefix + ".tar", modified)); tarLayer = - closer.register( - new RydeTarOutputStream(fileLayer, dataLength, modified, filenamePrefix + ".xml")); + closer.register(openTarWriter(fileLayer, dataLength, filenamePrefix + ".xml", modified)); this.out = tarLayer; } diff --git a/java/google/registry/rde/RydeTar.java b/java/google/registry/rde/RydeTar.java new file mode 100644 index 000000000..e2e85c1f3 --- /dev/null +++ b/java/google/registry/rde/RydeTar.java @@ -0,0 +1,123 @@ +// Copyright 2018 The Nomulus Authors. All Rights Reserved. +// +// 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 +// +// http://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. + +package google.registry.rde; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.io.ByteStreams; +import google.registry.util.ImprovedInputStream; +import google.registry.util.ImprovedOutputStream; +import google.registry.util.PosixTarHeader; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import javax.annotation.CheckReturnValue; +import javax.annotation.WillNotClose; +import org.joda.time.DateTime; + +/** Single-file POSIX tar archive creator that wraps an {@link OutputStream}. */ +final class RydeTar { + + /** + * Creates a new {@link ImprovedOutputStream} that creates a tar archive with a single file. + * + * @param os is the upstream {@link OutputStream} which is not closed by this object + * @param expectedSize is the length in bytes of the one file, which you will write to this object + * @param modified is the {@link PosixTarHeader.Builder#setMtime mtime} you want to set + * @param filename is the name of the one file that will be contained in this archive + */ + @CheckReturnValue + static ImprovedOutputStream openTarWriter( + @WillNotClose OutputStream os, long expectedSize, String filename, DateTime modified) { + + checkArgument(expectedSize >= 0); + checkArgument(filename.endsWith(".xml"), + "Ryde expects tar archive to contain a filename with an '.xml' extension."); + try { + os.write(new PosixTarHeader.Builder() + .setName(filename) + .setSize(expectedSize) + .setMtime(modified) + .build() + .getBytes()); + return new ImprovedOutputStream("RydeTarWriter", os) { + /** Writes the end of archive marker. */ + @Override + public void onClose() throws IOException { + if (getBytesWritten() != expectedSize) { + throw new IOException( + String.format( + "RydeTarOutputStream expected %,d bytes, but got %,d bytes", + expectedSize, getBytesWritten())); + } + // Round up to a 512-byte boundary and another 1024-bytes to indicate end of archive. + out.write(new byte[1024 + 512 - (int) (getBytesWritten() % 512L)]); + } + }; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** Input stream to a TAR archive file's data that also holds the file's metadata. */ + static final class TarInputStream extends ImprovedInputStream { + private final PosixTarHeader header; + + private TarInputStream(PosixTarHeader header, InputStream input) { + super("RydeTarReader", input); + this.header = header; + } + + /** Returns the file's TAR header. */ + PosixTarHeader getHeader() { + return header; + } + + /** Returns the original name of the file archived in this TAR. */ + String getFilename() { + return header.getName(); + } + + /** Returns the creation/modification time of the file archived in this TAR. */ + DateTime getModified() { + return header.getMtime(); + } + } + + /** + * Opens a stream to the first file archived in a TAR archive. + * + *
The result includes the file's metadata - the file name and modification time, as well as + * the full TAR header. Note that only the file name and modification times were actually set by + * {@link #openTarWriter}. + * + * @param input from where to read the TAR archive. + */ + @CheckReturnValue + static TarInputStream openTarReader(@WillNotClose InputStream input) { + try { + byte[] header = new byte[PosixTarHeader.HEADER_LENGTH]; + ByteStreams.readFully(input, header, 0, header.length); + PosixTarHeader tarHeader = PosixTarHeader.from(header); + checkState( + tarHeader.getType() == PosixTarHeader.Type.REGULAR, + "Only support TAR archives with a single regular file"); + return new TarInputStream(tarHeader, ByteStreams.limit(input, tarHeader.getSize())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/java/google/registry/rde/RydeTarOutputStream.java b/java/google/registry/rde/RydeTarOutputStream.java deleted file mode 100644 index 5ba47348d..000000000 --- a/java/google/registry/rde/RydeTarOutputStream.java +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2017 The Nomulus Authors. All Rights Reserved. -// -// 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 -// -// http://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. - -package google.registry.rde; - -import static com.google.common.base.Preconditions.checkArgument; - -import google.registry.util.ImprovedOutputStream; -import google.registry.util.PosixTarHeader; -import java.io.IOException; -import java.io.OutputStream; -import javax.annotation.WillNotClose; -import org.joda.time.DateTime; - -/** - * Single-file POSIX tar archive creator that wraps an {@link OutputStream}. - */ -public class RydeTarOutputStream extends ImprovedOutputStream { - - private final long expectedSize; - - /** - * Creates a new instance that outputs a tar archive. - * - * @param os is the upstream {@link OutputStream} which is not closed by this object - * @param size is the length in bytes of the one file, which you will write to this object - * @param modified is the {@link PosixTarHeader.Builder#setMtime mtime} you want to set - * @param filename is the name of the one file that will be contained in this archive - * @throws RuntimeException to rethrow {@link IOException} - * @throws IllegalArgumentException if {@code size} is negative - */ - public RydeTarOutputStream( - @WillNotClose OutputStream os, long size, DateTime modified, String filename) { - super("RydeTarOutputStream", os, false); - checkArgument(size >= 0); - this.expectedSize = size; - checkArgument(filename.endsWith(".xml"), - "Ryde expects tar archive to contain a filename with an '.xml' extension."); - try { - os.write(new PosixTarHeader.Builder() - .setName(filename) - .setSize(size) - .setMtime(modified) - .build() - .getBytes()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** Writes the end of archive marker. */ - @Override - public void onClose() throws IOException { - if (getBytesWritten() != expectedSize) { - throw new IOException( - String.format( - "RydeTarOutputStream expected %,d bytes, but got %,d bytes", - expectedSize, getBytesWritten())); - } - // Round up to a 512-byte boundary and another 1024-bytes to indicate end of archive. - out.write(new byte[1024 + 512 - (int) (getBytesWritten() % 512L)]); - } -} diff --git a/java/google/registry/util/PosixTarHeader.java b/java/google/registry/util/PosixTarHeader.java index d65b21be8..cf984e996 100644 --- a/java/google/registry/util/PosixTarHeader.java +++ b/java/google/registry/util/PosixTarHeader.java @@ -108,7 +108,7 @@ public final class PosixTarHeader { UNSUPPORTED; } - private static final int HEADER_LENGTH = 512; + public static final int HEADER_LENGTH = 512; private final byte[] header; diff --git a/javatests/google/registry/rde/RydeTarTest.java b/javatests/google/registry/rde/RydeTarTest.java new file mode 100644 index 000000000..f5ad09817 --- /dev/null +++ b/javatests/google/registry/rde/RydeTarTest.java @@ -0,0 +1,54 @@ +// Copyright 2018 The Nomulus Authors. All Rights Reserved. +// +// 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 +// +// http://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. + +package google.registry.rde; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.io.ByteStreams; +import google.registry.testing.ShardableTestCase; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import org.joda.time.DateTime; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class RydeTarTest extends ShardableTestCase { + + @Test + public void testWriteRead() throws Exception { + byte[] expectedContent = "Testing 1, 2, 3".getBytes(UTF_8); + String expectedFilename = "myFile.xml"; + DateTime expectedModified = DateTime.parse("2015-12-25T06:30:00.000Z"); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (OutputStream writer = + RydeTar.openTarWriter(output, expectedContent.length, expectedFilename, expectedModified)) { + writer.write(expectedContent); + } + byte[] tar = output.toByteArray(); + + ByteArrayInputStream input = new ByteArrayInputStream(tar); + try (RydeTar.TarInputStream reader = RydeTar.openTarReader(input)) { + assertThat(reader.getFilename()).isEqualTo(expectedFilename); + assertThat(reader.getModified()).isEqualTo(expectedModified); + assertThat(ByteStreams.toByteArray(reader)).isEqualTo(expectedContent); + } + + } +}