joeylib2/tools/winIni/winIni.c

316 lines
9.7 KiB
C

// 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}