main pico / pkg / rsync-receiver / rsyncopts / rsyncopts.go
Eric Bower  ·  2026-05-31
  1// Package rsyncopts implements a parser for command-line options that
  2// implements a subset of popt(3) semantics; just enough to parse typical
  3// rsync(1) invocations without the advanced popt features like aliases
  4// or option prefix matching (not --del, only --delete).
  5//
  6// If we encounter arguments that rsync(1) parses differently compared to this
  7// package, then this package should be adjusted to match rsync(1).
  8package rsyncopts
  9
 10import (
 11	"errors"
 12	"fmt"
 13	"log/slog"
 14	"math"
 15	"slices"
 16	"strconv"
 17	"strings"
 18	"syscall"
 19	"unicode"
 20)
 21
 22const (
 23	OPT_SERVER = 1000 + iota
 24	OPT_DAEMON
 25	OPT_SENDER
 26	OPT_EXCLUDE
 27	OPT_EXCLUDE_FROM
 28	OPT_FILTER
 29	OPT_COMPARE_DEST
 30	OPT_COPY_DEST
 31	OPT_LINK_DEST
 32	OPT_HELP
 33	OPT_INCLUDE
 34	OPT_INCLUDE_FROM
 35	OPT_MODIFY_WINDOW
 36	OPT_MIN_SIZE
 37	OPT_CHMOD
 38	OPT_READ_BATCH
 39	OPT_WRITE_BATCH
 40	OPT_ONLY_WRITE_BATCH
 41	OPT_MAX_SIZE
 42	OPT_NO_D
 43	OPT_APPEND
 44	OPT_NO_ICONV
 45	OPT_INFO
 46	OPT_DEBUG
 47	OPT_BLOCK_SIZE
 48	OPT_USERMAP
 49	OPT_GROUPMAP
 50	OPT_CHOWN
 51	OPT_BWLIMIT
 52	OPT_STDERR
 53	OPT_OLD_COMPRESS
 54	OPT_NEW_COMPRESS
 55	OPT_NO_COMPRESS
 56	OPT_OLD_ARGS
 57	OPT_STOP_AFTER
 58	OPT_STOP_AT
 59	OPT_REFUSED_BASE = 9000
 60)
 61
 62type infoLevel int
 63
 64const (
 65	INFO_BACKUP infoLevel = iota
 66	INFO_COPY
 67	INFO_DEL
 68	INFO_FLIST
 69	INFO_MISC
 70	INFO_MOUNT
 71	INFO_NAME
 72	INFO_NONREG
 73	INFO_PROGRESS
 74	INFO_REMOVE
 75	INFO_SKIP
 76	INFO_STATS
 77	INFO_SYMSAFE
 78	COUNT_INFO
 79)
 80
 81// NewOptions returns an Options struct with all options initialized to their
 82// default values. Note that ParseArguments will set some options (that default
 83// to -1) based on the encountered command-line flags and built-in rules.
 84func NewOptions() *Options {
 85	return &Options{
 86		msgs2stderr:          2, // Default: send errors to stderr for local & remote-shell transfers
 87		output_motd:          1,
 88		human_readable:       1,
 89		allow_inc_recurse:    1,
 90		xfer_dirs:            -1,
 91		relative_paths:       -1,
 92		implied_dirs:         1,
 93		max_delete:           math.MinInt32,
 94		whole_file:           -1,
 95		do_compression_level: math.MinInt32,
 96		rsync_path:           "rsync",
 97		default_af_hint:      syscall.AF_INET6,
 98		blocking_io:          -1,
 99		protocol_version:     27,
100	}
101}
102
103// GokrazyOptions contains additional command-line flags, prefixed with
104// gokr. (like --gokr.modulemap) to not clash with rsync flag names.
105type GokrazyOptions struct {
106	Config           string
107	Listen           string
108	MonitoringListen string
109	AnonSSHListen    string
110	ModuleMap        string
111}
112
113func (o *GokrazyOptions) table() []poptOption {
114	return []poptOption{
115		/* longName, shortName, argInfo, arg, val */
116		{"gokr.config", "", POPT_ARG_STRING, &o.Config, 0},
117		{"gokr.listen", "", POPT_ARG_STRING, &o.Listen, 0},
118		{"gokr.monitoring_listen", "", POPT_ARG_STRING, &o.MonitoringListen, 0},
119		{"gokr.anonssh_listen", "", POPT_ARG_STRING, &o.AnonSSHListen, 0},
120		{"gokr.modulemap", "", POPT_ARG_STRING, &o.ModuleMap, 0},
121	}
122}
123
124type Options struct {
125	Gokrazy GokrazyOptions
126
127	// not directly referenced in the table, but used in the special case code.
128	do_compression int
129	info           [COUNT_INFO]uint16
130	local_server   int
131
132	// order matches long_options order
133	verbose                int
134	msgs2stderr            int
135	quiet                  int
136	output_motd            int
137	do_stats               int
138	human_readable         int
139	dry_run                int
140	recurse                int
141	allow_inc_recurse      int
142	xfer_dirs              int
143	preserve_perms         int
144	preserve_executability int
145	preserve_acls          int
146	preserve_xattrs        int
147	preserve_mtimes        int
148	preserve_atimes        int
149	open_noatime           int
150	preserve_crtimes       int
151	omit_dir_times         int
152	omit_link_times        int
153	modify_window          int
154	am_root                int // 0 = normal, 1 = root, 2 = --super, -1 = --fake-super
155	preserve_uid           int
156	preserve_gid           int
157	preserve_devices       int
158	copy_devices           int
159	write_devices          int
160	preserve_specials      int
161	preserve_links         int
162	copy_links             int
163	copy_unsafe_links      int
164	safe_symlinks          int
165	munge_symlinks         int
166	copy_dirlinks          int
167	keep_dirlinks          int
168	preserve_hard_links    int
169	relative_paths         int
170	implied_dirs           int
171	ignore_times           int
172	size_only              int
173	one_file_system        int
174	update_only            int
175	ignore_non_existing    int
176	ignore_existing        int
177	max_size_arg           string
178	min_size_arg           string
179	max_alloc_arg          string
180	sparse_files           int
181	preallocate_files      int
182	inplace                int
183	append_mode            int
184	delete_during          int
185	delete_mode            int
186	delete_before          int
187	delete_after           int
188	delete_excluded        int
189	missing_args           int // 0 = FERROR_XFER, 1 = ignore, 2 = delete
190	remove_source_files    int
191	force_delete           int
192	ignore_errors          int
193	max_delete             int
194	cvs_exclude            int
195	// If 1, send the whole file as literal data rather than trying to create an
196	// incremental diff.
197	// If -1, then look at whether we're local or remote and go by that.
198	// See also disable_deltas_p()
199	whole_file           int
200	always_checksum      int
201	checksum_choice      string
202	fuzzy_basis          int
203	compress_choice      string
204	skip_compress        string
205	do_compression_level int
206	do_progress          int
207	keep_partial         int
208	partial_dir          string
209	delay_updates        int
210	prune_empty_dirs     int
211	logfile_name         string
212	logfile_format       string
213	stdout_format        string
214	itemize_changes      int
215	bwlimit_arg          string
216	bwlimit              int
217	make_backups         int
218	backup_dir           string
219	backup_suffix        string
220	list_only            int
221	batch_name           string
222	files_from           string
223	eol_nulls            int
224	old_style_args       int // intentionally set to 0; unsupported
225	protect_args         int // intentionally set to 0; currently unsupported
226	trust_sender         int
227	numeric_ids          int
228	io_timeout           int
229	connect_timeout      int
230	do_fsync             int
231	shell_cmd            string
232	rsync_path           string
233	tmpdir               string
234	iconv_opt            string
235	default_af_hint      int
236	allow_8bit_chars     int
237	mkpath_dest_arg      int
238	use_qsort            int
239	copy_as              string
240	bind_address         string // numeric IPv4 or IPv6, or a hostname
241	rsync_port           int
242	sockopts             string
243	password_file        string
244	early_input_file     string
245	blocking_io          int
246	outbuf_mode          string
247	protocol_version     int
248	checksum_seed        int
249	am_server            int
250	am_sender            int
251	am_daemon            int
252
253	daemon_bwlimit int
254	config_file    string
255	daemon_opt     int
256	no_detach      int
257}
258
259type priority int
260
261const (
262	DEFAULT_PRIORITY priority = iota
263	HELP_PRIORITY
264	USER_PRIORITY
265	LIMIT_PRIORITY
266)
267
268const (
269	W_CLI = 1 << iota
270	W_SRV
271	W_SND
272	W_REC
273)
274
275type output struct {
276	name  string
277	where int
278	help  string
279}
280
281var infoWords = [...]output{
282	{"BACKUP", W_REC, "Mention files backed up"},
283	{"COPY", W_REC, "Mention files copied locally on the receiving side"},
284	{"DEL", W_REC, "Mention deletions on the receiving side"},
285	{"FLIST", W_CLI, "Mention file-list receiving/sending (levels 1-2)"},
286	{"MISC", W_SND | W_REC, "Mention miscellaneous information (levels 1-2)"},
287	{"MOUNT", W_SND | W_REC, "Mention mounts that were found or skipped"},
288	{"NAME", W_SND | W_REC, "Mention 1) updated file/dir names, 2) unchanged names"},
289	{"NONREG", W_REC, "Mention skipped non-regular files (default 1, 0 disables)"},
290	{"PROGRESS", W_CLI, "Mention 1) per-file progress or 2) total transfer progress"},
291	{"REMOVE", W_SND, "Mention files removed on the sending side"},
292	{"SKIP", W_REC, "Mention files skipped due to transfer overrides (levels 1-2)"},
293	{"STATS", W_CLI | W_SRV, "Mention statistics at end of run (levels 1-3)"},
294	{"SYMSAFE", W_SND | W_REC, "Mention symlinks that are unsafe"},
295}
296
297func parseOutputWords(words []output, levels []uint16, str string, prio priority) {
298Level:
299	for s := range strings.SplitSeq(str, ",") {
300		if strings.TrimSpace(s) == "" {
301			continue
302		}
303		trimmed := strings.TrimRightFunc(s, unicode.IsNumber)
304		lev := 1
305		if len(trimmed) < len(s) {
306			var err error
307			lev, err = strconv.Atoi(s[len(trimmed):])
308			if err != nil {
309				continue
310			}
311		}
312		trimmed = strings.ToLower(trimmed)
313		all := false
314		switch trimmed {
315		case "none":
316			lev = 0
317		case "all":
318			all = true
319		}
320		for j := range words {
321			word := words[j]
322			if strings.ToLower(word.name) == trimmed || all {
323				levels[j] = uint16(lev)
324				if !all {
325					continue Level
326				}
327			}
328		}
329	}
330}
331
332func (o *Options) setOutputVerbosity(prio priority) {
333	// debugVerbosity is reserved for future use with debug logging levels.
334	// debugVerbosity := [...]string{
335	//     "",
336	//     "",
337	//     "BIND,CMD,CONNECT,DEL,DELTASUM,DUP,FILTER,FLIST,ICONV",
338	//     "ACL,BACKUP,CONNECT2,DELTASUM2,DEL2,EXIT,FILTER2,FLIST2,FUZZY,GENR,OWN,RECV,SEND,TIME",
339	//     "CMD2,DELTASUM3,DEL3,EXIT2,FLIST3,ICONV2,OWN2,PROTO,TIME2",
340	//     "CHDIR,DELTASUM4,FLIST4,FUZZY2,HASH,HLINK",
341	// }
342	infoVerbosity := [...]string{
343		"NONREG",
344		"COPY,DEL,FLIST,MISC,NAME,STATS,SYMSAFE",
345		"BACKUP,MISC2,MOUNT,NAME2,REMOVE,SKIP",
346	}
347	for j := 0; j <= o.verbose; j++ {
348		if j < len(infoVerbosity) {
349			parseOutputWords(infoWords[:], o.info[:], infoVerbosity[j], prio)
350		}
351		// TODO: enable debug verbosity when debug logging is implemented
352		// if j < len(debugVerbosity) {
353		//     parseOutputWords(debugWords[:], o.debug[:], debugVerbosity[j], prio)
354		// }
355	}
356}
357
358func (o *Options) Help() string {
359	return ""
360}
361
362func (o *Options) ShellCommand() string       { return o.shell_cmd }
363func (o *Options) UpdateOnly() bool           { return o.update_only != 0 }
364func (o *Options) DryRun() bool               { return o.dry_run != 0 }
365func (o *Options) PreserveLinks() bool        { return o.preserve_links != 0 }
366func (o *Options) PreserveUid() bool          { return o.preserve_uid != 0 }
367func (o *Options) PreserveGid() bool          { return o.preserve_gid != 0 }
368func (o *Options) PreserveDevices() bool      { return o.preserve_devices != 0 }
369func (o *Options) PreserveMTimes() bool       { return o.preserve_mtimes != 0 }
370func (o *Options) PreservePerms() bool        { return o.preserve_perms != 0 }
371func (o *Options) PreserveSpecials() bool     { return o.preserve_specials != 0 }
372func (o *Options) PreserveHardLinks() bool    { return o.preserve_hard_links != 0 }
373func (o *Options) Recurse() bool              { return o.recurse != 0 }
374func (o *Options) Verbose() bool              { return o.verbose != 0 }
375func (o *Options) DeleteMode() bool           { return o.delete_mode != 0 }
376func (o *Options) Sender() bool               { return o.am_sender != 0 }
377func (o *Options) SetSender()                 { o.am_sender = 1 }
378func (o *Options) LocalServer() bool          { return o.local_server != 0 }
379func (o *Options) SetLocalServer()            { o.local_server = 1 }
380func (o *Options) Server() bool               { return o.am_server != 0 }
381func (o *Options) Daemon() bool               { return o.am_daemon != 0 }
382func (o *Options) ConnectTimeoutSeconds() int { return o.connect_timeout }
383func (o *Options) AlwaysChecksum() bool       { return o.always_checksum != 0 }
384func (o *Options) Compress() bool             { return o.do_compression != 0 }
385func (o *Options) CompressChoice() string     { return o.compress_choice }
386func (o *Options) CompressLevel() int         { return o.do_compression_level }
387func (o *Options) IgnoreTimes() bool          { return o.ignore_times == 1 }
388func (o *Options) SizeOnly() bool             { return o.size_only == 1 }
389
390func (o *Options) daemonTable() []poptOption {
391	return []poptOption{
392		/* longName, shortName, argInfo, arg, val */
393		{"help", "", POPT_ARG_NONE, nil, OPT_HELP},
394		{"address", "", POPT_ARG_STRING, &o.bind_address, 0},
395		{"bwlimit", "", POPT_ARG_INT, &o.daemon_bwlimit, 0},
396		{"config", "", POPT_ARG_STRING, &o.config_file, 0},
397		{"daemon", "", POPT_ARG_NONE, &o.daemon_opt, 0},
398		{"dparam", "M", POPT_ARG_STRING, nil, 'M'},
399		{"ipv4", "4", POPT_ARG_VAL, &o.default_af_hint, syscall.AF_INET},
400		{"ipv6", "6", POPT_ARG_VAL, &o.default_af_hint, syscall.AF_INET6},
401		{"detach", "", POPT_ARG_VAL, &o.no_detach, 0},
402		{"no-detach", "", POPT_ARG_VAL, &o.no_detach, 1},
403		{"log-file", "", POPT_ARG_STRING, &o.logfile_name, 0},
404		{"log-file-format", "", POPT_ARG_STRING, &o.logfile_format, 0},
405		{"port", "", POPT_ARG_INT, &o.rsync_port, 0},
406		{"sockopts", "", POPT_ARG_STRING, &o.sockopts, 0},
407		{"protocol", "", POPT_ARG_INT, &o.protocol_version, 0},
408		{"server", "", POPT_ARG_NONE, &o.am_server, 0},
409		{"temp-dir", "T", POPT_ARG_STRING, &o.tmpdir, 0},
410		{"verbose", "v", POPT_ARG_NONE, 0, 'v'},
411		{"no-verbose", "", POPT_ARG_VAL, &o.verbose, 0},
412		{"no-v", "", POPT_ARG_VAL, &o.verbose, 0},
413		{"help", "h", POPT_ARG_NONE, 0, 'h'},
414	}
415}
416
417func (o *Options) table() []poptOption {
418	return []poptOption{
419		/* longName, shortName, argInfo, arg, val */
420		{"help", "", POPT_ARG_NONE, nil, OPT_HELP},
421		{"version", "V", POPT_ARG_NONE, nil, 'V'},
422		{"verbose", "v", POPT_ARG_NONE, nil, 'v'},
423		{"no-verbose", "", POPT_ARG_VAL, &o.verbose, 0},
424		{"no-v", "", POPT_ARG_VAL, &o.verbose, 0},
425		{"info", "", POPT_ARG_STRING, nil, OPT_INFO},
426		{"debug", "", POPT_ARG_STRING, nil, OPT_DEBUG},
427		{"stderr", "", POPT_ARG_STRING, nil, OPT_STDERR},
428		{"msgs2stderr", "", POPT_ARG_VAL, &o.msgs2stderr, 1},
429		{"no-msgs2stderr", "", POPT_ARG_VAL, &o.msgs2stderr, 0},
430		{"quiet", "q", POPT_ARG_NONE, nil, 'q'},
431		{"motd", "", POPT_ARG_VAL, &o.output_motd, 1},
432		{"no-motd", "", POPT_ARG_VAL, &o.output_motd, 0},
433		{"stats", "", POPT_ARG_NONE, &o.do_stats, 0},
434		{"human-readable", "h", POPT_ARG_NONE, nil, 'h'},
435		{"no-human-readable", "", POPT_ARG_VAL, &o.human_readable, 0},
436		{"no-h", "", POPT_ARG_VAL, &o.human_readable, 0},
437		{"dry-run", "n", POPT_ARG_NONE, &o.dry_run, 0},
438		{"archive", "a", POPT_ARG_NONE, nil, 'a'},
439		{"recursive", "r", POPT_ARG_VAL, &o.recurse, 2},
440		{"no-recursive", "", POPT_ARG_VAL, &o.recurse, 0},
441		{"no-r", "", POPT_ARG_VAL, &o.recurse, 0},
442		{"inc-recursive", "", POPT_ARG_VAL, &o.allow_inc_recurse, 1},
443		{"no-inc-recursive", "", POPT_ARG_VAL, &o.allow_inc_recurse, 0},
444		{"i-r", "", POPT_ARG_VAL, &o.allow_inc_recurse, 1},
445		{"no-i-r", "", POPT_ARG_VAL, &o.allow_inc_recurse, 0},
446		{"dirs", "d", POPT_ARG_VAL, &o.xfer_dirs, 2},
447		{"no-dirs", "", POPT_ARG_VAL, &o.xfer_dirs, 0},
448		{"no-d", "", POPT_ARG_VAL, &o.xfer_dirs, 0},
449		{"old-dirs", "", POPT_ARG_VAL, &o.xfer_dirs, 4},
450		{"old-d", "", POPT_ARG_VAL, &o.xfer_dirs, 4},
451		{"perms", "p", POPT_ARG_VAL, &o.preserve_perms, 1},
452		{"no-perms", "", POPT_ARG_VAL, &o.preserve_perms, 0},
453		{"no-p", "", POPT_ARG_VAL, &o.preserve_perms, 0},
454		{"executability", "E", POPT_ARG_NONE, &o.preserve_executability, 0},
455		{"acls", "A", POPT_ARG_NONE, nil, 'A'},
456		{"no-acls", "", POPT_ARG_VAL, &o.preserve_acls, 0},
457		{"no-A", "", POPT_ARG_VAL, &o.preserve_acls, 0},
458		{"xattrs", "X", POPT_ARG_NONE, nil, 'X'},
459		{"no-xattrs", "", POPT_ARG_VAL, &o.preserve_xattrs, 0},
460		{"no-X", "", POPT_ARG_VAL, &o.preserve_xattrs, 0},
461		{"times", "t", POPT_ARG_VAL, &o.preserve_mtimes, 1},
462		{"no-times", "", POPT_ARG_VAL, &o.preserve_mtimes, 0},
463		{"no-t", "", POPT_ARG_VAL, &o.preserve_mtimes, 0},
464		{"atimes", "U", POPT_ARG_NONE, nil, 'U'},
465		{"no-atimes", "", POPT_ARG_VAL, &o.preserve_atimes, 0},
466		{"no-U", "", POPT_ARG_VAL, &o.preserve_atimes, 0},
467		{"open-noatime", "", POPT_ARG_VAL, &o.open_noatime, 1},
468		{"no-open-noatime", "", POPT_ARG_VAL, &o.open_noatime, 0},
469		{"crtimes", "N", POPT_ARG_NONE, &o.preserve_crtimes, 1}, // refused
470		{"no-crtimes", "", POPT_ARG_VAL, &o.preserve_crtimes, 0},
471		{"no-N", "", POPT_ARG_VAL, &o.preserve_crtimes, 0},
472		{"omit-dir-times", "O", POPT_ARG_VAL, &o.omit_dir_times, 1},
473		{"no-omit-dir-times", "", POPT_ARG_VAL, &o.omit_dir_times, 0},
474		{"no-O", "", POPT_ARG_VAL, &o.omit_dir_times, 0},
475		{"omit-link-times", "J", POPT_ARG_VAL, &o.omit_link_times, 1},
476		{"no-omit-link-times", "", POPT_ARG_VAL, &o.omit_link_times, 0},
477		{"no-J", "", POPT_ARG_VAL, &o.omit_link_times, 0},
478		{"modify-window", "@", POPT_ARG_INT, &o.modify_window, OPT_MODIFY_WINDOW},
479		{"super", "", POPT_ARG_VAL, &o.am_root, 2},
480		{"no-super", "", POPT_ARG_VAL, &o.am_root, 0},
481		{"fake-super", "", POPT_ARG_VAL, &o.am_root, -1},
482		{"owner", "o", POPT_ARG_VAL, &o.preserve_uid, 1},
483		{"no-owner", "", POPT_ARG_VAL, &o.preserve_uid, 0},
484		{"no-o", "", POPT_ARG_VAL, &o.preserve_uid, 0},
485		{"group", "g", POPT_ARG_VAL, &o.preserve_gid, 1},
486		{"no-group", "", POPT_ARG_VAL, &o.preserve_gid, 0},
487		{"no-g", "", POPT_ARG_VAL, &o.preserve_gid, 0},
488		{"", "D", POPT_ARG_NONE, nil, 'D'},
489		{"no-D", "", POPT_ARG_NONE, nil, OPT_NO_D},
490		{"devices", "", POPT_ARG_VAL, &o.preserve_devices, 1},
491		{"no-devices", "", POPT_ARG_VAL, &o.preserve_devices, 0},
492		{"copy-devices", "", POPT_ARG_NONE, &o.copy_devices, 0},
493		{"write-devices", "", POPT_ARG_VAL, &o.write_devices, 1},
494		{"no-write-devices", "", POPT_ARG_VAL, &o.write_devices, 0},
495		{"specials", "", POPT_ARG_VAL, &o.preserve_specials, 1},
496		{"no-specials", "", POPT_ARG_VAL, &o.preserve_specials, 0},
497		{"links", "l", POPT_ARG_VAL, &o.preserve_links, 1},
498		{"no-links", "", POPT_ARG_VAL, &o.preserve_links, 0},
499		{"no-l", "", POPT_ARG_VAL, &o.preserve_links, 0},
500		{"copy-links", "L", POPT_ARG_NONE, &o.copy_links, 0},
501		{"copy-unsafe-links", "", POPT_ARG_NONE, &o.copy_unsafe_links, 0},
502		{"safe-links", "", POPT_ARG_NONE, &o.safe_symlinks, 0},
503		{"munge-links", "", POPT_ARG_VAL, &o.munge_symlinks, 1},
504		{"no-munge-links", "", POPT_ARG_VAL, &o.munge_symlinks, 0},
505		{"copy-dirlinks", "k", POPT_ARG_NONE, &o.copy_dirlinks, 0},
506		{"keep-dirlinks", "K", POPT_ARG_NONE, &o.keep_dirlinks, 0},
507		{"hard-links", "H", POPT_ARG_NONE, nil, 'H'},
508		{"no-hard-links", "", POPT_ARG_VAL, &o.preserve_hard_links, 0},
509		{"no-H", "", POPT_ARG_VAL, &o.preserve_hard_links, 0},
510		{"relative", "R", POPT_ARG_VAL, &o.relative_paths, 1},
511		{"no-relative", "", POPT_ARG_VAL, &o.relative_paths, 0},
512		{"no-R", "", POPT_ARG_VAL, &o.relative_paths, 0},
513		{"implied-dirs", "", POPT_ARG_VAL, &o.implied_dirs, 1},
514		{"no-implied-dirs", "", POPT_ARG_VAL, &o.implied_dirs, 0},
515		{"i-d", "", POPT_ARG_VAL, &o.implied_dirs, 1},
516		{"no-i-d", "", POPT_ARG_VAL, &o.implied_dirs, 0},
517		{"chmod", "", POPT_ARG_STRING, nil, OPT_CHMOD},
518		{"ignore-times", "I", POPT_ARG_NONE, &o.ignore_times, 0},
519		{"size-only", "", POPT_ARG_NONE, &o.size_only, 0},
520		{"one-file-system", "x", POPT_ARG_NONE, nil, 'x'},
521		{"no-one-file-system", "", POPT_ARG_VAL, &o.one_file_system, 0},
522		{"no-x", "", POPT_ARG_VAL, &o.one_file_system, 0},
523		{"update", "u", POPT_ARG_NONE, &o.update_only, 0},
524		{"existing", "", POPT_ARG_NONE, &o.ignore_non_existing, 0},
525		{"ignore-non-existing", "", POPT_ARG_NONE, &o.ignore_non_existing, 0},
526		{"ignore-existing", "", POPT_ARG_NONE, &o.ignore_existing, 0},
527		{"max-size", "", POPT_ARG_STRING, &o.max_size_arg, OPT_MAX_SIZE},
528		{"min-size", "", POPT_ARG_STRING, &o.min_size_arg, OPT_MIN_SIZE},
529		{"max-alloc", "", POPT_ARG_STRING, &o.max_alloc_arg, 0},
530		{"sparse", "S", POPT_ARG_VAL, &o.sparse_files, 1},
531		{"no-sparse", "", POPT_ARG_VAL, &o.sparse_files, 0},
532		{"no-S", "", POPT_ARG_VAL, &o.sparse_files, 0},
533		{"preallocate", "", POPT_ARG_NONE, &o.preallocate_files, 0},
534		{"inplace", "", POPT_ARG_VAL, &o.inplace, 1},
535		{"no-inplace", "", POPT_ARG_VAL, &o.inplace, 0},
536		{"append", "", POPT_ARG_NONE, nil, OPT_APPEND},
537		{"append-verify", "", POPT_ARG_VAL, &o.append_mode, 2},
538		{"no-append", "", POPT_ARG_VAL, &o.append_mode, 0},
539		{"del", "", POPT_ARG_NONE, &o.delete_during, 0},
540		{"delete", "", POPT_ARG_NONE, &o.delete_mode, 0},
541		{"delete-before", "", POPT_ARG_NONE, &o.delete_before, 0},
542		{"delete-during", "", POPT_ARG_VAL, &o.delete_during, 1},
543		{"delete-delay", "", POPT_ARG_VAL, &o.delete_during, 2},
544		{"delete-after", "", POPT_ARG_NONE, &o.delete_after, 0},
545		{"delete-excluded", "", POPT_ARG_NONE, &o.delete_excluded, 0},
546		{"delete-missing-args", "", POPT_BIT_SET, &o.missing_args, 2},
547		{"ignore-missing-args", "", POPT_BIT_SET, &o.missing_args, 1},
548		{"remove-sent-files", "", POPT_ARG_VAL, &o.remove_source_files, 2}, /* deprecated */
549		{"remove-source-files", "", POPT_ARG_VAL, &o.remove_source_files, 1},
550		{"force", "", POPT_ARG_VAL, &o.force_delete, 1},
551		{"no-force", "", POPT_ARG_VAL, &o.force_delete, 0},
552		{"ignore-errors", "", POPT_ARG_VAL, &o.ignore_errors, 1},
553		{"no-ignore-errors", "", POPT_ARG_VAL, &o.ignore_errors, 0},
554		{"max-delete", "", POPT_ARG_INT, &o.max_delete, 0},
555		{"", "F", POPT_ARG_NONE, nil, 'F'},
556		{"filter", "f", POPT_ARG_STRING, nil, OPT_FILTER},
557		{"exclude", "", POPT_ARG_STRING, nil, OPT_EXCLUDE},
558		{"include", "", POPT_ARG_STRING, nil, OPT_INCLUDE},
559		{"exclude-from", "", POPT_ARG_STRING, nil, OPT_EXCLUDE_FROM},
560		{"include-from", "", POPT_ARG_STRING, nil, OPT_INCLUDE_FROM},
561		{"cvs-exclude", "C", POPT_ARG_NONE, &o.cvs_exclude, 0},
562		{"whole-file", "W", POPT_ARG_VAL, &o.whole_file, 1},
563		{"no-whole-file", "", POPT_ARG_VAL, &o.whole_file, 0},
564		{"no-W", "", POPT_ARG_VAL, &o.whole_file, 0},
565		{"checksum", "c", POPT_ARG_VAL, &o.always_checksum, 1},
566		{"no-checksum", "", POPT_ARG_VAL, &o.always_checksum, 0},
567		{"no-c", "", POPT_ARG_VAL, &o.always_checksum, 0},
568		{"checksum-choice", "", POPT_ARG_STRING, &o.checksum_choice, 0},
569		{"cc", "", POPT_ARG_STRING, &o.checksum_choice, 0},
570		{"block-size", "B", POPT_ARG_STRING, nil, OPT_BLOCK_SIZE},
571		{"compare-dest", "", POPT_ARG_STRING, nil, OPT_COMPARE_DEST},
572		{"copy-dest", "", POPT_ARG_STRING, nil, OPT_COPY_DEST},
573		{"link-dest", "", POPT_ARG_STRING, nil, OPT_LINK_DEST},
574		{"fuzzy", "y", POPT_ARG_NONE, nil, 'y'},
575		{"no-fuzzy", "", POPT_ARG_VAL, &o.fuzzy_basis, 0},
576		{"no-y", "", POPT_ARG_VAL, &o.fuzzy_basis, 0},
577		{"compress", "z", POPT_ARG_NONE, nil, 'z'},
578		{"old-compress", "", POPT_ARG_NONE, nil, OPT_OLD_COMPRESS},
579		{"new-compress", "", POPT_ARG_NONE, nil, OPT_NEW_COMPRESS},
580		{"no-compress", "", POPT_ARG_NONE, nil, OPT_NO_COMPRESS},
581		{"no-z", "", POPT_ARG_NONE, nil, OPT_NO_COMPRESS},
582		{"compress-choice", "", POPT_ARG_STRING, &o.compress_choice, 0},
583		{"zc", "", POPT_ARG_STRING, &o.compress_choice, 0},
584		{"skip-compress", "", POPT_ARG_STRING, &o.skip_compress, 0},
585		{"compress-level", "", POPT_ARG_INT, &o.do_compression_level, 0},
586		{"zl", "", POPT_ARG_INT, &o.do_compression_level, 0},
587		{"", "P", POPT_ARG_NONE, nil, 'P'},
588		{"progress", "", POPT_ARG_VAL, &o.do_progress, 1},
589		{"no-progress", "", POPT_ARG_VAL, &o.do_progress, 0},
590		{"partial", "", POPT_ARG_VAL, &o.keep_partial, 1},
591		{"no-partial", "", POPT_ARG_VAL, &o.keep_partial, 0},
592		{"partial-dir", "", POPT_ARG_STRING, &o.partial_dir, 0},
593		{"delay-updates", "", POPT_ARG_VAL, &o.delay_updates, 1},
594		{"no-delay-updates", "", POPT_ARG_VAL, &o.delay_updates, 0},
595		{"prune-empty-dirs", "m", POPT_ARG_VAL, &o.prune_empty_dirs, 1},
596		{"no-prune-empty-dirs", "", POPT_ARG_VAL, &o.prune_empty_dirs, 0},
597		{"no-m", "", POPT_ARG_VAL, &o.prune_empty_dirs, 0},
598		{"log-file", "", POPT_ARG_STRING, &o.logfile_name, 0},
599		{"log-file-format", "", POPT_ARG_STRING, &o.logfile_format, 0},
600		{"out-format", "", POPT_ARG_STRING, &o.stdout_format, 0},
601		{"log-format", "", POPT_ARG_STRING, &o.stdout_format, 0}, /* DEPRECATED */
602		{"itemize-changes", "i", POPT_ARG_NONE, nil, 'i'},
603		{"no-itemize-changes", "", POPT_ARG_VAL, &o.itemize_changes, 0},
604		{"no-i", "", POPT_ARG_VAL, &o.itemize_changes, 0},
605		{"bwlimit", "", POPT_ARG_STRING, &o.bwlimit_arg, OPT_BWLIMIT},
606		{"no-bwlimit", "", POPT_ARG_VAL, &o.bwlimit, 0},
607		{"backup", "b", POPT_ARG_VAL, &o.make_backups, 1},
608		{"no-backup", "", POPT_ARG_VAL, &o.make_backups, 0},
609		{"backup-dir", "", POPT_ARG_STRING, &o.backup_dir, 0},
610		{"suffix", "", POPT_ARG_STRING, &o.backup_suffix, 0},
611		{"list-only", "", POPT_ARG_VAL, &o.list_only, 2},
612		{"read-batch", "", POPT_ARG_STRING, &o.batch_name, OPT_READ_BATCH},
613		{"write-batch", "", POPT_ARG_STRING, &o.batch_name, OPT_WRITE_BATCH},
614		{"only-write-batch", "", POPT_ARG_STRING, &o.batch_name, OPT_ONLY_WRITE_BATCH},
615		{"files-from", "", POPT_ARG_STRING, &o.files_from, 0},
616		{"from0", "0", POPT_ARG_VAL, &o.eol_nulls, 1},
617		{"no-from0", "", POPT_ARG_VAL, &o.eol_nulls, 0},
618		{"old-args", "", POPT_ARG_NONE, nil, OPT_OLD_ARGS},
619		{"no-old-args", "", POPT_ARG_VAL, &o.old_style_args, 0},
620		{"secluded-args", "s", POPT_ARG_VAL, &o.protect_args, 1},
621		{"no-secluded-args", "", POPT_ARG_VAL, &o.protect_args, 0},
622		{"protect-args", "", POPT_ARG_VAL, &o.protect_args, 1},
623		{"no-protect-args", "", POPT_ARG_VAL, &o.protect_args, 0},
624		{"no-s", "", POPT_ARG_VAL, &o.protect_args, 0},
625		{"trust-sender", "", POPT_ARG_VAL, &o.trust_sender, 1},
626		{"numeric-ids", "", POPT_ARG_VAL, &o.numeric_ids, 1},
627		{"no-numeric-ids", "", POPT_ARG_VAL, &o.numeric_ids, 0},
628		{"usermap", "", POPT_ARG_STRING, nil, OPT_USERMAP},
629		{"groupmap", "", POPT_ARG_STRING, nil, OPT_GROUPMAP},
630		{"chown", "", POPT_ARG_STRING, nil, OPT_CHOWN},
631		{"timeout", "", POPT_ARG_INT, &o.io_timeout, 0},
632		{"no-timeout", "", POPT_ARG_VAL, &o.io_timeout, 0},
633		{"contimeout", "", POPT_ARG_INT, &o.connect_timeout, 0},
634		{"no-contimeout", "", POPT_ARG_VAL, &o.connect_timeout, 0},
635		{"fsync", "", POPT_ARG_NONE, &o.do_fsync, 0},
636		{"stop-after", "", POPT_ARG_STRING, nil, OPT_STOP_AFTER},
637		{"time-limit", "", POPT_ARG_STRING, nil, OPT_STOP_AFTER}, /* earlier stop-after name */
638		{"stop-at", "", POPT_ARG_STRING, nil, OPT_STOP_AT},
639		{"rsh", "e", POPT_ARG_STRING, &o.shell_cmd, 0},
640		{"rsync-path", "", POPT_ARG_STRING, &o.rsync_path, 0},
641		{"temp-dir", "T", POPT_ARG_STRING, &o.tmpdir, 0},
642		{"iconv", "", POPT_ARG_STRING, &o.iconv_opt, 0},
643		{"no-iconv", "", POPT_ARG_NONE, nil, OPT_NO_ICONV},
644		{"ipv4", "4", POPT_ARG_VAL, &o.default_af_hint, syscall.AF_INET},
645		{"ipv6", "6", POPT_ARG_VAL, &o.default_af_hint, syscall.AF_INET6},
646		{"8-bit-output", "8", POPT_ARG_VAL, &o.allow_8bit_chars, 1},
647		{"no-8-bit-output", "", POPT_ARG_VAL, &o.allow_8bit_chars, 0},
648		{"no-8", "", POPT_ARG_VAL, &o.allow_8bit_chars, 0},
649		{"mkpath", "", POPT_ARG_VAL, &o.mkpath_dest_arg, 1},
650		{"no-mkpath", "", POPT_ARG_VAL, &o.mkpath_dest_arg, 0},
651		{"qsort", "", POPT_ARG_NONE, &o.use_qsort, 0},
652		{"copy-as", "", POPT_ARG_STRING, &o.copy_as, 0},
653		{"address", "", POPT_ARG_STRING, &o.bind_address, 0},
654		{"port", "", POPT_ARG_INT, &o.rsync_port, 0},
655		{"sockopts", "", POPT_ARG_STRING, &o.sockopts, 0},
656		{"password-file", "", POPT_ARG_STRING, &o.password_file, 0},
657		{"early-input", "", POPT_ARG_STRING, &o.early_input_file, 0},
658		{"blocking-io", "", POPT_ARG_VAL, &o.blocking_io, 1},
659		{"no-blocking-io", "", POPT_ARG_VAL, &o.blocking_io, 0},
660		{"outbuf", "", POPT_ARG_STRING, &o.outbuf_mode, 0},
661		{"remote-option", "M", POPT_ARG_STRING, nil, 'M'},
662		{"protocol", "", POPT_ARG_INT, &o.protocol_version, 0},
663		{"checksum-seed", "", POPT_ARG_INT, &o.checksum_seed, 0},
664		{"server", "", POPT_ARG_NONE, nil, OPT_SERVER},
665		{"sender", "", POPT_ARG_NONE, nil, OPT_SENDER},
666		/* All the following options switch us into daemon-mode option-parsing. */
667		{"config", "", POPT_ARG_STRING, nil, OPT_DAEMON},
668		{"daemon", "", POPT_ARG_NONE, nil, OPT_DAEMON},
669		{"dparam", "", POPT_ARG_STRING, nil, OPT_DAEMON},
670		{"detach", "", POPT_ARG_NONE, nil, OPT_DAEMON},
671		{"no-detach", "", POPT_ARG_NONE, nil, OPT_DAEMON},
672	}
673}
674
675var errNotYetImplemented = errors.New("option not yet implemented in gokrazy/rsync")
676
677// rsync/options.c:parse_arguments.
678func ParseArguments(args []string, gokrazyTable bool) (*Context, error) {
679	// NOTE: We do not implement support for refusing options per rsyncd.conf
680	// here, as we have our own configuration file.
681
682	version_opt_cnt := 0
683
684	opts := NewOptions()
685	table := opts.table()
686	if gokrazyTable {
687		// We need to make the --gokr.* flags known, otherwise the first parsing
688		// attempt fails and the daemon mode parsing is never run.
689		table = slices.Concat(opts.Gokrazy.table(), table)
690	}
691	pc := Context{
692		Options: opts,
693		table:   table,
694		args:    args,
695	}
696
697	for {
698		opt, err := pc.poptGetNextOpt()
699		if err != nil {
700			return nil, err
701		}
702		if opt == -1 {
703			break // done
704		}
705		// Most options are handled by poptGetNextOpt, only special cases
706		// are returned and handled here.
707		switch opt {
708		case 'V':
709			version_opt_cnt++
710
711		case OPT_SERVER:
712			opts.am_server = 1
713
714		case OPT_SENDER:
715			if opts.am_server == 0 {
716				return nil, fmt.Errorf("--sender only allowed with --server")
717			}
718			opts.am_sender = 1
719
720		case OPT_DAEMON:
721			// Parse the whole command-line using the daemon options table.
722			table := opts.daemonTable()
723			if gokrazyTable {
724				table = slices.Concat(opts.Gokrazy.table(), table)
725			}
726			pc := Context{
727				Options: opts,
728				table:   table,
729				args:    args,
730			}
731
732			for {
733				opt, err := pc.poptGetNextOpt()
734				if err != nil {
735					return nil, err
736				}
737				if opt == -1 {
738					break // done
739				}
740				// Most options are handled by poptGetNextOpt, only special cases
741				// are returned and handled here.
742				switch opt {
743				case 'M':
744					return nil, errNotYetImplemented
745				case 'v':
746					opts.verbose++
747				default:
748					return nil, fmt.Errorf("unhandled special case opt: %v", opt)
749				}
750			}
751
752			opts.am_daemon = 1
753
754			return &pc, nil
755
756		case OPT_FILTER,
757			OPT_EXCLUDE,
758			OPT_INCLUDE,
759			OPT_INCLUDE_FROM,
760			OPT_EXCLUDE_FROM:
761			return nil, errNotYetImplemented
762
763		case 'a':
764			if opts.recurse == 0 {
765				opts.recurse = 1
766			}
767			opts.preserve_links = 1
768			opts.preserve_perms = 1
769			opts.preserve_mtimes = 1
770			opts.preserve_gid = 1
771			opts.preserve_uid = 1
772			opts.preserve_devices = 1
773			opts.preserve_specials = 1
774
775		case 'D':
776			opts.preserve_devices = 1
777			opts.preserve_specials = 1
778
779		case OPT_NO_D:
780			opts.preserve_devices = 0
781			opts.preserve_specials = 0
782
783		case 'h':
784			opts.human_readable++
785
786		case 'H':
787			opts.preserve_hard_links = 1
788
789		case 'i':
790			opts.itemize_changes++
791
792		case 'U':
793			opts.preserve_atimes++
794			if opts.preserve_atimes > 1 {
795				opts.open_noatime = 1
796			}
797
798		case 'v':
799			opts.verbose++
800
801		case 'y':
802			return nil, errNotYetImplemented
803
804		case 'q':
805			opts.quiet++
806
807		case 'x':
808			opts.one_file_system++
809
810		case 'F':
811			return nil, errNotYetImplemented
812
813		case 'P':
814			opts.do_progress = 1
815			opts.keep_partial = 1
816
817		case 'z':
818			opts.do_compression++
819
820		case OPT_OLD_COMPRESS:
821			opts.compress_choice = "zlib"
822
823		case OPT_NEW_COMPRESS:
824			opts.compress_choice = "zlibx"
825
826		case OPT_NO_COMPRESS:
827			opts.do_compression = 0
828			opts.compress_choice = ""
829
830		case OPT_OLD_ARGS:
831			return nil, errNotYetImplemented
832
833		case 'M': // --remote-option
834			return nil, errNotYetImplemented
835
836		case OPT_WRITE_BATCH,
837			OPT_ONLY_WRITE_BATCH,
838			OPT_READ_BATCH:
839			return nil, errNotYetImplemented
840
841		case OPT_BLOCK_SIZE:
842			return nil, errNotYetImplemented
843
844		case OPT_MAX_SIZE, // (needs parse_size_arg)
845			OPT_MIN_SIZE,
846			OPT_BWLIMIT:
847			return nil, errNotYetImplemented
848
849		case OPT_APPEND:
850			return nil, errNotYetImplemented
851
852		case OPT_LINK_DEST,
853			OPT_COPY_DEST,
854			OPT_COMPARE_DEST:
855			return nil, errNotYetImplemented
856
857		case OPT_CHMOD: // (needs parse_chmod):
858			return nil, errNotYetImplemented
859
860		case OPT_INFO:
861			parseOutputWords(infoWords[:], opts.info[:], pc.poptGetOptArg(), USER_PRIORITY)
862
863		case OPT_DEBUG:
864			// TODO: plumb the debug level that make sense for our implementation
865			slog.Info("TODO: set debug level", "to", pc.poptGetOptArg())
866
867		case OPT_USERMAP,
868			OPT_GROUPMAP,
869			OPT_CHOWN:
870			return nil, errNotYetImplemented
871
872		case 'A':
873			return nil, fmt.Errorf("ACLs are not supported by gokrazy/rsync")
874
875		case 'X':
876			opts.preserve_xattrs++
877
878		case OPT_STOP_AFTER,
879			OPT_STOP_AT,
880			OPT_STDERR:
881			return nil, errNotYetImplemented
882
883		default:
884			return nil, fmt.Errorf("unhandled special case opt: %v", opt)
885		}
886	}
887
888	// rsync/options.c line 1973 and following set option defaults based on
889	// other options
890
891	opts.setOutputVerbosity(DEFAULT_PRIORITY)
892
893	if opts.recurse != 0 {
894		opts.xfer_dirs = 1
895	}
896	if opts.xfer_dirs < 0 {
897		if opts.list_only != 0 {
898			opts.xfer_dirs = 1
899		} else {
900			opts.xfer_dirs = 0
901		}
902	}
903
904	if opts.relative_paths < 0 {
905		if opts.files_from != "" {
906			opts.relative_paths = 1
907		} else {
908			opts.relative_paths = 0
909		}
910	}
911
912	if opts.relative_paths == 0 {
913		opts.implied_dirs = 0
914	}
915
916	// NOTE: This simplification means that even if we ignore POPT_ARGFLAG_OR
917	// and store ints without regards for bit sets, we get the same result.
918	// Nevertheless, we support bit to be future-proof as new options are added.
919	if opts.missing_args == 3 {
920		// simplify if both options were specified
921		opts.missing_args = 2
922	}
923
924	if opts.backup_suffix == "" && opts.backup_dir == "" {
925		opts.backup_suffix = "~"
926	}
927
928	if opts.backup_dir != "" {
929		opts.make_backups = 1 // --backup-dir implies --backup
930	}
931
932	if opts.do_progress != 0 /* && !opts.am_server */ {
933		if opts.info[INFO_NAME] == 0 {
934			opts.info[INFO_NAME] = 1
935		}
936	}
937
938	if opts.info[INFO_NAME] >= 1 && opts.stdout_format == "" {
939		opts.stdout_format = "%n%L"
940	}
941
942	return &pc, nil
943}