package org.tmatesoft.svn.core.internal.io.fs.revprop;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.internal.io.fs.FSFile;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc.SVNWCProperties;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.util.SVNLogType;
import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
public class SVNFSFSPackedRevProps {
public static final int INT64_BUFFER_SIZE = 21;
public static SVNFSFSPackedRevProps fromPackFile(File file) throws SVNException {
final byte[] buffer = new byte[(int) file.length()];
InputStream inputStream = null;
try {
inputStream = SVNFileUtil.openFileForReading(file);
int read = SVNFileUtil.readIntoBuffer(inputStream, buffer, 0, buffer.length);
if (read != buffer.length) {
SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.STREAM_UNEXPECTED_EOF);
SVNErrorManager.error(errorMessage, SVNLogType.FSFS);
}
return fromCompressedByteArray(buffer);
} catch (IOException e) {
SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.IO_ERROR);
SVNErrorManager.error(errorMessage, e, SVNLogType.FSFS);
} finally {
SVNFileUtil.closeFile(inputStream);
}
return null;
}
public static SVNFSFSPackedRevProps fromCompressedByteArray(byte[] compressedData) throws SVNException {
final byte[] uncompressedData = decompress(compressedData);
return fromUncompressedByteArray(uncompressedData);
}
private static SVNFSFSPackedRevProps fromUncompressedByteArray(byte[] uncompressedData) throws SVNException {
final ByteArrayInputStream inputStream = new ByteArrayInputStream(uncompressedData);
try {
final long firstRevision = readNumber(inputStream);
final long revisionsCount = readNumber(inputStream);
int offset = 0;
final List<Entry> entries = new ArrayList<Entry>((int) revisionsCount);
for (int i = 0; i < revisionsCount; i++) {
final Entry entry = new Entry(uncompressedData, offset, (int) readNumber(inputStream));
entries.add(entry);
offset += entry.length;
}
if (inputStream.read() != '\n') {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_CORRUPT, "Header end not found");
SVNErrorManager.error(err, SVNLogType.FSFS);
}
final int headerLength = uncompressedData.length - inputStream.available();
for (Entry entry : entries) {
entry.offset += headerLength;
}
return new SVNFSFSPackedRevProps(firstRevision, entries, uncompressedData);
} finally {
SVNFileUtil.closeFile(inputStream);
}
}
private final long firstRevision;
private final List<Entry> entries;
//cached values
private byte[] cachedUncompressedByteArray;
private SVNFSFSPackedRevProps(long firstRevision, List<Entry> entries, byte[] cachedUncompressedByteArray) {
this.firstRevision = firstRevision;
this.entries = entries;
this.cachedUncompressedByteArray = cachedUncompressedByteArray;
}
public long getFirstRevision() {
return firstRevision;
}
public long getRevisionsCount() {
return entries.size();
}
public byte[] asCompressedLevelNoneByteArray() throws SVNException {
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
OutputStream outputStream = byteArrayOutputStream;
try {
outputStream = writeCompressedLevelNone(byteArrayOutputStream);
return byteArrayOutputStream.toByteArray();
} finally {
SVNFileUtil.closeFile(outputStream);
}
}
public void writeToFile(File packFile, boolean compress) throws SVNException {
OutputStream outputStream = SVNFileUtil.openFileForWriting(packFile);
try {
if (compress) {
outputStream = writeCompressedLevelDefault(outputStream);
} else {
outputStream = writeCompressedLevelNone(outputStream);
}
} finally {
SVNFileUtil.closeFile(outputStream);
}
}
private OutputStream writeCompressedLevelDefault(OutputStream outputStream) throws SVNException {
return compressLevelDefault(asUncompressedByteArray(), outputStream);
}
private OutputStream writeCompressedLevelNone(OutputStream outputStream) throws SVNException {
return compressLevelNone(asUncompressedByteArray(), outputStream);
}
public SVNProperties parseProperties(long revision) throws SVNException {
final int revisionIndex = (int) (revision - getFirstRevision());
if (revisionIndex >= getRevisionsCount()) {
return null;
}
final Entry entry = entries.get(revisionIndex);
return parseProperties(entry.data, entry.offset, entry.length);
}
private long getTotalSize() {
long totalSize = 0;
for (Entry entry : entries) {
totalSize += entry.getSize();
}
return totalSize;
}
private long getSerializedSize() throws SVNException {
return asUncompressedByteArray().length;
}
public List<SVNFSFSPackedRevProps> setProperties(long revision, SVNProperties properties, long revPropPackSize) throws SVNException {
final byte[] propertiesByteArray = composePropertiesByteArray(properties);
final long revisionIndex = revision - getFirstRevision();
final long newTotalSize = getTotalSize() - getSerializedSize() + propertiesByteArray.length +
(getRevisionsCount() + 2) * INT64_BUFFER_SIZE;
setEntry(revision, propertiesByteArray);
if (newTotalSize < revPropPackSize || entries.size() == 1) {
return Collections.singletonList(this);
} else {
final List<SVNFSFSPackedRevProps> packs = new ArrayList<SVNFSFSPackedRevProps>();
int leftCount = 0;
int rightCount = 0;
int left = 0;
int right = (int) getRevisionsCount() - 1;
long leftSize = 2 * INT64_BUFFER_SIZE;
long rightSize = 2 * INT64_BUFFER_SIZE;
while (left <= right) {
final Entry leftEntry = entries.get(left);
final Entry rightEntry = entries.get(right);
if (leftSize + leftEntry.getSize() < rightSize + rightEntry.getSize()) {
leftSize += leftEntry.getSize();
left++;
} else {
rightSize += rightEntry.getSize();
right--;
}
}
leftCount = left;
rightCount = (int) (getRevisionsCount() - left);
if (leftSize > revPropPackSize || rightSize > revPropPackSize) {
leftCount = (int) revisionIndex;
rightCount = (int) (getRevisionsCount() - leftCount - 1);
}
if (leftCount != 0) {
final long leftFirstRevision = getFirstRevision();
packs.add(new SVNFSFSPackedRevProps(leftFirstRevision, entries.subList(0, leftCount), null));
}
if (leftCount + rightCount < getRevisionsCount()) {
final long middleFirstRevision = revision;
packs.add(new SVNFSFSPackedRevProps(middleFirstRevision, Collections.singletonList(entries.get((int) revisionIndex)), null));
}
if (rightCount != 0) {
final long rightFirstRevision = getRevisionsCount() - rightCount + getFirstRevision();
packs.add(new SVNFSFSPackedRevProps(rightFirstRevision, entries.subList((int) (getRevisionsCount() - rightCount), (int)getRevisionsCount()), null));
}
return packs;
}
}
private static byte[] decompress(byte[] compressedData) throws SVNException {
ByteArrayOutputStream outputStream = null;
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(compressedData);
InputStream inputStream = byteArrayInputStream;
try {
final long uncompressedSize = readEncodedUncompressedSize(byteArrayInputStream, 10);
if (uncompressedSize == byteArrayInputStream.available()) {
return arrayCopyOfRange(compressedData, (int)(compressedData.length - uncompressedSize), (int)uncompressedSize);
}
//otherwise pass the stream via inflater
outputStream = new ByteArrayOutputStream();
inputStream = new InflaterInputStream(inputStream);
final byte[] buffer = new byte[2048];
while (true) {
final int read = inputStream.read(buffer);
if (read < 0) {
break;
}
outputStream.write(buffer, 0, read);
}
return outputStream.toByteArray();
} catch (IOException e) {
SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.IO_ERROR);
SVNErrorManager.error(errorMessage, e, SVNLogType.FSFS);
} finally {
SVNFileUtil.closeFile(inputStream);
SVNFileUtil.closeFile(outputStream);
}
return null;
}
private static byte[] arrayCopyOfRange(byte[] bytes, int offset, int length) {
//we can't use Arrays.copyOf because it appeared in Java 6 only
final byte[] copiedBytes = new byte[length];
System.arraycopy(bytes, offset, copiedBytes, 0, length);
return copiedBytes;
}
protected static OutputStream compressLevelNone(byte[] uncompressedData, OutputStream outputStream) throws SVNException {
writeEncodedUnCompressedSize(uncompressedData.length, outputStream);
writeBody(uncompressedData, outputStream);
return outputStream;
}
private OutputStream compressLevelDefault(byte[] uncompressedData, OutputStream outputStream) throws SVNException {
writeEncodedUnCompressedSize(uncompressedData.length, outputStream);
outputStream = new DeflaterOutputStream(outputStream);
writeBody(uncompressedData, outputStream);
return outputStream;
}
private static void writeBody(byte[] bytes, OutputStream outputStream) throws SVNException {
try {
outputStream.write(bytes);
} catch (IOException e) {
SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.IO_ERROR);
SVNErrorManager.error(errorMessage, e, SVNLogType.FSFS);
}
}
private static void writeEncodedUnCompressedSize(int compressedSize, OutputStream outputStream) throws SVNException {
long v = compressedSize >> 7;
int n = 1;
while (v > 0) {
v = v >> 7;
n++;
}
while (--n >= 0) {
final byte cont = (byte) (((n > 0) ? 0x1 : 0x0) << 7);
final byte b = (byte) (((compressedSize >> (n * 7)) & 0x7f) | cont);
try {
outputStream.write(b);
} catch (IOException e) {
SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.IO_ERROR);
SVNErrorManager.error(errorMessage, e, SVNLogType.FSFS);
}
}
}
private static long readEncodedUncompressedSize(InputStream inputStream, int lengthRecordSize) throws SVNException {
int temp = 0;
int bytesRead = 0;
while (true) {
if (lengthRecordSize == bytesRead) {
SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.SVNDIFF_INVALID_HEADER, "Decompression of svndiff data failed: size too large");
SVNErrorManager.error(errorMessage, SVNLogType.FSFS);
}
int c = 0;
try {
c = inputStream.read();
} catch (IOException e) {
SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.IO_ERROR);
SVNErrorManager.error(errorMessage, e, SVNLogType.FSFS);
}
if (c < 0) {
SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.SVNDIFF_UNEXPECTED_END, "Decompression of svndiff data failed: no size");
SVNErrorManager.error(errorMessage, SVNLogType.FSFS);
}
bytesRead++;
temp = (temp << 7) | (c & 0x7f);
if (c < 0x80) {
return temp;
}
}
}
private byte[] asUncompressedByteArray() throws SVNException {
if (cachedUncompressedByteArray == null) {
cachedUncompressedByteArray = toUncompressedByteArray();
}
return cachedUncompressedByteArray;
}
private SVNProperties parseProperties(byte[] data, int offset, int length) throws SVNException {
final FSFile fsFile = new FSFile(data, offset, length);
try {
return fsFile.readProperties(false, true);
} finally {
fsFile.close();
}
}
private static byte[] composePropertiesByteArray(SVNProperties properties) throws SVNException {
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try {
SVNWCProperties.setProperties(properties, byteArrayOutputStream, SVNWCProperties.SVN_HASH_TERMINATOR);
return byteArrayOutputStream.toByteArray();
} finally {
SVNFileUtil.closeFile(byteArrayOutputStream);
}
}
private byte[] toUncompressedByteArray() throws SVNException {
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try {
writeNumber(byteArrayOutputStream, getFirstRevision());
writeNumber(byteArrayOutputStream, getRevisionsCount());
for (Entry entry : entries) {
writeNumber(byteArrayOutputStream, entry.getSize());
}
byteArrayOutputStream.write('\n');
for (Entry entry : entries) {
byteArrayOutputStream.write(entry.data, entry.offset, entry.length);
}
return byteArrayOutputStream.toByteArray();
} finally {
SVNFileUtil.closeFile(byteArrayOutputStream);
}
}
private void writeNumber(OutputStream outputStream, long number) throws SVNException {
try {
outputStream.write(String.valueOf(number).getBytes());
outputStream.write('\n');
} catch (IOException e) {
SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.IO_ERROR);
SVNErrorManager.error(errorMessage, e, SVNLogType.FSFS);
}
}
private static long readNumber(InputStream inputStream) throws SVNException {
char[] digits = new char[20];
int digitsCount = 0;
while (true) {
int c = 0;
try {
c = inputStream.read();
} catch (IOException e) {
SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.IO_ERROR);
SVNErrorManager.error(errorMessage, e, SVNLogType.FSFS);
}
if (c < 0) {
SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.FS_CORRUPT);
SVNErrorManager.error(errorMessage, SVNLogType.FSFS);
}
if (c != '\n' && (c < '0'|| c > '9')) {
SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.FS_CORRUPT);
SVNErrorManager.error(errorMessage, SVNLogType.FSFS);
}
if (c == '\n') {
return Long.parseLong(new String(digits, 0, digitsCount));
}
digits[digitsCount] = (char) c;
digitsCount++;
}
}
private void setEntry(long revision, byte[] data) {
invalidateCaches();
entries.set((int) (revision - getFirstRevision()), new Entry(data, 0, data.length));
}
private void invalidateCaches() {
this.cachedUncompressedByteArray = null;
}
public static class Builder {
private long firstRevision;
private final List<Entry> entries;
public Builder() {
this.firstRevision = -1;
this.entries = new ArrayList<Entry>();
}
public SVNFSFSPackedRevProps build() throws SVNException {
if (!SVNRevision.isValidRevisionNumber(firstRevision)) {
SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.INCORRECT_PARAMS, "First revision is not set");
SVNErrorManager.error(errorMessage, SVNLogType.FSFS);
}
return new SVNFSFSPackedRevProps(firstRevision, entries, null);
}
public void setFirstRevision(long firstRevision) {
this.firstRevision = firstRevision;
}
public void addByteArrayEntry(byte[] data) {
addByteArrayEntry(data, 0, data.length);
}
public void addByteArrayEntry(byte[] data, int offset, int length) {
entries.add(new Entry(data, offset, length));
}
public void addPropertiesEntry(SVNProperties properties) throws SVNException {
addByteArrayEntry(composePropertiesByteArray(properties));
}
}
private static class Entry {
private byte[] data;
private int offset;
private int length;
private Entry(byte[] data, int offset, int length) {
this.data = data;
this.offset = offset;
this.length = length;
}
public long getSize() {
return length;
}
}
}