Skip to content
FreeDOSDeep Dive Published Updated 8 min readViews unavailable

DOS NLS APIs: Case Conversion, Collation Tables, and Code-Page Boundaries

Use FreeDOS INT 21h NLS services correctly, separating uppercase conversion, filename rules, collation data, pointer ownership, and code-page assumptions.

A DOS program that uppercases text by clearing a bit in every byte is implementing neither a general national-language service nor a safe filename policy. DOS NLS exposes case-conversion operations and country/code-page data so programs can use the system’s configured rules. Those services are still byte-oriented and environment-dependent; they do not turn arbitrary input into Unicode text.

This article examines FreeDOS kernel source reviewed on October 11, 2026, particularly inthndlr.c, nls.c, nls.h, and the generated hardcoded tables. It focuses on application API semantics rather than configuring keyboard and console drivers. Behavior inferred from this implementation must be tested on the actual kernel, NLS package, and DOS-compatible environment you deploy.

Encoding, conversion, and collation are different operations

A code page determines how encoded byte values represent characters. Case conversion maps a character to an uppercase representation under particular rules. Collation supplies ordering weights for comparison. These operations are related but not interchangeable.

For example, a conversion table can map a lowercase byte to an uppercase byte, while a collation table can assign the same ordering weight to several distinct bytes. Neither operation preserves every distinction in the original text. Store the original spelling separately if your application needs to display or export it.

The FreeDOS hardcoded package identifies country 1 and code page 437. Its table contents are useful primary evidence for that package, not universal mappings for every DOS locale. Do not label a byte sequence “DOS text” and then apply those mappings without identifying the code page in which the bytes were produced.

Select the service whose buffer contract matches your data

In the kernel’s AH=65h dispatch, subfunction AL=20h uppercases one character passed in DL and returns the converted character there. AL=21h uppercases a memory region at DS:DX, using CX as the byte count. AL=22h operates on an ASCIZ string at DS:DX, ending at its null terminator.

Those contracts have practical consequences. A DOS $-terminated display string is not automatically an ASCIZ string. A length-delimited input buffer can contain null bytes, and converting it through the ASCIZ operation stops early. A memory-region operation can also overwrite bytes beyond the intended text if its count includes a header or unused capacity.

The following Intel-syntax real-mode fragment illustrates the count-based call. It assumes DS points to writable application data and is integrated into a surrounding program whose control flow cannot fall into the data definitions:

; Uppercase exactly five bytes, not a terminator or buffer capacity.
mov dx, text_bytes
mov cx, text_length
mov ax, 6521h
int 21h
; Inspect the modified bytes through the application's normal path.

text_bytes: db 'hello'
text_length equ $ - text_bytes

This is an API illustration, not a complete runtime-tested utility. The source’s ordinary uppercase implementation converts ASCII a through z directly and uses the package’s mapping for high-bit bytes. A portable application must still establish service availability and the relevant DOS-version contract; it should not assume a modern FreeDOS implementation describes every older kernel.

Filename conversion has a separate interface

The FreeDOS dispatch also provides AL=A0h, A1h, and A2h for uppercase conversion of a filename character, a counted filename region, and an ASCIZ filename string respectively. The kernel has distinct normal and filename conversion paths and tables.

Even if those tables happen to produce the same result for one package and test string, their roles remain different. Use the filename-specific operation where the application explicitly requires that FreeDOS API, and document the portability boundary instead of treating these subfunctions as universally supported everywhere.

Uppercasing a filename does not establish that it is syntactically legal, fits a short-name limit, refers to an existing file, or is the canonical name exposed by another filesystem interface. Preserve the actual path returned or accepted by the relevant file API. Do not invent a path by applying a text transform and assume it must identify the same file.

Query tables instead of reaching into kernel internals

For data-retrieval subfunctions, the interrupt dispatcher passes the subfunction, BX code page, DX country, CX buffer size, and ES:DI destination to DosGetData. The FFFFh value is the source’s default/current sentinel for country and code page selection.

The kernel can find an appropriate loaded NLS package and serve the request directly, or route it through the multiplexed NLS path. A caller must handle failure rather than assume that every requested country/code-page pair is loaded. Kernel-internal package structures are not a stable application ABI to traverse by guessed offsets.

The returned data shape also depends on the subfunction. Extended country information is copied as data; other supported table queries return a small structure containing the subfunction identifier and a far pointer to the table. The pointer is not a copy of all the table bytes.

For a collation-table query in this 16-bit interface, the returned pointer descriptor occupies five bytes: one identifier byte and a four-byte far pointer. An illustrative call is:

; DS addresses application data; ES is set to that same segment.
push ds
pop es
mov di, collation_pointer
mov bx, 0FFFFh             ; current/default code page
mov dx, 0FFFFh             ; current/default country
mov cx, 5
mov ax, 6506h
int 21h
jc nls_query_failed
; Validate the descriptor before using its returned far pointer.

collation_pointer: db 5 dup (0)

The surrounding application must supply the error handler and safe data placement. It must also understand the referenced table layout before dereferencing it. This fragment deliberately does not supply a generic sorting algorithm or assume that every table query returns the same payload length.

Treat returned pointers as borrowed system information

Do not free, overwrite, or repurpose a table returned by the NLS service. Its pointer belongs to the system’s data path, not to an allocation that your application owns. The presence of a far pointer does not give the caller permission to change country rules for other programs.

The interface does not give this example an unconditional lifetime guarantee across every external package reload or code-page change. If your program permits those changes, re-query and rebuild dependent comparison state. Copy the necessary table information into application-owned storage only after validating the documented layout and length, and associate that copy with its country/code-page context.

A persistent index sorted under one collation context can be inconsistent when reopened under another. Store the index’s comparison policy and encoding identity, and rebuild when they differ. This is an application consistency requirement, not an automatic feature of DOS NLS.

Collation equality needs an explicit tie policy

The generated hardcoded FreeDOS table is useful for inspecting actual collation weights. It gives some differently encoded characters equal or related comparison weights. A sorting routine that compares weights can therefore consider distinct byte strings equivalent at that level.

Decide whether such equivalence is appropriate for searching, grouping, or uniqueness. A human-friendly sort may use the original byte string as a deterministic tie-breaker, while a case-insensitive search may intentionally group variants. Do not use a collation comparison as proof that two stored identifiers are byte-identical.

Likewise, do not use uppercase output as a reversible encoding conversion. Mapping a byte to another byte can lose distinctions, and converting between code pages requires a separate mapping and an unrepresentable-character policy.

Keep double-byte and Unicode assumptions visible

The FreeDOS source exposes a separate DBCS table mechanism and distinguishes filename handling from ordinary memory uppercasing. That is a warning against casually applying a single-byte lookup independently to every byte in a multibyte representation.

Establish whether your supported input is a single-byte DOS code page, a double-byte encoding, or modern Unicode data imported from elsewhere. Restrict the example’s simple byte processing to the contract it actually supports. A UTF-8 continuation byte is not a CP437 character just because its high bit is set.

Modern interoperability should decode the known source encoding through a suitable conversion layer and retain an error policy. It should not claim that calling AX=6521h on UTF-8 implements Unicode case folding.

Verify mappings and recovery without altering originals

In a disposable DOS environment, record the kernel build, country, active code page, and NLS components. Test ASCII input, selected high-bit characters whose mapping is established by the loaded package, a counted buffer containing a null byte, and a properly terminated ASCIZ string. Compare modified bytes, not only glyphs drawn by the console.

For table queries, exercise the default pair, an available explicit pair, a missing pair, and a buffer too short for the requested descriptor. Retain the error result before another interrupt call. Test sort stability and equality ties separately from uppercase conversion.

If text becomes corrupted, restore the untouched input copy and identify the original encoding before rerunning conversion. If a query fails, repair the supported package configuration or use an explicitly restricted fallback; do not dereference the zero-filled descriptor. Reliable NLS integration comes from respecting each representation and buffer boundary, not from assuming every locale is ASCII with extra symbols.

Related:

Sources:

Comments