diff --git a/macos/BUILD.bazel b/macos/BUILD.bazel new file mode 100644 index 0000000..88d65f5 --- /dev/null +++ b/macos/BUILD.bazel @@ -0,0 +1,16 @@ +load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") +load("//bazel:copts.bzl", "COPTS") + +cc_test( + name = "pstrbuilder_test", + size = "small", + srcs = [ + "pstrbuilder.c", + "pstrbuilder.h", + "pstrbuilder_test.c", + ], + copts = COPTS, + deps = [ + "//lib", + ], +) diff --git a/macos/pstrbuilder.c b/macos/pstrbuilder.c new file mode 100644 index 0000000..70f5d74 --- /dev/null +++ b/macos/pstrbuilder.c @@ -0,0 +1,72 @@ +// Copyright 2022-2023 Dietrich Epp. +// This file is part of SyncFiles. SyncFiles is licensed under the terms of the +// Mozilla Public License, version 2.0. See LICENSE.txt for details. +#include "macos/pstrbuilder.h" + +#include + +static void PStrAppendMem(struct PStrBuilder *buf, const unsigned char *src, + int slen) +{ + int dlen; + + if (slen == 0 || buf->truncated) { + return; + } + dlen = buf->data[0]; + if (slen + dlen > 255) { + // Too long, truncate. + // FIXME: Handle other scripts: + // - Truncation should respect multibyte character boundaries. + // - Ellipsis should be taken from internationalization table. + slen = 255 - dlen; + memcpy(buf->data + 1 + dlen, src, slen); + dlen += slen; + if (dlen >= 255) { + dlen = 254; + } + buf->data[1 + dlen] = 0xc9; // 0xc9 is ellipsis. + dlen++; + buf->data[0] = dlen; + buf->truncated = true; + return; + } + memcpy(buf->data + 1 + dlen, src, slen); + buf->data[0] = dlen + slen; +} + +void PStrAppend(struct PStrBuilder *buf, const unsigned char *src) +{ + PStrAppendMem(buf, src + 1, src[0]); +} + +void PStrAppendSubstitute(struct PStrBuilder *buf, const unsigned char *src, + int paramcount, const unsigned char *const *params) +{ + int srclen, srcpos, start, ch; + const unsigned char *param; + + srclen = src[0]; + srcpos = 1; + start = 1; + while (srcpos <= srclen) { + if (src[srcpos] == '^' && srcpos + 1 <= srclen) { + ch = src[srcpos + 1]; + if ('1' <= ch && ch <= '9') { + ch -= '1'; + if (ch < paramcount && (param = params[ch]) != NULL) { + PStrAppendMem(buf, src + start, srcpos - start); + param = params[ch]; + PStrAppendMem(buf, param + 1, param[0]); + srcpos += 2; + start = srcpos; + } else { + srcpos += 2; + } + continue; + } + } + srcpos++; + } + PStrAppendMem(buf, src + start, srcpos - start); +} diff --git a/macos/pstrbuilder.h b/macos/pstrbuilder.h new file mode 100644 index 0000000..7fb4cb0 --- /dev/null +++ b/macos/pstrbuilder.h @@ -0,0 +1,40 @@ +// Copyright 2022-2023 Dietrich Epp. +// This file is part of SyncFiles. SyncFiles is licensed under the terms of the +// Mozilla Public License, version 2.0. See LICENSE.txt for details. +#ifndef MACOS_PSTRBUILDER_H +#define MACOS_PSTRBUILDER_H + +#include "lib/defs.h" + +/* +Pascal String Builder +===================== + +These functions operate on the Pascal-style Str255 string type, which consists +of a length byte, followed by 0-255 characters of text. Pascal string literals +can be written by putting \p at the beginning of the literal (this is a C +extension), and have an unsigned char type. C-style strings (null-terminated, +char type) are not used. + +The functions here construct strings for human consumption only, not +machine-readable strings. Strings which overflow the destination buffer are +silently truncated, with an ellipsis added to the end of the buffer to show that +it’s been truncated. +*/ + +// A PStrBuilder is a buffer for writing a string. This should be initialized by +// setting data[0] to 0 and truncated to false. +struct PStrBuilder { + unsigned char data[256]; + Boolean truncated; +}; + +// PStrAppend appends a string to the buffer. +void PStrAppend(struct PStrBuilder *buf, const unsigned char *src); + +// PStrAppendSubstitute appends a Pascal string to another string, expanding any +// ^N substitutions, where N is a positive digit, 1-9. +void PStrAppendSubstitute(struct PStrBuilder *buf, const unsigned char *src, + int paramcount, const unsigned char *const *params); + +#endif diff --git a/macos/pstrbuilder_test.c b/macos/pstrbuilder_test.c new file mode 100644 index 0000000..a61cf32 --- /dev/null +++ b/macos/pstrbuilder_test.c @@ -0,0 +1,163 @@ +// Copyright 2023 Dietrich Epp. +// This file is part of SyncFiles. SyncFiles is licensed under the terms of the +// Mozilla Public License, version 2.0. See LICENSE.txt for details. +#include "macos/pstrbuilder.h" + +#include +#include +#include + +// ============================================================================= +// Temporary buffer for Pascal strings + +static size_t PStrBufPos; +static unsigned char PStrBuf[1024]; + +static const unsigned char *P2CStr(const char *str) +{ + size_t len; + unsigned char *ptr; + + len = strlen(str); + if (len > 255) { + fputs("Error: PStr too long\n", stderr); + exit(1); + } + if (len + 1 > sizeof(PStrBuf) - PStrBufPos) { + fputs("Error: PStr overflow\n", stderr); + exit(1); + } + ptr = PStrBuf + PStrBufPos; + *ptr = len; + memcpy(ptr + 1, str, len); + PStrBufPos += len + 1; + return ptr; +} + +static void PStrReset(void) +{ + PStrBufPos = 0; +} + +#define PSTR(x) P2CStr(x) +#define PSTR_RESET() PStrReset() + +// ============================================================================= + +static Boolean TestFailed; + +static void PrintQuoted(const unsigned char *str) +{ + const unsigned char *ptr = str + 1; + const unsigned char *end = ptr + str[0]; + int ch; + + for (; ptr < end; ptr++) { + ch = *ptr; + if (32 <= ch && ch <= 126) { + if (ch == '\\' || ch == '"') { + fputc('\\', stderr); + } + fputc(ch, stderr); + } else { + fprintf(stderr, "\\x%02x", ch); + } + } +} + +static void TestString(int lineno, const struct PStrBuilder *buf, + const unsigned char *expect, Boolean expect_truncated) +{ + if (memcmp(buf->data, expect, expect[0] + 1) != 0 || + buf->truncated != expect_truncated) { + fprintf(stderr, "%s:%d: Incorrect result\n", __FILE__, lineno); + fputs(" expect: \"", stderr); + PrintQuoted(expect); + fprintf(stderr, "\" (truncated=%d)\n", expect_truncated); + fputs(" got: \"", stderr); + PrintQuoted(buf->data); + fprintf(stderr, "\" (truncated=%d)\n", buf->truncated); + TestFailed = true; + } +} + +#define TEST_STRING(buf, expect, expect_truncated) \ + TestString(__LINE__, buf, expect, expect_truncated) + +#define STR50 "0123456789abcdef0123456789abcdef0123456789abcdef0." +#define STR250 STR50 STR50 STR50 STR50 STR50 + +static void Clear(struct PStrBuilder *buf, const unsigned char **params) +{ + int i; + buf->data[0] = 0; + buf->truncated = false; + for (i = 0; i < 9; i++) { + params[i] = NULL; + } +} + +int main(int argc, char **argv) +{ + struct PStrBuilder buf; + const unsigned char *params[9]; + + (void)argc; + (void)argv; + + Clear(&buf, params); + TEST_STRING(&buf, PSTR(""), false); + PSTR_RESET(); + + // Append string to empty buffer. + PStrAppend(&buf, PSTR("String1")); + TEST_STRING(&buf, PSTR("String1"), false); + PSTR_RESET(); + + // Append string to non-empty buffer. + PStrAppend(&buf, PSTR("String2")); + TEST_STRING(&buf, PSTR("String1String2"), false); + PSTR_RESET(); + + // Append string with parameters. + Clear(&buf, params); + PStrAppend(&buf, PSTR("initial: ")); + params[0] = PSTR("(param1)"); + params[1] = PSTR("(param2)"); + params[8] = PSTR("(param9)"); + PStrAppendSubstitute(&buf, PSTR("^1 ^9 -> ^2"), 9, params); + TEST_STRING(&buf, PSTR("initial: (param1) (param9) -> (param2)"), false); + PSTR_RESET(); + + // Missing parameters are left alone, including NULL parameters. + Clear(&buf, params); + params[0] = PSTR("param1"); + PStrAppendSubstitute(&buf, PSTR("^0 ^1 ^2 ^3 ^9"), 2, params); + TEST_STRING(&buf, PSTR("^0 param1 ^2 ^3 ^9"), false); + + // String length 255 works. + Clear(&buf, params); + PStrAppend(&buf, PSTR(STR250)); + PStrAppend(&buf, PSTR("VWXYZ")); + TEST_STRING(&buf, PSTR(STR250 "VWXYZ"), false); + PSTR_RESET(); + + // String length 256 is truncated. + PStrAppend(&buf, PSTR(".")); + TEST_STRING(&buf, PSTR(STR250 "VWXY\xc9"), true); + PSTR_RESET(); + + // Truncate with parameters. + Clear(&buf, params); + params[0] = PSTR(STR50); + PStrAppendSubstitute(&buf, PSTR("^1^1^1^1^1^1"), 1, params); + TEST_STRING(&buf, PSTR(STR250 "0123\xc9"), true); + PSTR_RESET(); + + fputs("OK?\n", stderr); + if (TestFailed) { + fputs("Test failed\n", stderr); + exit(1); + } + return 0; +}