Technical Article

Delphi Code That Works by Accident: Five FPC Porting Traps

Bringing CCITT, TIFF, PNG, Flate, and stream-buffer code up on Free Pascal is a reliable way to find Pascal that only works on Delphi by accident. None of the five patterns below involves a compiler bug. Each is code that Delphi happens to execute correctly because of an implementation detail: a hidden result parameter that aliases the caller's array, an out-of-range branch nobody ever reads past, a zero-length buffer whose only guard is a range-check switch, a 1-based offset that only one code path ever passes as 1, and a TStream.Read contract that in-memory streams never exercise. Change the compiler, or feed the same code a malformed file, and the accident stops holding

What follows is the specific shape of each trap, the way PDF Library for Delphi handles it, and the discipline behind it: the same source has to produce the same document semantics on both compilers, and the only way to know is to check on both. The sibling article on hardening a Pascal PDF parser against malicious files covers integer width, recursion depth, and uninitialized buffers. This one is about a different failure class: code that is wrong all along while a compiler quietly covers for it

Why does a function returning a dynamic array work without SetLength on Delphi?

Because Delphi passes the caller's own variable in as the hidden result parameter, so a function that never allocates its result can still write into an array the caller allocated. TPLCCITTDecoder.GetNextChangingElement(a0: Integer; IsWhite: Boolean): TCCITTIntegerArray is the reference-line lookup at the heart of two-dimensional Group 3 and Group 4 decoding: given the current position a0 and the colour of the current run, it searches the previous scanline's changing elements, the b1 and b2 of the ITU-T T.4 and T.6 two-dimensional coding scheme, and returns them as a two-slot array. The tempting implementation writes Result[0] and Result[1] and never calls SetLength on Result at all

That should fault on the first write, and on Free Pascal it does. On Delphi it does not, as long as the call sites look like this: declare b: TCCITTIntegerArray, run SetLength(b, 2) once before the scanline loop, then inside the loop assign b := GetNextChangingElement(a0, IsWhite) and read b[0] and b[1]. The Delphi language guide states that a function whose result is a long string, dynamic array, or other managed type receives that result as an additional var parameter, and in practice the compiler passes the address of the assignment target. So Result inside the function is b itself, already two elements long, and every write lands in memory the caller owns. Free Pascal hands the function a fresh nil array and assigns it to b afterward, which is the reading of the contract the code should be written against in the first place

PDFlibPas CCITT decoding divergence: Delphi passes the caller array b as the hidden var Result of GetNextChangingElement so writes land in memory the caller owns and a missed lookup keeps the previous values, while Free Pascal hands the function a fresh nil array that the Length guard must size with SetLength before the first write
Delphi aliases the caller array as the hidden Result parameter so unguarded writes still land in owned memory, while Free Pascal arrives with nil and the one-line guard turns the fault into the intended behaviour without touching the Delphi decode path

The aliasing also carries a semantic the decoder depends on. Result[0] is only assigned when the scan finds an element greater than a0, and Result[1] only when there is an element after it, so on a miss the slots keep whatever the previous iteration left in b. The obvious fix, allocate two slots and zero them on every call, destroys that carry-over and changes decoded output on Delphi. PDF Library for Delphi uses a guard instead of a reset: on Delphi it is dead code and the decode path stays byte for byte the same, and on Free Pascal it turns a fault into the intended behaviour. That asymmetry is the whole point, since the guard has to be a no-op on the compiler where the code already produces verified output

Function TPLCCITTDecoder.GetNextChangingElement(a0: Integer;
  IsWhite: Boolean): TCCITTIntegerArray;
Begin
  // Delphi lands here with the caller's two-element array aliased
  // as Result, so this is a no-op there. FPC lands with nil.
  If (Length(Result) < 2) Then
    SetLength(Result, 2);
  ...
  // Result[0] / Result[1] are still written only on a hit, so a miss
  // keeps the previous iteration's values exactly as before
End;

A count that outlived its data: the TIFF directory entry

When you invalidate an array, you have to invalidate its count in the same statement, or the count will be believed by code that never sees the array. A TIFF image file directory entry (TIFF 6.0 §2, the 12-byte layout of tag, type, count, and value-or-offset) carries a 32-bit count straight out of the file, and PDF Library for Delphi reads each one through PopDE: TTIFFEntry, a record with Tag, TagType, Length, Offset, and the decoded IntegerValues and DoubleValues arrays. The naive range check tests whether Offset + TypeSize * Length runs past the end of the file and, if it does, sets both arrays to zero length while leaving Result.Length at the value from the file

Two things go wrong from there. The function ends with a fallback that reads "if Length is zero, give the entry one zero-valued element" so that callers can always read element zero. With Length never cleared on the out-of-range path, that fallback never fires for the one case it exists for. And the callers do read element zero, unconditionally: Width, Height, BitsPerSample, PhotometricInterpretation, FillOrder, SamplesPerPixel, RowsPerStrip, and a dozen more take E.IntegerValues[0], and the strip tables do Move(E.IntegerValues[0], StripOffsets[0], E.Length * 4), copying Length times four bytes out of an array that has none. A cleared array with a live count is strictly more dangerous than an unchecked one, because the unchecked one at least holds the bytes it claims to

The second trap is ordering. Run the two SetLength calls before the range test, sized from the file's count, and a hostile entry can request a multi-gigabyte allocation before a single validity check. On Delphi the resulting exception is typically caught by a handler further up the image-loading path and the file simply fails to load, which is why the problem is easy to miss; what actually happens is an out-of-memory event the file chose. The correct order allocates after the test and makes the count travel with the data

TIFF directory entry hardening in PDFlibPas: the 12-byte entry carries a file-supplied count, the naive order allocates arrays from that count before the range test and leaves Result.Length live after clearing them, and the safe order tests Int64 arithmetic against the file length first so the count is cleared together with the arrays
Allocating before the range test lets a hostile count request gigabytes and leaves a live count on an emptied array, so the safe order tests the offset first and clears Result.Length in the same statement as the arrays
OutOfRange := Int64(ValueOffset) + Int64(TypeSize) * Result.Length
              > Length(Source);
If OutOfRange Then
Begin
  Result.Length := 0;              // the count goes with the values
  SetLength(Result.IntegerValues, 0);
  SetLength(Result.DoubleValues, 0);
End
Else
Begin
  SetLength(Result.IntegerValues, Result.Length);  // only now
  SetLength(Result.DoubleValues, Result.Length);
End;
// ... later, the fallback reaches the case it exists for:
If (Result.Length = 0) Then
Begin
  SetLength(Result.IntegerValues, 1);
  Result.IntegerValues[0] := 0;
End;

Nothing about this one is compiler-specific, which is what makes it belong in this list. The trap stays latent on Delphi for the same reason it stays latent on Free Pascal: it takes a directory entry pointing past the end of the file, and ordinary test files do not have one. A port does not expose it. Reading the code with the question "what does Delphi do for me here that I am not doing myself" does

What happens when a PNG IHDR claims a colour type the format does not define?

PDF Library for Delphi rejects the image before the row filters run. ISO 15948 §11.2.2 defines the IHDR chunk and Table 11.1 lists the six legal colour type and bit depth combinations: greyscale at 1, 2, 4, 8, or 16 bits, indexed colour at 1, 2, 4, or 8, and truecolour, greyscale with alpha, and truecolour with alpha at 8 or 16. The trap is a reader that validates the compression method and the filter method fields of IHDR and passes FColorType and the bit depth through untouched: an illegal colour type then yields a zero-byte scanline, and the unfilter loops get an empty buffer

Row filter code typically sizes everything from a Case FColorType Of that maps each colour type to a component count. A colour type outside the six falls into the Else branch, where SourceComponents is 0, so ScanlineByteCount is 0, so SetLength(PreviousScanline, 0) is followed immediately by FillChar(PreviousScanline[0], ScanlineByteCount, 0). Indexing element zero of an empty dynamic array is an address computed from nil. With range checking off, a zero-byte fill through that address is a silent no-op and the decoder marches on through rows that do not exist; with range checking on it is an ERangeError on the first image; and the Move calls that follow it are a step away from an access violation. Which of those you get depends on the compiler and on build switches rather than on anything the decoder decided, and that is the tell that the decoder never decided at all

PDF Library for Delphi applies the table from the specification where the other IHDR fields are checked: COLOR_GRAYSCALE accepts FSourceBitDepth in [1, 2, 4, 8, 16], COLOR_PALETTE accepts [1, 2, 4, 8], and COLOR_RGB, COLOR_GRAYSCALEALPHA, and COLOR_RGBALPHA accept [8, 16]; anything else clears ValidImage and the image is refused with its width and height intact for diagnostics. A pHYs chunk shorter than its nine bytes is checked in the same place, so the DPI reader never indexes S[1] through S[8] of a string the short chunk left empty

A 1-based offset treated as a 0-based pointer

InflateStrFromPosition(Const Input: AnsiString; StartPos, MaxOutput: Integer; Out Consumed: Integer): AnsiString takes a 1-based StartPos, because its input is an AnsiString and the Delphi implementation addresses the zlib input as @Input[StartPos]. The Free Pascal implementation, written against paszlib so both Windows targets link compression statically, has to address the same input through pointer arithmetic, and pointer arithmetic is 0-based. Set next_in to PAnsiChar(Input) + StartPos and avail_in to Length(Input) - StartPos, pass 1, which is what "start at the beginning" means for this function, and the FPC build starts inflating at the second byte and stops one byte before the end

This one hides easily because the caller most tests reach is InflateStr, which passes 0. Zero happens to be the correct 0-based offset, so the two builds agree on every plain InflateStr call and every test that goes through it. TPDFDocument.DecodeAllStreams, the routine SaveQDFToFile and ConvertFileToQDF use to expand single-FlateDecode streams into readable form, passes 1. With the off-by-one in place, the skipped zlib header makes the inflate fail, but the zlib stream still reports a nonzero Consumed for the bytes it examined, so a caller that trusts Consumed takes the empty payload as a successful decode and replaces every content stream with an empty string. The resulting QDF has the right page count, valid structure, and no page content, which is a file that opens without an error in every viewer and shows nothing

// FPC branch of InflateStrFromPosition.
// StartPos is 1-based like the Delphi branch; clamp it, then convert
// to a 0-based pointer offset exactly once, at the boundary.
If (StartPos < 1) Then
  StartPos := 1;
If (Length(Input) = 0) Or (StartPos > Length(Input)) Then
  Exit;
...
strm.next_in  := Pointer(PAnsiChar(Input) + StartPos - 1);
strm.avail_in := Length(Input) - StartPos + 1;

The check that catches it is the smallest possible one: deflate a payload, inflate it from position 0 and from position 1, and confirm both return the same payload and both report Consumed equal to the full stream length. An RFC 1950 stream has a two-byte header and a four-byte Adler-32 trailer, so an off-by-one at either end is not a subtle corruption, it is a stream that either fails to start or fails to finish. The lesson is about the boundary, not zlib: when a function's parameter is defined in one index base and the implementation underneath uses the other, the conversion belongs in exactly one line, and a test has to call it with the value that distinguishes the two bases

Why is a short TStream.Read not the end of the stream?

Because TStream.Read is allowed to return fewer bytes than requested for any reason it likes, and only a return of 0 means there is nothing more. TMemoryStream and TFileStream on a local disk almost always fill the request, which is why code that treats "returned less than I asked for" as end-of-file passes every test that uses them. Network-backed streams, decompression streams, and any TStream descendant a customer wrote can return two bytes when asked for sixty-four thousand and still have gigabytes behind them

TPLBuffer is the reader every parser in PDF Library for Delphi goes through, and it can wrap an AnsiString, a pointer, a byte array, or a TStream. Its four scanning queries, DistanceToByte, DistanceToOtherByte, DistanceToAnyByte, and DistanceToOtherBytes, all returning Int64, read the source in 64 KB blocks looking for a delimiter and report how far away it is without moving the logical position. The natural way to end such a loop is Until ReadCount < BlockSize. For the three in-memory sources that is correct, since ReadIntoBuffer always delivers the full block until the last one. For a stream source it means the scan gives up on the first short read, reports the delimiter as absent, and the tokenizer above it decides the object ends where it does not, which is why TPLBuffer loops until a read returns 0

Short-read handling in the PDFlibPas stream buffer: DistanceToByte scans 64 KB blocks, the old loop treated Until ReadCount < BlockSize as end of data and gave up on the first short read, while the fixed loop runs until ReadCount equals zero, finds the delimiter and restores the position in a finally block
A stream may return two bytes when asked for sixty-four thousand, so zero is the only end-of-data signal the scan may trust, and the finally clause restores the logical position when the delimiter is found and the loop exits early
// TPLBuffer.DistanceToByte scan loop.
// Zero is the only end-of-data signal TStream.Read defines.
TempPosition := FPosition;
Try
  Repeat
    ReadCount := ReadIntoBuffer(@TempBuffer[0], BlockSize);
    For TestPos := 0 To ReadCount - 1 Do
      If TempBuffer[TestPos] = Value Then
      Begin
        Result := TotalSkipped + TestPos;
        Exit;
      End;
    Inc(TotalSkipped, ReadCount);
  Until ReadCount = 0;
Finally
  FPosition := TempPosition;   // a peek must not move the reader
End;

The way to pin this down is a TMemoryStream descendant whose Read override caps every request at two bytes. Wrap the string aaaaaX in it, set the buffer position to 1, and all four queries must report a distance of 4 to the X, leave the position at 1 afterward, and report -1 for a byte that is not there. A scan that stops on the first short read sees two bytes, concludes the stream is exhausted, and returns -1. The finally matters as much as the loop condition: an Exit from inside the scan is the normal success path, and the logical position has to be restored on that path too, not only when the loop runs to completion

One source, two compilers, one set of assertions

The discipline behind these five is that "the Delphi build passes" is evidence about Delphi, not about the source. A cross-compiler check worth having runs one routine under both compilers: build a two-page document with compressed content through TPDFlib, save it, save it again as QDF through SaveQDFToFile, repair the QDF with RepairQDFFile, encrypt the plain file with AES-128 through EncryptFile and a permission mask from EncodePermissions, and then reload every artifact and check the same things on both compilers: page count is 2, the title survives, page two's text extracts intact from the plain, repaired, and encrypted files, the wrong password is refused with a nonzero LastErrorCode, EncryptionStrength is 128, EncryptionAlgorithm is 2, and the individual permission bits from GetUserPermissions come back exactly as encoded

Compare normalised semantics rather than bytes. Encryption draws random salts and the writer assigns document identifiers, so the two builds are not expected to emit identical files; they are expected to emit files that mean the same thing, and the checks should be phrased at that level. The QDF leg earns its place because of the offset trap: a QDF with two pages and no content passes a page-count check and fails a text-extraction check, so check the second one. Any change that is a no-op on one compiler and a behaviour change on the other, which describes four of the five above, has to clear the same checks on both

The link-time half of the same port, getting Delphi's OMF objects and Free Pascal's COFF expectations to agree, is its own story in FPC Win32 OMF to COFF object linking, and the structural hardening of the same TIFF reader against BigTIFF and tiled files is in the built-in TIFF decoder notes. The decoders in this article ship in the PDF Library for Delphi for Delphi, C++Builder, and Free Pascal, where the same source is expected to earn the same result on every compiler it targets rather than be granted it by one