git: 2a896ee9704c - main - misc/ctm: fix build warnings, add symlink and filename encoding support
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Wed, 25 Mar 2026 14:43:48 UTC
The branch main has been updated by tz:
URL: https://cgit.FreeBSD.org/ports/commit/?id=2a896ee9704c42d897d7446cf1494cbb763c83bd
commit 2a896ee9704c42d897d7446cf1494cbb763c83bd
Author: Torsten Zuehlsdorff <tz@FreeBSD.org>
AuthorDate: 2026-03-24 22:05:39 +0000
Commit: Torsten Zuehlsdorff <tz@FreeBSD.org>
CommitDate: 2026-03-25 14:43:04 +0000
misc/ctm: fix build warnings, add symlink and filename encoding support
Fix compiler warnings in mkctm.c, resolve logf name clash, fix change
counting, and add symlink handling needed for src-13/14 deltas.
Also add filename encoding for paths with special characters and
ownermail support for ctm_smail.
PR: 275921
Approved by: se@FreeBSD.org (maintainer timeout)
---
misc/ctm/Makefile | 2 +-
.../files/patch-r01-fix_warnings_in_mkCTM_mkctm.c | 20 ++
.../patch-r02-fix_logf_name_clash_in_mkCTM_mkctm.c | 120 +++++++++
...-r03-fix_recreating_failure_in_ctm_ctm__pass2.c | 35 +++
...04-fix_ignoring_to_few_changes_in_mkCTM_mkctm.c | 38 +++
...patch-r10-add_symlink_handling_to_mkCTM_mkctm.c | 94 +++++++
.../files/patch-r11-add_symlink_handling_to_ctm | 286 +++++++++++++++++++++
.../patch-r15-add_filenameencode_to_mkCTM_mkctm.c | 130 ++++++++++
misc/ctm/files/patch-r16-add_filenameencode_to_ctm | 107 ++++++++
...ch-r20-add_ownermail_to_ctm__smail_ctm__smail.c | 156 +++++++++++
10 files changed, 987 insertions(+), 1 deletion(-)
diff --git a/misc/ctm/Makefile b/misc/ctm/Makefile
index 2f943e1206fd..e4ef9cee31ca 100644
--- a/misc/ctm/Makefile
+++ b/misc/ctm/Makefile
@@ -1,6 +1,6 @@
PORTNAME= ctm
PORTVERSION= 2.0
-PORTREVISION= 4
+PORTREVISION= 5
CATEGORIES= misc
MAINTAINER= se@FreeBSD.org
diff --git a/misc/ctm/files/patch-r01-fix_warnings_in_mkCTM_mkctm.c b/misc/ctm/files/patch-r01-fix_warnings_in_mkCTM_mkctm.c
new file mode 100644
index 000000000000..85b82ce3abfd
--- /dev/null
+++ b/misc/ctm/files/patch-r01-fix_warnings_in_mkCTM_mkctm.c
@@ -0,0 +1,20 @@
+--- mkCTM/mkctm.c.ORI 2023-12-24 08:11:59.287756000 +0100
++++ mkCTM/mkctm.c 2023-12-24 08:14:49.514747000 +0100
+@@ -113,7 +113,7 @@
+ }
+
+ int
+-dirselect(struct dirent *de)
++dirselect(const struct dirent *de)
+ {
+ if (!strcmp(de->d_name, ".")) return 0;
+ if (!strcmp(de->d_name, "..")) return 0;
+@@ -221,7 +221,7 @@
+
+ {
+ u_long l = s2.st_size + 2;
+- u_char *cmd = alloca(strlen(buf1)+strlen(buf2)+100);
++ char *cmd = alloca(strlen(buf1)+strlen(buf2)+100);
+ u_char *ob = malloc(l), *p;
+ int j;
+ FILE *F;
diff --git a/misc/ctm/files/patch-r02-fix_logf_name_clash_in_mkCTM_mkctm.c b/misc/ctm/files/patch-r02-fix_logf_name_clash_in_mkCTM_mkctm.c
new file mode 100644
index 000000000000..e968286800c3
--- /dev/null
+++ b/misc/ctm/files/patch-r02-fix_logf_name_clash_in_mkCTM_mkctm.c
@@ -0,0 +1,120 @@
+--- mkCTM/mkctm.c.ORI 2023-12-24 08:03:42.335824000 +0100
++++ mkCTM/mkctm.c 2023-12-24 08:04:21.959999000 +0100
+@@ -42,7 +42,7 @@
+ int damage, damage_limit;
+ int change;
+
+-FILE *logf;
++FILE *logfile;
+
+ u_long s1_ignored, s2_ignored;
+ u_long s1_bogus, s2_bogus;
+@@ -134,7 +134,7 @@
+ printf("%s %s%s %u %u %o",
+ pfx, name, de->d_name,
+ st->st_uid, st->st_gid, st->st_mode & ~S_IFMT);
+- fprintf(logf, "%s %s%s\n", pfx, name, de->d_name);
++ fprintf(logfile, "%s %s%s\n", pfx, name, de->d_name);
+ if (verbose > 1) {
+ fprintf(stderr, "%s %s%s\n", pfx, name, de->d_name);
+ }
+@@ -362,7 +362,7 @@
+ strcpy(p, name); strcat(p, de->d_name); strcat(p, "/");
+ DoDir(dir1, dir2, p);
+ printf("CTMDR %s%s\n", name, de->d_name);
+- fprintf(logf, "CTMDR %s%s\n", name, de->d_name);
++ fprintf(logfile, "CTMDR %s%s\n", name, de->d_name);
+ if (verbose > 1) {
+ fprintf(stderr, "CTMDR %s%s\n", name, de->d_name);
+ }
+@@ -376,7 +376,7 @@
+ strcat(buf1, "/"); strcat(buf1, de->d_name);
+ m1 = MD5File(buf1, md5_1);
+ printf("CTMFR %s%s %s\n", name, de->d_name, m1);
+- fprintf(logf, "CTMFR %s%s %s\n", name, de->d_name, m1);
++ fprintf(logfile, "CTMFR %s%s %s\n", name, de->d_name, m1);
+ if (verbose > 1) {
+ fprintf(stderr, "CTMFR %s%s\n", name, de->d_name);
+ }
+@@ -403,14 +403,14 @@
+ if (flag_ignore &&
+ !regexec(®_ignore, buf1, 0, 0, 0)) {
+ (*ignored)++;
+- fprintf(logf, "Ignore %s\n", buf1);
++ fprintf(logfile, "Ignore %s\n", buf1);
+ if (verbose > 2) {
+ fprintf(stderr, "Ignore %s\n", buf1);
+ }
+ } else if (flag_bogus &&
+ !regexec(®_bogus, buf1, 0, 0, 0)) {
+ (*bogus)++;
+- fprintf(logf, "Bogus %s\n", buf1);
++ fprintf(logfile, "Bogus %s\n", buf1);
+ fprintf(stderr, "Bogus %s\n", buf1);
+ damage++;
+ } else {
+@@ -524,8 +524,8 @@
+ strcpy(tmpfilename, tmpdir); strcat(tmpfilename, tmpfilebase);
+ mktemp(tmpfilename);
+
+- snprintf(command,command_size,"tar -C %s -cvf %s %s 2>&%d\n",dir2,tmpfilename,de->d_name,fileno(logf));
+- fflush(logf);
++ snprintf(command,command_size,"tar -C %s -cvf %s %s 2>&%d\n",dir2,tmpfilename,de->d_name,fileno(logfile));
++ fflush(logfile);
+ ret_val = system(command);
+ if (ret_val!=0) errx(1,"The command \"%s\" failed with return value %d",command,ret_val);
+ printf("CTMTR ");
+@@ -576,8 +576,8 @@
+ errx(1,"No db/release in %s",buf2);
+
+ if (release2 > release1) {
+- snprintf(command,command_size,"svnadmin dump %s/%s -r %ld:%ld --incremental --deltas 2>&%d > %s\n",dir2,de->d_name,release1+1,release2,fileno(logf),tmpfilename);
+- fflush(logf);
++ snprintf(command,command_size,"svnadmin dump %s/%s -r %ld:%ld --incremental --deltas 2>&%d > %s\n",dir2,de->d_name,release1+1,release2,fileno(logfile),tmpfilename);
++ fflush(logfile);
+ ret_val = system(command);
+ if (ret_val!=0) errx(1,"The command \"%s\" failed with return value %d",command,ret_val);
+ printf("CTMSV %s %ld ", de->d_name, release1);
+@@ -723,10 +723,10 @@
+ flag_bogus = 1;
+ break;
+ case 'l':
+- logf = fopen(optarg, "w");
+- if (!logf)
++ logfile = fopen(optarg, "w");
++ if (!logfile)
+ err(1, "%s", optarg);
+- setlinebuf(logf);
++ setlinebuf(logfile);
+ break;
+ case 'q':
+ verbose--;
+@@ -742,8 +742,8 @@
+ argc -= optind;
+ argv += optind;
+
+- if (!logf)
+- logf = fopen(_PATH_DEVNULL, "w");
++ if (!logfile)
++ logfile = fopen(_PATH_DEVNULL, "w");
+
+ setbuf(stdout, 0);
+
+@@ -756,7 +756,7 @@
+
+ fprintf(stderr, "CTM_BEGIN 2.0 %s %s %s %s\n",
+ argv[0], argv[1], argv[2], argv[3]);
+- fprintf(logf, "CTM_BEGIN 2.0 %s %s %s %s\n",
++ fprintf(logfile, "CTM_BEGIN 2.0 %s %s %s %s\n",
+ argv[0], argv[1], argv[2], argv[3]);
+ printf("CTM_BEGIN 2.0 %s %s %s %s\n",
+ argv[0], argv[1], argv[2], argv[3]);
+@@ -773,7 +773,7 @@
+ errx(4, "no changes");
+ } else {
+ printf("CTM_END ");
+- fprintf(logf, "CTM_END\n");
++ fprintf(logfile, "CTM_END\n");
+ if (strncmp(argv[0],"svn",3) != 0)
+ print_stat(stderr, "END: ");
+ }
diff --git a/misc/ctm/files/patch-r03-fix_recreating_failure_in_ctm_ctm__pass2.c b/misc/ctm/files/patch-r03-fix_recreating_failure_in_ctm_ctm__pass2.c
new file mode 100644
index 000000000000..71ade52bb330
--- /dev/null
+++ b/misc/ctm/files/patch-r03-fix_recreating_failure_in_ctm_ctm__pass2.c
@@ -0,0 +1,35 @@
+--- ctm/ctm_pass2.c.ORI 2023-12-29 08:41:10.082775000 +0100
++++ ctm/ctm_pass2.c 2023-12-29 08:43:18.156987000 +0100
+@@ -15,6 +15,8 @@
+ #include "ctm.h"
+ #define BADREAD 32
+
++char LastRemoved[ PATH_MAX + 1 ] = "";
++
+ /*---------------------------------------------------------------------------*/
+ /* Pass2 -- Validate the incoming CTM-file.
+ */
+@@ -86,6 +88,11 @@
+ switch (j & CTM_F_MASK) {
+ case CTM_F_Name:
+ GETNAMECOPY(name,sep,j,0);
++
++ /* If we remove anything, record its name */
++ if( strcmp(sp->Key,"FR") == 0 || strcmp(sp->Key,"DR") == 0 || strcmp(sp->Key,"LR") == 0 )
++ strncpy( LastRemoved, name, sizeof LastRemoved -1 );
++
+ /* If `keep' was specified, we won't remove any files,
+ so don't check if the file exists */
+ if (KeepIt &&
+@@ -104,6 +111,11 @@
+
+ /* XXX Check DR DM rec's for parent-dir */
+ if(j & CTM_Q_Name_New) {
++
++ /* Don't check for existence of the new item if it had
++ been removed just before. */
++ if( strcmp( LastRemoved, name ) != 0 )
++
+ /* XXX Check DR FR rec's for item */
+ if(-1 != stat(name,&st)) {
+ fprintf(stderr," %s: %s exists.\n",
diff --git a/misc/ctm/files/patch-r04-fix_ignoring_to_few_changes_in_mkCTM_mkctm.c b/misc/ctm/files/patch-r04-fix_ignoring_to_few_changes_in_mkCTM_mkctm.c
new file mode 100644
index 000000000000..73402187978c
--- /dev/null
+++ b/misc/ctm/files/patch-r04-fix_ignoring_to_few_changes_in_mkCTM_mkctm.c
@@ -0,0 +1,38 @@
+--- mkCTM/mkctm.c.ORI 2023-12-29 08:47:22.656993000 +0100
++++ mkCTM/mkctm.c 2023-12-29 08:48:29.101390000 +0100
+@@ -32,6 +32,7 @@
+ #include <err.h>
+ #include <paths.h>
+ #include <signal.h>
++#include <limits.h>
+
+ #define DEFAULT_IGNORE "/CVS$|/\\.#|00_TRANS\\.TBL$"
+ #define DEFAULT_BOGUS "\\.core$|\\.orig$|\\.rej$|\\.o$"
+@@ -41,6 +42,7 @@
+ int verbose;
+ int damage, damage_limit;
+ int change;
++int Have_ctm_status, Have_svn_revision;
+
+ FILE *logfile;
+
+@@ -138,6 +140,10 @@
+ if (verbose > 1) {
+ fprintf(stderr, "%s %s%s\n", pfx, name, de->d_name);
+ }
++ if( strcmp( de->d_name, ".ctm_status" ) == 0 )
++ Have_ctm_status = 1;
++ if( strcmp( de->d_name, ".svn_revision" ) == 0 )
++ Have_svn_revision = 1;
+ }
+
+ void
+@@ -769,7 +839,7 @@
+ errx(1, "damage of %d would exceed %d files",
+ damage, damage_limit);
+ /* change <= 2 means no change because of .ctm_status and .svn_revision */
+- } else if (change < 3) {
++ } else if (change < 1 + Have_ctm_status + Have_svn_revision ) {
+ errx(4, "no changes");
+ } else {
+ printf("CTM_END ");
diff --git a/misc/ctm/files/patch-r10-add_symlink_handling_to_mkCTM_mkctm.c b/misc/ctm/files/patch-r10-add_symlink_handling_to_mkCTM_mkctm.c
new file mode 100644
index 000000000000..262ac2b88c78
--- /dev/null
+++ b/misc/ctm/files/patch-r10-add_symlink_handling_to_mkCTM_mkctm.c
@@ -0,0 +1,94 @@
+--- mkCTM/mkctm.c.ORI 2023-12-29 09:07:16.210417000 +0100
++++ mkCTM/mkctm.c 2023-12-29 09:07:16.211918000 +0100
+@@ -155,6 +155,39 @@
+ strcpy(p, name); strcat(p, de->d_name); strcat(p, "/");
+ DoDir(dir1, dir2, p);
+ s_same_dirs++;
++
++ } else if( de->d_type == DT_LNK ) {
++
++ char lbuf1[ PATH_MAX ];
++ char lbuf2[ PATH_MAX ];
++ char* buf1;
++ char* buf2;
++ ssize_t ret1, ret2;
++
++ if( asprintf( &buf1, "%s/%s/%s", dir1, name, de->d_name ) <= 0 )
++ err( 3, "asprintf: %s", dir2 );
++ if( asprintf( &buf2, "%s/%s/%s", dir2, name, de->d_name ) <= 0 )
++ err( 3, "asprintf: %s", dir2 );
++
++ if( (ret1 = readlink( buf1, lbuf1, sizeof lbuf1 - 1 )) == -1 )
++ err( 3, "readlink: %s", buf1 );
++ lbuf1[ ret1 ] = '\0';
++ if( (ret2 = readlink( buf2, lbuf2, sizeof lbuf2 - 1 )) == -1 )
++ err( 3, "readlink: %s", buf2 );
++ lbuf2[ ret2 ] = '\0';
++
++ if( strcmp( lbuf1, lbuf2 ) == 0 )
++ return;
++
++ change++;
++
++ printf( "CTMLR %s%s\n", name, de->d_name );
++ name_stat("CTMLM", dir2, name, de);
++ printf( " %s\n", lbuf2 );
++
++ free( buf1 );
++ free( buf2 );
++
+ } else {
+ char *buf1 = alloca(strlen(dir1) + strlen(name) +
+ strlen(de->d_name) + 3);
+@@ -326,6 +359,25 @@
+ putchar('\n');
+ s_new_dirs++;
+ DoDir(dir1, dir2, p);
++
++ } else if( de->d_type == DT_LNK ) {
++
++ char* buf2;
++ char lbuf[ PATH_MAX ];
++ ssize_t ret;
++
++ if( asprintf( &buf2, "%s/%s/%s", dir2, name, de->d_name ) <= 0 )
++ err( 3, "asprintf: %s", dir2 );
++
++ if( (ret = readlink( buf2, lbuf, sizeof lbuf - 1 )) == -1 )
++ err( 3, "readlink: %s", buf2 );
++ lbuf[ ret ] = '\0';
++
++ name_stat( "CTMLM", dir2, name, de );
++ printf( " %s\n", lbuf );
++
++ free( buf2 );
++
+ } else if (de->d_type == DT_REG) {
+ char *buf2 = alloca(strlen(dir2) + strlen(name) +
+ strlen(de->d_name) + 3);
+@@ -373,6 +425,14 @@
+ fprintf(stderr, "CTMDR %s%s\n", name, de->d_name);
+ }
+ s_del_dirs++;
++
++ } else if( de->d_type == DT_LNK ) {
++
++ printf( "CTMLR %s%s\n", name, de->d_name );
++ fprintf( logfile, "CTMLR %s%s\n", name, de->d_name );
++ if( verbose > 1 )
++ fprintf( stderr, "CTMLR %s%s\n", name, de->d_name );
++
+ } else if (de->d_type == DT_REG) {
+ char *buf1 = alloca(strlen(dir1) + strlen(name) +
+ strlen(de->d_name) + 3);
+@@ -436,6 +496,10 @@
+ nl[*i]->d_type = IFTODT(StatFile(buf)->st_mode);
+ if (nl[*i]->d_type == DT_REG || nl[*i]->d_type == DT_DIR)
+ break;
++
++ if( nl[*i]->d_type == DT_LNK )
++ break;
++
+ (*wrong)++;
+ if (verbose > 0)
+ fprintf(stderr, "Wrong %s\n", buf);
diff --git a/misc/ctm/files/patch-r11-add_symlink_handling_to_ctm b/misc/ctm/files/patch-r11-add_symlink_handling_to_ctm
new file mode 100644
index 000000000000..aa43ad4b0d5c
--- /dev/null
+++ b/misc/ctm/files/patch-r11-add_symlink_handling_to_ctm
@@ -0,0 +1,286 @@
+--- ctm/ctm.h.ORI 2023-12-29 08:53:14.808809000 +0100
++++ ctm/ctm.h 2023-12-29 08:53:14.827122000 +0100
+@@ -25,6 +25,7 @@
+ #include <sys/file.h>
+ #include <sys/time.h>
+ #include <stdint.h>
++#include <limits.h>
+
+ #define VERSION "2.0"
+
+@@ -43,6 +44,7 @@
+ #define CTM_F_Bytes 0x07
+ #define CTM_F_Release 0x08
+ #define CTM_F_Forward 0x09
++#define CTM_F_Targetname 0x0a
+
+ /* The qualifiers... */
+ #define CTM_Q_MASK 0xff00
+@@ -51,6 +53,7 @@
+ #define CTM_Q_Name_New 0x0400
+ #define CTM_Q_Name_Subst 0x0800
+ #define CTM_Q_Name_Svnbase 0x1000
++#define CTM_Q_Name_Link 0x2000
+ #define CTM_Q_MD5_After 0x0100
+ #define CTM_Q_MD5_Before 0x0200
+ #define CTM_Q_MD5_Chunk 0x0400
+--- ctm/ctm_syntax.c.ORI 2023-12-29 08:53:14.816487000 +0100
++++ ctm/ctm_syntax.c 2023-12-29 08:59:53.103142000 +0100
+@@ -68,6 +68,13 @@
+ static int ctmSV[] = /* Forward to svnadmin load */
+ { Name|Dir|Svnbase, Release, Count, Forward|SVN, 0 };
+
++static int ctmLM[] = /* Link Make */
++ { Name|CTM_Q_Name_Link|New, Uid, Gid, Mode, CTM_F_Targetname, 0 };
++
++static int ctmLR[] = /* Link Remove */
++ { Name|CTM_Q_Name_Link, 0 };
++
++
+ struct CTM_Syntax Syntax[] = {
+ { "FM", ctmFM },
+ { "FS", ctmFS },
+@@ -79,4 +86,6 @@
+ { "DR", ctmDR },
+ { "TR", ctmTR },
+ { "SV", ctmSV },
++ { "LM", ctmLM },
++ { "LR", ctmLR },
+ { 0, 0} };
+--- ctm/ctm_pass1.c.ORI 2023-12-29 08:53:14.811803000 +0100
++++ ctm/ctm_pass1.c 2023-12-29 08:53:14.827382000 +0100
+@@ -27,6 +27,7 @@
+ int i,j,sep;
+ intmax_t cnt, rel;
+ u_char *md5=0,*name=0,*trash=0;
++ u_char* targetname = NULL;
+ struct CTM_Syntax *sp;
+ int slashwarn=0, match=0, total_matches=0;
+ unsigned current;
+@@ -71,6 +72,7 @@
+ Delete(md5);
+ Delete(name);
+ Delete(trash);
++ Delete( targetname );
+ cnt = -1;
+ /* if a filter list is defined we assume that all pathnames require
+ an action opposite to that requested by the first filter in the
+@@ -224,6 +226,9 @@
+ return Exit_Garbage;
+ }
+ GETFORWARD(cnt,NULL);
++ break;
++ case CTM_F_Targetname:
++ GETFIELDCOPY( targetname, sep );
+ break;
+ default:
+ fprintf(stderr,"List = 0x%x\n",j);
+--- ctm/ctm_pass2.c.ORI 2023-12-29 08:53:14.823718000 +0100
++++ ctm/ctm_pass2.c 2023-12-29 08:53:14.827299000 +0100
+@@ -117,14 +117,14 @@
+ if( strcmp( LastRemoved, name ) != 0 )
+
+ /* XXX Check DR FR rec's for item */
+- if(-1 != stat(name,&st)) {
++ if(-1 != lstat(name,&st)) {
+ fprintf(stderr," %s: %s exists.\n",
+ sp->Key,name);
+ ret |= Exit_Forcible;
+ }
+ break;
+ }
+- if(-1 == stat(name,&st)) {
++ if(-1 == lstat(name,&st)) {
+ fprintf(stderr," %s: %s doesn't exist.\n",
+ sp->Key,name);
+ if (sp->Key[1] == 'R')
+@@ -173,10 +173,18 @@
+ }
+ break;
+ }
++ if( j & CTM_Q_Name_Link ) {
++ if( ( st.st_mode & S_IFMT ) != S_IFLNK ) {
++ fprintf( stderr, " %s: %s exist, but isn't link.\n", sp->Key,name );
++ ret |= Exit_NotOK;
++ }
++ break;
++ }
+ break;
+ case CTM_F_Uid:
+ case CTM_F_Gid:
+ case CTM_F_Mode:
++ case CTM_F_Targetname:
+ GETFIELD(p,sep);
+ break;
+ case CTM_F_MD5:
+--- ctm/ctm_pass3.c.ORI 2023-12-29 08:53:14.814984000 +0100
++++ ctm/ctm_pass3.c 2023-12-29 08:59:08.227903000 +0100
+@@ -23,7 +23,7 @@
+ settime(const char *name, const struct timeval *times)
+ {
+ if (SetTime)
+- if (utimes(name,times)) {
++ if (lutimes(name,times)) {
+ warn("utimes(): %s", name);
+ return -1;
+ }
+@@ -33,7 +33,7 @@
+ int
+ setmodefromchar(const char *name, const u_char *mode)
+ {
+- return chmod(name, strtol(mode, NULL, 8));
++ return lchmod(name, strtol(mode, NULL, 8));
+ }
+
+ int
+@@ -45,6 +45,7 @@
+ intmax_t cnt,rel;
+ char *svn_command = NULL;
+ u_char *md5=0,*md5before=0,*trash=0,*name=0,*uid=0,*gid=0,*mode=0;
++ u_char* targetname = NULL;
+ struct CTM_Syntax *sp;
+ FILE *ed=0, *fd_to;
+ struct stat st;
+@@ -124,6 +125,7 @@
+ Delete(md5before);
+ Delete(trash);
+ Delete(name);
++ Delete( targetname );
+ cnt = -1;
+
+ GETFIELD(p,' ');
+@@ -198,6 +200,10 @@
+ }
+ }
+ break;
++ case CTM_F_Targetname:
++ //GETNAMECOPY( targetname, sep, j, Verbose );
++ GETFIELDCOPY( targetname, sep );
++ break;
+ default: WRONG
+ }
+ }
+@@ -337,6 +343,70 @@
+ }
+ if(!strcmp(sp->Key,"TR") || !strcmp(sp->Key,"SV"))
+ continue;
++
++
++ if( strcmp( sp->Key, "LR" ) == 0 ) {
++
++ if( KeepIt ) {
++ if( Verbose > 1 )
++ printf( " <%s> not removed\n", name );
++
++ } else {
++ if( unlink( name )) {
++ fprintf( stderr, "unlink %s: %s\n", name, strerror( errno ));
++ WRONG
++ }
++ }
++
++ continue;
++ }
++
++
++ if( strcmp( sp->Key, "LM" ) == 0 ) {
++
++ char* bn; // basename
++ char cwd[ PATH_MAX ];
++
++ if( getcwd( cwd, sizeof cwd ) == NULL ) {
++ fprintf( stderr, "getcwd: %s\n", strerror( errno ) );
++ WRONG
++ }
++
++ if( (bn = strrchr( name, '/' )) == NULL ) // no path component
++ bn = name; // basename
++
++ else { // have path component
++ *bn++ = '\0'; // terminate path component
++ if( chdir( name )) {
++ fprintf( stderr, "chdir %s: %s\n", name, strerror( errno ));
++ WRONG
++ }
++ }
++
++ if( symlink( targetname, bn )) {
++ fprintf( stderr, "symlink %s to %s: %s\n", targetname, bn, strerror( errno ));
++ WRONG
++ }
++
++ if( chdir( cwd )) { // go back
++ fprintf( stderr, "chdir %s: %s\n", cwd, strerror( errno ));
++ WRONG
++ }
++
++ *--bn = '/'; // restore name with path
++ if( lstat( name, &st )) {
++ fprintf( stderr, "stat %s: %s\n", name, strerror( errno ));
++ WRONG
++ } else if( (st.st_mode & S_IFMT) != S_IFLNK ) {
++ fprintf( stderr, "%s: no link\n", name );
++ WRONG
++ }
++
++ if( settime( name, times )) WRONG
++ if( setmodefromchar( name, mode )) WRONG
++ continue;
++ }
++
+ WRONG
+ }
+
+--- ctm/ctm_passb.c.ORI 2023-04-25 21:04:20.000000000 +0200
++++ ctm/ctm_passb.c 2023-12-29 09:03:07.551061000 +0100
+@@ -26,6 +26,7 @@
+ MD5_CTX ctx;
+ int i,j,sep,cnt;
+ u_char *md5=0,*md5before=0,*trash=0,*name=0,*uid=0,*gid=0,*mode=0;
++ u_char* targetname = NULL;
+ struct CTM_Syntax *sp;
+ FILE *b = 0; /* backup command */
+ u_char buf[BUFSIZ];
+@@ -57,6 +58,7 @@
+ Delete(md5before);
+ Delete(trash);
+ Delete(name);
++ Delete( targetname );
+ cnt = -1;
+
+ GETFIELD(p,' ');
+@@ -90,6 +92,7 @@
+ break;
+ case CTM_F_Count: GETBYTECNT(cnt,sep); break;
+ case CTM_F_Bytes: GETDATA(trash,cnt); break;
++ case CTM_F_Targetname: GETFIELDCOPY( targetname, sep ); break;
+ default: WRONG
+ }
+ }
+@@ -98,7 +101,7 @@
+ if(name[j] == '/') name[j] = '\0';
+
+ if (KeepIt &&
+- (!strcmp(sp->Key,"DR") || !strcmp(sp->Key,"FR")))
++ (!strcmp(sp->Key,"DR") || !strcmp(sp->Key,"LR") || !strcmp(sp->Key,"FR")))
+ continue;
+
+ /* match the name against the elements of the filter list. The
+@@ -113,7 +116,9 @@
+ if (CTM_FILTER_DISABLE == match)
+ continue;
+
++ // Do we have to backup symlinks???
+ if (!strcmp(sp->Key,"FS") || !strcmp(sp->Key,"FN") ||
++ !strcmp(sp->Key,"LR") ||
+ !strcmp(sp->Key,"AS") || !strcmp(sp->Key,"DR") ||
+ !strcmp(sp->Key,"FR")) {
+ /* send name to the archiver for a backup */
+@@ -135,6 +140,7 @@
+ Delete(md5before);
+ Delete(trash);
+ Delete(name);
++ Delete( targetname );
+
+ q = MD5End (&ctx,md5_1);
+ GETFIELD(p,'\n'); /* <MD5> */
diff --git a/misc/ctm/files/patch-r15-add_filenameencode_to_mkCTM_mkctm.c b/misc/ctm/files/patch-r15-add_filenameencode_to_mkCTM_mkctm.c
new file mode 100644
index 000000000000..371bfd6352a4
--- /dev/null
+++ b/misc/ctm/files/patch-r15-add_filenameencode_to_mkCTM_mkctm.c
@@ -0,0 +1,130 @@
+--- mkCTM/mkctm.c.ORI 2025-08-10 16:21:45.903739000 +0000
++++ mkCTM/mkctm.c 2025-08-14 06:30:11.360142000 +0000
+@@ -44,6 +44,8 @@
+ int change;
+ int Have_ctm_status, Have_svn_revision;
+
++static char Enb[ BUFSIZ ];
++
+ FILE *logfile;
+
+ u_long s1_ignored, s2_ignored;
+@@ -122,6 +124,63 @@
+ return 1;
+ }
+
++int name_encode( const char* name, const char* d_name )
++{
++ static char b[ BUFSIZ ];
++ char* in;
++ char* cp;
++ uint16_t cnts, cntp;
++
++
++ // Create file name with path
++ //
++ if( snprintf( b, BUFSIZ, "%s%s", name, d_name ) >= BUFSIZ ) {
++ fprintf( stderr, "name_encode: snprintf\n" );
++ exit( 1 );
++ }
++
++
++ // Determine number of spaces and '%'
++ //
++ cntp = cnts = 0;
++ for( in=b; *in != '\0'; in++ ) {
++ if( *in == ' ' )
++ cnts++;
++ if( *in == '%' )
++ cntp++;
++ }
++
++
++ // No spaces found, return
++ //
++ if( cnts == 0 )
++ return 0;
++
++
++ // Die if encoded filename would become too long
++ //
++ if( strlen( b ) + cnts + cntp + 1 > BUFSIZ ) {
++ fprintf( stderr, "name_encode: name too long\n" );
++ exit( 1 );
++ }
++
++
++ // Encode file name (w/o leading '/')
++ //
++ cp = &Enb[0];
++ for( in = b; *in != '\0'; in++ ) {
++
++ if( *in == ' ' || *in == '%' ) { // found space or '%'
++ *cp++ = '%'; // store magic
++ *cp++ = *in + 0x20; // store encoded char
++
++ } else // no space, no '%', copy
++ *cp++ = *in;
++ }
++
++ return 1;
++}
++
+ void
+ name_stat(const char *pfx, const char *dir, const char *name, struct dirent *de)
+ {
+@@ -133,6 +192,9 @@
+ strcat(buf, "/"); strcat(buf, name);
+ strcat(buf, "/"); strcat(buf, de->d_name);
+ st = StatFile(buf);
++ if( name_encode( name, de->d_name ) )
++ printf("%s /%s %u %u %o", pfx, Enb, st->st_uid, st->st_gid, st->st_mode & ~S_IFMT);
++ else
+ printf("%s %s%s %u %u %o",
+ pfx, name, de->d_name,
+ st->st_uid, st->st_gid, st->st_mode & ~S_IFMT);
+@@ -181,8 +243,14 @@
+
+ change++;
+
++ if( name_encode( name, de->d_name ) )
++ printf( "CTMLR /%s\n", Enb );
++ else
+ printf( "CTMLR %s%s\n", name, de->d_name );
+ name_stat("CTMLM", dir2, name, de);
++ if( name_encode( lbuf2, "" ) )
++ printf( " /%s\n", Enb );
++ else
+ printf( " %s\n", lbuf2 );
+
+ free( buf1 );
+@@ -419,6 +487,9 @@
+ char *p = alloca(strlen(name)+strlen(de->d_name)+2);
+ strcpy(p, name); strcat(p, de->d_name); strcat(p, "/");
+ DoDir(dir1, dir2, p);
++ if( name_encode( name, de->d_name ) )
++ printf("CTMDR /%s\n", Enb );
++ else
+ printf("CTMDR %s%s\n", name, de->d_name);
+ fprintf(logfile, "CTMDR %s%s\n", name, de->d_name);
+ if (verbose > 1) {
+@@ -428,6 +499,9 @@
+
+ } else if( de->d_type == DT_LNK ) {
+
++ if( name_encode( name, de->d_name ) )
++ printf( "CTMLR /%s\n", Enb );
++ else
+ printf( "CTMLR %s%s\n", name, de->d_name );
+ fprintf( logfile, "CTMLR %s%s\n", name, de->d_name );
+ if( verbose > 1 )
+@@ -441,6 +515,9 @@
+ strcat(buf1, "/"); strcat(buf1, name);
+ strcat(buf1, "/"); strcat(buf1, de->d_name);
+ m1 = MD5File(buf1, md5_1);
++ if( name_encode( name, de->d_name ) )
++ printf("CTMFR /%s %s\n", Enb, m1);
++ else
+ printf("CTMFR %s%s %s\n", name, de->d_name, m1);
+ fprintf(logfile, "CTMFR %s%s %s\n", name, de->d_name, m1);
+ if (verbose > 1) {
diff --git a/misc/ctm/files/patch-r16-add_filenameencode_to_ctm b/misc/ctm/files/patch-r16-add_filenameencode_to_ctm
new file mode 100644
index 000000000000..f49d199ae8a8
--- /dev/null
+++ b/misc/ctm/files/patch-r16-add_filenameencode_to_ctm
@@ -0,0 +1,107 @@
+--- ctm/ctm_pass3.c.ORI 2025-08-10 16:21:45.923332000 +0000
++++ ctm/ctm_pass3.c 2025-08-14 07:29:01.065066000 +0000
+@@ -203,6 +203,8 @@
+ case CTM_F_Targetname:
+ //GETNAMECOPY( targetname, sep, j, Verbose );
+ GETFIELDCOPY( targetname, sep );
++ if( FnameDecode( targetname ) )
++ return BADREAD;
+ break;
+ default: WRONG
+ }
+--- ctm/ctm_input.c.ORI 2025-08-10 16:21:45.852199000 +0000
++++ ctm/ctm_input.c 2025-08-14 07:30:26.721303000 +0000
+@@ -14,6 +14,38 @@
+
+ #include "ctm.h"
+
++u_char FnameDecode( u_char* in )
++{
++ u_char* o = in;
++
++ // Return OK if this no encoded name
++ //
++ if( *in++ != '/' )
++ return 0;
++
++
++ // Copy name, unescaping chars
++ //
++ while( 1 ) {
++
++ if( (*o = *in++) == '\0' ) // copy char and return OK
++ return 0;
++
++ if( *o++ != '%' ) // no magic, next char
++ continue;
++
++ o--; // "delete" magic
++
++ if( (*o = *in++) == '\0' ) { // no char after magic
++ Fatal( "Badly encoded filename." );
++ return 1; // return BAD
++ }
++
++ *o++ -= 0x20; // decode
++ }
++}
++
++
+ /*---------------------------------------------------------------------------*/
+ void
+ Fatal_(int ln, char *fn, char *kind)
+@@ -152,6 +184,9 @@
+ struct stat st;
+
+ if ((p = Ffield(fd,ctx,term)) == NULL) return(NULL);
++
++ if( FnameDecode( p ) )
++ return NULL;
+
+ strcpy(CatPtr, p);
+
+--- ctm/ctm.h.ORI 2025-08-10 16:21:45.922547000 +0000
++++ ctm/ctm.h 2025-08-14 07:00:34.944434000 +0000
+@@ -153,6 +153,7 @@
+
+ u_char * Ffield(FILE *fd, MD5_CTX *ctx,u_char term);
+ u_char * Fname(FILE *fd, MD5_CTX *ctx,u_char term,int qual, int verbose);
++u_char FnameDecode( u_char* n );
+
+ intmax_t Fbytecnt(FILE *fd, MD5_CTX *ctx, u_char term);
+
+--- ctm/ctm_pass1.c.ORI 2025-08-10 16:21:45.922914000 +0000
++++ ctm/ctm_pass1.c 2025-08-14 07:28:32.124418000 +0000
+@@ -112,6 +112,8 @@
+ switch (j & CTM_F_MASK) {
+ case CTM_F_Name: /* XXX check for garbage and .. */
+ GETFIELDCOPY(name,sep);
++ if( FnameDecode( name ) )
++ return BADREAD;
+ j = strlen(name);
+ if(name[j-1] == '/' && !slashwarn) {
+ fprintf(stderr,"Warning: contains trailing slash\n");
+@@ -229,6 +231,8 @@
+ break;
+ case CTM_F_Targetname:
+ GETFIELDCOPY( targetname, sep );
++ if( FnameDecode( targetname ) )
++ return BADREAD;
+ break;
+ default:
+ fprintf(stderr,"List = 0x%x\n",j);
+--- ctm/ctm_passb.c.ORI 2025-08-10 16:21:45.923530000 +0000
++++ ctm/ctm_passb.c 2025-08-14 07:29:29.050430000 +0000
+@@ -92,7 +92,11 @@
+ break;
+ case CTM_F_Count: GETBYTECNT(cnt,sep); break;
+ case CTM_F_Bytes: GETDATA(trash,cnt); break;
+- case CTM_F_Targetname: GETFIELDCOPY( targetname, sep ); break;
++ case CTM_F_Targetname:
++ GETFIELDCOPY( targetname, sep );
++ if( FnameDecode( targetname ) )
++ return BADREAD;
++ break;
+ default: WRONG
+ }
+ }
diff --git a/misc/ctm/files/patch-r20-add_ownermail_to_ctm__smail_ctm__smail.c b/misc/ctm/files/patch-r20-add_ownermail_to_ctm__smail_ctm__smail.c
new file mode 100644
index 000000000000..43ad90992bc9
--- /dev/null
+++ b/misc/ctm/files/patch-r20-add_ownermail_to_ctm__smail_ctm__smail.c
@@ -0,0 +1,156 @@
+--- ctm_smail/ctm_smail.c.ORI 2023-04-25 21:04:20.000000000 +0200
++++ ctm_smail/ctm_smail.c 2019-07-29 03:53:20.090358000 +0200
+@@ -32,18 +32,18 @@
+ #define LINE_LENGTH 72 /* Chars per encoded line. Divisible by 4. */
+
+ int chop_and_send_or_queue(FILE *dfp, char *delta, off_t ctm_size,
+- long max_msg_size, char *mail_alias, char *queue_dir);
++ long max_msg_size, char *mail_alias, char *owner_alias, char *queue_dir);
+ int chop_and_send(FILE *dfp, char *delta, long msg_size, int npieces,
+- char *mail_alias);
++ char *mail_alias, char *owner_alias);
+ int chop_and_queue(FILE *dfp, char *delta, long msg_size, int npieces,
+- char *mail_alias, char *queue_dir);
++ char *mail_alias, char *owner_alias, char *queue_dir);
+ void clean_up_queue(char *queue_dir);
+ int encode_body(FILE *sm_fp, FILE *delta_fp, long msg_size, unsigned *sum);
+-void write_header(FILE *sfp, char *mail_alias, char *delta, int pce,
++void write_header(FILE *sfp, char *mail_alias, char *owner_alias, char *delta, int pce,
+ int npieces);
+ void write_trailer(FILE *sfp, unsigned sum);
+ int apologise(char *delta, off_t ctm_size, long max_ctm_size,
+- char *mail_alias, char *queue_dir);
++ char *mail_alias, char *owner_alias, char *queue_dir);
+ FILE *open_sendmail(void);
+ int close_sendmail(FILE *fp);
+
+@@ -53,6 +53,7 @@
+ int status = 0;
+ char *delta_file;
+ char *mail_alias;
++ char *owner_alias = NULL;
+ long max_msg_size = DEF_MAX_MSG;
+ long max_ctm_size = 0;
+ char *log_file = NULL;
+@@ -63,11 +64,12 @@
+
+ err_prog_name(argv[0]);
+
+- OPTIONS("[-l log] [-m maxmsgsize] [-c maxctmsize] [-q queuedir] ctm-delta mail-alias")
++ OPTIONS("[-l log] [-m maxmsgsize] [-c maxctmsize] [-q queuedir] [-o owner_alias] ctm-delta mail-alias")
+ NUMBER('m', max_msg_size)
+ NUMBER('c', max_ctm_size)
+ STRING('l', log_file)
+ STRING('q', queue_dir)
++ STRING('o', owner_alias)
+ ENDOPTS
+
+ if (argc != 3)
+@@ -91,11 +93,11 @@
+ }
+
+ if (max_ctm_size != 0 && sb.st_size > max_ctm_size)
+- status = apologise(delta, sb.st_size, max_ctm_size, mail_alias,
++ status = apologise(delta, sb.st_size, max_ctm_size, mail_alias, owner_alias,
+ queue_dir);
+ else
+ status = chop_and_send_or_queue(dfp, delta, sb.st_size, max_msg_size,
+- mail_alias, queue_dir);
++ mail_alias, owner_alias, queue_dir);
*** 97 LINES SKIPPED ***