Browse Source

initial support for encoded databases, MSISAM support (issue #3065010)

git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@488 f203690c-595d-4dc9-a70b-905162fa7fd2
tags/jackcess-1.2.2
James Ahlborn 13 years ago
parent
commit
bacd054614

+ 69
- 0
src/java/com/healthmarketscience/jackcess/CodecHandler.java View File

/*
Copyright (c) 2005 Health Market Science, Inc.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA

You can contact Health Market Science at info@healthmarketscience.com
or at the following address:

Health Market Science
2700 Horizon Drive
Suite 200
King of Prussia, PA 19406
*/

package com.healthmarketscience.jackcess;

import java.io.IOException;
import java.nio.ByteBuffer;

/**
* Interface for a handler which can encode/decode a specific access page
* encoding.
*
* @author James Ahlborn
*/
public interface CodecHandler
{

/**
* Decodes the given page buffer inline.
*
* @param page the page to be decoded
* @param pageNumber the page number of the given page
*
* @throws IOException if an exception occurs during decoding
*/
public void decodePage(ByteBuffer page, int pageNumber) throws IOException;

/**
* Encodes the given page buffer into a new page buffer and returns it. The
* returned page buffer will be used immediately and discarded so that it
* may be re-used for subsequent page encodings.
*
* @param page the page to be encoded, should not be modified
* @param pageNumber the page number of the given page
* @param pageOffset offset within the page at which to start writing the
* page data
*
* @throws IOException if an exception occurs during decoding
*
* @return the properly encoded page buffer for the given page buffer
*/
public ByteBuffer encodePage(ByteBuffer page, int pageNumber,
int pageOffset)
throws IOException;
}

+ 56
- 0
src/java/com/healthmarketscience/jackcess/CodecProvider.java View File

/*
Copyright (c) 2005 Health Market Science, Inc.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA

You can contact Health Market Science at info@healthmarketscience.com
or at the following address:

Health Market Science
2700 Horizon Drive
Suite 200
King of Prussia, PA 19406
*/

package com.healthmarketscience.jackcess;

import java.io.IOException;

/**
* Interface for a provider which can generate CodecHandlers for various types
* of database encodings. The {@link DefaultCodecProvider} is the default
* implementation of this inferface, but it does not have any actual
* encoding/decoding support (due to possible export issues with calling
* encryption APIs). See the separate
* <a href="https://sourceforge.net/projects/jackcessencrypt/">Jackcess
* Encrypt</a> project for an implementation of this interface which supports
* various access database encryption types.
*
* @author James Ahlborn
*/
public interface CodecProvider
{
/**
* Returns a new CodecHandler for the database associated with the given
* PageChannel.
*
* @param channel the PageChannel for a Database
*
* @return a new CodecHandler, may not be {@code null}
*/
public CodecHandler createHandler(PageChannel channel)
throws IOException;
}

+ 36
- 4
src/java/com/healthmarketscience/jackcess/Database.java View File

Charset charset, TimeZone timeZone) Charset charset, TimeZone timeZone)
throws IOException throws IOException
{ {
return open(mdbFile, readOnly, autoSync, charset, timeZone, null);
}

/**
* Open an existing Database. If the existing file is not writeable or the
* readOnly flag is <code>true</code>, the file will be opened read-only.
* @param mdbFile File containing the database
* @param readOnly iff <code>true</code>, force opening file in read-only
* mode
* @param autoSync whether or not to enable auto-syncing on write. if
* {@code true}, writes will be immediately flushed to disk.
* This leaves the database in a (fairly) consistent state
* on each write, but can be very inefficient for many
* updates. if {@code false}, flushing to disk happens at
* the jvm's leisure, which can be much faster, but may
* leave the database in an inconsistent state if failures
* are encountered during writing.
* @param charset Charset to use, if {@code null}, uses default
* @param timeZone TimeZone to use, if {@code null}, uses default
* @param provider CodecProvider for handling page encoding/decoding, may be
* {@code null} if no special encoding is necessary
*/
public static Database open(File mdbFile, boolean readOnly, boolean autoSync,
Charset charset, TimeZone timeZone,
CodecProvider provider)
throws IOException
{
if(!mdbFile.exists() || !mdbFile.canRead()) { if(!mdbFile.exists() || !mdbFile.canRead()) {
throw new FileNotFoundException("given file does not exist: " + mdbFile); throw new FileNotFoundException("given file does not exist: " + mdbFile);
} }
} }
} }


return new Database(channel, autoSync, null, charset, timeZone);
return new Database(channel, autoSync, null, charset, timeZone, provider);
} }
/** /**
channel.transferFrom(Channels.newChannel( channel.transferFrom(Channels.newChannel(
Thread.currentThread().getContextClassLoader().getResourceAsStream( Thread.currentThread().getContextClassLoader().getResourceAsStream(
fileFormat._emptyFile)), 0, Integer.MAX_VALUE); fileFormat._emptyFile)), 0, Integer.MAX_VALUE);
return new Database(channel, autoSync, fileFormat, charset, timeZone);
return new Database(channel, autoSync, fileFormat, charset, timeZone,
null);
} }


/** /**
* @param timeZone TimeZone to use, if {@code null}, uses default * @param timeZone TimeZone to use, if {@code null}, uses default
*/ */
protected Database(FileChannel channel, boolean autoSync, protected Database(FileChannel channel, boolean autoSync,
FileFormat fileFormat, Charset charset, TimeZone timeZone)
FileFormat fileFormat, Charset charset, TimeZone timeZone,
CodecProvider provider)
throws IOException throws IOException
{ {
boolean success = false; boolean success = false;
_fileFormat = fileFormat; _fileFormat = fileFormat;
_pageChannel = new PageChannel(channel, _format, autoSync); _pageChannel = new PageChannel(channel, _format, autoSync);
_timeZone = ((timeZone == null) ? getDefaultTimeZone() : timeZone); _timeZone = ((timeZone == null) ? getDefaultTimeZone() : timeZone);
if(provider == null) {
provider = DefaultCodecProvider.INSTANCE;
}
// note, it's slighly sketchy to pass ourselves along partially // note, it's slighly sketchy to pass ourselves along partially
// constructed, but only our _format and _pageChannel refs should be // constructed, but only our _format and _pageChannel refs should be
// needed // needed
_pageChannel.initialize(this);
_pageChannel.initialize(this, provider);
_buffer = _pageChannel.createPageBuffer(); _buffer = _pageChannel.createPageBuffer();
readSystemCatalog(); readSystemCatalog();
success = true; success = true;

+ 125
- 0
src/java/com/healthmarketscience/jackcess/DefaultCodecProvider.java View File

/*
Copyright (c) 2005 Health Market Science, Inc.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA

You can contact Health Market Science at info@healthmarketscience.com
or at the following address:

Health Market Science
2700 Horizon Drive
Suite 200
King of Prussia, PA 19406
*/

package com.healthmarketscience.jackcess;

import java.io.IOException;
import java.nio.ByteBuffer;

/**
* Default implementation of CodecProvider which does not have any actual
* encoding/decoding support. See {@link CodecProvider} for details on a more
* useful implementation.
*
* @author James Ahlborn
*/
public class DefaultCodecProvider implements CodecProvider
{
/** common instance of DefaultCodecProvider */
public static final CodecProvider INSTANCE =
new DefaultCodecProvider();

/** common instance of {@link DummyHandler} */
public static final CodecHandler DUMMY_HANDLER =
new DummyHandler();

/** common instance of {@link UnsupportedHandler} */
public static final CodecHandler UNSUPPORTED_HANDLER =
new UnsupportedHandler();


/**
* {@inheritDoc}
* <p>
* This implementation returns DUMMY_HANDLER for databases with no encoding
* and UNSUPPORTED_HANDLER for databases with any encoding.
*/
public CodecHandler createHandler(PageChannel channel)
throws IOException
{
JetFormat format = channel.getFormat();
switch(format.CODEC_TYPE) {
case NONE:
// no encoding, all good
return DUMMY_HANDLER;

case JET:
// check for an encode key. if 0, not encoded
ByteBuffer bb = channel.createPageBuffer();
channel.readPage(bb, 0);
int codecKey = bb.getInt(format.OFFSET_ENCODING_KEY);
return((codecKey == 0) ? DUMMY_HANDLER : UNSUPPORTED_HANDLER);

case MSISAM:
// always encoded, we don't handle it
return UNSUPPORTED_HANDLER;
default:
throw new RuntimeException("Unknown codec type " + format.CODEC_TYPE);
}
}

/**
* CodecHandler implementation which does nothing, useful for databases with
* no extra encoding.
*/
public static class DummyHandler implements CodecHandler
{
public void decodePage(ByteBuffer page, int pageNumber) throws IOException
{
// does nothing
}

public ByteBuffer encodePage(ByteBuffer page, int pageNumber,
int pageOffset)
throws IOException
{
// does nothing
return page;
}
}

/**
* CodecHandler implementation which always throws
* UnsupportedOperationException, useful for databases with unsupported
* encodings.
*/
public static class UnsupportedHandler implements CodecHandler
{
public void decodePage(ByteBuffer page, int pageNumber) throws IOException
{
throw new UnsupportedOperationException("Decoding not supported");
}

public ByteBuffer encodePage(ByteBuffer page, int pageNumber,
int pageOffset)
throws IOException
{
throw new UnsupportedOperationException("Encoding not supported");
}
}

}

+ 60
- 5
src/java/com/healthmarketscience/jackcess/JetFormat.java View File

public static final short TEXT_FIELD_UNIT_SIZE = 2; public static final short TEXT_FIELD_UNIT_SIZE = 2;
/** Maximum size of a text field */ /** Maximum size of a text field */
public static final short TEXT_FIELD_MAX_LENGTH = 255 * TEXT_FIELD_UNIT_SIZE; public static final short TEXT_FIELD_MAX_LENGTH = 255 * TEXT_FIELD_UNIT_SIZE;

public enum CodecType {
NONE, JET, MSISAM;
}
/** Offset in the file that holds the byte describing the Jet format /** Offset in the file that holds the byte describing the Jet format
version */ version */
private static final long OFFSET_VERSION = 20L;
private static final int OFFSET_VERSION = 20;
/** Version code for Jet version 3 */ /** Version code for Jet version 3 */
private static final byte CODE_VERSION_3 = 0x0; private static final byte CODE_VERSION_3 = 0x0;
/** Version code for Jet version 4 */ /** Version code for Jet version 4 */
/** Version code for Jet version 5 */ /** Version code for Jet version 5 */
private static final byte CODE_VERSION_5 = 0x2; private static final byte CODE_VERSION_5 = 0x2;


/** location of the engine name in the header */
private static final int OFFSET_ENGINE_NAME = 0x4;
/** amount of initial data to be read to determine database type */
private static final int HEADER_LENGTH = 21;
private final static byte[] MSISAM_ENGINE = new byte[] {
'M', 'S', 'I', 'S', 'A', 'M', ' ', 'D', 'a', 't', 'a', 'b', 'a', 's', 'e'
};

/** mask used to obfuscate the db header */ /** mask used to obfuscate the db header */
private static final byte[] BASE_HEADER_MASK = new byte[]{ private static final byte[] BASE_HEADER_MASK = new byte[]{
(byte)0xB5, (byte)0x6F, (byte)0x03, (byte)0x62, (byte)0x61, (byte)0x08, (byte)0xB5, (byte)0x6F, (byte)0x03, (byte)0x62, (byte)0x61, (byte)0x08,
/** whether or not we can use indexes in this format */ /** whether or not we can use indexes in this format */
public final boolean INDEXES_SUPPORTED; public final boolean INDEXES_SUPPORTED;

/** type of page encoding supported */
public final CodecType CODEC_TYPE;
/** Database page size in bytes */ /** Database page size in bytes */
public final int PAGE_SIZE; public final int PAGE_SIZE;
public final int OFFSET_HEADER_DATE; public final int OFFSET_HEADER_DATE;
public final int OFFSET_PASSWORD; public final int OFFSET_PASSWORD;
public final int SIZE_PASSWORD; public final int SIZE_PASSWORD;
public final int OFFSET_ENCODING_KEY;
public final int OFFSET_NEXT_TABLE_DEF_PAGE; public final int OFFSET_NEXT_TABLE_DEF_PAGE;
public final int OFFSET_NUM_ROWS; public final int OFFSET_NUM_ROWS;
public final int OFFSET_NEXT_AUTO_NUMBER; public final int OFFSET_NEXT_AUTO_NUMBER;
public static final JetFormat VERSION_3 = new Jet3Format(); public static final JetFormat VERSION_3 = new Jet3Format();
public static final JetFormat VERSION_4 = new Jet4Format(); public static final JetFormat VERSION_4 = new Jet4Format();
public static final JetFormat VERSION_MSISAM = new MSISAMFormat();
public static final JetFormat VERSION_5 = new Jet5Format(); public static final JetFormat VERSION_5 = new Jet5Format();


/** /**
* @throws IOException if the database file format is unsupported. * @throws IOException if the database file format is unsupported.
*/ */
public static JetFormat getFormat(FileChannel channel) throws IOException { public static JetFormat getFormat(FileChannel channel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1);
int bytesRead = channel.read(buffer, OFFSET_VERSION);
if(bytesRead < 1) {
ByteBuffer buffer = ByteBuffer.allocate(HEADER_LENGTH);
int bytesRead = channel.read(buffer, 0L);
if(bytesRead < HEADER_LENGTH) {
throw new IOException("Empty database file"); throw new IOException("Empty database file");
} }
buffer.flip(); buffer.flip();
byte version = buffer.get();
byte version = buffer.get(OFFSET_VERSION);
if (version == CODE_VERSION_3) { if (version == CODE_VERSION_3) {
return VERSION_3; return VERSION_3;
} else if (version == CODE_VERSION_4) { } else if (version == CODE_VERSION_4) {
if(ByteUtil.matchesRange(buffer, OFFSET_ENGINE_NAME, MSISAM_ENGINE)) {
return VERSION_MSISAM;
}
return VERSION_4; return VERSION_4;
} else if (version == CODE_VERSION_5) { } else if (version == CODE_VERSION_5) {
return VERSION_5; return VERSION_5;
READ_ONLY = defineReadOnly(); READ_ONLY = defineReadOnly();
INDEXES_SUPPORTED = defineIndexesSupported(); INDEXES_SUPPORTED = defineIndexesSupported();
CODEC_TYPE = defineCodecType();
PAGE_SIZE = definePageSize(); PAGE_SIZE = definePageSize();
MAX_DATABASE_SIZE = defineMaxDatabaseSize(); MAX_DATABASE_SIZE = defineMaxDatabaseSize();
OFFSET_HEADER_DATE = defineOffsetHeaderDate(); OFFSET_HEADER_DATE = defineOffsetHeaderDate();
OFFSET_PASSWORD = defineOffsetPassword(); OFFSET_PASSWORD = defineOffsetPassword();
SIZE_PASSWORD = defineSizePassword(); SIZE_PASSWORD = defineSizePassword();
OFFSET_ENCODING_KEY = defineOffsetEncodingKey();
OFFSET_NEXT_TABLE_DEF_PAGE = defineOffsetNextTableDefPage(); OFFSET_NEXT_TABLE_DEF_PAGE = defineOffsetNextTableDefPage();
OFFSET_NUM_ROWS = defineOffsetNumRows(); OFFSET_NUM_ROWS = defineOffsetNumRows();
OFFSET_NEXT_AUTO_NUMBER = defineOffsetNextAutoNumber(); OFFSET_NEXT_AUTO_NUMBER = defineOffsetNextAutoNumber();
protected abstract boolean defineReadOnly(); protected abstract boolean defineReadOnly();
protected abstract boolean defineIndexesSupported(); protected abstract boolean defineIndexesSupported();
protected abstract CodecType defineCodecType();
protected abstract int definePageSize(); protected abstract int definePageSize();
protected abstract long defineMaxDatabaseSize(); protected abstract long defineMaxDatabaseSize();
protected abstract int defineOffsetHeaderDate(); protected abstract int defineOffsetHeaderDate();
protected abstract int defineOffsetPassword(); protected abstract int defineOffsetPassword();
protected abstract int defineSizePassword(); protected abstract int defineSizePassword();
protected abstract int defineOffsetEncodingKey();
protected abstract int defineOffsetNextTableDefPage(); protected abstract int defineOffsetNextTableDefPage();
protected abstract int defineOffsetNumRows(); protected abstract int defineOffsetNumRows();
protected abstract int defineOffsetNextAutoNumber(); protected abstract int defineOffsetNextAutoNumber();
@Override @Override
protected boolean defineIndexesSupported() { return false; } protected boolean defineIndexesSupported() { return false; }

@Override
protected CodecType defineCodecType() {
return CodecType.JET;
}
@Override @Override
protected int definePageSize() { return 2048; } protected int definePageSize() { return 2048; }
@Override @Override
protected int defineSizePassword() { return 20; } protected int defineSizePassword() { return 20; }
@Override @Override
protected int defineOffsetEncodingKey() { return 62; }
@Override
protected int defineOffsetNextTableDefPage() { return 4; } protected int defineOffsetNextTableDefPage() { return 4; }
@Override @Override
protected int defineOffsetNumRows() { return 12; } protected int defineOffsetNumRows() { return 12; }
@Override @Override
protected boolean defineIndexesSupported() { return true; } protected boolean defineIndexesSupported() { return true; }
@Override
protected CodecType defineCodecType() {
return CodecType.JET;
}

@Override @Override
protected int definePageSize() { return 4096; } protected int definePageSize() { return 4096; }
@Override @Override
protected int defineSizePassword() { return 40; } protected int defineSizePassword() { return 40; }
@Override @Override
protected int defineOffsetEncodingKey() { return 62; }
@Override
protected int defineOffsetNextTableDefPage() { return 4; } protected int defineOffsetNextTableDefPage() { return 4; }
@Override @Override
protected int defineOffsetNumRows() { return 16; } protected int defineOffsetNumRows() { return 16; }


} }
private static final class MSISAMFormat extends Jet4Format {
private MSISAMFormat() {
super("MSISAM");
}

@Override
protected boolean defineReadOnly() {
return true;
}

@Override
protected CodecType defineCodecType() {
return CodecType.MSISAM;
}
}

private static final class Jet5Format extends Jet4Format { private static final class Jet5Format extends Jet4Format {
private Jet5Format() { private Jet5Format() {
super("VERSION_5"); super("VERSION_5");

+ 20
- 8
src/java/com/healthmarketscience/jackcess/PageChannel.java View File

private final ByteBuffer _forceBytes = ByteBuffer.allocate(1); private final ByteBuffer _forceBytes = ByteBuffer.allocate(1);
/** Tracks free pages in the database. */ /** Tracks free pages in the database. */
private UsageMap _globalUsageMap; private UsageMap _globalUsageMap;
/** handler for the current database encoding type */
private CodecHandler _codecHandler = DefaultCodecProvider.DUMMY_HANDLER;
/** /**
* @param channel Channel containing the database * @param channel Channel containing the database
/** /**
* Does second-stage initialization, must be called after construction. * Does second-stage initialization, must be called after construction.
*/ */
public void initialize(Database database)
public void initialize(Database database, CodecProvider codecProvider)
throws IOException throws IOException
{ {
// note the global usage map is a special map where any page outside of // note the global usage map is a special map where any page outside of
// the current range is assumed to be "on" // the current range is assumed to be "on"
_globalUsageMap = UsageMap.read(database, PAGE_GLOBAL_USAGE_MAP, _globalUsageMap = UsageMap.read(database, PAGE_GLOBAL_USAGE_MAP,
ROW_GLOBAL_USAGE_MAP, true); ROW_GLOBAL_USAGE_MAP, true);

// initialize page en/decoding support
_codecHandler = codecProvider.createHandler(this);
} }
/** /**
} }


if(pageNumber == 0) { if(pageNumber == 0) {
// de-mask header
// de-mask header (note, page 0 never has additional encoding)
applyHeaderMask(buffer); applyHeaderMask(buffer);
} else {
_codecHandler.decodePage(buffer, pageNumber);
} }
} }
* @param pageOffset offset within the page at which to start writing the * @param pageOffset offset within the page at which to start writing the
* page data * page data
*/ */
public void writePage(ByteBuffer page, int pageNumber,
int pageOffset)
public void writePage(ByteBuffer page, int pageNumber, int pageOffset)
throws IOException throws IOException
{ {
validatePageNumber(pageNumber); validatePageNumber(pageNumber);
"Page buffer is too large, size " + (page.remaining() - pageOffset)); "Page buffer is too large, size " + (page.remaining() - pageOffset));
} }
ByteBuffer encodedPage = page;
if(pageNumber == 0) { if(pageNumber == 0) {
// re-mask header // re-mask header
applyHeaderMask(page); applyHeaderMask(page);
} else {
// re-encode page
encodedPage = _codecHandler.encodePage(page, pageNumber, pageOffset);
} }
try { try {
page.position(pageOffset);
_channel.write(page, (getPageOffset(pageNumber) + pageOffset));
encodedPage.position(pageOffset);
_channel.write(encodedPage, (getPageOffset(pageNumber) + pageOffset));
if(_autoSync) { if(_autoSync) {
flush(); flush();
} }
// push the buffer to the end of the page, so that a full page's worth of // push the buffer to the end of the page, so that a full page's worth of
// data is written regardless of the incoming buffer size (we use a tiny // data is written regardless of the incoming buffer size (we use a tiny
// buffer in allocateNewPage) // buffer in allocateNewPage)
long offset = size + (getFormat().PAGE_SIZE - page.remaining());
_channel.write(page, offset);
int pageOffset = (getFormat().PAGE_SIZE - page.remaining());
long offset = size + pageOffset;
int pageNumber = getNextPageNumber(size); int pageNumber = getNextPageNumber(size);
_channel.write(_codecHandler.encodePage(page, pageNumber, pageOffset),
offset);
_globalUsageMap.removePageNumber(pageNumber); //force is done here _globalUsageMap.removePageNumber(pageNumber); //force is done here
return pageNumber; return pageNumber;
} }

Loading…
Cancel
Save