diff --git a/Makefile b/Makefile index f9210fc..a61c404 100644 --- a/Makefile +++ b/Makefile @@ -8,14 +8,17 @@ COptions-68K = {COptions} {Sym-68K} ### Source Files ### SrcFiles = ∂ + mac_to_unix.c ∂ sync.c ### Object Files ### ObjFiles-PPC = ∂ + mac_to_unix.c.x ∂ sync.c.x ObjFiles-68K = ∂ + mac_to_unix.c.o ∂ sync.c.o ### Libraries ### @@ -69,8 +72,19 @@ SyncFiles ƒƒ {ObjFiles-68K} {LibFiles-68K} Dependencies ƒ $OutOfDate MakeDepend ∂ - -append {MAKEFILE} ∂ + -append Makefile ∂ -ignore "{CIncludes}" ∂ -objext .x ∂ -objext .o ∂ {SrcFiles} + +#*** Dependencies: Cut here *** +# These dependencies were produced at 10:09:51 PM on Sat, Mar 6, 2021 by MakeDepend + +:mac_to_unix.c.x :mac_to_unix.c.o ƒ ∂ + :mac_to_unix.c ∂ + :defs.h + +:sync.c.x :sync.c.o ƒ ∂ + :sync.c ∂ + :defs.h diff --git a/defs.h b/defs.h new file mode 100644 index 0000000..306950c --- /dev/null +++ b/defs.h @@ -0,0 +1,2 @@ +void mac_to_unix(unsigned char **outptr, unsigned char *outend, + const unsigned char **inptr, const unsigned char *inend); diff --git a/mac_to_unix.c b/mac_to_unix.c new file mode 100644 index 0000000..d024cd4 --- /dev/null +++ b/mac_to_unix.c @@ -0,0 +1,65 @@ +#include "defs.h" + +#include + +// Table that converts Macintosh Roman characters to UTF-8, and CR to LF. +static const unsigned short kToUnixTable[256] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 12, 10, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, + 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, + 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, + 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, + 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, + 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, + 120, 121, 122, 123, 124, 125, 126, 127, 196, 197, 199, 201, + 209, 214, 220, 225, 224, 226, 228, 227, 229, 231, 233, 232, + 234, 235, 237, 236, 238, 239, 241, 243, 242, 244, 246, 245, + 250, 249, 251, 252, 8224, 176, 162, 163, 167, 8226, 182, 223, + 174, 169, 8482, 180, 168, 8800, 198, 216, 8734, 177, 8804, 8805, + 165, 181, 8706, 8721, 8719, 960, 8747, 170, 186, 937, 230, 248, + 191, 161, 172, 8730, 402, 8776, 8710, 171, 187, 8230, 160, 192, + 195, 213, 338, 339, 8211, 8212, 8220, 8221, 8216, 8217, 247, 9674, + 255, 376, 8260, 8364, 8249, 8250, 64257, 64258, 8225, 183, 8218, 8222, + 8240, 194, 202, 193, 203, 200, 205, 206, 207, 204, 211, 212, + 63743, 210, 218, 219, 217, 305, 710, 732, 175, 728, 729, 730, + 184, 733, 731, 711, +}; + +void mac_to_unix(unsigned char **outptr, unsigned char *outend, + const unsigned char **inptr, const unsigned char *inend) { + unsigned char *op = *outptr; + const unsigned char *ip = *inptr; + unsigned cp; + + while (ip < inend) { + cp = kToUnixTable[*ip]; + if (cp < 0x80) { + if (outend - op < 1) { + break; + } + op[0] = cp; + op += 1; + } else if (cp < 0x400) { + if (outend - op < 2) { + break; + } + op[0] = (cp >> 6) | 0xc0; + op[1] = (cp & 0x3f) | 0x80; + op += 2; + } else { + if (outend - op < 3) { + break; + } + op[0] = (cp >> 12) | 0xe0; + op[1] = ((cp >> 6) & 0x3f) | 0x80; + op[2] = (cp & 0x3f) | 0x80; + op += 3; + } + ip++; + } + *outptr = op; + *inptr = ip; +} diff --git a/sync.c b/sync.c index 3ced761..1820d53 100644 --- a/sync.c +++ b/sync.c @@ -1,4 +1,7 @@ +#include "defs.h" + #include +#include #include #include @@ -6,6 +9,15 @@ #include #include +enum { + // Maximum file size that we will copy. + kMaxFileSize = 64 * 1024, +}; + +typedef void (*convert_func)(unsigned char **outptr, unsigned char *outend, + const unsigned char **inptr, + const unsigned char *inend); + static void print_err(const char *msg, ...) { va_list ap; fputs("## Error: ", stderr); @@ -15,15 +27,63 @@ static void print_err(const char *msg, ...) { fputc('\n', stderr); } +// Map from OSErr codes to messages. As a heuristic, this should include error +// codes caused by toolbox calls in this program which are readily understood by +// the user. It can exclude error codes that are likely to indicate a +// programming error. +struct error_message { + OSErr err; + const char *msg; +}; +const struct error_message kErrorMessages[] = { + {dirFulErr, "directory full"}, // -33 + {dskFulErr, "disk full"}, // -34 + {ioErr, "I/O error"}, // -36 + {bdNamErr, "bad name"}, // -37 + {fnfErr, "file not found"}, // -43 + {wPrErr, "disk is write-protected"}, // -44 + {fLckdErr, "file is locked"}, // -45 + {vLckdErr, "volume is locked"}, // -46 + {dupFNErr, "destination already exists"}, // -48 + {opWrErr, "file already open for writing"}, // -49 + {paramErr, "parameter error"}, // -50 + {permErr, "cannot write to locked file"}, // -54 + {dirNFErr, "directory not found"}, // -120 + {wrgVolTypErr, "not an HFS volume"}, // -123 + {diffVolErr, "files on different volumes"}, // -1303 + {afpAccessDenied, "user does not have access privileges (AFP)"}, // -5000 + {afpObjectTypeErr, + "file/directory specified where directory/file expected"}, // -5025 + {afpSameObjectErr, "objects are the same"}, // -5038 +}; + +static const char *mac_strerror(OSErr err) { + int i, n = sizeof(kErrorMessages) / sizeof(*kErrorMessages); + for (i = 0; i < n; i++) { + if (kErrorMessages[i].err == err) { + return kErrorMessages[i].msg; + } + } + return NULL; +} + static void print_errcode(OSErr err, const char *msg, ...) { va_list ap; + const char *emsg; + fputs("## Error: ", stderr); va_start(ap, msg); vfprintf(stderr, msg, ap); va_end(ap); - fprintf(stderr, ": err=%d\n", err); + emsg = mac_strerror(err); + if (emsg != NULL) { + fprintf(stderr, ": %s (%d)\n", emsg, err); + } else { + fprintf(stderr, ": err=%d\n", err); + } } +// Convert a C to Pascal string. static int c2pstr(Str255 ostr, const char *istr) { size_t n = strlen(istr); if (n > 255) { @@ -36,6 +96,13 @@ static int c2pstr(Str255 ostr, const char *istr) { return 0; } +// Convert a Pascal to C string. +static void p2cstr(char *ostr, const unsigned char *istr) { + unsigned len = istr[0]; + memcpy(ostr, istr + 1, len); + ostr[len] = '\0'; +} + struct file_meta { Boolean exists; long modTime; @@ -46,15 +113,24 @@ enum { kDestDir, }; +enum { + kModeAuto, + kModePush, + kModePull, +}; + struct file_info { Str31 name; struct file_meta meta[2]; + int mode; }; +// Array of metadata entries for files. static Handle gFiles; static unsigned gFileCount; static unsigned gFileAlloc; +// Get the metadata entry for a file with the given name. static struct file_info *get_file(const unsigned char *name) { unsigned i, n = gFileCount, len, newAlloc; struct file_info *file, *array; @@ -103,6 +179,7 @@ static struct file_info *get_file(const unsigned char *name) { return file; } +// Get the volume and directory ID for a directory from its path. static int dir_from_path(short *vRefNum, long *dirID, const char *dirpath) { Str255 ppath; FSSpec spec; @@ -139,6 +216,8 @@ static int dir_from_path(short *vRefNum, long *dirID, const char *dirpath) { return 0; } +// Return true if a file with the given name should be included. The name is a +// Pascal string. static int filter_name(unsigned char *name) { int len, i, stem; unsigned char *ext; @@ -167,6 +246,8 @@ static int filter_name(unsigned char *name) { return 0; } +// List files in a directory, filter them, and add the files matching the filter +// to the file list. The value of 'which' should be kSrcDir or kDestDir. static int list_dir(short vRefNum, long dirID, int which) { Str255 ppath; CInfoPBRec ci; @@ -200,14 +281,318 @@ static int list_dir(short vRefNum, long dirID, int which) { return 0; } -static int command_main(char *destpath) { - short srcVol, destVol; - long srcDir, destDir; - struct file_info *array, *file; +// Read the entire data fork of a file. The result must be freed with +// DisposePtr. +static int read_file(FSSpec *spec, Ptr *data, long *length) { + CInfoPBRec ci; + Ptr ptr; + long dataLength, pos, count; + OSErr err; + short fref; + + // Get file size. + memset(&ci, 0, sizeof(ci)); + ci.hFileInfo.ioNamePtr = spec->name; + ci.hFileInfo.ioVRefNum = spec->vRefNum; + ci.hFileInfo.ioDirID = spec->parID; + err = PBGetCatInfoSync(&ci); + if (err != 0) { + print_errcode(err, "could not get file metadata"); + return 1; + } + if ((ci.hFileInfo.ioFlAttrib & kioFlAttribDirMask) != 0) { + print_err("is a directory"); + return 1; + } + dataLength = ci.hFileInfo.ioFlLgLen; + if (dataLength > kMaxFileSize) { + print_err("file is too large: size=%ld, max=%ld", dataLength, + kMaxFileSize); + return 1; + } + + // Allocate memory. + ptr = NewPtr(dataLength); + err = MemError(); + if (err != 0) { + print_errcode(err, "out of memory"); + return 1; + } + + // Read file. + err = FSpOpenDF(spec, fsRdPerm, &fref); + if (err != 0) { + DisposePtr(ptr); + print_errcode(err, "could not open file"); + return 1; + } + pos = 0; + while (pos < dataLength) { + count = dataLength - pos; + err = FSRead(fref, &count, ptr + pos); + if (err != 0) { + DisposePtr(ptr); + FSClose(fref); + print_errcode(err, "could not read file"); + return 1; + } + pos += count; + } + FSClose(fref); + *data = ptr; + *length = dataLength; + return 0; +} + +// Make an FSSpec for a temporary file. +static int make_temp(FSSpec *temp, short vRefNum, long dirID, + const unsigned char *name) { + Str31 tname; + unsigned pfxlen, maxpfx = 31 - 4; + OSErr err; + + pfxlen = name[0]; + if (pfxlen > maxpfx) { + pfxlen = maxpfx; + } + memcpy(tname + 1, name + 1, pfxlen); + memcpy(tname + 1 + pfxlen, ".tmp", 4); + tname[0] = pfxlen + 4; + err = FSMakeFSSpec(vRefNum, dirID, tname, temp); + if (err == 0) { + print_err("temporary file exists"); + return 1; + } else if (err == fnfErr) { + return 0; + } else { + print_errcode(err, "could not create temp file spec"); + return 1; + } +} + +// Write the entire contents of a file. +static int write_file(FSSpec *dest, short tempVol, long tempDir, Ptr data, + long length, long modTime, Boolean destExists) { + FSSpec temp; + long pos, amt; + short ref; + HParamBlockRec pb; + CMovePBRec cm; + CInfoPBRec ci; + Str31 name; + OSErr err; + int r; + + // Save the data to a temporary file. + r = make_temp(&temp, tempVol, tempDir, dest->name); + if (r != 0) { + return 1; + } + err = FSpCreate(&temp, 'MPS ', 'TEXT', smSystemScript); + if (err != 0) { + print_errcode(err, "could not create file"); + return 1; + } + err = FSpOpenDF(&temp, fsRdWrPerm, &ref); + if (err != 0) { + print_errcode(err, "could not open temp file"); + goto error; + } + pos = 0; + while (pos < length) { + amt = length - pos; + err = FSWrite(ref, &amt, data + pos); + if (err != 0) { + FSClose(ref); + print_errcode(err, "could not write temp file"); + goto error; + } + pos += amt; + } + err = FSClose(ref); + if (err != 0) { + print_errcode(err, "could not close temp file"); + goto error; + } + + // Update the modification time. + memset(&ci, 0, sizeof(ci)); + memcpy(name, temp.name, temp.name[0] + 1); + ci.hFileInfo.ioNamePtr = name; + ci.hFileInfo.ioVRefNum = temp.vRefNum; + ci.hFileInfo.ioDirID = temp.parID; + err = PBGetCatInfoSync(&ci); + if (err != 0) { + print_errcode(err, "could not get temp file info"); + goto error; + } + memcpy(name, temp.name, temp.name[0] + 1); + ci.hFileInfo.ioNamePtr = name; + ci.hFileInfo.ioVRefNum = temp.vRefNum; + ci.hFileInfo.ioDirID = temp.parID; + ci.hFileInfo.ioFlMdDat = modTime; + err = PBSetCatInfoSync(&ci); + if (err != 0) { + print_errcode(err, "could not set temp file info"); + goto error; + } + + // First, try to exchange files if destination exists. + if (destExists) { + err = FSpExchangeFiles(&temp, dest); + if (err == 0) { + err = FSpDelete(&temp); + if (err != 0) { + print_errcode(err, "could not rename temporary file"); + return 1; + } + return 0; + } + // paramErr: function not supported by volume. + if (err != paramErr) { + print_errcode(err, "could not delete temp file"); + return 1; + } + // Otherwise, delete destination and move temp file over. + err = FSpDelete(dest); + if (err != 0) { + print_errcode(err, "could not remove destination file"); + goto error; + } + } + + // Next, try MoveRename. + memset(&pb, 0, sizeof(pb)); + pb.copyParam.ioNamePtr = temp.name; + pb.copyParam.ioVRefNum = temp.vRefNum; + pb.copyParam.ioNewName = dest->name; + pb.copyParam.ioNewDirID = dest->parID; + pb.copyParam.ioDirID = temp.parID; + err = PBHMoveRenameSync(&pb); + if (err == 0) { + return 0; + } + // paramErr: function not supported by volume. + if (err != paramErr) { + print_errcode(err, "could not rename temporary file"); + goto error; + } + + // Finally, try move and then rename. + if (dest->parID != temp.parID) { + memset(&cm, 0, sizeof(cm)); + cm.ioNamePtr = temp.name; + cm.ioVRefNum = temp.vRefNum; + cm.ioNewDirID = dest->parID; + cm.ioDirID = temp.parID; + err = PBCatMoveSync(&cm); + if (err != 0) { + print_errcode(err, "could not move temporary file"); + goto error; + } + temp.parID = dest->parID; + } + if (memcmp(dest->name, temp.name, dest->name[0] + 1) != 0) { + err = FSpRename(&temp, dest->name); + if (err != 0) { + print_errcode(err, "could not rename temporary file"); + goto error; + } + } + + return 0; + +error: + err = FSpDelete(&temp); + if (err != 0) { + print_errcode(err, "could not delete temp file"); + } + return 1; +} + +// Copy the source file to the destination file. A temporary file is created in +// the specified temporary directory. +static int sync_file(struct file_info *file, convert_func func, long srcVol, + short srcDir, long destVol, short destDir, long tempVol, + short tempDir, long modTime) { + FSSpec src, dest; + Ptr srcData = NULL, destData = NULL; + long srcLength, destLength; + int r; + OSErr err; + unsigned char *outptr, *outend; + const unsigned char *inptr, *inend; + Boolean destExists; + + // Create file specs. + err = FSMakeFSSpec(srcVol, srcDir, file->name, &src); + if (err != 0) { + print_errcode(err, "could not create source spec"); + return 1; + } + err = FSMakeFSSpec(destVol, destDir, file->name, &dest); + if (err == 0) { + destExists = TRUE; + } else if (err == fnfErr) { + destExists = FALSE; + } else if (err != 0) { + print_errcode(err, "could not create destination spec"); + return 1; + } + + // Read the source file into memory. + r = read_file(&src, &srcData, &srcLength); + if (r != 0) { + return 1; + } + // Convert data. + destLength = srcLength + (srcLength >> 2) + 16; + destData = NewPtr(destLength); + err = MemError(); + if (err != 0) { + print_errcode(err, "out of memory"); + goto error; + } + outptr = (unsigned char *)destData; + outend = outptr + destLength; + inptr = (unsigned char *)srcData; + inend = inptr + srcLength; + func(&outptr, outend, &inptr, inend); + if (inptr != inend) { + print_err("conversion function failed"); + goto error; + } + destLength = outptr - (unsigned char *)destData; + r = write_file(&dest, tempVol, tempDir, destData, destLength, destExists, + modTime); + if (r != 0) { + goto error; + } + + // Clean up. + DisposePtr(srcData); + DisposePtr(destData); + return 0; + +error: + if (srcData != NULL) { + DisposePtr(srcData); + } + if (destData != NULL) { + DisposePtr(destData); + } + return 1; +} + +static int command_main(char *destpath, int mode) { + short srcVol, destVol, tempVol; + long srcDir, destDir, tempDir; + struct file_info *array, *file, *srcNewer, *destNewer; OSErr err; int r, i, n; char name[32]; + // Get handles to src and dest directories. err = HGetVol(NULL, &srcVol, &srcDir); if (err != 0) { print_errcode(err, "HGetVol"); @@ -217,6 +602,8 @@ static int command_main(char *destpath) { if (r != 0) { return 1; } + + // List files in src and dest directories. r = list_dir(srcVol, srcDir, kSrcDir); if (r != 0) { return 1; @@ -229,32 +616,124 @@ static int command_main(char *destpath) { print_err("no files"); return 1; } + HLock(gFiles); array = (struct file_info *)*gFiles; n = gFileCount; + + // Figure out the direction for each file. + srcNewer = NULL; + destNewer = NULL; for (i = 0; i < n; i++) { file = &array[i]; - memcpy(name, file->name + 1, file->name[0]); - name[file->name[0]] = '\0'; - printf("File: %s\n", name); - printf(" exist: %c %c\n", file->meta[0].exists ? 'Y' : '-', - file->meta[1].exists ? 'Y' : '-'); - printf(" modTime: %ld %ld\n", file->meta[0].modTime, - file->meta[1].modTime); + if (!file->meta[kSrcDir].exists) { + file->mode = kModePull; + destNewer = file; + } else if (!file->meta[kDestDir].exists) { + file->mode = kModePush; + srcNewer = file; + } else if (file->meta[kSrcDir].modTime < file->meta[kDestDir].modTime) { + file->mode = kModePull; + destNewer = file; + } else if (file->meta[kSrcDir].modTime > file->meta[kDestDir].modTime) { + file->mode = kModePush; + srcNewer = file; + } + } + + // Figure out the mode: push or pull. + if (mode == kModeAuto) { + if (srcNewer != NULL) { + if (destNewer != NULL) { + fputs("## Error: both source and destination have new files\n", + stderr); + p2cstr(name, srcNewer->name); + fprintf(stderr, "## New file in source: %s\n", name); + p2cstr(name, destNewer->name); + fprintf(stderr, "## New file in destination: %s\n", name); + return 1; + } + mode = kModePush; + fputs("## Mode: push\n", stderr); + } else if (destNewer != NULL) { + mode = kModePull; + fputs("## Mode: pull\n", stderr); + } else { + fputs("## No changes.\n", stderr); + return 0; + } + } + + // Synchronize the files. + tempVol = 0; + tempDir = 0; + for (i = 0; i < n; i++) { + file = &array[i]; + if (file->mode == mode) { + if (mode == kModePush) { + // When pushing, we use the destination directory as the + // temporary folder, to avoid crossing filesystem boundaries on + // the host. + r = sync_file(file, mac_to_unix, srcVol, srcDir, destVol, + destDir, destVol, destDir, + file->meta[kSrcDir].modTime); + } else { + if (tempDir == 0) { + err = FindFolder(destVol, kTemporaryFolderType, TRUE, + &tempVol, &tempDir); + if (err != 0) { + print_errcode(err, "could not find temporary folder"); + return 1; + } + } + r = sync_file(file, mac_to_unix, destVol, destDir, srcVol, + srcDir, tempVol, tempDir, + file->meta[kDestDir].modTime); + } + if (r) { + p2cstr(name, file->name); + print_err("failed to copy file: %s", name); + return 1; + } + } else if (file->mode != kModeAuto) { + p2cstr(name, file->name); + fprintf(stderr, "## Refusing to overwrite '%s', file is newer\n", + name); + } } - HUnlock(gFiles); return 0; } int main(int argc, char **argv) { - int r; + char *destDir = NULL, *arg, *opt; + int i, r, mode = kModeAuto; - if (argc != 2) { - fputs("## Usage: SyncFiles \n", stderr); - return 1; + for (i = 1; i < argc; i++) { + arg = argv[i]; + if (*arg == '-') { + opt = arg + 1; + if (*opt == '-') { + opt++; + } + if (strcmp(opt, "push") == 0) { + mode = kModePush; + } else if (strcmp(opt, "pull") == 0) { + mode = kModePull; + } else { + print_err("unknown flag: %s", arg); + return 1; + } + } else { + if (destDir != NULL) { + print_err("unexpected argument: %s", arg); + return 1; + } + destDir = arg; + } } - r = command_main(argv[1]); + + r = command_main(argv[1], mode); if (gFiles != NULL) { DisposeHandle(gFiles); }