[Pkg-shadow-devel] [Git][debian/adduser][debian/latest] 7 commits: improve log level handling

Marc Haber (@zugschlus) gitlab at salsa.debian.org
Sat Jan 17 08:30:30 GMT 2026



Marc Haber pushed to branch debian/latest at Debian / adduser


Commits:
51c81c3e by Marc Haber at 2026-01-16T07:59:20+01:00
improve log level handling

Thanks: Matt Barry
Git-Dch: ignore

- - - - -
b7725726 by Marc Haber at 2026-01-16T07:59:20+01:00
clarify documentation of exit value 31

Git-Dch: ignore

- - - - -
571f8054 by Marc Haber at 2026-01-16T07:59:20+01:00
rename to RET_INVALID_CHARS_IN_INPUT, apply to comment as well

That was RET_INVALID_CHARS_IN_INPUT previously. The check is now
applied to the comment as well and the error message adapted.

Git-Dch: ignore

- - - - -
783d4d09 by Marc Haber at 2026-01-16T07:59:20+01:00
move interactive command loops to a function

This is more streamlined and handles running on no terminal better

- - - - -
8ec9dd8a by Marc Haber at 2026-01-16T07:59:20+01:00
re-work logic around remove-home etc

Git-Dch: ignore

- - - - -
ab1adc30 by Marc Haber at 2026-01-16T07:59:20+01:00
prepare testsuite libraries to properly handle EXISTING_

this brings the simplifications to the test suite libraries

Git-Dch: ignore

- - - - -
67c314d0 by Marc Haber at 2026-01-16T07:59:20+01:00
add assert_path_is_a_file to debian/tests/lib

Git-Dch: ignore

- - - - -


6 changed files:

- AdduserRetvalues.pm
- adduser
- debian/tests/lib/AdduserTestsCommon.pm
- deluser
- doc/adduser.8
- testsuite/lib_test.pm


Changes:

=====================================
AdduserRetvalues.pm
=====================================
@@ -20,7 +20,7 @@ use vars qw(@EXPORT $VAR1);
     'RET_ID_IN_USE',
     'RET_NO_ID_IN_RANGE',
     'RET_NO_PRIMARY_GID',
-    'RET_INVALID_CHARS_IN_NAME',
+    'RET_INVALID_CHARS_IN_INPUT',
     'RET_INVALID_HOME_DIRECTORY',
     'RET_INVALID_NAME_FROM_USERADD',
     'RET_GROUP_NOT_EMPTY',
@@ -61,8 +61,8 @@ use constant RET_NO_PRIMARY_GID => 23; # requested primary GID does not exist
 
 # object name errors
 
-use constant RET_INVALID_CHARS_IN_NAME => 31; # the provided name contains invalid characters
-use constant RET_INVALID_HOME_DIRECTORY => 32; # the provided name contains invalid characters
+use constant RET_INVALID_CHARS_IN_INPUT => 31; # provided input (name/comment) contains invalid characters
+use constant RET_INVALID_HOME_DIRECTORY => 32; # the provided home directory is invalid
 use constant RET_INVALID_NAME_FROM_USERADD => 32; # useradd returned 19 "invalid user or group name"
 
 # group membership errors


=====================================
adduser
=====================================
@@ -103,9 +103,9 @@ $0 =~ s+.*/++;
 our $action;
 our $verbose;		# should we be verbose?
 my $name_check_level = 0;		# should we allow bad names?
-our $stdoutmsglevel = "warn";
-our $stderrmsglevel = "warn";
-our $logmsglevel = "info";
+our $stdoutmsglevel = undef;
+our $stderrmsglevel = undef;
+our $logmsglevel = undef;
 my $allow_badname = 0;		# should we allow bad names?
 my $ask_passwd = 1;		# ask for a passwd?
 my $disabled_login = 0;		# leave the new account disabled?
@@ -199,10 +199,11 @@ if (!@configfiles) {
 
 # make sure that message levels apply for reading configuration
 # this will be overridden again after reading configuration
-$stdoutmsglevel = sanitize_string($stdoutmsglevel);
-$stderrmsglevel = sanitize_string($stderrmsglevel);
-$logmsglevel = sanitize_string($logmsglevel);
-set_msglevel( $stderrmsglevel, $stdoutmsglevel, $logmsglevel );
+set_msglevel( 
+    sanitize_string($stderrmsglevel or STDERRDEFLEVEL),
+    sanitize_string($stdoutmsglevel or STDOUTDEFLEVEL),
+    sanitize_string($logmsglevel or LOGMSGDEFLEVEL),
+);
 log_trace("ARGV %s", join(@ARGV,"-"));
 log_trace("special_home %s", $special_home);
 log_trace("special_home %s", encode($charset, $special_home));
@@ -223,8 +224,13 @@ if( $> != 0) {
     log_fatal( mtx("Only root may add a user or group to the system.") );
     exit( RET_ROOT_NEEDED );
 }
-
-# TODO: Handle configuration file input, allow bare input there.
+# ARGV > adduser.conf > (default)
+$stdoutmsglevel //= $config{'stdoutmsglevel'};
+$stdoutmsglevel //= STDOUTDEFLEVEL;
+$stderrmsglevel //= $config{'stderrmsglevel'}; 
+$stderrmsglevel //= STDERRDEFLEVEL;
+$logmsglevel    //= $config{'logmsglevel'};
+$logmsglevel    //= LOGMSGDEFLEVEL;
 $stdoutmsglevel = sanitize_string($stdoutmsglevel);
 $stderrmsglevel = sanitize_string($stderrmsglevel);
 $logmsglevel = sanitize_string($logmsglevel);
@@ -235,6 +241,7 @@ if( defined $verbose ) {
     } elsif( $verbose == 1 ) {
         set_msglevel( $stderrmsglevel, "info", $logmsglevel );
     } elsif( $verbose == 2 ) {
+        set_msglevel( $stdoutmsglevel, "debug", $logmsglevel );
         set_msglevel( $stderrmsglevel, "debug", $logmsglevel );
     }
 }
@@ -364,7 +371,8 @@ if ( defined $comment_tainted ) {
     log_trace("check comment %s for unwanted chars", $special_home);
     # do not sanitize, can't be done without libperl
     if ( $comment_tainted !~ qr/^([^\x00-\x1F\x7F:]*)$/ ) {
-        die( "unwanted chars in comment" );
+        log_fatal( mtx("unwanted chars in comment") );
+        exit( RET_INVALID_CHARS_IN_INPUT );
     }
 }
 if( defined $special_shell ) {
@@ -403,8 +411,8 @@ if( defined $gid_option ) {
 }
 
 if ((defined($special_home)) && ($special_home !~ m+^/+ )) {
-  log_fatal( mtx("The home dir must be an absolute path.") );
-  exit( RET_INVALID_HOME_DIRECTORY );
+    log_fatal( mtx("The home dir must be an absolute path.") );
+    exit( RET_INVALID_HOME_DIRECTORY );
 }
 
 
@@ -942,49 +950,20 @@ if ($action eq "adduser") {
     create_homedir ($no_copy_skel ? 0 : 1, 0); # copy skeleton data
 
     # useradd without -p has left the account disabled (password string is '!')
-    my $yesexpr = langinfo(YESEXPR());
-    my $noexpr = langinfo(NOEXPR());
     if ($ask_passwd) {
-        PASSWD: for (;;) {
-            my $passwd = which('passwd');
-            my $ok = systemcall_or_warn($passwd, $new_name);
-            $ok = $ok >> 8;
-            log_debug( "systemcall_or_warn %s %s return value %s", $passwd, $new_name, $ok);
-            if ($ok != 0) {
-                my $answer;
-                # hm, error, should we break now?
-                if ($ok == 1) {
-                    log_warn( mtx("Permission denied"));
-                } elsif ($ok == 2) {
-                    log_warn( mtx("invalid combination of options"));
-                } elsif ($ok == 3) {
-                    log_warn( mtx("unexpected failure, nothing done"));
-                } elsif ($ok == 4) {
-                    log_warn( mtx("unexpected failure, passwd file missing"));
-                } elsif ($ok == 5) {
-                    log_warn( mtx("passwd file busy, try again"));
-                } elsif ($ok == 6) {
-                    log_warn( mtx("invalid argument to option"));
-                } elsif ($ok == 10) {
-                    log_warn( mtx("wrong password given or password retyped incorrectly"));
-                } else {
-                    log_warn( mtx("unexpected return code %s given from passwd"), $ok );
-                }
+        my %passwd_errors = (
+            1  => "Permission denied",
+            2  => "Invalid combination of options",
+            3  => "Unexpected failure, nothing done",
+            4  => "Passwd file missing",
+            5  => "Passwd file busy, try again",
+            6  => "Invalid argument to option",
+            10 => "Wrong password given or retyped incorrectly",
+        );
 
-                # Translators: [y/N] has to be replaced by values defined in your
-                # locale.  You can see by running "locale noexpr" which regular
-                # expression will be checked to find positive answer.
-                PROMPT: for (;;) {
-                    print (gtx("Try again? [y/N] "));
-                    chop ($answer=<STDIN>);
-                    last PROMPT if ($answer =~ m/$yesexpr/o);
-                    last PASSWD if ($answer =~ m/$noexpr/o);
-                    last PASSWD if (!$answer);
-                }
-            } else {
-                last; ## passwd ok
-            }
-        }
+        interactive_command_with_confirmation(
+            'passwd', $new_name, \%passwd_errors
+        );
     }
 
     if (defined($comment_tainted)) {
@@ -992,22 +971,14 @@ if ($action eq "adduser") {
     } elsif ($uid_pool{$new_name}{'comment'}) {
         ch_comment($new_name, $uid_pool{$new_name}{'comment'});
     } else {
-        my $noexpr = langinfo(NOEXPR());
-        my $yesexpr = langinfo(YESEXPR());
-        CHFN: for (;;) {
-            my $chfn = &which('chfn');
-            systemcall($chfn, $new_name);
-            # Translators: [y/N] has to be replaced by values defined in your
-            # locale.  You can see by running "locale yesexpr" which regular
-            # expression will be checked to find positive answer.
-            PROMPT: for (;;) {
-                print (gtx("Is the information correct? [Y/n] "));
-                chop (my $answer=<STDIN>);
-                last PROMPT if ($answer =~ m/$noexpr/o);
-                last CHFN if ($answer =~ m/$yesexpr/o);
-                last CHFN if (!$answer);
-            }
-        }
+        my %chfn_errors = (
+            1 => "Permission denied",
+            2  => "Invalid combination of options",
+        );
+
+        interactive_command_with_confirmation(
+            'chfn', $new_name, \%chfn_errors, 1
+        );
     }
 
     if ( ( $add_extra_groups || $config{"add_extra_groups"} ) && defined($config{"extra_groups"}) ) {
@@ -1309,21 +1280,21 @@ sub sanitize_name {
         # this check cannot be turned off
         log_err( mtx("To avoid ambiguity with numerical UIDs, usernames which" .
             "resemble numbers or negative numbers are not allowed.") );
-        exit( RET_INVALID_CHARS_IN_NAME );
+        exit( RET_INVALID_CHARS_IN_INPUT );
     }
 
     log_trace("sanitize_name testing single or double period");
     if ( $name =~ qr/^\.\.?$/ ) {
         # this check cannot be turned off
         log_err( mtx("Usernames must not be a single or a double period.") );
-        exit( RET_INVALID_CHARS_IN_NAME );
+        exit( RET_INVALID_CHARS_IN_INPUT );
     }
 
     log_trace("sanitize_name testing > 32 chars");
     if (length( encode($charset, $name) ) > 32) {
         # this check cannot be turned off
         log_err( mtx("Usernames must be no more than 32 bytes in length.") );
-        exit( RET_INVALID_CHARS_IN_NAME );
+        exit( RET_INVALID_CHARS_IN_INPUT );
     }
 
     log_trace("sanitize_name testing %s against insane chars %s", $name, def_min_regex);
@@ -1333,7 +1304,7 @@ sub sanitize_name {
             "dash, plus sign, or tilde, and it must not contain any of the" .
             "following: colon, comma, slash, or any whitespace characters" .
             "including spaces, tabs, and newlines.") );
-        exit( RET_INVALID_CHARS_IN_NAME );
+        exit( RET_INVALID_CHARS_IN_INPUT );
     }
 
     log_trace("sanitize_name checking %s against %s (%s)", $name, $name_regex, $name_regex_var);
@@ -1350,7 +1321,7 @@ sub sanitize_name {
             "compatibility with Samba machine accounts, \$ is also supported" .
             "at the end of the username.  (Use the `--allow-all-names' option" .
             "to bypass this restriction.)") );
-        exit( RET_INVALID_CHARS_IN_NAME );
+        exit( RET_INVALID_CHARS_IN_INPUT );
     }
 
     if ($name_check_level) {
@@ -1360,7 +1331,7 @@ sub sanitize_name {
             "configured via the %s configuration variable.  Use the" .
             "`--allow-bad-names' option to relax this check or reconfigure" .
             "%s in configuration."), $name_regex_var, $name_regex_var );
-        exit( RET_INVALID_CHARS_IN_NAME );
+        exit( RET_INVALID_CHARS_IN_INPUT );
     }
 
     log_trace("sanitize_name checking %s against %s", $name, anynamere);
@@ -1466,6 +1437,64 @@ sub user_is_member {
 }
 
 
+sub interactive_command_with_confirmation {
+    my ($cmd, $user_name, $error_map, $always_confirm) = @_;
+
+    $always_confirm //= 0;   # default to false if not provided
+    return unless -t STDIN;  # skip non-interactive
+
+    my $yesexpr = langinfo(YESEXPR);
+    my $noexpr  = langinfo(NOEXPR);
+
+    my $retry = 1;
+
+    while ($retry) {
+        $retry = 0;
+
+        my $full_cmd = which($cmd);
+        my $ok = systemcall_or_warn($full_cmd, $user_name);
+        $ok = $ok >> 8;  # extract exit code
+
+        log_debug("systemcall_or_warn %s %s return value %s", $full_cmd, $user_name, $ok);
+
+        if ($ok != 0 && $error_map) {
+            if (exists $error_map->{$ok}) {
+                log_warn($error_map->{$ok});
+            } else {
+                log_warn("Unexpected return code %s from %s", $ok, $cmd);
+            }
+        }
+
+        # Prompt for confirmation if requested
+        if ($always_confirm) {
+            my $answer;
+            do {
+                print gtx("Is the information correct? [Y/n] ");
+                $answer = <STDIN>;
+
+                unless (defined $answer) {
+                    warn "No input available, assuming 'yes'\n";
+                    last;
+                }
+
+                chomp($answer);
+                $answer =~ s/^\s+|\s+$//g;
+
+                if ($answer =~ m/$yesexpr/o || $answer eq '') {
+                    $retry = 0;  # accepted
+                    last;
+                } elsif ($answer =~ m/$noexpr/o) {
+                    $retry = 1;  # retry command
+                } else {
+                    print "Please answer yes or no.\n";
+                    $answer = undef;  # repeat prompt
+                }
+
+            } while (!defined $answer);
+        }
+    }
+}
+
 sub cleanup {
     if ($undohome) {
         log_info( mtx("Removing directory `%s' ..."), $undohome);


=====================================
debian/tests/lib/AdduserTestsCommon.pm
=====================================
@@ -30,6 +30,21 @@ END {
     if (-f '/var/cache/adduser/tests/state.tar');
 }
 
+use constant {
+    EXISTING_NOT_FOUND => 0,
+    EXISTING_FOUND => 1,
+    EXISTING_SYSTEM => 2,
+    EXISTING_ID_MISMATCH => 4,
+    EXISTING_LOCKED => 8,
+    EXISTING_HAS_PASSWORD => 16,
+    EXISTING_EXPIRED => 32,
+    EXISTING_NOLOGIN => 64,
+    SYS_MIN => 100,
+    SYS_MAX => 999,
+    USER_MIN => 1000,
+    USER_MAX => 9999,
+};
+
 my $charset = langinfo(CODESET);
 binmode(STDOUT, ":encoding($charset)");
 binmode(STDERR, ":encoding($charset)");
@@ -293,7 +308,7 @@ sub assert_path_has_ownership {
 
 sub assert_path_is_a_file {
     my $path = shift;
-    ok(-f $path, "path is a file $path");
+    ok(-f $path, "path is a file: $path");
 }
 
 sub assert_path_is_a_directory {
@@ -459,6 +474,83 @@ sub apply_config_hash {
     close(CONF);
 }
 
+sub assert_user_status {
+    my ($username, $mask, $desc, $invert) = @_;
+
+    # Determine inversion from description if it starts with 'NOT '
+    if ($desc =~ /^NOT\s+(.*)/i) {
+        die "Cannot set invert=1 if description starts with NOT" if $invert;
+        $desc = $1;      # remove 'NOT ' prefix
+        $invert = 1;     # automatically invert
+    }
+
+    $invert //= 0;       # default to positive assertion
+
+    my $status = existing_user_status($username);
+    my $ok = ($status & $mask) == $mask;
+    $ok = !$ok if $invert;
+
+    my $message = $invert
+        ? "User '$username' is NOT $desc (status $status)"
+        : "User '$username' $desc (status $status)";
+
+    ok($ok, $message);
+}
+
+
+sub existing_user_status {
+    my ($user_name,$user_uid) = @_;
+    my $ret = EXISTING_NOT_FOUND;
+        my (
+        $egpwn_name, $egpwn_passwd, $egpwn_uid, $egpwn_gid, $egpwn_quota,
+        $egpwn_comment, $egpwn_gcos, $egpwn_dir, $egpwn_shell, $egpwn_expire,
+        $egpwn_rest
+    ) = getpwnam($user_name);
+
+    if (defined $egpwn_uid) {
+        $ret |= EXISTING_FOUND;
+        $ret |= EXISTING_ID_MISMATCH if (defined($user_uid) && $egpwn_uid != $user_uid);
+        $ret |= EXISTING_SYSTEM if \
+            ($egpwn_uid >= SYS_MIN && $egpwn_uid <= SYS_MAX);
+
+        $ret |= EXISTING_NOLOGIN if ($egpwn_shell =~ /bin\/nologin/);
+        $ret |= EXISTING_HAS_PASSWORD if
+            (defined $egpwn_passwd && $egpwn_passwd ne '' && ($egpwn_passwd =~ s/^[!*]+//r ne ''));
+        $ret |= EXISTING_LOCKED if
+            (defined $egpwn_passwd && $egpwn_passwd =~ /^[!*]/);
+
+        my $age = `chage -l $user_name`;
+        if ($age =~ /Account expires\s*:\s*(.+)/i) {
+            my $exp = $1;
+            use POSIX qw(strftime);
+            use Time::Local;
+            if ($exp ne 'never') {
+                my $expiry_epoch = eval { `date -d "$exp" +%s` };
+                my $now = time;
+                $ret |= EXISTING_EXPIRED if ($expiry_epoch < $now);
+            }
+        }
+    } elsif ($user_uid && getpwuid($user_uid)) {
+        $ret |= EXISTING_ID_MISMATCH;
+    }
+    return $ret;
+}
+
+sub existing_group_status {
+    my ($group_name,$group_gid) = @_;
+    my $gid;
+    my $ret = EXISTING_NOT_FOUND;
+    if ((undef,undef,$gid) = egetgrnam($group_name)) {
+        $ret |= EXISTING_FOUND;
+        $ret |= EXISTING_ID_MISMATCH if (defined($group_gid) && $gid != $group_gid);
+        $ret |= EXISTING_SYSTEM if \
+            ($gid >= SYS_MIN && $gid <= SYS_MAX);
+    } elsif ($group_gid && getgrgid($group_gid)) {
+        $ret |= EXISTING_ID_MISMATCH;
+    }
+    return $ret;
+}
+
 1;
 
 # vim: tabstop=4 shiftwidth=4 expandtab


=====================================
deluser
=====================================
@@ -265,21 +265,28 @@ if(defined($group)) {
 if($action eq "deluser") {
     my($name, $passwd, ,$uid, $rest);
 
+    if (($config{remove_home} || $config{remove_all_files} || $config{backup}) && ($install_more_packages)) {
+        log_warn( mtx("In order to use the --remove-home, --remove-all-files, and --backup features, you need to install the `perl' package. To accomplish that, run apt-get install perl.") );
+        $config{remove_home}=undef;
+        $config{remove_all_files}=undef;
+        $config{backup}=undef;
+        $config{backup_to}=undef;
+        exit ( RET_MORE_PACKAGES) unless( $config{"system"} );
+    }
+
+    my (
+        $egpwn_name, $egpwn_passwd, $egpwn_uid, $egpwn_gid, $egpwn_quota,
+        $egpwn_comment, $egpwn_gcos, $egpwn_dir, $egpwn_shell, $egpwn_expire,
+        $egpwn_rest
+    ) = egetpwnam(encode($charset, $user));
+
     # Don't allow a non-system user to be deleted when --system is given
     # Also, "user does not exist" is only a warning with --system, but an
     # error without --system.
     if( $config{"system"} ) {
-        if (($config{remove_home} || $config{remove_all_files} || $config{backup}) && ($install_more_packages)) {
-            log_warn( mtx("In order to use the --remove-home, --remove-all-files, and --backup features, you need to install the `perl' package. To accomplish that, run apt-get install perl.") );
-            $config{remove_home}=undef;
-            $config{remove_all_files}=undef;
-            $config{backup}=undef;
-            $config{backup_to}=undef;
-        }
-
-        if( ($name, $passwd, $uid, $rest) = egetpwnam(encode($charset, $user)) ) {
-            if ( ($uid < $config{"first_system_uid"} ||
-                $uid > $config{"last_system_uid" } ) ) {
+        if( defined $egpwn_uid ) {
+            if ( ($egpwn_uid < $config{"first_system_uid"} ||
+                $egpwn_uid > $config{"last_system_uid" } ) ) {
                 log_warn( mtx("The user `%s' is not a system user. Exiting."), $user);
                 exit( RET_WRONG_OBJECT_PROPERTIES );
             }
@@ -287,14 +294,9 @@ if($action eq "deluser") {
             log_info( mtx("The user `%s' does not exist, but --system was given. Exiting."), $user);
             exit( RET_OK );
         }
-    } else {
-        if (($config{remove_home} || $config{remove_all_files} || $config{backup}) && ($install_more_packages)) {
-            log_fatal( mtx("In order to use the --remove-home, --remove-all-files, and --backup features, you need to install the `perl' package. To accomplish that, run apt-get install perl.") );
-            exit( RET_MORE_PACKAGES );
-        }
     }
 
-    unless(exist_user($user)) {
+    unless(defined $egpwn_uid) {
         log_fatal( mtx("The user `%s' does not exist."), $user );
         exit( RET_OBJECT_DOES_NOT_EXIST );
     }


=====================================
doc/adduser.8
=====================================
@@ -613,8 +613,8 @@ There is no group with the requested GID for the primary group
 for a new user.
 .TP
 .B 31
-The chosen name for a new user or a new group does not conform to
-the selected naming rules.
+The chosen name or comment for a new user or a new group
+does not conform to the selected naming rules.
 .TP
 .B 32
 The home directory of a new user must be an absolute path.


=====================================
testsuite/lib_test.pm
=====================================
@@ -322,6 +322,94 @@ sub check_user_status {
 
 
 
+sub testsuite_existing_user_status {
+    my ($user_name,$user_uid) = @_;
+    my $ret = EXISTING_NOT_FOUND;
+
+    my (
+        $egpwn_name, $egpwn_passwd, $egpwn_uid, $egpwn_gid, $egpwn_quota,
+        $egpwn_comment, $egpwn_gcos, $egpwn_dir, $egpwn_shell, $egpwn_expire,
+        $egpwn_rest
+    ) = getpwnam($user_name);
+
+    if (defined $egpwn_uid) {
+        $ret |= EXISTING_FOUND;
+        $ret |= EXISTING_ID_MISMATCH if (defined($user_uid) && $egpwn_uid != $user_uid);
+        $ret |= EXISTING_SYSTEM if \
+            ($egpwn_uid >= SYS_MIN && $egpwn_uid <= SYS_MAX);
+
+        $ret |= EXISTING_NOLOGIN if ($egpwn_shell =~ /bin\/nologin/);
+        $ret |= EXISTING_HAS_PASSWORD if
+            (defined $egpwn_passwd && $egpwn_passwd ne '' && ($egpwn_passwd =~ s/^[!*]+//r ne ''));
+        $ret |= EXISTING_LOCKED if
+            (defined $egpwn_passwd && $egpwn_passwd =~ /^[!*]/);
+
+        # this is deliberately implemented differently from the actual program
+        my $age = `chage -l $user_name`;
+
+        if ($age =~ /Account expires\s*:\s*(.+)/i) {
+            my $exp = $1;
+            if ($exp ne 'never') {
+                chomp $exp;
+                # Convert to epoch using GNU date
+                my $expiry_epoch = `date -d "$exp" +%s 2>/dev/null`;
+                chomp $expiry_epoch;
+
+                if (defined $expiry_epoch && $expiry_epoch =~ /^\d+$/) {
+                    $ret |= EXISTING_EXPIRED if ($expiry_epoch < time);
+                } else {
+                    warn "Failed to parse expiry date '$exp' with date command\n";
+                }
+            }
+        }
+    } elsif ($user_uid && getpwuid($user_uid)) {
+        $ret |= EXISTING_ID_MISMATCH;
+    }
+    return $ret;
+}
+
+# Map human-readable status names to bitmask constants
+my %USER_STATUS_MASK = (
+    locked      => EXISTING_LOCKED,
+    haspasswd   => EXISTING_HAS_PASSWORD,
+    nologin     => EXISTING_NOLOGIN,
+    expired     => EXISTING_EXPIRED,
+);
+
+sub check_user_status {
+    my ($username, $check, $do_print) = @_;
+    $do_print //= 0;
+
+    my $invert = 0;
+    my $result;
+
+    # Check for negative prefix "not_"
+    if ($check =~ /^not_(.+)$/) {
+        $invert = 1;
+        $check = $1;
+    }
+
+    my $mask = $USER_STATUS_MASK{$check}
+        or die "Unknown user status '$check'";
+
+    my $status = testsuite_existing_user_status($username);
+    # returns 0 if status is as desired so that it can be used in assertion
+    $result = (($status & $mask) == $mask) ? 0 : 1;
+
+    if ($do_print) {
+        my $msg = $result
+                ? "User '$username' $check"
+                : "User '$username' NOT $check";
+        print "$msg";
+    }
+
+    $result = !$result if $invert;
+    print " (status $status, returning ", $result ? 1 : 0, ")\n";
+    return $result;
+}
+
+
+
 return 1
 
 # vim: tabstop=4 shiftwidth=4 expandtab



View it on GitLab: https://salsa.debian.org/debian/adduser/-/compare/d8b8b51509b797d0bbd544153c94cf13da1b9838...67c314d0a72efeb4bbc748f1fcd3f2f174aed853

-- 
View it on GitLab: https://salsa.debian.org/debian/adduser/-/compare/d8b8b51509b797d0bbd544153c94cf13da1b9838...67c314d0a72efeb4bbc748f1fcd3f2f174aed853
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-shadow-devel/attachments/20260117/b5c39cbc/attachment-0001.htm>


More information about the Pkg-shadow-devel mailing list