Move the RDE TAR file encoding to a dedicated file

The "tar file encoding" saves the file + metadata (filename and modification) in a "tar" format that is required in the RDE spec, even though it only contains a single file.

This is only relevant for RyDE, and not for Ghostryde. In fact, the only reason Ghostryde exists is to not have the TAR layer.

Currently we only encrypt RyDE, so we only need the TAR encoding. We plan to add decryption ability so we can test files we sent to IronMountain if there's a problem - so we will need TAR decoding for that.

The new file - RydeTar.java - has both encoding and decoding. We keep the format used for all other Input/OutputStreams for consistency, even though in this case it could be a private part of the RyDE encoder / decoder.

This is one of a series of CLs - each merging a single "part" of the encoding.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=208056757
This commit is contained in:
guyben 2018-08-09 09:02:54 -07:00 committed by jianglai
parent 81fce674d2
commit 801c8efbc1
5 changed files with 180 additions and 77 deletions

View file

@ -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.RYDE_USE_INTEGRITY_PACKET;
import static google.registry.rde.RydeEncryption.openEncryptor; import static google.registry.rde.RydeEncryption.openEncryptor;
import static google.registry.rde.RydeFileEncoding.openPgpFileWriter; import static google.registry.rde.RydeFileEncoding.openPgpFileWriter;
import static google.registry.rde.RydeTar.openTarWriter;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.io.Closer; import com.google.common.io.Closer;
@ -76,8 +77,7 @@ public final class RydeEncoder extends FilterOutputStream {
kompressor = closer.register(openCompressor(encryptLayer)); kompressor = closer.register(openCompressor(encryptLayer));
fileLayer = closer.register(openPgpFileWriter(kompressor, filenamePrefix + ".tar", modified)); fileLayer = closer.register(openPgpFileWriter(kompressor, filenamePrefix + ".tar", modified));
tarLayer = tarLayer =
closer.register( closer.register(openTarWriter(fileLayer, dataLength, filenamePrefix + ".xml", modified));
new RydeTarOutputStream(fileLayer, dataLength, modified, filenamePrefix + ".xml"));
this.out = tarLayer; this.out = tarLayer;
} }

View file

@ -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.
*
* <p>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);
}
}
}

View file

@ -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)]);
}
}

View file

@ -108,7 +108,7 @@ public final class PosixTarHeader {
UNSUPPORTED; UNSUPPORTED;
} }
private static final int HEADER_LENGTH = 512; public static final int HEADER_LENGTH = 512;
private final byte[] header; private final byte[] header;

View file

@ -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);
}
}
}