// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // winIni.c - edit a Windows 3.x .INI file in place, CRLF line ends kept. // // winIni FILE OP SECTION KEY VALUE [OP SECTION KEY VALUE ...] // // OP is one of: // set KEY=VALUE in SECTION: the first KEY line there is replaced (keys // and sections match without regard to case), else the line is // added at the end of the section, the section at the end of the // file if it is not there. // add another KEY=VALUE line in SECTION unless that exact line is there // already: for the keys Windows reads every line of (device= in // [386Enh]). // // The file is Latin-1 text: its case and its spaces are Latin-1's. Used by // the installer's win311 component (installer/lib/win16.sh) to set up // Windows the way its Setup and Control Panel would, and by the win16 gates // and joey-run to name the program Windows runs as its shell. #include #include #include #include "toolBuf.h" #include "toolFile.h" #define TOOL_NAME "winIni" #define GROUP 4 #define NEWLINE "\r\n" #define NEWLINE_BYTES 2u // Latin-1's upper-case letters (but the multiplication sign) are their // lower-case ones less this. #define LATIN1_UPPER 0xC0u #define LATIN1_UPPER_END 0xDEu #define LATIN1_TIMES 0xD7u #define CASE_OFFSET 0x20u // The characters Python counts as space in Latin-1 text, beyond the ASCII // ones: the file, group, record, and unit separators, NEL, and NBSP. #define SEPARATOR_FIRST 0x1Cu #define SEPARATOR_LAST 0x1Fu #define LATIN1_NEL 0x85u #define LATIN1_NBSP 0xA0u // The file's lines (without their CRLFs), each a malloc'd string. typedef struct LinesT { char **line; size_t count; size_t cap; } LinesT; static void apply(LinesT *lines, const char *op, const char *section, const char *key, const char *value); static bool blank(const char *s); static bool equalFolded(const char *a, size_t aLen, const char *b, size_t bLen); static char fold(char c); static void insertLine(LinesT *lines, size_t at, const char *text, size_t len); static bool isSpace(char c); static bool keyMatches(const char *line, const char *key); static bool sectionBounds(const LinesT *lines, const char *section, size_t *start, size_t *end); static void strip(const char *s, size_t *from, size_t *to); static char *textCopy(const char *s, size_t len); static void usage(void); // One OP SECTION KEY VALUE group. static void apply(LinesT *lines, const char *op, const char *section, const char *key, const char *value) { ToolBufT line; ToolBufT header; size_t start; size_t end; size_t at; size_t i; size_t from; size_t to; memset(&line, 0, sizeof(line)); toolBufAdd(&line, key, strlen(key)); toolBufByte(&line, '='); toolBufAdd(&line, value, strlen(value)); toolBufByte(&line, '\0'); if (!sectionBounds(lines, section, &start, &end)) { while (lines->count > 0u && blank(lines->line[lines->count - 1u])) { free(lines->line[--lines->count]); } memset(&header, 0, sizeof(header)); toolBufByte(&header, '['); toolBufAdd(&header, section, strlen(section)); toolBufByte(&header, ']'); toolBufByte(&header, '\0'); insertLine(lines, lines->count, "", 0u); insertLine(lines, lines->count, (const char *)header.data, header.len - 1u); insertLine(lines, lines->count, (const char *)line.data, line.len - 1u); insertLine(lines, lines->count, "", 0u); toolBufFree(&header); toolBufFree(&line); return; } for (i = start + 1u; i < end; i++) { if (strcmp(op, "set") == 0 && keyMatches(lines->line[i], key)) { free(lines->line[i]); lines->line[i] = textCopy((const char *)line.data, line.len - 1u); toolBufFree(&line); return; } strip(lines->line[i], &from, &to); if (strcmp(op, "add") == 0 && equalFolded(lines->line[i] + from, to - from, (const char *)line.data, line.len - 1u)) { toolBufFree(&line); return; } } // At the end of the section, before the blank lines that close it. at = end; while (at > start + 1u && blank(lines->line[at - 1u])) { at--; } insertLine(lines, at, (const char *)line.data, line.len - 1u); toolBufFree(&line); } static bool blank(const char *s) { size_t from; size_t to; strip(s, &from, &to); return from == to; } // a and b the same but for case (Latin-1's). static bool equalFolded(const char *a, size_t aLen, const char *b, size_t bLen) { size_t i; if (aLen != bLen) { return false; } for (i = 0; i < aLen; i++) { if (fold(a[i]) != fold(b[i])) { return false; } } return true; } // A character in lower case (Latin-1's letters too). static char fold(char c) { unsigned char u; u = (unsigned char)c; if ((u >= 'A' && u <= 'Z') || (u >= LATIN1_UPPER && u <= LATIN1_UPPER_END && u != LATIN1_TIMES)) { return (char)(u + CASE_OFFSET); } return c; } static void insertLine(LinesT *lines, size_t at, const char *text, size_t len) { char **grown; if (lines->count == lines->cap) { lines->cap = lines->cap == 0u ? 64u : lines->cap * 2u; grown = (char **)realloc(lines->line, lines->cap * sizeof(char *)); if (grown == NULL) { toolFail("out of memory"); } lines->line = grown; } memmove(lines->line + at + 1u, lines->line + at, (lines->count - at) * sizeof(char *)); lines->line[at] = textCopy(text, len); lines->count++; } static bool isSpace(char c) { unsigned char u; u = (unsigned char)c; return u == ' ' || u == '\t' || u == '\n' || u == '\r' || u == '\v' || u == '\f' || (u >= SEPARATOR_FIRST && u <= SEPARATOR_LAST) || u == LATIN1_NEL || u == LATIN1_NBSP; } // The line sets key: its part before the first '=', stripped, is key but // for case. static bool keyMatches(const char *line, const char *key) { const char *eq; size_t from; size_t to; eq = strchr(line, '='); if (eq == NULL) { return false; } from = 0u; to = (size_t)(eq - line); while (from < to && isSpace(line[from])) { from++; } while (to > from && isSpace(line[to - 1u])) { to--; } return equalFolded(line + from, to - from, key, strlen(key)); } // The line of section's header and the line after its last; false when the // file has no such section. static bool sectionBounds(const LinesT *lines, const char *section, size_t *start, size_t *end) { size_t from; size_t to; size_t i; size_t n; n = strlen(section); for (i = 0; i < lines->count; i++) { strip(lines->line[i], &from, &to); if (to - from == n + 2u && lines->line[i][from] == '[' && lines->line[i][to - 1u] == ']' && equalFolded(lines->line[i] + from + 1u, n, section, n)) { *start = i; for (*end = i + 1u; *end < lines->count; (*end)++) { for (from = 0; isSpace(lines->line[*end][from]); from++) { } if (lines->line[*end][from] == '[') { break; } } return true; } } return false; } // s without its leading and trailing space: [from, to). static void strip(const char *s, size_t *from, size_t *to) { *from = 0u; *to = strlen(s); while (*from < *to && isSpace(s[*from])) { (*from)++; } while (*to > *from && isSpace(s[*to - 1u])) { (*to)--; } } static char *textCopy(const char *s, size_t len) { char *out; out = (char *)malloc(len + 1u); if (out == NULL) { toolFail("out of memory"); } memcpy(out, s, len); out[len] = '\0'; return out; } static void usage(void) { fprintf(stderr, "usage: %s FILE OP SECTION KEY VALUE [OP SECTION KEY VALUE ...]\n", TOOL_NAME); fprintf(stderr, " OP set: KEY=VALUE in SECTION, the first KEY line replaced or the line added\n"); fprintf(stderr, " OP add: another KEY=VALUE line in SECTION unless that exact line is there\n"); exit(1); } int main(int argc, char **argv) { LinesT lines; ToolBufT out; uint8_t *data; size_t len; size_t start; size_t i; int a; toolFileName(TOOL_NAME); if (argc < 2 + GROUP || (argc - 2) % GROUP != 0) { usage(); } for (a = 2; a < argc; a += GROUP) { if (strcmp(argv[a], "set") != 0 && strcmp(argv[a], "add") != 0) { toolFail("unknown op %s", argv[a]); } } if (!toolReadFile(argv[1], &data, &len)) { toolFail("cannot read %s", argv[1]); } // The lines between CRLFs (a lone CR or LF stays in its line). memset(&lines, 0, sizeof(lines)); start = 0u; for (i = 0; i + NEWLINE_BYTES <= len; i++) { if (memcmp(data + i, NEWLINE, NEWLINE_BYTES) == 0) { insertLine(&lines, lines.count, (const char *)data + start, i - start); start = i + NEWLINE_BYTES; i++; } } insertLine(&lines, lines.count, (const char *)data + start, len - start); free(data); for (a = 2; a < argc; a += GROUP) { apply(&lines, argv[a], argv[a + 1], argv[a + 2], argv[a + 3]); } memset(&out, 0, sizeof(out)); for (i = 0; i < lines.count; i++) { if (i > 0u) { toolBufAdd(&out, NEWLINE, NEWLINE_BYTES); } toolBufAdd(&out, lines.line[i], strlen(lines.line[i])); } if (!toolWriteFile(argv[1], out.data, out.len)) { toolFail("cannot write %s", argv[1]); } return 0; }