316 lines
9.7 KiB
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;
|
|
}
|