/* * Copyright (c) 2026 joshua stein * * 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 #include #include #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; }