blob: ccd563036c51332c48a3870ebdc2cbf6e8ee4b9b [file] [log] [blame]
// SPDX-FileCopyrightText: © 2021 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
//
// SPDX-License-Identifier: LGPL-3.0-or-later
package eu.mulk.jgvariant.ostree;
import eu.mulk.jgvariant.core.Decoder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.Channels;
import java.util.ArrayList;
import java.util.List;
import org.tukaani.xz.LZMA2Options;
import org.tukaani.xz.XZInputStream;
import org.tukaani.xz.XZOutputStream;
/**
* A payload file from a static delta.
*
* <p>The first byte is a compression byte: {@code 0} for none, {@code 'x'} for LZMA. The actual
* GVariant data begins right after.
*
* <p>Reference: {@code
* ostree-repo-static-delta-private.h#OSTREE_STATIC_DELTA_PART_PAYLOAD_FORMAT_V0}
*
* @param fileModes the {@link FileMode}s of the files generated by this delta payload.
* @param xattrs the {@link Xattr}s of the files generated by this delta payload.
* @param rawDataSource the data bytes used in the delta operations.
* @param operations the operations to apply during delta patching.
* @see DeltaSuperblock
*/
public record DeltaPartPayload(
List<FileMode> fileModes,
List<List<Xattr>> xattrs,
ByteString rawDataSource,
List<DeltaOperation> operations) {
private static ByteBuffer decompress(ByteBuffer byteBuffer) {
byte compressionByte = byteBuffer.get(0);
var dataSlice = byteBuffer.slice(1, byteBuffer.limit() - 1);
return switch (compressionByte) {
case 0 -> dataSlice;
case (byte) 'x' -> {
try {
var dataBytes = new byte[dataSlice.limit()];
dataSlice.get(dataBytes);
var decompressingInputStream = new XZInputStream(new ByteArrayInputStream(dataBytes));
var decompressedOutputStream = new ByteArrayOutputStream();
decompressingInputStream.transferTo(decompressedOutputStream);
yield ByteBuffer.wrap(decompressedOutputStream.toByteArray());
} catch (IOException e) {
// impossible
throw new IllegalStateException(e);
}
}
default -> throw new IllegalArgumentException(
"unrecognized compression byte '%d'".formatted(compressionByte));
};
}
private static ByteBuffer compress(ByteBuffer dataSlice) {
var dataBytes = new byte[dataSlice.limit()];
dataSlice.get(dataBytes);
var compressedOutputStream = new ByteArrayOutputStream();
byte compressionByte = 'x';
compressedOutputStream.write(compressionByte);
try (var compressingOutputStream =
new XZOutputStream(compressedOutputStream, new LZMA2Options());
var compressingChannel = Channels.newChannel(compressingOutputStream)) {
compressingChannel.write(dataSlice);
compressingOutputStream.write(dataBytes);
} catch (IOException e) {
// impossible
throw new IllegalStateException(e);
}
var compressedBytes = compressedOutputStream.toByteArray();
return ByteBuffer.wrap(compressedBytes);
}
private static byte[] serializeDeltaOperationList(List<DeltaOperation> deltaOperations) {
var output = new ByteArrayOutputStream();
for (var currentOperation : deltaOperations) {
currentOperation.writeTo(output);
}
return output.toByteArray();
}
private static List<DeltaOperation> parseDeltaOperationList(
byte[] bytes, List<ObjectType> objectTypes) {
List<DeltaOperation> deltaOperations = new ArrayList<>();
var byteBuffer = ByteBuffer.wrap(bytes);
int objectIndex = 0;
while (byteBuffer.hasRemaining()) {
var currentOperation = DeltaOperation.readFrom(byteBuffer, objectTypes.get(objectIndex));
deltaOperations.add(currentOperation);
if (currentOperation instanceof DeltaOperation.Close
|| currentOperation instanceof DeltaOperation.OpenSpliceAndCloseMeta
|| currentOperation instanceof DeltaOperation.OpenSpliceAndCloseReal) {
++objectIndex;
}
}
return deltaOperations;
}
/**
* A file mode triple (UID, GID, and permission bits).
*
* @param uid the user ID that owns the file.
* @param gid the group ID that owns the file.
* @param mode the POSIX permission bits.
*/
public record FileMode(int uid, int gid, int mode) {
private static final Decoder<FileMode> DECODER =
Decoder.ofStructure(
FileMode.class,
Decoder.ofInt().withByteOrder(ByteOrder.BIG_ENDIAN),
Decoder.ofInt().withByteOrder(ByteOrder.BIG_ENDIAN),
Decoder.ofInt().withByteOrder(ByteOrder.BIG_ENDIAN));
/**
* Acquires a {@link Decoder} for the enclosing type.
*
* @return a possibly shared {@link Decoder}.
*/
public static Decoder<FileMode> decoder() {
return DECODER;
}
}
/**
* Acquires a {@link Decoder} for the enclosing type.
*
* @return a possibly shared {@link Decoder}.
*/
public static Decoder<DeltaPartPayload> decoder(DeltaMetaEntry deltaMetaEntry) {
var objectTypes =
deltaMetaEntry.objects().stream().map(DeltaMetaEntry.DeltaObject::objectType).toList();
return Decoder.ofStructure(
DeltaPartPayload.class,
Decoder.ofArray(FileMode.decoder()),
Decoder.ofArray(Decoder.ofArray(Xattr.decoder())),
ByteString.decoder(),
Decoder.ofByteArray()
.map(
bytes -> parseDeltaOperationList(bytes, objectTypes),
DeltaPartPayload::serializeDeltaOperationList))
.contramap(DeltaPartPayload::decompress, DeltaPartPayload::compress);
}
}