Skip to content

SchLib round-trip corrupts/loses pin data: >255-char pin strings and pin sibling streams (PinDesc/PinTextData/PinPackageLength/PinFunctionData) #35

@flukeee

Description

@flukeee

Summary

When reading and re-writing a .SchLib, AltiumSharp can corrupt or silently drop pin data. There are two related root causes, and they affect both the published NuGet release (OriginalCircuit.AltiumSharp 1.0.2, v1) and the current master (v2 rewrite). I hit this on a ~3,300-component library where a single pin had a 268-character description; the file Altium opens fine but AltiumSharp could not round-trip it.

Symptoms

  • v1 (NuGet 1.0.2): new SchLibReader().Read(path) throws
    System.IndexOutOfRangeException: Read past the end of the block
    in CompoundFileReader.ReadBlockSchReader.ReadPrimitivesSchLibReader.ReadComponent, when a component contains a pin whose binary Data record has an inline string longer than 255 chars.
  • v2 (master): no exception, but the pin is silently mis-parsed (wrong description/name/designator/electrical/location), and several pin sibling streams are dropped on write.

Root cause 1 — pin string fields are 1-byte Pascal short strings with no >255 handling

Altium's binary pin record (RECORD=2) stores Description/Name/Designator/etc. as Pascal short strings (single length byte). Altium itself supports descriptions longer than 255 chars by storing the inline copy truncated and the full text in a sibling stream (e.g. PinWideText / PinDesc).

  • Reader (v1): SchReader.ReadPinRecord does ReadPascalShortString() for each field. On disk a 268-char description writes its length as 268 & 0xFF = 12; the reader takes 12 bytes, desyncs, reads the remaining ~256 bytes of text as later fields, and a bogus length byte then runs the cursor past the block → ReadBlock throws "Read past the end of the block".
  • Reader (v2): SchLibReader.ReadBinaryPinRecord (Serialization/Readers/SchLibReader.cs) likewise reads description = reader.ReadPascalShortString(). There's no overrun guard (it only does if (consumed < dataSize) Skip(...)), so instead of throwing it silently mis-parses. It also does not read PinWideText to recover the full description, so descriptions stored via the truncated-inline + sibling-stream convention are silently truncated.

Root cause 2 — writer can't emit >255 strings and drops pin sibling streams

  • Writer (v1): SchWriter.WritePinRecord writes WritePascalShortString(pin.Description). For a >255-char value the length byte wraps ((byte)len), producing a record AltiumSharp's own reader can no longer parse (and that doesn't match Altium's truncate-to-sibling convention).
  • Writer (v2): BinaryFormatWriter.WritePascalShortString writes (byte)Math.Min(bytes.Length, 255) then writes all bytes — i.e. it clamps the length byte to 255 but still emits the full payload, producing an inconsistent record for >255 strings. There is no fallback to a sibling stream.
  • Dropped sibling streams: on write, v1 emits PinFrac/PinWideText/PinTextData/PinSymbolLineWidth; v2 emits only PinFrac/PinSymbolLineWidth. Neither writes PinDesc, PinPackageLength, or PinFunctionData, and v2 additionally omits PinWideText/PinTextData. Any component using those streams loses that data on round-trip, even if nothing in it was edited.

Reproduction

  1. Take a .SchLib containing a component whose pin has a >255-char description (stored by Altium as truncated inline + full text in PinWideText), and/or components with PinDesc/PinTextData/PinPackageLength/PinFunctionData streams.
  2. v1: new SchLibReader().Read(path)IndexOutOfRangeException: Read past the end of the block.
  3. v1/v2: read then SchLibWriter().Write(...) → re-open in Altium / re-read: the long description and the listed sibling streams are missing/corrupted.

Evidence (real case)

  • Component pin: 268-char description. On disk: inline length byte 0xFE (254, truncated) + full text in PinWideText. AltiumSharp v1's writer re-emitted the full 268 chars inline with length 0x0C (12) → its own reader then threw "Read past the end of the block". Comparing a pristine 3,376-component library against an AltiumSharp round-trip showed the streams PinTextData, PinDesc, PinPackageLength, PinFunctionData (and one PinWideText) silently dropped.

Suggested fix

  • Reader: when a pin inline string's length is ambiguous (>255 case), recover the true length from the block boundary, and/or read the authoritative value from the PinWideText/PinDesc sibling stream when present.
  • Writer: for strings >255 chars, write the inline copy truncated and the full value to the appropriate sibling stream (matching Altium's own convention) instead of wrapping/clamping the length byte.
  • Round-trip: preserve PinDesc, PinTextData, PinPackageLength, PinFunctionData (and PinWideText in v2) — e.g. read them and re-emit, or pass through verbatim for unmodified components.

Environment

  • OriginalCircuit.AltiumSharp 1.0.2 (NuGet, net6.0) — confirmed via runtime exception + byte-level analysis.
  • master / v2 (commit dfe90e4) — confirmed by source inspection (Serialization/Readers/SchLibReader.cs, Serialization/Binary/BinaryFormatReader.cs & BinaryFormatWriter.cs, Serialization/Writers/SchLibWriter.cs); not runtime-tested.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions