Skip to content

CarlosGtrz/TpsReader

Repository files navigation

TpsReader for .NET

TpsReader is a simple, read-only .NET library for opening TopSpeed (.TPS) files and reading their tables and records. TpsReader.Tool supplies the tps command for schema discovery, filtering, JSON/JSONL output, and CSV export.

Both packages target .NET 8 and support .NET 8 or later. The tool automatically rolls forward to a newer major runtime when .NET 8 is not installed. Version 0.3.0 is a breaking rename from TpsParser and TpsInspector; no compatibility namespace or shim package is provided.

Library usage

dotnet add package TpsReader --version 0.3.5

Basic usage

Open a file and iterate through its tables and records:

using TpsReader;

var file = TpsFile.Open(@"C:\data\CUSTOMER.TPS");

foreach (var table in file.Tables)
{
    Console.WriteLine($"{table.Name}: {table.Records.Count} records");

    foreach (var record in table.Records)
    {
        var customerNumber = record.GetInt32("CUS:CUSTNUMBER");
        var company = record.GetString("COMPANY");

        Console.WriteLine($"{customerNumber}: {company}");
    }
}

LINQ filtering and projection

TpsTable.Records is LINQ-ready, so consumers can use normal LINQ without a package-specific query API:

using TpsReader;

var file = TpsFile.Open(@"C:\data\CUSTOMER.TPS");
var customers = file.GetTable();

var companies = customers.Records
    .Where(record => record.GetString("STATE") == "AZ")
    .Select(record => new
    {
        Number = record.Get<int>("CUSTNUMBER"),
        Company = record.GetString("COMPANY")
    })
    .ToArray();

Use GetTable(string) for a case-insensitive table name or GetTable(int) for a table number. Parameterless GetTable() succeeds only when the file contains exactly one table.

Table names prefer meaningful TPS metadata. For an unnamed single-table file opened from a path, the name is the source filename without its path or final extension (CUSTOMER.TPS becomes CUSTOMER). Unnamed tables from streams, byte arrays, and superfiles fall back to their field prefix, then their table number.

Records support an indexer, generic conversion, and familiar typed helpers:

var record = customers.Records[0];

var company = (string?)record["COMPANY"];
var customerNumber = record.Get<int>("CUSTNUMBER");
var phones = record.Get<string[]>("PHONES");

if (record.TryGet<decimal>("BALANCE", out var balance))
{
    Console.WriteLine(balance);
}

GetValue, the indexer, Get<T>, and TryGet<T> resolve ordinary fields, MEMOs, and BLOBs through the same case-insensitive name rules. Full schema names and unambiguous short names are accepted. BLOB and other byte-array results are defensive copies.

Numeric Get<T> conversions are checked. A DECIMAL value converts exactly to decimal when it is representable; otherwise it remains available losslessly through GetDecimalString. Arbitrary strings are not parsed as numbers. Ordinary fixed-width STRING values have trailing NUL and space padding removed.

GROUP values

GROUP fields default to a fixed-width string projection. Only schema-declared STRING, CSTRING, and PSTRING leaves are placed at their byte offsets; binary members and gaps become spaces. Internal and trailing spaces are preserved.

string groupText = record.Get<string>("ADDRESS_GROUP")!;
byte[] originalGroupBytes = record.Get<byte[]>("ADDRESS_GROUP")!;

string[] groupArray = record.Get<string[]>("LINES")!;
byte[][] originalArrayBytes = record.Get<byte[][]>("LINES")!;

GROUP width is measured in bytes. Array elements repeat the first element's child layout, including nested string leaves and string arrays. Raw byte results are cloned.

Other inputs and options

Open and TryOpen accept a path, readable Stream, or complete byte[]. Streams are consumed from their current position and are never disposed or rewound by the library. Input arrays are treated as read-only.

var encrypted = TpsFile.Open(
    @"C:\data\encrypted.tps",
    new TpsOpenOptions { Owner = "owner-password" });

if (!TpsFile.TryOpen(tpsBytes, out var parsed, out var error))
{
    Console.Error.WriteLine(error!.Message);
}

Set IgnoreErrors = true only for intentional partial recovery. A malformed data page is discarded atomically; no partial record from that page is returned. StringEncoding controls schema names, string fields, GROUP text, and MEMO text.

Command-line tool

Install the tool package while keeping the short tps command:

dotnet tool install --global TpsReader.Tool --version 0.3.5
tps --help

Each GitHub release also includes self-contained native AOT archives:

  • tps-v<version>-win-x64.zip contains tps.exe for 64-bit Windows.
  • tps-v<version>-linux-x64.zip contains tps for 64-bit glibc-based Linux.
  • tps-v<version>-osx-arm64.zip contains tps for Apple silicon macOS.

None of these executables require a separate .NET installation.

Build or run it from this repository:

dotnet build TpsReader.sln -c Release
dotnet test TpsReader.sln -c Release
dotnet run --project src\TpsReader.Tool -c Release -- schema C:\data\CUSTOMER.TPS

To create local packages without publishing them:

dotnet pack src\TpsReader -c Release -o artifacts\packages
dotnet pack src\TpsReader.Tool -c Release -o artifacts\packages

Discover and read

Inspect structure before requesting records:

tps inspect C:\data --recursive
tps schema C:\data\CUSTOMER.TPS
tps schema C:\data\CUSTOMER.TPS --table CUSTOMER
tps rows C:\data\CUSTOMER.TPS --table CUSTOMER --fields CUSTNUMBER,COMPANY --limit 20

tps --version prints the CLI package version as a machine-readable MAJOR.MINOR.PATCH value.

schema and JSON row documents retain formatVersion: 1. rows returns at most 100 records by default; use --limit, --skip, or explicit --all. JSONL emits one versioned record per line.

CLI filters

The library does not define a query model: application consumers use LINQ. The tool keeps its dynamic field selection and --where compiler internal. Repeated CLI predicates are combined with AND.

Value type Operators
Number, DECIMAL, DATE, TIME eq, ne, lt, le, gt, ge, is-null, is-not-null
STRING, CSTRING, PSTRING, GROUP, MEMO eq, ne, contains, starts-with, ends-with, is-null, is-not-null
BLOB is-null, is-not-null
tps rows CUSTOMER.TPS --where STATE eq AZ --where CUSTNUMBER ge 100
tps rows DATA.TPS --where CONTACT_GROUP contains Smith
tps rows DATA.TPS --where GROUP_ARRAY[2] starts-with West

Use @recordNumber as a numeric pseudo-field. Array selectors are one-based. Text matching is case-insensitive unless --case-sensitive is supplied. GROUP operators ignore trailing NUL/space padding while preserving internal spacing. DATE literals use yyyy-MM-dd; TIME accepts HH:mm:ss, HH:mm:ss.f, or HH:mm:ss.ff. DECIMAL comparisons remain lossless.

JSON and JSONL emit GROUP projections as full fixed-width strings. DECIMAL values remain strings, dates and times use ISO text, and BLOBs default to length plus SHA-256 metadata. Add --blob-mode base64 when complete BLOB content is required.

CSV export

tps export CUSTOMER.TPS --output C:\export
tps export CUSTOMER.TPS --fields CUSTNUMBER,COMPANY --where STATE eq AZ --output C:\export

GROUP cells are always quoted and preserve their full width, including internal and trailing spaces. Text MEMOs are columns. BLOBs are separate .blob files referenced by their CSV cells. Writes are atomic and overwrite existing export files; source TPS files are never modified.

Encrypted and damaged files

Prefer an environment variable over placing an owner value in command history:

$env:TPS_OWNER = 'secret'
tps rows encrypted.tps --owner-env TPS_OWNER --limit 10

Use --ignore-errors only when incomplete recovery is acceptable. Exit codes are 0 for success, 1 for invalid path/arguments/query, and 2 for parsing or export failures. Structured commands keep diagnostics on stderr.

Agent skill

The repository includes the portable read-tps-files Agent Skill, packaged as the tpsreader plugin for Codex and Claude Code and as @carlosgtrz/tpsreader-agent-skill for Pi.

codex plugin marketplace add CarlosGtrz/TpsReader --sparse .agents/plugins --sparse plugins
codex plugin add tpsreader@tpsreader
claude plugin marketplace add CarlosGtrz/TpsReader --sparse .claude-plugin plugins
claude plugin install tpsreader@tpsreader
pi install npm:@carlosgtrz/tpsreader-agent-skill@1.0.1

See the agent-skill package documentation for prerequisites, supported platforms, security behavior, examples, and update or removal commands.

Maintainers can validate the exact release tarball and perform the interactive npm/2FA publication with scripts/publish-agent-skill.ps1. Run it with -WhatIf first to complete every pre-publication check without logging in or publishing.

.\scripts\publish-agent-skill.ps1 -WhatIf
.\scripts\publish-agent-skill.ps1

Migrating to 0.3.0

  • Replace the TpsParser package, project, assembly, and namespace with TpsReader.
  • Replace TpsInspector with TpsReader.Tool; the executable command remains tps.
  • Keep existing TpsFile, TpsTable, TpsRecord, and other Tps* class names.
  • Ordinary fixed-width STRING values are now returned without trailing padding.
  • GROUP values now default to fixed-width text rather than raw bytes or JSON hex. Request byte[] or byte[][] explicitly when raw GROUP storage is needed.
  • CLI JSON keeps formatVersion: 1 despite the deliberate GROUP row-value semantic change.

Attribution and license

This parser adapts logic from ctrl-alt-dev/tps-parse, copyright 2012-2021 Erik Hooijmeijer, under the Apache License 2.0. See Apache-2.0.txt.

TPS parsing is based on reverse engineering and may be incomplete. Verify output before relying on it for critical work.

About

Read-only C#/.NET parser and inspector for Clarion TopSpeed TPS files

Resources

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages