/*
* ====================================================================
* Copyright (c) 2004-2012 TMate Software Ltd. All rights reserved.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at http://svnkit.com/license.html
* If newer versions of this license are posted there, you may use a
* newer version instead, at your option.
* ====================================================================
*/
package org.tmatesoft.svn.core.internal.io.fs;
import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.internal.util.SVNHashMap;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.util.SVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.logging.Level;
/**
* @version 1.3
* @author TMate Software Ltd.
*/
public class FSFile {
private File myFile;
private final byte[] myData;
private int myOffset;
private int myLength;
private FileChannel myChannel;
private InputStream myInputStream;
private long myPosition;
private long myBufferPosition;
private ByteBuffer myBuffer;
private ByteBuffer myReadLineBuffer;
private CharsetDecoder myDecoder;
private MessageDigest myDigest;
public FSFile(File file) {
myFile = file;
myData = null;
myPosition = 0;
myBufferPosition = 0;
myBuffer = ByteBuffer.allocate(1024);
myReadLineBuffer = ByteBuffer.allocate(1024);
myDecoder = Charset.forName("UTF-8").newDecoder();
myDecoder = myDecoder.onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT);
}
public FSFile(byte[] data) {
this(data, 0, data.length);
}
public FSFile(byte[] data, int offset, int length) {
myFile = null;
myData = data;
myOffset = offset;
myLength = length;
myPosition = 0;
myBufferPosition = 0;
myBuffer = ByteBuffer.allocate(1024);
myReadLineBuffer = ByteBuffer.allocate(1024);
myDecoder = Charset.forName("UTF-8").newDecoder();
myDecoder = myDecoder.onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT);
}
public void seek(long position) {
myPosition = position;
}
public long position() {
return myPosition;
}
public long size() {
return myData == null ? myFile.length() : myLength;
}
public void resetDigest() {
if (myDigest == null) {
try {
myDigest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
}
}
myDigest.reset();
}
public String digest() {
String digest = SVNFileUtil.toHexDigest(myDigest);
myDigest = null;
return digest;
}
public int readInt() throws SVNException, NumberFormatException {
String line = readLine(80);
if (line == null) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_VERSION_FILE_FORMAT,
"First line of ''{0}'' contains non-digit", myFile);
SVNErrorManager.error(err, SVNLogType.DEFAULT);
}
return Integer.parseInt(line);
}
public long readLong() throws SVNException, NumberFormatException {
String line = readLine(80);
if (line == null) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_VERSION_FILE_FORMAT,
"First line of ''{0}'' contains non-digit", myFile);
SVNErrorManager.error(err, SVNLogType.DEFAULT);
}
return Long.parseLong(line);
}
public String readLine(int limit) throws SVNException {
long currentLimit = limit < 0 ? 1024 : limit; //if limit < 0, read line buffer should have infinite size
allocateReadBuffer((int) currentLimit);
try {
while(myReadLineBuffer.hasRemaining()) {
int b = read();
if (b < 0) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_UNEXPECTED_EOF, "Can''t read length line from file {0}", getFile());
SVNErrorManager.error(err, SVNLogType.DEFAULT);
} else if (b == '\n') {
break;
}
myReadLineBuffer.put((byte) (b & 0XFF));
if (limit < 0 && !myReadLineBuffer.hasRemaining()) {
//make myReadLineBuffer twice as larger
byte[] oldArray = myReadLineBuffer.array();
int oldLimit = (int) currentLimit;
currentLimit = currentLimit * 2;
allocateReadBuffer((int) currentLimit);
myReadLineBuffer.put(oldArray, 0, oldLimit);
}
}
myReadLineBuffer.flip();
return myDecoder.decode(myReadLineBuffer).toString();
} catch (IOException e) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_CORRUPT, "Can''t read length line from file {0}: {1}", new Object[]{getFile(), e.getLocalizedMessage()});
SVNErrorManager.error(err, e, SVNLogType.DEFAULT);
}
return null;
}
public String readLine(StringBuffer buffer) throws SVNException {
if (buffer == null) {
buffer = new StringBuffer();
}
boolean endOfLineMet = false;
boolean lineStart = true;
try {
while (!endOfLineMet) {
allocateReadBuffer(160);
while(myReadLineBuffer.hasRemaining()) {
int b = read();
if (b < 0) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.STREAM_UNEXPECTED_EOF,
"Can''t read length line from file {0}", getFile());
SVNErrorManager.error(err, lineStart ? Level.FINEST : Level.FINE, SVNLogType.DEFAULT);
} else if (b == '\n') {
endOfLineMet = true;
break;
}
myReadLineBuffer.put((byte) (b & 0XFF));
lineStart = false;
}
myReadLineBuffer.flip();
buffer.append(myDecoder.decode(myReadLineBuffer).toString());
}
} catch (IOException e) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_CORRUPT, "Can''t read length line from file {0}: {1}", new Object[]{getFile(), e.getLocalizedMessage()});
SVNErrorManager.error(err, e, SVNLogType.DEFAULT);
}
return buffer.toString();
}
public SVNProperties readProperties(boolean allowEOF, boolean allowBinaryValues) throws SVNException {
SVNProperties properties = new SVNProperties();
String line = null;
try {
while(true) {
try {
line = readLine(160); // K length or END, there may be EOF.
} catch (SVNException e) {
if (allowEOF && e.getErrorMessage().getErrorCode() == SVNErrorCode.STREAM_UNEXPECTED_EOF) {
break;
}
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MALFORMED_FILE);
SVNErrorManager.error(err, e, SVNLogType.DEFAULT);
}
if (line == null || "".equals(line)) {
break;
} else if (!allowEOF && "END".equals(line)) {
break;
}
char kind = line.charAt(0);
int length = -1;
if ((kind != 'K' && kind != 'D') || line.length() < 3 || line.charAt(1) != ' ' || line.length() < 3) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MALFORMED_FILE);
SVNErrorManager.error(err, SVNLogType.DEFAULT);
}
try {
length = Integer.parseInt(line.substring(2));
} catch (NumberFormatException nfe) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MALFORMED_FILE);
SVNErrorManager.error(err, SVNLogType.DEFAULT);
}
if (length < 0) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MALFORMED_FILE);
SVNErrorManager.error(err, SVNLogType.DEFAULT);
}
allocateReadBuffer(length + 1);
read(myReadLineBuffer);
myReadLineBuffer.flip();
myReadLineBuffer.limit(myReadLineBuffer.limit() - 1);
int pos = myReadLineBuffer.position();
int limit = myReadLineBuffer.limit();
String key = null;
try {
key = myDecoder.decode(myReadLineBuffer).toString();
} catch (MalformedInputException mfi) {
key = new String(myReadLineBuffer.array(), myReadLineBuffer.arrayOffset() + pos, limit - pos);
}
if (kind == 'D') {
properties.put(key, (SVNPropertyValue) null);
continue;
}
line = readLine(160);
if (line == null || line.length() < 3 || line.charAt(0) != 'V' || line.charAt(1) != ' ') {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MALFORMED_FILE);
SVNErrorManager.error(err, SVNLogType.DEFAULT);
}
try {
length = Integer.parseInt(line.substring(2));
} catch (NumberFormatException nfe) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MALFORMED_FILE);
SVNErrorManager.error(err, SVNLogType.DEFAULT);
}
if (length < 0) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MALFORMED_FILE);
SVNErrorManager.error(err, SVNLogType.DEFAULT);
}
allocateReadBuffer(length + 1);
read(myReadLineBuffer);
myReadLineBuffer.flip();
myReadLineBuffer.limit(myReadLineBuffer.limit() - 1);
pos = myReadLineBuffer.position();
limit = myReadLineBuffer.limit();
try {
properties.put(key, myDecoder.decode(myReadLineBuffer).toString());
} catch (CharacterCodingException cce) {
if (allowBinaryValues){
byte[] dst = new byte[limit - pos];
myReadLineBuffer.position(pos);
myReadLineBuffer.get(dst);
properties.put(key, dst);
} else {
SVNErrorMessage error = SVNErrorMessage.create(SVNErrorCode.FS_CORRUPT, "File ''{0}'' contains unexpected binary property value", getFile());
SVNErrorManager.error(error, cce, SVNLogType.DEFAULT);
}
}
}
} catch (IOException e) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.MALFORMED_FILE);
SVNErrorManager.error(err, e, SVNLogType.DEFAULT);
}
return properties;
}
public Map readHeader() throws SVNException {
Map map = new SVNHashMap();
String line;
while(true) {
line = readLine(-1);
if ("".equals(line)) {
break;
}
int colonIndex = line.indexOf(':');
if (colonIndex <= 0 || line.length() <= colonIndex + 2) {
SVNDebugLog.getDefaultLog().logFine(SVNLogType.DEFAULT, line);
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_CORRUPT,
"Found malformed header in revision file");
SVNErrorManager.error(err, SVNLogType.DEFAULT);
} else if (line.charAt(colonIndex + 1) != ' ') {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_CORRUPT,
"Found malformed header in revision file");
SVNErrorManager.error(err, SVNLogType.DEFAULT);
}
String key = line.substring(0, colonIndex);
String value = line.substring(colonIndex + 2);
map.put(key, value);
}
return map;
}
public int read() throws IOException {
if (myData != null) {
if (myPosition >= myLength) {
return -1;
}
myPosition++;
if (myDigest != null) {
myDigest.update((byte) (myData[((int) (myOffset + myPosition - 1))] & 0xff));
}
return myData[((int) (myOffset + myPosition - 1))] & 0xff;
}
if ((myChannel == null && myInputStream == null) || myPosition < myBufferPosition || myPosition >= myBufferPosition + myBuffer.limit()) {
if (fill() <= 0) {
return -1;
}
} else {
myBuffer.position((int) (myPosition - myBufferPosition));
}
int r = (myBuffer.get() & 0xFF);
if (myDigest != null) {
myDigest.update((byte) r);
}
myPosition++;
return r;
}
public int read(ByteBuffer target) throws IOException {
if (myData != null) {
int couldRead = (int) Math.min(myLength - myPosition, target.remaining());
target.put(myData, (int) myPosition + myOffset, couldRead);
if (myDigest != null) {
myDigest.update(myData, (int) myPosition + myOffset, couldRead);
}
myPosition += couldRead;
return couldRead > 0 ? couldRead : -1;
}
int read = 0;
while(target.hasRemaining()) {
if (fill() < 0) {
return read > 0 ? read : -1;
}
myBuffer.position((int) (myPosition - myBufferPosition));
int couldRead = Math.min(myBuffer.remaining(), target.remaining());
int readFrom = myBuffer.position() + myBuffer.arrayOffset();
target.put(myBuffer.array(), readFrom, couldRead);
if (myDigest != null) {
myDigest.update(myBuffer.array(), readFrom, couldRead);
}
myPosition += couldRead;
read += couldRead;
myBuffer.position(myBuffer.position() + couldRead);
}
return read;
}
public int read(byte[] buffer, int offset, int length) throws IOException {
if (myData != null) {
int couldRead = (int) Math.min(myLength - myPosition, length);
System.arraycopy(myData, (int) myPosition + myOffset, buffer, offset, couldRead);
if (myDigest != null) {
myDigest.update(myData, (int) myPosition + myOffset, couldRead);
}
myPosition += couldRead;
return couldRead > 0 ? couldRead : -1;
}
int read = 0;
int toRead = length;
while(toRead > 0) {
if (fill() < 0) {
return read > 0 ? read : -1;
}
myBuffer.position((int) (myPosition - myBufferPosition));
int couldRead = Math.min(myBuffer.remaining(), toRead);
myBuffer.get(buffer, offset, couldRead);
if (myDigest != null) {
myDigest.update(buffer, offset, couldRead);
}
toRead -= couldRead;
offset += couldRead;
myPosition += couldRead;
read += couldRead;
}
return read;
}
public File getFile() {
return myFile;
}
public void close() {
if (myChannel != null) {
try {
myChannel.close();
} catch (IOException e) {}
}
SVNFileUtil.closeFile(myInputStream);
myChannel = null;
myInputStream = null;
myPosition = 0;
myDigest = null;
}
private int fill() throws IOException {
if ((myChannel == null && myInputStream == null) || myPosition < myBufferPosition || (myPosition >= myBufferPosition + myBuffer.limit())) {
myBufferPosition = myPosition;
getChannel().position(myBufferPosition);
myBuffer.clear();
int read = getChannel().read(myBuffer);
myBuffer.position(0);
myBuffer.limit(read >= 0 ? read : 0);
return read;
}
return 0;
}
private void allocateReadBuffer(int limit) {
if (limit > myReadLineBuffer.capacity()) {
myReadLineBuffer = ByteBuffer.allocate(limit*3/2);
}
myReadLineBuffer.clear();
myReadLineBuffer.limit(limit);
}
private FileChannel getChannel() throws IOException {
if (myChannel == null) {
final FileInputStream fileInputStream = SVNFileUtil.createFileInputStream(myFile);
myChannel = fileInputStream.getChannel();
myInputStream = fileInputStream;
}
return myChannel;
}
public PathInfo readPathInfoFromReportFile() throws IOException, SVNException {
int firstByte = read();
if (firstByte == -1 || firstByte == '-') {
return null;
}
String path = readStringFromReportFile();
String linkPath = read() == '+' ? readStringFromReportFile() : null;
long revision = readRevisionFromReportFile();
SVNDepth depth = SVNDepth.INFINITY;
if (read() == '+') {
int id = read();
switch(id) {
case 'X':
depth = SVNDepth.EXCLUDE;
break;
case 'E':
depth = SVNDepth.EMPTY;
break;
case 'F':
depth = SVNDepth.FILES;
break;
case 'M':
depth = SVNDepth.IMMEDIATES;
break;
default: {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_BAD_REVISION_REPORT, "Invalid depth ({0}) for path ''{1}''", new Object[]{new Integer(id), path});
SVNErrorManager.error(err, SVNLogType.WC);
}
}
}
boolean startEmpty = read() == '+';
String lockToken = read() == '+' ? readStringFromReportFile() : null;
return new PathInfo(path, linkPath, lockToken, revision, depth, startEmpty);
}
private String readStringFromReportFile() throws IOException {
int length = readNumberFromReportFile();
if (length == 0) {
return "";
}
byte[] buffer = new byte[length];
read(buffer, 0, length);
return new String(buffer, "UTF-8");
}
private int readNumberFromReportFile() throws IOException {
int b;
ByteArrayOutputStream resultStream = new ByteArrayOutputStream();
while ((b = read()) != ':') {
resultStream.write(b);
}
return Integer.parseInt(new String(resultStream.toByteArray(), "UTF-8"), 10);
}
private long readRevisionFromReportFile() throws IOException {
if (read() == '+') {
return readNumberFromReportFile();
}
return SVNRepository.INVALID_REVISION;
}
}