userdb: add birthDate field to JSON user records

Add a birthDate field to the JSON user record, stored internally as a
struct tm with INT_MIN/negative sentinels for unset fields. The field
is serialized as a YYYY-MM-DD string in JSON and validated via
parse_birth_date(), which shares its core logic with
parse_calendar_date() through a new parse_calendar_date_full()
function.

For birth dates, timegm() is called directly (rather than
mktime_or_timegm_usec) to support pre-epoch dates. The wday field is
used to distinguish timegm() failure from a valid (time_t) -1 return.

birthDate is excluded from user_record_self_modifiable_fields(), so
only administrators can set or change it via homectl. The field
remains in the regular (non-privileged) JSON section, keeping it
readable by the user and applications.
This commit is contained in:
Dylan M. Taylor
2026-03-18 17:33:22 -04:00
parent 770958e24a
commit 7a858878a0
10 changed files with 148 additions and 9 deletions
+26 -7
View File
@@ -1893,9 +1893,8 @@ TimestampStyle timestamp_style_from_string(const char *s) {
return t;
}
int parse_calendar_date(const char *s, usec_t *ret) {
int parse_calendar_date_full(const char *s, bool allow_pre_epoch, usec_t *ret_usec, struct tm *ret_tm) {
struct tm parsed_tm = {}, copy_tm;
usec_t usec;
const char *k;
int r;
@@ -1906,9 +1905,22 @@ int parse_calendar_date(const char *s, usec_t *ret) {
return -EINVAL;
copy_tm = parsed_tm;
r = mktime_or_timegm_usec(&copy_tm, /* utc= */ true, &usec);
if (r < 0)
return r;
usec_t usec = USEC_INFINITY;
if (allow_pre_epoch) {
/* For birth dates we use timegm() directly since we need to accept pre-epoch dates.
* timegm() returns (time_t) -1 both on error and for one second before the epoch.
* Initialize wday to -1 beforehand: if it remains -1 after the call, it's a genuine
* error; if timegm() changed it, the date was successfully normalized. */
copy_tm.tm_wday = -1;
if (timegm(&copy_tm) == (time_t) -1 && copy_tm.tm_wday == -1)
return -EINVAL;
} else {
r = mktime_or_timegm_usec(&copy_tm, /* utc= */ true, &usec);
if (r < 0)
return r;
}
/* Refuse non-normalized dates, e.g. Feb 30 */
if (copy_tm.tm_mday != parsed_tm.tm_mday ||
@@ -1916,8 +1928,15 @@ int parse_calendar_date(const char *s, usec_t *ret) {
copy_tm.tm_year != parsed_tm.tm_year)
return -EINVAL;
if (ret)
*ret = usec;
if (ret_usec)
*ret_usec = usec;
if (ret_tm) {
/* Reset to unset, then fill in only the date fields we parsed and validated */
*ret_tm = BIRTH_DATE_UNSET;
ret_tm->tm_mday = parsed_tm.tm_mday;
ret_tm->tm_mon = parsed_tm.tm_mon;
ret_tm->tm_year = parsed_tm.tm_year;
}
return 0;
}
+18 -1
View File
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include <limits.h>
#include <time.h>
#include "basic-forward.h"
@@ -180,7 +181,23 @@ const char* etc_localtime(void);
int mktime_or_timegm_usec(struct tm *tm, bool utc, usec_t *ret);
int localtime_or_gmtime_usec(usec_t t, bool utc, struct tm *ret);
int parse_calendar_date(const char *s, usec_t *ret);
int parse_calendar_date_full(const char *s, bool allow_pre_epoch, usec_t *ret_usec, struct tm *ret_tm);
static inline int parse_calendar_date(const char *s, usec_t *ret) {
return parse_calendar_date_full(s, /* allow_pre_epoch= */ false, ret, NULL);
}
#define BIRTH_DATE_UNSET \
(const struct tm) { \
.tm_year = INT_MIN, \
}
#define BIRTH_DATE_IS_SET(tm) ((tm).tm_year != INT_MIN)
static inline int parse_birth_date(const char *s, struct tm *ret) {
return parse_calendar_date_full(s, /* allow_pre_epoch= */ true, NULL, ret);
}
uint32_t usec_to_jiffies(usec_t usec);
usec_t jiffies_to_usec(uint32_t jiffies);