Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,39 @@ using (var dbfDataReader = new DbfDataReader(dbfPath))
}
```

Visual FoxPro compound index files (`.cdx`) can be opened and searched, and search results
combined with `Seek` to jump straight to the matching records — `CdxKeyEntry.RecordIndex`
converts the index's one-based record numbers to the zero-based indexes `Seek` expects:

```csharp
using DbfDataReader.Cdx;

var dbfPath = "path/file.dbf";
var cdxPath = "path/file.cdx";

using (var dbfTable = new DbfTable(dbfPath))
using (var cdxFile = new CdxFile(cdxPath, dbfTable.CurrentEncoding))
{
var tagNames = cdxFile.TagNames; // the named indexes ("tags") in the file

var index = cdxFile.GetIndex("CONTACT_ID"); // one tag; index.KeyExpression describes the key
var dbfRecord = new DbfRecord(dbfTable);

foreach (var entry in index.Search("C0000000042"))
{
dbfTable.Seek(entry.RecordIndex);
dbfTable.Read(dbfRecord);
// dbfRecord now holds the matching row
}
}
```

`CdxIndex` also supports `EnumerateEntries()` (full in-order scan), `Count()`, and a
`Search(Func<byte[], int>)` overload for range or prefix searches. Current limitations:
only ascending indexes with byte-wise (MACHINE collation) character keys are searchable,
index key expressions are exposed as text but not evaluated, and index entries include
deleted records (check `DbfRecord.IsDeleted` after seeking).

There is also an implementation of DbConnection so you can query a folder of files e.g.

```csharp
Expand Down
56 changes: 56 additions & 0 deletions src/DbfDataReader/Cdx/BaseCdxNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.Buffers.Binary;
using System.Text;

namespace DbfDataReader.Cdx
{
internal abstract class BaseCdxNode
{
public const int NodeSize = 512;
public const int NoSibling = -1;

protected BaseCdxNode(long offset, CdxIndexHeader indexHeader, CdxNodeAttributes attributes, int keyCount,
int leftSibling, int rightSibling)
{
Offset = offset;
IndexHeader = indexHeader;
Attributes = attributes;
KeyCount = keyCount;
LeftSibling = leftSibling;
RightSibling = rightSibling;
}

public long Offset { get; }

public CdxIndexHeader IndexHeader { get; }

public CdxNodeAttributes Attributes { get; }

public int KeyCount { get; }

public int LeftSibling { get; }

public int RightSibling { get; }

public static BaseCdxNode Read(CdxIndexHeader indexHeader, long offset, ReadOnlySpan<byte> bytes,
Encoding encoding)
{
var attributes = (CdxNodeAttributes)BinaryPrimitives.ReadUInt16LittleEndian(bytes.Slice(0, 2));
if ((attributes | CdxNodeAttributes.All) != CdxNodeAttributes.All)
throw new CdxException(CdxErrorCode.InvalidNodeAttributes);

return attributes.HasFlag(CdxNodeAttributes.LeafNode)
? LeafCdxNode.Read(indexHeader, offset, attributes, bytes, encoding)
: (BaseCdxNode)InteriorCdxNode.Read(indexHeader, offset, attributes, bytes);
}

internal static (int KeyCount, int LeftSibling, int RightSibling) ReadCommonFields(ReadOnlySpan<byte> bytes)
{
var keyCount = BinaryPrimitives.ReadUInt16LittleEndian(bytes.Slice(2, 2));
var leftSibling = BinaryPrimitives.ReadInt32LittleEndian(bytes.Slice(4, 4));
var rightSibling = BinaryPrimitives.ReadInt32LittleEndian(bytes.Slice(8, 4));

return (keyCount, leftSibling, rightSibling);
}
}
}
40 changes: 40 additions & 0 deletions src/DbfDataReader/Cdx/CdxEnums.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;

namespace DbfDataReader.Cdx
{
[Flags]
public enum CdxIndexOptions : byte
{
None = 0,
Unique = 1,
// observed in real-world files (e.g. custom indexes), mentioned but not described in most CDX documentation
CustomIndex = 4,
HasForClause = 8,
BitVector = 16,
IsCompactIndex = 32,
IsCompoundIndexHeader = 64,
IsStructuralIndex = 128,

All = Unique | CustomIndex | HasForClause | BitVector | IsCompactIndex | IsCompoundIndexHeader |
IsStructuralIndex
}

public enum CdxIndexOrder
{
Ascending = 0,
Descending = 1
}

[Flags]
internal enum CdxNodeAttributes
{
// a node with neither RootNode nor LeafNode set is an interior node
None = 0,
RootNode = 1,
LeafNode = 2,
// observed in real-world files, undocumented
Unknown = 4,

All = RootNode | LeafNode | Unknown
}
}
34 changes: 34 additions & 0 deletions src/DbfDataReader/Cdx/CdxException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;

namespace DbfDataReader.Cdx
{
public class CdxException : Exception
{
public CdxException(CdxErrorCode code)
: base($"Invalid CDX index file: {code}")
{
Code = code;
}

public CdxErrorCode Code { get; }
}

public enum CdxErrorCode
{
None,
NotACompoundIndexHeader,
RootNodeDoesNotHaveRootAttribute,
LeftmostNodeHasLeftSibling,
InvalidNodeAttributes,
InvalidIndexOptions,
InvalidExpressionPoolLength,
InvalidInteriorNodeKeyCount,
InvalidInteriorNodeSibling,
InvalidLeafNodeKeyCount,
InvalidLeafNodeCalculatedKeyStartIndex,
FirstLeafNodeKeyEntryHasDuplicateBytes,
PackedKeyEntryLengthTooLong,
InteriorNodeHasNoKeyEntries,
NodeChainTooLong
}
}
141 changes: 141 additions & 0 deletions src/DbfDataReader/Cdx/CdxFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace DbfDataReader.Cdx
{
public class CdxFile : Disposable
{
private readonly bool _leaveOpen;
private readonly long _startOffset;
private readonly byte[] _nodeBuffer = new byte[BaseCdxNode.NodeSize];

private Dictionary<string, CdxIndex> _taggedIndexes;

public CdxFile(string path, Encoding encoding = null)
{
if (!File.Exists(path)) throw new FileNotFoundException();

Path = path;
CurrentEncoding = encoding ?? Encoding.ASCII;
Stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

Init();
}

public CdxFile(Stream stream, Encoding encoding = null, bool leaveOpen = false)
{
Path = string.Empty;
CurrentEncoding = encoding ?? Encoding.ASCII;
Stream = stream;
_leaveOpen = leaveOpen;
_startOffset = stream.CanSeek ? stream.Position : 0;

Init();
}

private void Init()
{
Header = ReadHeader(0);
if (!Header.Options.HasFlag(CdxIndexOptions.IsCompoundIndexHeader))
throw new CdxException(CdxErrorCode.NotACompoundIndexHeader);

RootNode = ReadNode(Header.RootNodePointer, Header);
}

public string Path { get; }

public Encoding CurrentEncoding { get; }

public Stream Stream { get; private set; }

public CdxIndexHeader Header { get; private set; }

public bool IsClosed => Stream == null;

// The file root node is a tag directory: its keys are tag names and its record
// numbers are the file offsets of the tag index headers.
internal BaseCdxNode RootNode { get; private set; }

internal int MaxNodeCount => (int)(Stream.Length / BaseCdxNode.NodeSize) + 1;

protected override void Dispose(bool disposing)
{
try
{
if (!disposing) return;
if (!_leaveOpen) Stream?.Dispose();
}
finally
{
Stream = null;
}
}

public IReadOnlyCollection<string> TagNames
{
get
{
ReadTaggedIndexes();
return _taggedIndexes.Keys;
}
}

public IReadOnlyDictionary<string, CdxIndex> ReadTaggedIndexes()
{
if (_taggedIndexes != null) return _taggedIndexes;

var tagDirectory = new CdxIndex(this, Header, RootNode);

var taggedIndexes = new Dictionary<string, CdxIndex>(StringComparer.OrdinalIgnoreCase);
foreach (var entry in tagDirectory.EnumerateEntries())
{
taggedIndexes[entry.Key] = ReadIndex(entry.RecordNumber);
}

_taggedIndexes = taggedIndexes;
return _taggedIndexes;
}

public CdxIndex GetIndex(string tagName)
{
if (tagName == null) throw new ArgumentNullException(nameof(tagName));

if (!ReadTaggedIndexes().TryGetValue(tagName, out var index))
throw new ArgumentException($"Tag '{tagName}' was not found in the index file.", nameof(tagName));

return index;
}

private CdxIndex ReadIndex(long offset)
{
var header = ReadHeader(offset);
if (!header.Options.HasFlag(CdxIndexOptions.IsCompoundIndexHeader))
throw new CdxException(CdxErrorCode.NotACompoundIndexHeader);

var rootNode = ReadNode(header.RootNodePointer, header);
if (!rootNode.Attributes.HasFlag(CdxNodeAttributes.RootNode))
throw new CdxException(CdxErrorCode.RootNodeDoesNotHaveRootAttribute);

return new CdxIndex(this, header, rootNode);
}

private CdxIndexHeader ReadHeader(long offset)
{
var buffer = new byte[CdxIndexHeader.HeaderSize];
Stream.Seek(_startOffset + offset, SeekOrigin.Begin);
Stream.ReadExactly(buffer, 0, buffer.Length);

return new CdxIndexHeader(buffer, offset);
}

internal BaseCdxNode ReadNode(long offset, CdxIndexHeader indexHeader)
{
Stream.Seek(_startOffset + offset, SeekOrigin.Begin);
Stream.ReadExactly(_nodeBuffer, 0, BaseCdxNode.NodeSize);

return BaseCdxNode.Read(indexHeader, offset, _nodeBuffer, CurrentEncoding);
}
}
}
Loading