git: 2a896ee9704c - main - misc/ctm: fix build warnings, add symlink and filename encoding support

From: Torsten Zuehlsdorff <tz_at_FreeBSD.org>
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(&reg_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(&reg_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 ***