AmendHub

Download (as of this amendment):

jcs

/

amend

/

amendments

/

178

*: Add native resource file handling

When a file ending in .rsrc is diffed, create a temporary file with an
ASCII hex dump of all the resources found in its resource fork, in the same
format that SaDerez does, and diff against that.
 
Store this dumped format in the bile repo to avoid having to convert twice,
and when doing a checkout of such a file, convert it from the hexdump
format stored in the repo to an actual resource file on disk.

jcs made amendment 178 14 days ago
--- committer.c Mon Sep 28 13:52:39 2026 +++ committer.c Mon Sep 28 17:38:56 2026 @@ -167,13 +167,19 @@ bool committer_close(struct focusable *focusable) { struct committer *committer = (struct committer *)focusable->cookie; - + short n; + committer->browser->committer = NULL; if (committer->diff_line != NULL) xfree(&committer->diff_line); - if (committer->diffed_files != NULL) + if (committer->diffed_files != NULL) { + for (n = 0; n < committer->ndiffed_files; n++) { + if (committer->diffed_files[n].derez != NULL) + xfree(&committer->diffed_files[n].derez); + } xfree(&committer->diffed_files); + } TEDispose(committer->log_te); TVDispose(committer->diff_tv); @@ -469,7 +475,8 @@ committer_generate_diff(struct committer *committer) DIFFED_FILE_METADATA; progress("Diffing %s...", file->filename); - ret = repo_diff_file(committer->browser->repo, file); + ret = repo_diff_file(committer->browser->repo, + &committer->diffed_files[committer->ndiffed_files]); if (ret == 1) { committer->diffed_files[committer->ndiffed_files].flags |= --- repo.c Mon Sep 28 15:10:40 2026 +++ repo.c Mon Sep 28 21:07:13 2026 @@ -22,6 +22,7 @@ #include "bile.h" #include "diff.h" #include "repo.h" +#include "rsrc.h" #include "settings.h" #include "strnatcmp.h" #include "util.h" @@ -695,22 +696,58 @@ repo_checkout_file(struct repo *repo, struct repo_file short vrefnum, Str255 filename) { struct bile_object *textob; + Str255 filepath; size_t size; - short error, frefnum; + short error, frefnum, ret = 0; char *text; - + textob = bile_find(repo->bile, REPO_TEXT_RTYPE, file->id); if (textob == NULL) { warn("No copy of file %s exists in repo", file->filename); return -1; } + /* TODO: add offset to bile_read to read in chunks */ + text = xmalloc(textob->size); + size = bile_read_object(repo->bile, textob, text, textob->size); + if (size != textob->size) + panic("Failed to read text object %ld: %d", textob->id, + bile_error(repo->bile)); + xfree(&textob); + + if (rsrc_is_rsrc(file->filename)) { + error = FSDelete(filename, vrefnum); + if (error && error != fnfErr) { + warn("Failed to delete existing %s: %d", PtoCstr(filename), + error); + ret = -1; + goto checkout_done; + } + + error = Create(filename, vrefnum, file->creator, file->type); + if (error) { + warn("Failed to create file %s: %d", PtoCstr(filename), error); + return -1; + } + + if (getpath(vrefnum, filename, &filepath, true) != 0) + panic("getpath(%d, %s) failed", vrefnum, PtoCstr(filename)); + + error = rsrc_rez(filepath, text, size); + if (error) { + if (error != -1) + warn("Failed to rez %s: %d", PtoCstr(filename), error); + ret = -1; + } + goto checkout_done; + } + error = Create(filename, vrefnum, file->creator, file->type); if (error && error != dupFNErr) { - warn("Failed to create file %s: %d", PtoCstr(filename), - error); + warn("Failed to create file %s: %d", PtoCstr(filename), error); xfree(&textob); - return -1; + ret = -1; + goto checkout_done; } error = FSOpen(filename, vrefnum, &frefnum); @@ -721,22 +758,16 @@ repo_checkout_file(struct repo *repo, struct repo_file if (error) panic("Failed to truncate file %s: %d", PtoCstr(filename), error); - /* TODO: add offset to bile_read to read in chunks */ - text = xmalloc(textob->size); - size = bile_read_object(repo->bile, textob, text, textob->size); - if (size != textob->size) - panic("Failed to read text object %ld: %d", textob->id, - bile_error(repo->bile)); - error = FSWrite(frefnum, &size, text); if (error) panic("Failed to write file to %s: %d", PtoCstr(filename), error); - xfree(&text); - xfree(&textob); FSClose(frefnum); + +checkout_done: + xfree(&text); - return 0; + return ret; } void @@ -768,29 +799,28 @@ repo_sort_files(struct repo *repo) } short -repo_diff_file(struct repo *repo, struct repo_file *file) +repo_diff_file(struct repo *repo, struct diffed_file *dfile) { Str255 fromfilename, tofilename; Str255 fromfilepath, tofilepath; Str255 label0, label1; struct repo_file_attrs attrs; + struct repo_file *file; size_t size = 0; char *text = NULL; - short error, ret, tofile_empty = 0; + short error, ret, tofile_tmp = 0; + file = dfile->file; + /* write out old file */ snprintf((char *)fromfilename, HFS_FILENAME_MAX + 1, "[%lu] %s", Time, file->filename); CtoPstr(fromfilename); - if (file->flags & REPO_FILE_DELETED) { - /* don't write any TEXT resource, the from file should be blank */ - } else { + if (!(file->flags & REPO_FILE_DELETED)) /* if there's no existing TEXT resource, it's a new file */ - size = bile_read_alloc(repo->bile, REPO_TEXT_RTYPE, file->id, &text); - } repo_write_temp_file(repo, file, fromfilename, text, size); if (text != NULL) @@ -822,10 +852,30 @@ repo_diff_file(struct repo *repo, struct repo_file *fi true) != 0) panic("getpath(%d, %s) failed", repo->bile->vrefnum, PtoCstr(tofilename)); - tofile_empty = 1; - } else if (error) + tofile_tmp = 1; + } else if (error) { panic("Failed to get info for %s", PtoCstr(tofilename)); + } else if (rsrc_is_rsrc(file->filename)) { + error = rsrc_derez(tofilepath, &text, &size); + if (error) { + warn("Failed to derez %s: %d", file->filename, error); + FSDelete(fromfilename, repo->bile->vrefnum); + return -1; + } + snprintf((char *)tofilename, HFS_FILENAME_MAX + 1, + "[derez] %s", file->filename); + CtoPstr(tofilename); + repo_write_temp_file(repo, file, tofilename, text, size); + dfile->derez = text; + dfile->derez_len = size; + tofile_tmp = 1; + } + if (tofile_tmp && getpath(repo->bile->vrefnum, tofilename, + &tofilepath, true) != 0) + panic("getpath(%d, %s) failed", repo->bile->vrefnum, + PtoCstr(tofilename)); + /* specify diff header labels to avoid printing tmp filename */ /* (TODO: use paths relative to repo) */ label[0] = (char *)&label0; @@ -839,17 +889,22 @@ repo_diff_file(struct repo *repo, struct repo_file *fi label0[strlen((char *)label0) - 1] = '\0'; label1[strlen((char *)label1) - 1] = '\0'; + /* diffreg's files_differ uses stb1 and stb2 */ + if (FStat(fromfilepath, &stb1) != 0 || FStat(tofilepath, &stb2) != 0) + panic("Failed to stat temp files for diff"); + PtoCstr(fromfilepath); PtoCstr(tofilepath); ret = diffreg((char *)fromfilepath, (char *)tofilepath, D_PROTOTYPE); - /* delete temp file */ + /* delete temp file of in-repo version */ error = FSDelete(fromfilename, repo->bile->vrefnum); if (error) panic("Failed to delete temp file %s: %d", PtoCstr(fromfilename), error); - if (tofile_empty) { + if (tofile_tmp) { + /* delete temp file of derez'd or blank version */ error = FSDelete(tofilename, repo->bile->vrefnum); if (error) panic("Failed to delete temp file %s: %d", @@ -857,12 +912,11 @@ repo_diff_file(struct repo *repo, struct repo_file *fi } if (ret == D_SAME) - return 0; - - if (ret == D_ABORTED) - return -1; + ret = 0; + else if (ret == D_ABORTED) + ret -1; - return 1; + return ret; } short @@ -924,7 +978,7 @@ repo_file_changed(struct repo *repo, struct repo_file return 1; } - if (sb.st_size != fsize) + if (!rsrc_is_rsrc(file->filename) && sb.st_size != fsize) return 1; if (sb.st_ctime != file->ctime) return 1; @@ -992,11 +1046,11 @@ repo_amend(struct repo *repo, struct diffed_file *diff { Str255 tfilename; struct repo_amendment *amendment; - unsigned long datalen, fsize; + unsigned long datalen; unsigned char *tdata; char *amendment_data; FInfo finfo; - size_t size; + size_t fsize, size; short i, error, frefnum; amendment = xmalloczero(sizeof(struct repo_amendment)); @@ -1034,7 +1088,7 @@ repo_amend(struct repo *repo, struct diffed_file *diff /* store diff */ HLock(diff); - progress("Storing diff..."); + progress("Storing amendment..."); size = bile_write(repo->bile, REPO_DIFF_RTYPE, amendment->id, *diff, difflen); if (size != difflen) @@ -1066,24 +1120,30 @@ repo_amend(struct repo *repo, struct diffed_file *diff PtoCstr(tfilename)); if (error != fnfErr) { - error = FSOpen(tfilename, repo->bile->vrefnum, &frefnum); - if (error) - panic("Failed to open file %s: %d", PtoCstr(tfilename), - error); - - error = GetEOF(frefnum, &fsize); - if (error) - panic("Failed to get size of file %s: %d", - PtoCstr(tfilename), error); + if (diffed_files[i].derez != NULL) { + tdata = (unsigned char *)diffed_files[i].derez; + fsize = diffed_files[i].derez_len; + diffed_files[i].derez = NULL; + } else { + error = FSOpen(tfilename, repo->bile->vrefnum, &frefnum); + if (error) + panic("Failed to open file %s: %d", + PtoCstr(tfilename), error); - tdata = xmalloc(fsize); - error = FSRead(frefnum, &fsize, tdata); - if (error) - panic("Failed to read %lu of file %s: %d", fsize, - PtoCstr(tfilename), error); + error = GetEOF(frefnum, &fsize); + if (error) + panic("Failed to get size of file %s: %d", + PtoCstr(tfilename), error); - FSClose(frefnum); + tdata = xmalloc(fsize); + error = FSRead(frefnum, &fsize, tdata); + if (error) + panic("Failed to read %lu of file %s: %d", fsize, + PtoCstr(tfilename), error); + FSClose(frefnum); + } + size = bile_write(repo->bile, REPO_TEXT_RTYPE, diffed_files[i].file->id, tdata, fsize); if (size != fsize) --- repo.h Thu Oct 24 20:57:23 2024 +++ repo.h Mon Sep 28 17:40:00 2026 @@ -59,6 +59,8 @@ struct diffed_file { short flags; #define DIFFED_FILE_TEXT (1 << 0) #define DIFFED_FILE_METADATA (1 << 1) + char *derez; + size_t derez_len; }; struct repo_amendment { @@ -97,7 +99,7 @@ void repo_show_diff_text(struct repo *repo, struct rep TVHandle tv); struct repo_file *repo_add_file(struct repo *repo); void repo_file_mark_for_deletion(struct repo *repo, struct repo_file *file); -short repo_diff_file(struct repo *repo, struct repo_file *file); +short repo_diff_file(struct repo *repo, struct diffed_file *dfile); short repo_file_changed(struct repo *repo, struct repo_file *file); short repo_checkout_file(struct repo *repo, struct repo_file *file, short vrefnum, Str255 filename); --- rsrc.c Mon Sep 28 20:49:36 2026 +++ rsrc.c Mon Sep 28 20:49:36 2026 @@ -0,0 +1,631 @@ +/* + * Copyright (c) 2026 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <ctype.h> +#include <stdio.h> +#include <string.h> + +#include "rsrc.h" +#include "util.h" + +/* column after the leading tab where the ascii comment starts */ +#define RSRC_COMMENT_COL 54 +#define RSRC_BYTES_PER_LINE 16 +#define RSRC_BUF_GROW 4096 + +struct rsrc_buf { + char *data; + size_t size; + size_t len; +}; + +struct rsrc_attr { + char *name; + short flag; +} rsrc_attrs[] = { + { "sysheap", resSysHeap }, + { "purgeable", resPurgeable }, + { "locked", resLocked }, + { "protected", resProtected }, + { "preload", resPreload }, +}; + +enum { + RSRC_PARSE_HEADER, + RSRC_PARSE_DATA +}; + +static const char rsrc_hex[] = "0123456789ABCDEF"; + +bool rsrc_buf_append(struct rsrc_buf *buf, const void *data, size_t len); +bool rsrc_derez_resource(struct rsrc_buf *out, Handle h, OSType type, + short id, Str255 name, short attrs); +size_t rsrc_escape(unsigned char *src, size_t len, char quote, char *dst, + size_t dstsize); +short rsrc_unescape(char **strp, char *end, char quote, unsigned char *dst, + size_t dstsize, size_t *dstlen); +short rsrc_hexval(char c); +char * rsrc_skip_ws(char *p, char *end); +short rsrc_parse_header(char *line, char *end, OSType *type, short *id, + Str255 name, short *attrs); +short rsrc_parse_hex(char *line, char *end, struct rsrc_buf *data); +short rsrc_add(OSType type, short id, Str255 name, short attrs, + struct rsrc_buf *data); + +bool +rsrc_is_rsrc(const char *filename) +{ + size_t len, slen; + + len = strlen(filename); + if (len <= 5) /* .rsrc */ + return false; + + return (strcasecmp(filename + len - 5, ".rsrc") == 0); +} + +short +rsrc_derez(Str255 path, char **ret, size_t *retlen) +{ + struct rsrc_buf out = { 0 }; + Handle h; + Str255 name; + OSType type; + short old_res, res, error = 0, ntypes, nres, t, r, id, attrs; + bool already_open = false; + + *ret = NULL; + *retlen = 0; + + old_res = CurResFile(); + + res = OpenRFPerm(path, 0, fsRdPerm); + if (res == -1) { + error = ResError(); + if (error != eofErr) + return error; + + /* no resource fork at all, treat it as empty */ + error = 0; + goto derez_done; + } + + if (res == old_res) + already_open = true; + + UseResFile(res); + SetResLoad(false); + + ntypes = Count1Types(); + for (t = ntypes; t >= 1; t--) { + Get1IndType(&type, t); + nres = Count1Resources(type); + for (r = 1; r <= nres; r++) { + h = Get1IndResource(type, r); + if (h == NULL) { + error = ResError(); + goto derez_bail; + } + LoadResource(h); + if ((error = ResError()) != noErr) + goto derez_bail; + + GetResInfo(h, &id, &type, name); + attrs = GetResAttrs(h); + + if (!rsrc_derez_resource(&out, h, type, id, name, attrs)) { + if (!already_open) + ReleaseResource(h); + error = -1; + goto derez_bail; + } + if (!already_open) + ReleaseResource(h); + } + } + +derez_bail: + SetResLoad(true); + if (!already_open) + CloseResFile(res); + UseResFile(old_res); + + if (error) { + if (out.data != NULL) + xfree(&out.data); + return error; + } + +derez_done: + /* bile can't store empty objects */ + if (out.len == 0 && !rsrc_buf_append(&out, "/* no resources */\r", 19)) + return -1; + + *ret = out.data; + *retlen = out.len; + + return 0; +} + +bool +rsrc_derez_resource(struct rsrc_buf *out, Handle h, OSType type, short id, + Str255 name, short attrs) +{ + char line[128], esc[1024]; + unsigned char *data, c; + size_t size, off, n, pos, len; + char hstate; + bool star, printable; + + /* data 'TYPE' (id, "name", attrs) { */ + rsrc_escape((unsigned char *)&type, sizeof(type), '\'', esc, + sizeof(esc)); + len = snprintf(line, sizeof(line), "data '%s' (%d", esc, id); + if (!rsrc_buf_append(out, line, len)) + return false; + + if (name[0]) { + len = rsrc_escape(name + 1, name[0], '"', esc, sizeof(esc)); + if (!rsrc_buf_append(out, ", \"", 3) || + !rsrc_buf_append(out, esc, len) || + !rsrc_buf_append(out, "\"", 1)) + return false; + } + + for (n = 0; n < nitems(rsrc_attrs); n++) { + if (!(attrs & rsrc_attrs[n].flag)) + continue; + len = snprintf(line, sizeof(line), ", %s", rsrc_attrs[n].name); + if (!rsrc_buf_append(out, line, len)) + return false; + } + + if (!rsrc_buf_append(out, ") {\r", 4)) + return false; + + size = GetHandleSize(h); + hstate = HGetState(h); + HLock(h); + data = (unsigned char *)*h; + + for (off = 0; off < size; off += RSRC_BYTES_PER_LINE) { + pos = 0; + line[pos++] = '\t'; + line[pos++] = '$'; + line[pos++] = '"'; + for (n = 0; n < RSRC_BYTES_PER_LINE && off + n < size; n++) { + if (n > 0 && (n % 2) == 0) + line[pos++] = ' '; + c = data[off + n]; + line[pos++] = rsrc_hex[c >> 4]; + line[pos++] = rsrc_hex[c & 0xf]; + } + line[pos++] = '"'; + + /* pad out and show the bytes as text in a comment like DeRez */ + while (pos < RSRC_COMMENT_COL + 1) + line[pos++] = ' '; + line[pos++] = '/'; + line[pos++] = '*'; + line[pos++] = ' '; + star = false; + for (n = 0; n < RSRC_BYTES_PER_LINE && off + n < size; n++) { + c = data[off + n]; + printable = (c >= ' ' && c != 0x7f && c <= 0xd8); + + // avoid printing */ in the ascii dump + if (c == '*') + star = true; + else if (c == '/') { + if (star) + c = '.'; + star = false; + } else if (printable) + star = false; + + if (c == '\t') + c = '∆'; /* 0xc6 */ + else if (c == '\r') + c = '¬'; /* 0xc2 */ + else if (!printable) + c = '.'; + line[pos++] = c; + } + line[pos++] = ' '; + line[pos++] = '*'; + line[pos++] = '/'; + line[pos++] = '\r'; + + if (!rsrc_buf_append(out, line, pos)) { + HUnlock(h); + return false; + } + } + + HSetState(h, hstate); + + if (!rsrc_buf_append(out, "};\r\r", 4)) + return false; + + return true; +} + +short +rsrc_rez(Str255 path, const char *text, size_t len) +{ + struct rsrc_buf data = { 0 }; + Str255 name; + OSType type; + char *line, *end, *eol; + char err[128]; + unsigned long linenum = 0; + short old_res, res, error = 0, state, id, attrs, ret; + + err[0] = '\0'; + old_res = CurResFile(); + + CreateResFile(path); + error = ResError(); + if (error != noErr && error != dupFNErr) + return error; + + res = OpenRFPerm(path, 0, fsRdWrPerm); + if (res == -1) + return ResError(); + + UseResFile(res); + + state = RSRC_PARSE_HEADER; + end = (char *)text + len; + for (line = (char *)text; line < end; line = eol + 1) { + linenum++; + for (eol = line; eol < end && *eol != '\r' && *eol != '\n'; eol++) + ; + + /* skip blank lines and comments */ + line = rsrc_skip_ws(line, eol); + if (line == eol || + (eol - line > 1 && line[0] == '/' && line[1] == '*')) + continue; + + switch (state) { + case RSRC_PARSE_HEADER: + if (rsrc_parse_header(line, eol, &type, &id, name, + &attrs) != 0) { + snprintf(err, sizeof(err), "Malformed resource header on " + "line %lu", linenum); + goto rez_bail; + } + data.len = 0; + state = RSRC_PARSE_DATA; + break; + case RSRC_PARSE_DATA: + if (line[0] == '}') { + ret = rsrc_add(type, id, name, attrs, &data); + if (ret == -1) { + snprintf(err, sizeof(err), "Duplicate resource %s %d " + "on line %lu", OSTypeToString(type), id, linenum); + goto rez_bail; + } + if (ret != noErr) { + snprintf(err, sizeof(err), "Failed adding resource %s " + "%d on line %lu: %d", OSTypeToString(type), id, + linenum, ret); + goto rez_bail; + } + state = RSRC_PARSE_HEADER; + break; + } + if (rsrc_parse_hex(line, eol, &data) != 0) { + snprintf(err, sizeof(err), "Malformed resource data on " + "line %lu", linenum); + goto rez_bail; + } + break; + } + } + + if (state != RSRC_PARSE_HEADER) + snprintf(err, sizeof(err), "Unexpected end of resource data on " + "line %lu", linenum); + +rez_bail: + if (data.data != NULL) + xfree(&data.data); + + CloseResFile(res); + UseResFile(old_res); + + if (err[0]) { + warn("%s", err); + return -1; + } + + return 0; +} + +short +rsrc_parse_header(char *line, char *end, OSType *type, short *id, + Str255 name, short *attrs) +{ + unsigned char tmp[256]; + char word[32]; + size_t tmplen, n; + long val = 0; + bool neg = false; + + /* data 'LDEF' (129, "file list", purgeable) { */ + + if (end - line < 4 || strncmp(line, "data", 4) != 0) + return -1; + line = rsrc_skip_ws(line + 4, end); + + /* 'TYPE' */ + if (line >= end || *line != '\'') + return -1; + line++; + if (rsrc_unescape(&line, end, '\'', tmp, sizeof(tmp), &tmplen) != 0 || + tmplen != sizeof(OSType)) + return -1; + memcpy(type, tmp, sizeof(OSType)); + + /* (id */ + line = rsrc_skip_ws(line, end); + if (line >= end || *line != '(') + return -1; + line = rsrc_skip_ws(line + 1, end); + if (line < end && *line == '-') { + neg = true; + line++; + } + if (line >= end || !isdigit((unsigned char)*line)) + return -1; + for (; line < end && isdigit((unsigned char)*line); line++) + val = (val * 10) + (*line - '0'); + *id = (short)(neg ? -val : val); + + /* then any of , "name" and , attribute until ) */ + name[0] = 0; + *attrs = 0; + for (;;) { + line = rsrc_skip_ws(line, end); + if (line >= end) + return -1; + if (*line == ')') { + line++; + break; + } + if (*line != ',') + return -1; + line = rsrc_skip_ws(line + 1, end); + if (line >= end) + return -1; + + if (*line == '"') { + line++; + if (rsrc_unescape(&line, end, '"', tmp, sizeof(tmp), + &tmplen) != 0) + return -1; + name[0] = tmplen; + memcpy(name + 1, tmp, tmplen); + continue; + } + + for (n = 0; line < end && n < sizeof(word) - 1 && + isalpha((unsigned char)*line); n++, line++) + word[n] = *line; + word[n] = '\0'; + if (n == 0) + return -1; + + for (n = 0; n < nitems(rsrc_attrs); n++) { + if (strcasecmp(word, rsrc_attrs[n].name) == 0) { + *attrs |= rsrc_attrs[n].flag; + break; + } + } + if (n == nitems(rsrc_attrs)) + return -1; + } + + /* { */ + line = rsrc_skip_ws(line, end); + if (line >= end || *line != '{') + return -1; + + return 0; +} + +short +rsrc_parse_hex(char *line, char *end, struct rsrc_buf *data) +{ + unsigned char c; + short hi, lo; + + if (end - line < 2 || line[0] != '$' || line[1] != '"') + return -1; + line += 2; + + for (;;) { + line = rsrc_skip_ws(line, end); + if (line >= end) + /* no closing quote */ + return -1; + if (*line == '"') + break; + if (end - line < 2) + return -1; + if ((hi = rsrc_hexval(line[0])) == -1 || + (lo = rsrc_hexval(line[1])) == -1) + return -1; + c = (hi << 4) | lo; + if (!rsrc_buf_append(data, &c, 1)) + return -1; + line += 2; + } + + /* anything after the closing quote is the ascii comment */ + return 0; +} + +short +rsrc_add(OSType type, short id, Str255 name, short attrs, + struct rsrc_buf *data) +{ + Handle h; + short error; + + /* AddResource doesn't check for duplicates */ + SetResLoad(false); + h = Get1Resource(type, id); + SetResLoad(true); + if (h != NULL) + return -1; + + h = NewHandle(data->len); + if (h == NULL) + return memFullErr; + if (data->len) { + HLock(h); + memcpy(*h, data->data, data->len); + HUnlock(h); + } + + AddResource(h, type, id, name); + if ((error = ResError()) != noErr) { + DisposHandle(h); + return error; + } + + WriteResource(h); + if ((error = ResError()) != noErr) + return error; + + if (attrs) { + SetResAttrs(h, attrs); + if ((error = ResError()) != noErr) + return error; + } + + ReleaseResource(h); + + return 0; +} + +bool +rsrc_buf_append(struct rsrc_buf *buf, const void *data, size_t len) +{ + EXPAND_TO_FIT(buf->data, buf->size, buf->len, len, RSRC_BUF_GROW); + if (buf->data == NULL) + return false; + + memcpy(buf->data + buf->len, data, len); + buf->len += len; + + return true; +} + +/* escape quote, backslash, and non-printables the way DeRez does */ +size_t +rsrc_escape(unsigned char *src, size_t len, char quote, char *dst, + size_t dstsize) +{ + size_t n, pos = 0; + unsigned char c; + + for (n = 0; n < len && pos + 5 < dstsize; n++) { + c = src[n]; + if (c == quote || c == '\\') { + dst[pos++] = '\\'; + dst[pos++] = c; + } else if (c < ' ' || c == 0x7f) { + dst[pos++] = '\\'; + dst[pos++] = '0'; + dst[pos++] = 'x'; + dst[pos++] = rsrc_hex[c >> 4]; + dst[pos++] = rsrc_hex[c & 0xf]; + } else + dst[pos++] = c; + } + dst[pos] = '\0'; + + return pos; +} + +/* read up to the closing quote, leaving *strp after it */ +short +rsrc_unescape(char **strp, char *end, char quote, unsigned char *dst, + size_t dstsize, size_t *dstlen) +{ + char *p = *strp; + size_t len = 0; + short hi, lo; + + while (p < end && *p != quote) { + if (len + 1 >= dstsize) + return -1; + + if (*p != '\\') { + dst[len++] = *p++; + continue; + } + + if (++p >= end) + return -1; + + if (*p == '0' && end - p >= 4 && (p[1] == 'x' || p[1] == 'X')) { + if ((hi = rsrc_hexval(p[2]) == -1) || + (lo = rsrc_hexval(p[3]) == -1)) + return -1; + dst[len++] = (hi << 4) | lo; + p += 4; + } else if (*p == '\\' || *p == '\'' || *p == '"') + dst[len++] = *p++; + else + return -1; + } + + if (p >= end) + /* no closing quote */ + return -1; + + *strp = p + 1; + *dstlen = len; + + return 0; +} + +short +rsrc_hexval(char c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + + return -1; +} + +char * +rsrc_skip_ws(char *p, char *end) +{ + while (p < end && (*p == ' ' || *p == '\t')) + p++; + + return p; +} --- rsrc.h Mon Sep 28 20:38:49 2026 +++ rsrc.h Mon Sep 28 20:38:49 2026 @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef __RSRC_H__ +#define __RSRC_H__ + +#include "util.h" + +bool rsrc_is_rsrc(const char *filename); +short rsrc_derez(Str255 path, char **ret, size_t *retlen); +short rsrc_rez(Str255 path, const char *text, size_t len); + +#endif