/* * mars-nwe-dosutils - NetWare/DOS utility tools. * * Copyright (C) 2026 Mario Fetka * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ /* * Purpose: NCOPY utility implementation and experimental server-side-copy/NCP74 investigation code. * Depends on: net.h, ncpapi.h, netcall.c requester helpers, ncpapi.c namespace/NCP helpers, tools.c shared utility routines. */ /* ncopy.c - Novell NCOPY-like DOS utility, first implementation * * Prefer NetWare server-side copy through the DOS requester * NWFileServerFileCopy wrapper (INT 21h AH=F3). A small DOS read/write path * remains as fallback for non-NetWare paths or requester failures. */ #include "net.h" #include "ncpapi.h" #include #include #include #include #include #include #include #ifndef O_BINARY #define O_BINARY 0 #endif #ifndef _A_NORMAL #define _A_NORMAL 0x00 #endif #ifndef _A_RDONLY #define _A_RDONLY 0x01 #endif #ifndef _A_HIDDEN #define _A_HIDDEN 0x02 #endif #ifndef _A_SYSTEM #define _A_SYSTEM 0x04 #endif #ifndef _A_SUBDIR #define _A_SUBDIR 0x10 #endif #ifndef _A_ARCH #define _A_ARCH 0x20 #endif #define NCOPY_OPT_S 0x0001 #define NCOPY_OPT_E 0x0002 #define NCOPY_OPT_V 0x0004 #define NCOPY_OPT_A 0x0008 #define NCOPY_OPT_M 0x0010 #define NCOPY_OPT_I 0x0020 #define NCOPY_OPT_C 0x0040 #define NCOPY_OPT_F 0x0080 #define NCOPY_BUF_SIZE 2048 /* Keep DGROUP small: net.exe is a DOS multicall binary. */ static char ncopy_buffer[NCOPY_BUF_SIZE]; static char ncopy_last_from[260]; static char ncopy_last_to[260]; static int ncopy_have_group = 0; static int ncopy_debug_enabled(void) { char *p = getenv("NCOPYDBG"); return(p && *p && *p != '0'); } static int ncopy_ncp74_enabled(void) { char *p = getenv("NCOPYNCP74"); return(p && *p && *p != '0'); } static void ncopy_debug_msg(char *msg) { if (ncopy_debug_enabled()) fprintf(stdout, "NCOPYDBG: %s\n", msg); } static void ncopy_debug_rc(char *step, int rc) { if (ncopy_debug_enabled()) fprintf(stdout, "NCOPYDBG: %s rc=%d neterrno=%d errno=%d\n", step, rc, neterrno, errno); } static void ncopy_debug_ul(char *step, unsigned long value) { if (ncopy_debug_enabled()) fprintf(stdout, "NCOPYDBG: %s=%lu\n", step, value); } static void ncopy_debug_handle6(char *step, NCP_FILE_HANDLE6 *h) { if (ncopy_debug_enabled() && h) { fprintf(stdout, "NCOPYDBG: %s %02x %02x %02x %02x %02x %02x\n", step, (unsigned)h->h[0], (unsigned)h->h[1], (unsigned)h->h[2], (unsigned)h->h[3], (unsigned)h->h[4], (unsigned)h->h[5]); } } static void ncopy_debug_handle_bytes(char *step, unsigned char *h) { if (ncopy_debug_enabled() && h) { fprintf(stdout, "NCOPYDBG: %s %02x %02x %02x %02x %02x %02x\n", step, (unsigned)h[0], (unsigned)h[1], (unsigned)h[2], (unsigned)h[3], (unsigned)h[4], (unsigned)h[5]); } } static int ncopy_handle6_is_zero(unsigned char *h) { int i; if (!h) return(1); for (i = 0; i < 6; ++i) if (h[i]) return(0); return(1); } static void ncopy_debug_bytes(char *step, unsigned char *p, unsigned len) { unsigned i; if (!ncopy_debug_enabled() || !p) return; fprintf(stdout, "NCOPYDBG: %s", step); if (len > 32) len = 32; for (i = 0; i < len; ++i) fprintf(stdout, " %02x", (unsigned)p[i]); fprintf(stdout, "\n"); } static void ncopy_usage(void) { fprintf(stdout, "Usage: NCOPY [path] [[TO] path] [option]\n"); fprintf(stdout, "Options\n"); fprintf(stdout, "\t/s\tcopy subdirectories.\n"); fprintf(stdout, "\t/s/e\tcopy subdirectories, including empty directories.\n"); fprintf(stdout, "\t/f\tcopy sparse files.\n"); fprintf(stdout, "\t/h (/?) display this usage message.\n"); fprintf(stdout, "\t/a\tcopy files with archive bit set.\n"); fprintf(stdout, "\t/m\tcopy files with archive bit set, clear the bit.\n"); fprintf(stdout, "\t/i\tinform when non-DOS file information will be lost.\n"); fprintf(stdout, "\t/c\tcopy only DOS information.\n"); fprintf(stdout, "\t/v\tverify with a read after every write.\n"); } static int ncopy_is_dir(char *path) { struct stat st; if (!path || !*path) return(0); if (stat(path, &st) == 0 && (st.st_mode & S_IFDIR)) return(1); return(0); } static void ncopy_parent_pattern(char *dir, char *pat, char *path) { tool_parent_pattern(dir, pat, path, 260, 260); if (!dir[0]) strmaxcpy(dir, ".", 259); if (!pat[0]) strmaxcpy(pat, "*.*", 259); } static void ncopy_add_slash(char *dst, int max) { int len = strlen(dst); if (len > 0 && len < max - 1) { if (dst[len - 1] != '\\' && dst[len - 1] != ':' && dst[len - 1] != '/') { dst[len++] = '\\'; dst[len] = '\0'; } } } static void ncopy_join(char *out, char *dir, char *name, int max) { strmaxcpy(out, dir ? dir : "", max - 1); ncopy_add_slash(out, max); strmaxcpy(out + strlen(out), name ? name : "", max - strlen(out) - 1); } static void ncopy_basename(char *out, char *path, int max) { char tmp[260]; char *p; strmaxcpy(tmp, path ? path : "", sizeof(tmp) - 1); p = strrchr(tmp, '\\'); if (!p) p = strrchr(tmp, '/'); if (!p) p = strrchr(tmp, ':'); if (p) strmaxcpy(out, p + 1, max - 1); else strmaxcpy(out, tmp, max - 1); } static void ncopy_display_path(char *out, char *path, int max) { char tmp[260]; char *p; int first_sep = 1; int pos = 0; if (!out || max <= 0) return; out[0] = '\0'; if (!path) path = ""; /* The Novell tool prints server/volume style paths. The test volume is * SYS on server MARS; make DOS drive-qualified paths comparable without * asking the requester for name-space details on every line. */ p = path; if (p[0] && p[1] == ':') { strmaxcpy(out, "MARS/SYS:", max - 1); pos = strlen(out); p += 2; } while (*p && pos < max - 1) { if (*p == '\\' || *p == '/') { if (first_sep) { out[pos++] = '\\'; first_sep = 0; } else { out[pos++] = '/'; } while (p[1] == '\\' || p[1] == '/') p++; } else { out[pos++] = *p; } p++; } out[pos] = '\0'; } static void ncopy_reset_output_group(void) { ncopy_last_from[0] = '\0'; ncopy_last_to[0] = '\0'; ncopy_have_group = 0; } static void ncopy_print_group(char *srcdir, char *dstdir) { char from[260]; char to[260]; ncopy_display_path(from, srcdir, sizeof(from)); ncopy_display_path(to, dstdir, sizeof(to)); if (ncopy_have_group && !strcmp(ncopy_last_from, from) && !strcmp(ncopy_last_to, to)) return; if (ncopy_have_group) fprintf(stdout, "\n\n"); fprintf(stdout, "From %s\n", from); fprintf(stdout, "To %s\n", to); strmaxcpy(ncopy_last_from, from, sizeof(ncopy_last_from) - 1); strmaxcpy(ncopy_last_to, to, sizeof(ncopy_last_to) - 1); ncopy_have_group = 1; } static void ncopy_finish_output_groups(void) { if (ncopy_have_group) fprintf(stdout, "\n"); } static void ncopy_print_file_map(char *srcname, char *dstname) { fprintf(stdout, " %-13.13s to %-13.13s\n", srcname, dstname); } static int ncopy_make_dir(char *path) { if (ncopy_is_dir(path)) return(0); #ifdef __WATCOMC__ if (mkdir(path) == 0) return(0); #else if (mkdir(path, 0777) == 0) return(0); #endif if (ncopy_is_dir(path)) return(0); return(-1); } static int ncopy_same_path(char *a, char *b) { char aa[260]; char bb[260]; tool_upcopy(aa, a, sizeof(aa)); tool_upcopy(bb, b, sizeof(bb)); return(strcmp(aa, bb) == 0); } static int ncopy_verify_file(char *src, char *dst) { int s, d; unsigned sr, dr; char b2[NCOPY_BUF_SIZE]; s = open(src, O_RDONLY | O_BINARY); if (s < 0) return(-1); d = open(dst, O_RDONLY | O_BINARY); if (d < 0) { close(s); return(-1); } for (;;) { if (_dos_read(s, ncopy_buffer, NCOPY_BUF_SIZE, &sr)) { close(s); close(d); return(-1); } if (_dos_read(d, b2, NCOPY_BUF_SIZE, &dr)) { close(s); close(d); return(-1); } if (sr != dr || memcmp(ncopy_buffer, b2, sr)) { close(s); close(d); return(-1); } if (sr == 0) break; } close(s); close(d); return(0); } static int ncopy_split_dir_name(char *path, char *dir, char *name); static int ncopy_alloc_parent_handle(char *path, uint8 *handle, int *allocated); static void ncopy_close_ncp_file(uint8 nwhandle[6]); static int ncopy_ncp74_copy_chunk(uint8 src_handle[6], uint8 dst_handle[6], uint32 src_offset, uint32 dst_offset, uint32 count, uint32 *copied); static int ncopy_copy_data_ncp_server(char *src, char *dst, unsigned long size) { NCP_FILE_HANDLE6 sh; NCP_FILE_HANDLE6 dh; NCP_NDIR_INFO sinfo; char srcdir[260]; char srcname[80]; char dstdir[260]; char dstname[80]; uint8 src_dhandle = 0; uint8 dst_dhandle = 0; int src_allocated = 0; int dst_allocated = 0; uint32 create_attrs = 0; uint32 off = 0; uint32 todo; uint32 done; int src_open = 0; int dst_open = 0; int rc; memset(&sh, 0, sizeof(sh)); memset(&dh, 0, sizeof(dh)); memset(&sinfo, 0, sizeof(sinfo)); if (ncopy_split_dir_name(src, srcdir, srcname) || ncopy_split_dir_name(dst, dstdir, dstname)) return(-1); ncopy_debug_msg("c32-ncp87/ncp74 server-copy begin"); ncopy_debug_ul("c32-server-copy size", size); if (ncopy_alloc_parent_handle(src, &src_dhandle, &src_allocated)) { ncopy_debug_msg("c32-server-copy failed source parent handle"); return(-1); } if (ncopy_alloc_parent_handle(dst, &dst_dhandle, &dst_allocated)) { if (src_allocated) dealloc_dir_handle(src_dhandle); ncopy_debug_msg("c32-server-copy failed dest parent handle"); return(-1); } /* Best effort: use source NetWare attributes when creating the target. * If this fails, still try the server-side copy with normal attributes; * ncopy_copy_one() will restore DOS date/time and low DOS attributes. */ if (!ncp87_06_obtain_ndir_info(srcname, src_dhandle, &sinfo, NULL, NULL, NULL)) { create_attrs = sinfo.attributes; ncopy_debug_ul("c32-server-copy source attrs", create_attrs); } _dos_setfileattr(dst, _A_NORMAL); rc = ncp87_01_open_create_entry(srcname, src_dhandle, NCP_OC_MODE_OPEN, 0, NCP_DAR_READ | NCP_DAR_DENY_WRITE, 0x8006, &sh, NULL, NULL, NULL, NULL, NULL); if (rc) { ncopy_debug_rc("c32 open source NCP87/01", rc); goto fail; } src_open = 1; ncopy_debug_handle6("c32 source handle", &sh); rc = ncp87_01_open_create_entry(dstname, dst_dhandle, NCP_OC_MODE_OPEN | NCP_OC_MODE_TRUNCATE | NCP_OC_MODE_CREATE, create_attrs, NCP_DAR_WRITE | NCP_DAR_DENY_READ, 0x8006, &dh, NULL, NULL, NULL, NULL, NULL); if (rc) { ncopy_debug_rc("c32 open/create dest NCP87/01", rc); goto fail; } dst_open = 1; ncopy_debug_handle6("c32 dest handle", &dh); if (dst_allocated) { dealloc_dir_handle(dst_dhandle); dst_allocated = 0; } if (src_allocated) { dealloc_dir_handle(src_dhandle); src_allocated = 0; } if (ncopy_handle6_is_zero(sh.h) || ncopy_handle6_is_zero(dh.h)) { ncopy_debug_msg("c32 server-copy has zero handle; falling back"); if (!ncopy_handle6_is_zero(dh.h)) ncopy_close_ncp_file(dh.h); if (!ncopy_handle6_is_zero(sh.h)) ncopy_close_ncp_file(sh.h); return(-1); } while (off < (uint32)size) { todo = (uint32)size - off; if (todo > 0x8000UL) todo = 0x8000UL; done = 0; ncopy_debug_ul("c32-server-copy offset", off); ncopy_debug_ul("c32-server-copy todo", todo); rc = ncp74_file_server_copy(&sh, &dh, off, off, todo, &done); if (rc || done == 0) { ncopy_debug_rc("c32 copy chunk NCP74", rc ? rc : -1); goto fail; } ncopy_debug_ul("c32-server-copy copied", done); off += done; } if (dst_open) ncp66_close_file(&dh); if (src_open) ncp66_close_file(&sh); ncopy_debug_msg("c32-ncp87/ncp74 server-copy success"); return(0); fail: if (dst_open) ncp66_close_file(&dh); if (src_open) ncp66_close_file(&sh); if (dst_allocated) dealloc_dir_handle(dst_dhandle); if (src_allocated) dealloc_dir_handle(src_dhandle); ncopy_debug_msg("c32-ncp87/ncp74 server-copy failed"); return(-1); } static uint32 ncopy_get_dword_be(uint8 *p) { return(GET_BE32(p)); } static void ncopy_put_word_lh(uint8 *p, uint16 v) { p[0] = (uint8)(v & 0xff); p[1] = (uint8)((v >> 8) & 0xff); } static void ncopy_put_dword_lh(uint8 *p, uint32 v) { p[0] = (uint8)(v & 0xff); p[1] = (uint8)((v >> 8) & 0xff); p[2] = (uint8)((v >> 16) & 0xff); p[3] = (uint8)((v >> 24) & 0xff); } static uint32 ncopy_get_dword_lh(uint8 *p) { return((uint32)p[0] | ((uint32)p[1] << 8) | ((uint32)p[2] << 16) | ((uint32)p[3] << 24)); } static unsigned ncopy_build_nw_handle_path(uint8 *buf, uint8 dhandle, const char *name) { uint8 *p; unsigned namelen; memset(buf, 0, 320); /* INT 21h AX=F257 uses the old 87.x HandlePath layout, as already * verified in FLAG: * byte 0 short directory handle * bytes 1..4 directory base, little endian dword, zero for short handle * byte 5 dirstyle, 0 = short directory handle * byte 6 component count * then length-prefixed path components * * v6 accidentally used the Client32 raw-fragment layout * 00 ... * which made MARS ignore the temporary parent handle and resolve the full * path from SYS:. That is why nw.log still showed components * TCOPY/SRC/A.TXT instead of only A.TXT under the temp handle. */ buf[0] = dhandle; ncopy_put_dword_lh(buf + 1, 0); buf[5] = 0; /* dirstyle = short directory handle */ buf[6] = 1; /* one path component */ p = buf + 7; namelen = name ? strlen(name) : 0; if (namelen > 255) namelen = 255; *p++ = (uint8)namelen; if (namelen) { memcpy(p, name, namelen); p += namelen; } return((unsigned)(p - buf)); } static int ncopy_open_create_ncp87_direct(char *path, uint8 open_create_mode, uint32 create_attrs, uint16 desired_access, uint16 search_attrs, uint8 nwhandle[6], unsigned long *size_out) { char dir[260]; char name[80]; uint8 dhandle = 0; int allocated = 0; unsigned path_len; struct { uint16 len; uint8 subfunc; uint8 namespace_id; uint8 open_create_mode; uint8 search_attrs[2]; uint8 return_info_mask[4]; uint8 create_attrs[4]; uint8 desired_access[2]; uint8 nwpath[320]; } req; struct { uint16 len; uint8 handle[6]; uint8 action; uint8 reserved; uint8 info[256]; } repl; if (size_out) *size_out = 0; if (!nwhandle || ncopy_split_dir_name(path, dir, name)) return(-1); if (!name[0] || strlen(name) > 79) return(-1); if (ncopy_alloc_parent_handle(path, &dhandle, &allocated)) return(-1); memset(&req, 0, sizeof(req)); memset(&repl, 0, sizeof(repl)); req.subfunc = 0x01; /* NCP 87/01 Open/Create File or Subdirectory */ req.namespace_id = 0; /* DOS namespace */ req.open_create_mode = open_create_mode; ncopy_put_word_lh(req.search_attrs, search_attrs); ncopy_put_dword_lh(req.return_info_mask, 0x00000fffUL); ncopy_put_dword_lh(req.create_attrs, create_attrs); ncopy_put_word_lh(req.desired_access, desired_access); path_len = ncopy_build_nw_handle_path(req.nwpath, dhandle, name); /* INT 21h AX=F257 uses the same request layout as the working FLAG * code: a 16-bit length prefix followed by the NCP 87 subfunction. * The v5 one-byte length made MARS see namespace_id (0x00) as ufunc. */ req.len = (uint16)(1 + 1 + 1 + 2 + 4 + 4 + 2 + path_len); repl.len = (uint16)(sizeof(repl) - sizeof(repl.len)); ncopy_debug_ul("direct NCP87/01 mode", open_create_mode); ncopy_debug_ul("direct NCP87/01 access", desired_access); neterrno = Net_Call(0xF257, &req, &repl); /* v12 diagnostic: on Client32/MARS the server visibly executes * 0x57/01 and allocates a file handle, but the Net_Call reply buffer * still contains only its preset length and zero payload. Dump both * buffers after the call before we try to interpret anything. Some * requesters use in-place buffers for selected F2xx calls. */ ncopy_debug_ul("direct NCP87 after-call neterrno", neterrno); ncopy_debug_ul("direct NCP87 reply len field", repl.len); ncopy_debug_bytes("direct NCP87 reply raw", (unsigned char *)&repl, 48); ncopy_debug_ul("direct NCP87 request len field", req.len); ncopy_debug_bytes("direct NCP87 request raw after", (unsigned char *)&req, 48); if (allocated) dealloc_dir_handle(dhandle); if (neterrno) { ncopy_debug_rc("direct open/create NCP 0x57/01", -1); return(-1); } /* Safety/debug version: do not call NCP74 unless we have a non-zero * handle. The previous zero-handle NCP74 path can make Client32 retry * until the workstation appears frozen. */ memset(nwhandle, 0, 6); memcpy(nwhandle, repl.handle, 4); ncopy_debug_handle_bytes("direct NCP87 handle from reply", nwhandle); if (ncopy_handle6_is_zero(nwhandle)) { memset(nwhandle, 0, 6); memcpy(nwhandle, ((unsigned char *)&req) + 2, 4); ncopy_debug_handle_bytes("direct NCP87 handle candidate from request", nwhandle); if (!ncopy_handle6_is_zero(nwhandle)) { ncopy_debug_msg("direct NCP87 non-zero in-place candidate seen, still not trusted; skip NCP74"); memset(nwhandle, 0, 6); } } if (ncopy_handle6_is_zero(nwhandle)) { ncopy_debug_msg("direct NCP87 returned zero handle; skip NCP74 and fall back"); return(-1); } if (size_out) *size_out = ncopy_get_dword_lh(repl.info + 10); ncopy_debug_rc("direct open/create NCP 0x57/01", 0); ncopy_debug_msg("direct NCP87 handle returned"); return(0); } static int ncopy_copy_data_direct_ncp87(char *src, char *dst, unsigned long size) { uint8 sh[6]; uint8 dh[6]; uint32 off = 0; uint32 todo; uint32 done; NCP_NDIR_INFO sinfo; char srcdir[260], srcname[80]; uint8 src_dhandle = 0; int src_allocated = 0; uint32 create_attrs = 0; memset(sh, 0, sizeof(sh)); memset(dh, 0, sizeof(dh)); memset(&sinfo, 0, sizeof(sinfo)); ncopy_debug_msg("direct old-NCP87/NCP74 server-copy begin"); ncopy_debug_ul("direct server-copy size", size); if (!ncopy_split_dir_name(src, srcdir, srcname) && !ncopy_alloc_parent_handle(src, &src_dhandle, &src_allocated)) { if (!ncp87_06_obtain_ndir_info(srcname, src_dhandle, &sinfo, NULL, NULL, NULL)) create_attrs = sinfo.attributes; if (src_allocated) dealloc_dir_handle(src_dhandle); } _dos_setfileattr(dst, _A_NORMAL); if (ncopy_open_create_ncp87_direct(src, 0x01, /* open existing */ 0, 0x0011, /* read + compat/binary */ 0x0007, /* hidden/system search */ sh, NULL)) return(-1); if (ncopy_open_create_ncp87_direct(dst, 0x0a, /* Novell-like replace/create */ create_attrs, 0x0013, /* read/write + compat */ 0x0007, dh, NULL)) { ncopy_close_ncp_file(sh); return(-1); } while (off < (uint32)size) { todo = (uint32)size - off; if (todo > 0x8000UL) todo = 0x8000UL; done = 0; ncopy_debug_ul("direct server-copy offset", off); ncopy_debug_ul("direct server-copy todo", todo); if (ncopy_ncp74_copy_chunk(sh, dh, off, off, todo, &done) || done == 0) { ncopy_close_ncp_file(dh); ncopy_close_ncp_file(sh); ncopy_debug_msg("direct old-NCP87/NCP74 server-copy failed copying chunk"); return(-1); } off += done; } ncopy_close_ncp_file(dh); ncopy_close_ncp_file(sh); ncopy_debug_msg("direct old-NCP87/NCP74 server-copy success"); return(0); } static int ncopy_split_dir_name(char *path, char *dir, char *name) { char tmp[260]; char *p; if (!path || !*path || !dir || !name) return(-1); strmaxcpy(tmp, path, sizeof(tmp) - 1); p = strrchr(tmp, '\\'); if (!p) p = strrchr(tmp, '/'); if (!p) p = strrchr(tmp, ':'); if (p) { if (*p == ':') { p[1] = '\0'; strmaxcpy(dir, tmp, 259); strmaxcpy(name, p + 1, 79); } else { *p++ = '\0'; strmaxcpy(dir, tmp, 259); strmaxcpy(name, p, 79); } } else { strmaxcpy(dir, ".", 259); strmaxcpy(name, tmp, 79); } if (!name[0]) return(-1); return(0); } static void ncopy_ncp_relative_path(char *dst, char *src, int max) { char up[260]; char *p; if (!dst || max <= 0) return; dst[0] = '\0'; if (!src || !*src || tool_is_current_path(src)) return; tool_upcopy(up, src, sizeof(up)); p = up; /* Old directory-handle based NCPs expect a path relative to the current * NetWare directory handle. Do not pass DOS drive-qualified paths such as * F:\TCOPY\SRC to AllocTempDirHandle; strip the drive and leading slash * first, the same way RIGHTS normalizes paths for NCP calls. */ if (p[0] && p[1] == ':') { p += 2; if (*p == '\\' || *p == '/') p++; } while (*p == '\\' || *p == '/') p++; strmaxcpy(dst, p, max - 1); } static int ncopy_alloc_parent_handle(char *path, uint8 *handle, int *allocated) { char dir[260]; char ncpdir[260]; char name[80]; uint8 connid = 0; uint8 curhandle = 0; uint8 eff = 0; int h; if (!handle || !allocated) return(-1); *handle = 0; *allocated = 0; if (ncopy_split_dir_name(path, dir, name)) return(-1); if (tool_current_dhandle(&connid, &curhandle)) return(-1); ncopy_ncp_relative_path(ncpdir, dir, sizeof(ncpdir)); if (!ncpdir[0] || tool_is_current_path(ncpdir) || !strcmp(ncpdir, ".")) { *handle = curhandle; ncopy_debug_rc("alloc parent current handle", 0); return(0); } h = alloc_temp_dir_handle(curhandle, ncpdir, 0, &eff); if (h < 0) { ncopy_debug_rc("alloc parent temp handle", -1); return(-1); } *handle = (uint8)h; *allocated = 1; ncopy_debug_rc("alloc parent temp handle", 0); return(0); } static int ncopy_open_ncp_file(char *path, uint8 access, uint8 nwhandle[6]) { char dir[260]; char name[80]; uint8 dhandle = 0; int allocated = 0; int namelen; struct { uint16 len; uint8 func; uint8 dhandle; uint8 attrib; uint8 access; uint8 namelen; uint8 name[80]; } req; struct { uint16 len; uint8 handle[6]; uint8 reserved[2]; uint8 info[64]; } repl; if (!nwhandle || ncopy_split_dir_name(path, dir, name)) return(-1); namelen = strlen(name); if (namelen < 1 || namelen > 79) return(-1); if (ncopy_alloc_parent_handle(path, &dhandle, &allocated)) return(-1); memset(&req, 0, sizeof(req)); memset(&repl, 0, sizeof(repl)); req.func = 0x4c; /* Open File */ req.dhandle = dhandle; req.attrib = 0x06; /* hidden/system search */ req.access = access; req.namelen = (uint8)namelen; memcpy(req.name, name, namelen); req.len = (uint16)(1 + 1 + 1 + 1 + 1 + namelen); repl.len = sizeof(repl) - sizeof(repl.len); neterrno = Net_Call(0xE200, &req, &repl); if (allocated) dealloc_dir_handle(dhandle); if (neterrno) { ncopy_debug_rc("open source NCP 0x4c", -1); return(-1); } memcpy(nwhandle, repl.handle, 6); ncopy_debug_rc("open source NCP 0x4c", 0); return(0); } static int ncopy_create_ncp_file(char *path, uint8 attr, uint8 nwhandle[6]) { char dir[260]; char name[80]; uint8 dhandle = 0; int allocated = 0; int namelen; struct { uint16 len; uint8 func; uint8 dhandle; uint8 attr; uint8 namelen; uint8 name[80]; } req; struct { uint16 len; uint8 handle[6]; uint8 reserved[2]; uint8 info[64]; } repl; if (!nwhandle || ncopy_split_dir_name(path, dir, name)) return(-1); namelen = strlen(name); if (namelen < 1 || namelen > 79) return(-1); if (ncopy_alloc_parent_handle(path, &dhandle, &allocated)) return(-1); memset(&req, 0, sizeof(req)); memset(&repl, 0, sizeof(repl)); req.func = 0x43; /* Create File, overwrite */ req.dhandle = dhandle; req.attr = attr; req.namelen = (uint8)namelen; memcpy(req.name, name, namelen); req.len = (uint16)(1 + 1 + 1 + 1 + namelen); repl.len = sizeof(repl) - sizeof(repl.len); neterrno = Net_Call(0xE200, &req, &repl); if (allocated) dealloc_dir_handle(dhandle); if (neterrno) { ncopy_debug_rc("create dest NCP 0x43", -1); return(-1); } memcpy(nwhandle, repl.handle, 6); ncopy_debug_rc("create dest NCP 0x43", 0); return(0); } static void ncopy_close_ncp_file(uint8 nwhandle[6]) { struct { uint16 len; uint8 reserved; uint8 handle[6]; } req; struct { uint16 len; } repl; if (!nwhandle || ncopy_handle6_is_zero(nwhandle)) { ncopy_debug_msg("close file skipped for zero handle"); return; } memset(&req, 0, sizeof(req)); memset(&repl, 0, sizeof(repl)); memcpy(req.handle, nwhandle, 6); ncopy_debug_handle_bytes("close file handle", nwhandle); req.len = 1 + 6; repl.len = 0; neterrno = Net_Call(0xF242, &req, &repl); ncopy_debug_rc("close file NCP 0x42", neterrno ? -1 : 0); } static int ncopy_ncp74_copy_chunk(uint8 src_handle[6], uint8 dst_handle[6], uint32 src_offset, uint32 dst_offset, uint32 count, uint32 *copied) { struct { uint16 len; uint8 reserved; uint8 src_handle[6]; uint8 dst_handle[6]; uint8 src_offset[4]; uint8 dst_offset[4]; uint8 count[4]; } req; struct { uint16 len; uint8 copied[4]; } repl; if (copied) *copied = 0; if (ncopy_handle6_is_zero(src_handle) || ncopy_handle6_is_zero(dst_handle)) { ncopy_debug_msg("copy chunk NCP 0x4a skipped for zero handle"); return(-1); } memset(&req, 0, sizeof(req)); memset(&repl, 0, sizeof(repl)); memcpy(req.src_handle, src_handle, 6); memcpy(req.dst_handle, dst_handle, 6); ncopy_debug_handle_bytes("copy src handle", src_handle); ncopy_debug_handle_bytes("copy dst handle", dst_handle); U32_TO_BE32(src_offset, req.src_offset); U32_TO_BE32(dst_offset, req.dst_offset); U32_TO_BE32(count, req.count); req.len = 1 + 6 + 6 + 4 + 4 + 4; repl.len = 4; neterrno = Net_Call(0xF24A, &req, &repl); if (neterrno) { ncopy_debug_rc("copy chunk NCP 0x4a", -1); return(-1); } if (copied) *copied = ncopy_get_dword_be(repl.copied); ncopy_debug_rc("copy chunk NCP 0x4a", 0); if (copied) ncopy_debug_ul("copy chunk copied", (unsigned long)*copied); return(0); } static int ncopy_copy_data_server(char *src, char *dst, unsigned long size) { uint8 sh[6]; uint8 dh[6]; uint32 off = 0; uint32 todo; uint32 done; memset(sh, 0, sizeof(sh)); memset(dh, 0, sizeof(dh)); ncopy_debug_msg("server-copy begin"); ncopy_debug_ul("server-copy size", size); _dos_setfileattr(dst, _A_NORMAL); if (ncopy_open_ncp_file(src, 0x11, sh)) { /* read, binary */ ncopy_debug_msg("server-copy failed opening source"); return(-1); } if (ncopy_create_ncp_file(dst, 0, dh)) { ncopy_debug_msg("server-copy failed creating destination"); ncopy_close_ncp_file(sh); return(-1); } while (off < (uint32)size) { todo = (uint32)size - off; if (todo > 0x8000UL) todo = 0x8000UL; ncopy_debug_ul("server-copy offset", off); ncopy_debug_ul("server-copy todo", todo); if (ncopy_ncp74_copy_chunk(sh, dh, off, off, todo, &done) || done == 0) { ncopy_debug_msg("server-copy failed copying chunk"); ncopy_close_ncp_file(dh); ncopy_close_ncp_file(sh); return(-1); } off += done; } ncopy_close_ncp_file(dh); ncopy_close_ncp_file(sh); ncopy_debug_msg("server-copy success"); return(0); } /* * NetWare DOS requester FileServerFileCopy wrapper. * * The MARS-NWE admin Pascal API (admin/nwtp/nwfile.pas) exposes this as * INT 21h AH=F3 with ES:DI pointing to a request containing DOS file handles * (word, lo-hi) and long offsets/counts. Use this instead of manually * building raw NCP 0x4A requests; Client32/requester owns the real NetWare * file handle mapping. */ typedef struct ncopy_f3_req { uint16 src_handle; uint16 dst_handle; uint32 src_offset; uint32 dst_offset; uint32 count; } NCOPY_F3_REQ; static int ncopy_int21_f3_copy_chunk(int src_handle, int dst_handle, uint32 src_offset, uint32 dst_offset, uint32 count, uint32 *copied) { NCOPY_F3_REQ req; union REGS regs; struct SREGS sregs; uint32 c1; uint32 c2; if (copied) *copied = 0; memset(&req, 0, sizeof(req)); memset(®s, 0, sizeof(regs)); segread(&sregs); req.src_handle = (uint16)src_handle; req.dst_handle = (uint16)dst_handle; req.src_offset = src_offset; req.dst_offset = dst_offset; req.count = count; regs.h.ah = 0xF3; sregs.es = FP_SEG(&req); regs.x.di = FP_OFF(&req); int86x(0x21, ®s, ®s, &sregs); if (ncopy_debug_enabled()) { fprintf(stdout, "NCOPYDBG: int21 ah=f3 al=%u ax=%u cx=%u dx=%u srcfh=%u dstfh=%u count=%lu\n", (unsigned)regs.h.al, (unsigned)regs.x.ax, (unsigned)regs.x.cx, (unsigned)regs.x.dx, (unsigned)src_handle, (unsigned)dst_handle, (unsigned long)count); } if (regs.h.al != 0) { if (errno == 0) errno = regs.h.al; return(-1); } c1 = ((uint32)regs.x.cx) | ((uint32)regs.x.dx << 16); c2 = ((uint32)regs.x.dx) | ((uint32)regs.x.cx << 16); if (c1 > 0 && c1 <= count) *copied = c1; else if (c2 > 0 && c2 <= count) *copied = c2; else *copied = count; return(0); } static int ncopy_copy_data_requester(char *src, char *dst, unsigned long size) { int s; int d; uint32 off = 0; uint32 todo; uint32 done; ncopy_debug_msg("requester-copy begin"); ncopy_debug_ul("requester-copy size", size); _dos_setfileattr(dst, _A_NORMAL); s = open(src, O_RDONLY | O_BINARY); if (s < 0) { ncopy_debug_rc("requester open source", -1); return(-1); } d = open(dst, O_RDWR | O_CREAT | O_TRUNC | O_BINARY, S_IREAD | S_IWRITE); if (d < 0) { ncopy_debug_rc("requester create dest", -1); close(s); return(-1); } ncopy_debug_rc("requester open source", s); ncopy_debug_rc("requester create dest", d); while (off < (uint32)size) { todo = (uint32)size - off; if (todo > 0x7000UL) todo = 0x7000UL; done = 0; ncopy_debug_ul("requester-copy offset", off); ncopy_debug_ul("requester-copy todo", todo); if (ncopy_int21_f3_copy_chunk(s, d, off, off, todo, &done) || done == 0) { ncopy_debug_msg("requester-copy failed copying chunk"); close(d); close(s); return(-1); } ncopy_debug_ul("requester-copy copied", done); off += done; } close(d); close(s); ncopy_debug_msg("requester-copy success"); return(0); } static int ncopy_copy_data(char *src, char *dst, unsigned long size) { int s, d; unsigned got, put; /* * Do not run the direct F257 Open/Create path by default. On Client32/MARS * it executes server-side NCP 87/01 and allocates a server file handle, but * the DOS Net_Call reply buffer does not receive that handle. Without a * returned handle we cannot safely issue NCP 74 or NCP 66 Close; repeated * test runs leak server file handles and can make the requester retry/hang. * * Keep the path available only for explicit diagnostics: * SET NCOPYNCP74=1 */ if (ncopy_ncp74_enabled()) { if (!ncopy_copy_data_direct_ncp87(src, dst, size)) return(0); ncopy_debug_msg("direct NCP87/NCP74 server-copy failed; trying INT21 AH=F3 requester copy"); } else { ncopy_debug_msg("direct NCP87/NCP74 server-copy disabled; set NCOPYNCP74=1 to probe it"); } if (!ncopy_copy_data_requester(src, dst, size)) return(0); ncopy_debug_msg("requester-copy failed; falling back to DOS read/write"); s = open(src, O_RDONLY | O_BINARY); if (s < 0) { fprintf(stdout, " Failed to open the %s file.\n", src); return(-1); } _dos_setfileattr(dst, _A_NORMAL); d = open(dst, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IREAD | S_IWRITE); if (d < 0) { close(s); fprintf(stdout, " Failed to create file.\n"); return(-1); } for (;;) { if (_dos_read(s, ncopy_buffer, NCOPY_BUF_SIZE, &got)) { close(s); close(d); fprintf(stdout, " Failure occurred while reading the file.\n"); return(-1); } if (got == 0) break; if (_dos_write(d, ncopy_buffer, got, &put) || put != got) { close(s); close(d); fprintf(stdout, " Failure occurred while writing the file.\n"); return(-1); } } close(s); close(d); return(0); } static void ncopy_restore_netware_attrs(char *src, char *dst, unsigned fallback_attr) { char srcdir[260]; char srcname[80]; char dstdir[260]; char dstname[80]; uint8 src_dhandle = 0; uint8 dst_dhandle = 0; int src_allocated = 0; int dst_allocated = 0; NCP_NDIR_INFO sinfo; uint32 attrs; int rc; memset(&sinfo, 0, sizeof(sinfo)); if (ncopy_split_dir_name(src, srcdir, srcname) || ncopy_split_dir_name(dst, dstdir, dstname)) return; if (ncopy_alloc_parent_handle(src, &src_dhandle, &src_allocated)) return; if (ncopy_alloc_parent_handle(dst, &dst_dhandle, &dst_allocated)) { if (src_allocated) dealloc_dir_handle(src_dhandle); return; } attrs = (uint32)(fallback_attr & (_A_RDONLY | _A_HIDDEN | _A_SYSTEM | _A_ARCH)); rc = ncp87_06_obtain_ndir_info(srcname, src_dhandle, &sinfo, NULL, NULL, NULL); if (!rc) attrs = sinfo.attributes; rc = ncp87_07_modify_dos_attributes(dstname, dst_dhandle, attrs, NULL, NULL, NULL); ncopy_debug_rc("restore NetWare attrs NCP87/07", rc); ncopy_debug_ul("restore NetWare attrs", attrs); if (dst_allocated) dealloc_dir_handle(dst_dhandle); if (src_allocated) dealloc_dir_handle(src_dhandle); } static int ncopy_copy_one(char *src, char *dst, struct find_t *ff, int options) { unsigned attr; int d; char srcdir[260]; char srcname[80]; char dstdir[260]; char dstname[80]; if (ncopy_same_path(src, dst)) { fprintf(stdout, " The file will NOT be copied because the source and destination file names are the same.\n"); return(-1); } if ((options & (NCOPY_OPT_A | NCOPY_OPT_M)) && !(ff->attrib & _A_ARCH)) return(1); if (ncopy_split_dir_name(src, srcdir, srcname)) { strmaxcpy(srcdir, ".", sizeof(srcdir) - 1); ncopy_basename(srcname, src, sizeof(srcname)); } if (ncopy_split_dir_name(dst, dstdir, dstname)) { strmaxcpy(dstdir, ".", sizeof(dstdir) - 1); ncopy_basename(dstname, dst, sizeof(dstname)); } ncopy_print_group(srcdir, dstdir); ncopy_print_file_map(srcname, dstname); if (ncopy_copy_data(src, dst, (unsigned long)ff->size)) return(-1); d = open(dst, O_WRONLY | O_BINARY); if (d >= 0) { _dos_setftime(d, ff->wr_date, ff->wr_time); close(d); } else { fprintf(stdout, " Failure setting destination file date and time.\n"); } attr = ff->attrib & (_A_RDONLY | _A_HIDDEN | _A_SYSTEM | _A_ARCH); if (_dos_setfileattr(dst, attr)) fprintf(stdout, " Error setting file information.\n"); /* Preserve high NetWare DOS attributes such as Delete/Rename Inhibit. * DOS setfileattr can only express the low DOS bits, which was the reason * B.TXT ended up as [Ro-A--------------] instead of carrying DI/RI. */ ncopy_restore_netware_attrs(src, dst, attr); if ((options & NCOPY_OPT_V) && ncopy_verify_file(src, dst)) fprintf(stdout, " Error copying file.\n"); if (options & NCOPY_OPT_M) { unsigned newattr = ff->attrib & ~_A_ARCH; _dos_setfileattr(src, newattr & (_A_RDONLY | _A_HIDDEN | _A_SYSTEM | _A_ARCH)); } return(0); } static int ncopy_copy_pattern(char *srcdir, char *pattern, char *dstdir, int options, int *copied, int *failed); static int ncopy_copy_subdirs(char *srcdir, char *dstdir, int options, int *copied, int *failed) { struct find_t ff; char search[260]; int rc; ncopy_join(search, srcdir, "*.*", sizeof(search)); rc = _dos_findfirst(search, _A_NORMAL | _A_RDONLY | _A_HIDDEN | _A_SYSTEM | _A_ARCH | _A_SUBDIR, &ff); while (rc == 0) { if ((ff.attrib & _A_SUBDIR) && !tool_is_dot_dir(ff.name)) { char childsrc[260]; char childdst[260]; int before; ncopy_join(childsrc, srcdir, ff.name, sizeof(childsrc)); ncopy_join(childdst, dstdir, ff.name, sizeof(childdst)); before = *copied; ncopy_print_group(childsrc, childdst); if (options & NCOPY_OPT_E) ncopy_make_dir(childdst); ncopy_copy_pattern(childsrc, "*.*", childdst, options, copied, failed); ncopy_copy_subdirs(childsrc, childdst, options, copied, failed); if (*copied > before) ncopy_make_dir(childdst); } rc = _dos_findnext(&ff); } return(0); } static int ncopy_copy_pattern(char *srcdir, char *pattern, char *dstdir, int options, int *copied, int *failed) { struct find_t ff; char search[260]; int rc; int any = 0; ncopy_join(search, srcdir, pattern, sizeof(search)); rc = _dos_findfirst(search, _A_NORMAL | _A_RDONLY | _A_HIDDEN | _A_SYSTEM | _A_ARCH, &ff); while (rc == 0) { if (!(ff.attrib & _A_SUBDIR)) { char src[260]; char dst[260]; int r; any = 1; ncopy_join(src, srcdir, ff.name, sizeof(src)); ncopy_join(dst, dstdir, ff.name, sizeof(dst)); if (ncopy_make_dir(dstdir)) { fprintf(stdout, " Illegal destination.\n"); return(-1); } r = ncopy_copy_one(src, dst, &ff, options); if (r == 0) (*copied)++; else if (r < 0) (*failed)++; } rc = _dos_findnext(&ff); } if ((options & NCOPY_OPT_S) != 0) ncopy_copy_subdirs(srcdir, dstdir, options, copied, failed); return(any ? 0 : 1); } static int ncopy_copy_single(char *src, char *dstarg, int options, int *copied, int *failed) { struct find_t ff; char dst[260]; char base[80]; int rc; rc = _dos_findfirst(src, _A_NORMAL | _A_RDONLY | _A_HIDDEN | _A_SYSTEM | _A_ARCH, &ff); if (rc) return(1); if (ff.attrib & _A_SUBDIR) return(1); if (ncopy_is_dir(dstarg)) { ncopy_basename(base, src, sizeof(base)); ncopy_join(dst, dstarg, base, sizeof(dst)); } else { strmaxcpy(dst, dstarg, sizeof(dst) - 1); } rc = ncopy_copy_one(src, dst, &ff, options); if (rc == 0) (*copied)++; else if (rc < 0) (*failed)++; return(0); } static void ncopy_summary(int copied, int failed) { if (copied == 0) fprintf(stdout, " No files copied.\n"); else fprintf(stdout, " %d file%s copied.\n", copied, copied == 1 ? "" : "s"); if (failed) fprintf(stdout, " %d file%s NOT copied.\n", failed, failed == 1 ? "" : "s"); } static int ncopy_upper_char(int c) { if (c >= 'a' && c <= 'z') return(c - ('a' - 'A')); return(c); } static int ncopy_parse_option(char *s, int *options) { char *p; int c1; int c2; int c3; if (!s || !*s) return(0); /* Novell NCOPY accepts case-insensitive options anywhere on the * command line, including after the destination path: * NCOPY SRC DST /V * NCOPY SRC TO DST /S /E * NCOPY /A SRC TO DST /S * Keep this parser independent of shared helpers so DOS/Watcom argv * strings such as lowercase /v are handled exactly here. */ if (s[0] != '/' && s[0] != '-') return(0); p = s; while (*p == '/' || *p == '-') p++; if (!*p) return(-1); c1 = ncopy_upper_char((unsigned char)p[0]); c2 = ncopy_upper_char((unsigned char)p[1]); c3 = ncopy_upper_char((unsigned char)p[2]); if (c1 == '?' && p[1] == '\0') return(2); if (c1 == 'H' && p[1] == '\0') return(2); if (c1 == 'H' && c2 == 'E' && c3 == 'L' && ncopy_upper_char((unsigned char)p[3]) == 'P' && p[4] == '\0') return(2); if (c1 == 'S' && p[1] == '\0') { *options |= NCOPY_OPT_S; return(1); } if (c1 == 'E' && p[1] == '\0') { *options |= NCOPY_OPT_E; return(1); } if (c1 == 'V' && p[1] == '\0') { *options |= NCOPY_OPT_V; return(1); } if (c1 == 'A' && p[1] == '\0') { *options |= NCOPY_OPT_A; return(1); } if (c1 == 'M' && p[1] == '\0') { *options |= NCOPY_OPT_M | NCOPY_OPT_A; return(1); } if (c1 == 'I' && p[1] == '\0') { *options |= NCOPY_OPT_I; return(1); } if (c1 == 'C' && p[1] == '\0') { *options |= NCOPY_OPT_C; return(1); } if (c1 == 'F' && p[1] == '\0') { *options |= NCOPY_OPT_F; return(1); } if (c1 == 'S' && c2 == '/' && c3 == 'E' && p[3] == '\0') { *options |= NCOPY_OPT_S | NCOPY_OPT_E; return(1); } if (c1 == 'S' && c2 == 'E' && p[2] == '\0') { *options |= NCOPY_OPT_S | NCOPY_OPT_E; return(1); } return(-1); } static int ncopy_is_to_separator(char *s) { return(s && tool_strsame(s, "TO")); } int func_ncopy(int argc, char *argv[], int mode) { char *src = NULL; char *dst = NULL; int options = 0; int i; int copied = 0; int failed = 0; int saw_to = 0; int result; char srcdir[260]; char pat[260]; (void)mode; if (ncopy_debug_enabled()) { fprintf(stdout, "NCOPYDBG: func_ncopy entered argc=%d\n", argc); for (i = 0; i < argc; i++) fprintf(stdout, "NCOPYDBG: argv[%d]=<%s>\n", i, argv[i] ? argv[i] : ""); } if (argc <= 1) { ncopy_usage(); return(1); } for (i = 1; i < argc; i++) { int opt = ncopy_parse_option(argv[i], &options); /* Options are legal before the source, between source and TO, and after * the destination. Parse them before treating anything as a path. */ if (opt == 2) { ncopy_usage(); return(0); } if (opt == 1) continue; if (opt < 0) { ncopy_usage(); return(1); } /* TO is an optional, case-insensitive separator, not a required keyword. * NCOPY SRC DST and NCOPY SRC TO DST are equivalent. */ if (ncopy_is_to_separator(argv[i])) { saw_to = 1; continue; } if (!src) src = argv[i]; else if (!dst) dst = argv[i]; else { fprintf(stdout, "You cannot copy multiple files to a single file.\n"); return(1); } } if (saw_to && !dst) { ncopy_usage(); return(1); } if ((options & NCOPY_OPT_E) && !(options & NCOPY_OPT_S)) { fprintf(stdout, "The /E parameter (copy empty directories) is only valid with the /S parameter (copy sub directories).\n"); return(1); } if (!src || !dst) { ncopy_usage(); return(1); } ncopy_reset_output_group(); if (tool_has_wildcards(src) || (options & NCOPY_OPT_S)) { if (!ncopy_is_dir(dst)) { fprintf(stdout, " Illegal destination.\n"); ncopy_summary(0, 0); return(1); } if ((options & NCOPY_OPT_S) && ncopy_is_dir(src) && !tool_has_wildcards(src)) { /* Novell NCOPY accepts a directory as the source root with /S. */ strmaxcpy(srcdir, src, sizeof(srcdir) - 1); strmaxcpy(pat, "*.*", sizeof(pat) - 1); } else { ncopy_parent_pattern(srcdir, pat, src); } result = ncopy_copy_pattern(srcdir, pat, dst, options, &copied, &failed); } else { result = ncopy_copy_single(src, dst, options, &copied, &failed); } if (result > 0) { fprintf(stdout, " Files not found.\n"); ncopy_summary(0, failed); return(1); } ncopy_finish_output_groups(); ncopy_summary(copied, failed); return(failed ? 1 : 0); }