. "$BST_LIB" require xdg-user-dirs-update xdg-user-dirs/present # xdg-user-dirs is two programs sharing one job. xdg-user-dirs-update (C) CREATES # the well-known user directories and writes the shell-sourceable # ~/.config/user-dirs.dirs; xdg-user-dir (an eight-line /bin/sh script) READS that # file back and prints one directory. Everything runs against a scratch HOME so # the real one is never touched -- except the last block, which has to run on the # ROOT filesystem to mean anything: $HOME here is on virtio-fs and /tmp is a # tmpfs, and mkstemp+rename is the pair that would tell them apart. scratch=${TMPDIR:-/tmp}/bst-xdg-user-dirs.$$ H=$scratch/home mkdir -p "$H" || { bst_notok xdg-user-dirs/scratch "cannot create $H"; exit 0; } XDGHOME=$H/.config unset XDG_CONFIG_HOME XDG_CONFIG_DIRS XDG_DATA_DIRS HOME=$H; export HOME DIRS='Desktop Downloads Templates Public Documents Music Pictures Videos' # ---------------------------------------------------------------- packaging -- command -v xdg-user-dir >/dev/null 2>&1 \ && bst_ok xdg-user-dirs/lookup-present \ || bst_notok xdg-user-dirs/lookup-present "xdg-user-dir (the reader half) not on PATH" check_eq xdg-user-dirs/update-path /usr/bin/xdg-user-dirs-update \ "$(command -v xdg-user-dirs-update)" check_eq xdg-user-dirs/lookup-path /usr/bin/xdg-user-dir \ "$(command -v xdg-user-dir)" check xdg-user-dirs/conf-shipped test -f /etc/xdg/user-dirs.conf check xdg-user-dirs/defaults-shipped test -f /etc/xdg/user-dirs.defaults # The autostart entry ships too. Nothing on this image reads it -- there is no # session manager -- but it is what a desktop would use to run the updater at # login, and it is the only file in the package that names the other half. check xdg-user-dirs/autostart-shipped test -f /etc/xdg/autostart/xdg-user-dirs.desktop check_out xdg-user-dirs/autostart-exec "Exec=xdg-user-dirs-update" \ cat /etc/xdg/autostart/xdg-user-dirs.desktop # The shipped encoding decides whether iconv is on the default path at all. check_out xdg-user-dirs/conf-utf8 "filename_encoding=UTF-8" cat /etc/xdg/user-dirs.conf check_out xdg-user-dirs/conf-enabled "enabled=True" cat /etc/xdg/user-dirs.conf KEYS='DESKTOP DOWNLOAD TEMPLATES PUBLICSHARE DOCUMENTS MUSIC PICTURES VIDEOS' missing="" for k in $KEYS; do grep -q "^$k=" /etc/xdg/user-dirs.defaults 2>/dev/null || missing="$missing $k" done if [ -z "$missing" ]; then bst_ok xdg-user-dirs/defaults-eight else bst_notok xdg-user-dirs/defaults-eight "keys absent from user-dirs.defaults:$missing"; fi # ------------------------------------------------------------- CLI surface -- # Measured against a native x86_64 build of the same 0.18 tarball, not assumed. # --help exits 0 here, the opposite of tunctl's -h, and the error text for an # unknown option goes to STDOUT rather than stderr. check_rc xdg-user-dirs/help-rc0 0 xdg-user-dirs-update --help check_out xdg-user-dirs/help-usage \ "Usage: xdg-user-dirs-update [--force] [--dummy-output ] [--set DIR path]" \ xdg-user-dirs-update --help check_rc xdg-user-dirs/badopt-rc1 1 xdg-user-dirs-update --no-such-option out=$(xdg-user-dirs-update --no-such-option 2>/dev/null) case "$out" in *"Invalid argument --no-such-option"*) bst_ok xdg-user-dirs/badopt-stdout ;; *) bst_notok xdg-user-dirs/badopt-stdout "not on stdout: '$(_bst_1line "$out")'" ;; esac check_rc xdg-user-dirs/set-relative-rc1 1 xdg-user-dirs-update --set MUSIC notabsolute out=$(xdg-user-dirs-update --set MUSIC notabsolute 2>&1) case "$out" in *"directory value must be absolute path (was notabsolute)"*) bst_ok xdg-user-dirs/set-relative-msg ;; *) bst_notok xdg-user-dirs/set-relative-msg "got:'$(_bst_1line "$out")'" ;; esac # --------------------------------------------------- --dummy-output is inert -- # It writes the config and creates NO directories. That is what makes the rest # of this suite safe to run against a home that must stay untouched. dummy=$scratch/dummy.dirs check_rc xdg-user-dirs/dummy-rc0 0 xdg-user-dirs-update --dummy-output "$dummy" check xdg-user-dirs/dummy-written test -s "$dummy" check_out xdg-user-dirs/dummy-header "# This file is written by xdg-user-dirs-update" cat "$dummy" check_eq xdg-user-dirs/dummy-eight-keys 8 "$(grep -c '^XDG_.*_DIR=' "$dummy" 2>/dev/null)" check_out xdg-user-dirs/dummy-desktop-line 'XDG_DESKTOP_DIR="$HOME/Desktop"' cat "$dummy" check_out xdg-user-dirs/dummy-publicshare-line 'XDG_PUBLICSHARE_DIR="$HOME/Public"' cat "$dummy" made="" for d in $DIRS; do [ -d "$H/$d" ] && made="$made $d"; done if [ -z "$made" ]; then bst_ok xdg-user-dirs/dummy-no-dirs else bst_notok xdg-user-dirs/dummy-no-dirs "--dummy-output created:$made"; fi if [ -e "$XDGHOME/user-dirs.dirs" ]; then bst_notok xdg-user-dirs/dummy-no-config "--dummy-output wrote the real config too" else bst_ok xdg-user-dirs/dummy-no-config; fi # ------------------------------------------------- the reader, before update -- check_eq xdg-user-dirs/lookup-desktop-default "$H/Desktop" "$(xdg-user-dir DESKTOP)" check_eq xdg-user-dirs/lookup-music-default "$H" "$(xdg-user-dir MUSIC)" check_eq xdg-user-dirs/lookup-unknown-key "$H" "$(xdg-user-dir NOSUCHKEY)" check_eq xdg-user-dirs/lookup-no-arg "$H" "$(xdg-user-dir)" # ------------------------------------------------------ the update, on tmpfs -- check_rc xdg-user-dirs/update-rc0 0 xdg-user-dirs-update absent="" for d in $DIRS; do [ -d "$H/$d" ] || absent="$absent $d"; done if [ -z "$absent" ]; then bst_ok xdg-user-dirs/update-dirs-created else bst_notok xdg-user-dirs/update-dirs-created "not created:$absent"; fi check xdg-user-dirs/update-config-written test -s "$XDGHOME/user-dirs.dirs" check_out xdg-user-dirs/update-config-music 'XDG_MUSIC_DIR="$HOME/Music"' cat "$XDGHOME/user-dirs.dirs" check xdg-user-dirs/update-locale-written test -s "$XDGHOME/user-dirs.locale" # A second run must change nothing -- this is what runs at every login. cp "$XDGHOME/user-dirs.dirs" "$scratch/first.dirs" check_rc xdg-user-dirs/update-idempotent-rc0 0 xdg-user-dirs-update check xdg-user-dirs/update-idempotent cmp -s "$scratch/first.dirs" "$XDGHOME/user-dirs.dirs" # ------------------------------------------------- the reader, after update -- check_eq xdg-user-dirs/lookup-music-after "$H/Music" "$(xdg-user-dir MUSIC)" check_eq xdg-user-dirs/lookup-pictures-after "$H/Pictures" "$(xdg-user-dir PICTURES)" check_eq xdg-user-dirs/lookup-desktop-after "$H/Desktop" "$(xdg-user-dir DESKTOP)" # XDG_CONFIG_HOME redirects BOTH halves. Point it at an empty dir: the reader # must fall back to the compiled-in defaults, not keep reading ~/.config. altcfg=$scratch/altconfig mkdir -p "$altcfg" check_eq xdg-user-dirs/lookup-xdg-config-home "$H" \ "$(XDG_CONFIG_HOME=$altcfg xdg-user-dir MUSIC)" # ------------------------------------------------------------------- --set -- check_rc xdg-user-dirs/set-abs-rc0 0 xdg-user-dirs-update --set MUSIC "$H/Tunes" check_out xdg-user-dirs/set-abs-recorded 'XDG_MUSIC_DIR="$HOME/Tunes"' cat "$XDGHOME/user-dirs.dirs" check_eq xdg-user-dirs/set-abs-lookup "$H/Tunes" "$(xdg-user-dir MUSIC)" # --set records the value; it deliberately does not create the directory. if [ -d "$H/Tunes" ]; then bst_notok xdg-user-dirs/set-no-mkdir "--set created $H/Tunes" else bst_ok xdg-user-dirs/set-no-mkdir; fi # A path outside $HOME stays absolute rather than gaining a $HOME/ prefix. check_rc xdg-user-dirs/set-outside-rc0 0 xdg-user-dirs-update --set VIDEOS "$scratch/Flicks" check_out xdg-user-dirs/set-outside-abs "XDG_VIDEOS_DIR=\"$scratch/Flicks\"" cat "$XDGHOME/user-dirs.dirs" check_eq xdg-user-dirs/set-outside-lookup "$scratch/Flicks" "$(xdg-user-dir VIDEOS)" # ------------------------------------------------- removal, and --force back -- # A directory the user deleted is reassigned to the homedir on the next run; # --force then moves it back to the default. Both paths are how this tool # behaves at a login after the user rearranged their home. out=$(xdg-user-dirs-update 2>&1); rc=$? case "$out" in *"was removed, reassigning MUSIC to homedir"*) bst_ok xdg-user-dirs/removed-reassign-msg ;; *) bst_notok xdg-user-dirs/removed-reassign-msg "rc=$rc got:'$(_bst_1line "$out")'" ;; esac check_out xdg-user-dirs/removed-reassign-recorded 'XDG_MUSIC_DIR="$HOME/"' cat "$XDGHOME/user-dirs.dirs" check_eq xdg-user-dirs/removed-reassign-lookup "$H/" "$(xdg-user-dir MUSIC)" out=$(xdg-user-dirs-update --force 2>&1); rc=$? if [ "$rc" -eq 0 ]; then bst_ok xdg-user-dirs/force-rc0 else bst_notok xdg-user-dirs/force-rc0 "rc=$rc $(_bst_1line "$out")"; fi case "$out" in *"Moving MUSIC directory from"*) bst_ok xdg-user-dirs/force-moves ;; *) bst_notok xdg-user-dirs/force-moves "got:'$(_bst_1line "$out")'" ;; esac check_out xdg-user-dirs/force-restored 'XDG_MUSIC_DIR="$HOME/Music"' cat "$XDGHOME/user-dirs.dirs" check xdg-user-dirs/force-recreated test -d "$H/Music" # -------------------------------------------- $HOME from the passwd database -- # With $HOME unset, get_home_dir() falls through to getpwuid(getuid()). That is # the builtin 'files' NSS service 06-base-passwd asserts through id(1), reached # here by a third caller (tunctl reaches it through getpwnam). The differential # is --set's own home-prefix stripping: an absolute path UNDER the resolved home # is rewritten to $HOME/..., so a lookup that silently failed would leave the # path absolute instead. uid=$(id -u 2>/dev/null) pwhome=$(awk -F: -v u="$uid" '$3==u{print $6; exit}' /etc/passwd 2>/dev/null) if [ -z "$pwhome" ]; then bst_skip xdg-user-dirs/home-from-passwd "uid $uid has no /etc/passwd row to resolve a home from" else d=$scratch/pw.dirs ( unset HOME; xdg-user-dirs-update --dummy-output "$d" --set MUSIC "$pwhome/Tunes" ) \ >/dev/null 2>&1; rc=$? got=$(grep '^XDG_MUSIC_DIR=' "$d" 2>/dev/null) if [ "$rc" -ne 0 ]; then bst_notok xdg-user-dirs/home-from-passwd "rc=$rc with HOME unset" elif [ "$got" = 'XDG_MUSIC_DIR="$HOME/Tunes"' ]; then bst_ok xdg-user-dirs/home-from-passwd else bst_notok xdg-user-dirs/home-from-passwd "getpwuid home not applied: '$got'" fi fi # --------------------------------------------------------- config behaviours -- mkdir -p "$XDGHOME" uconf=$XDGHOME/user-dirs.conf # enabled=False is a clean no-op: rc 0, nothing written, nothing created. printf 'enabled=False\n' > "$uconf" rm -f "$XDGHOME/user-dirs.dirs"; rm -rf "$H/Documents" check_rc xdg-user-dirs/disabled-rc0 0 xdg-user-dirs-update if [ -e "$XDGHOME/user-dirs.dirs" ]; then bst_notok xdg-user-dirs/disabled-noop "wrote user-dirs.dirs while enabled=False" else bst_ok xdg-user-dirs/disabled-noop; fi if [ -d "$H/Documents" ]; then bst_notok xdg-user-dirs/disabled-no-dirs "created Documents while enabled=False" else bst_ok xdg-user-dirs/disabled-no-dirs; fi # ------------------------------------------------------------ the iconv half -- # filename_encoding ships as UTF-8, which sets the converter to NULL -- so the # DEFAULT path never calls iconv_open at all and is clear of glibc's gconv # machinery. Name any other charset and iconv_open(, "UTF-8") runs. This # image links statically and ships no /usr/lib/gconv, so what survives is # whatever glibc has built in. Probed at runtime rather than asserted either # way, so the day gconv ships these become real assertions instead of a stale # skip. enc_rc() { # $1 = charset -> exit status of an inert update under it printf 'enabled=True\nfilename_encoding=%s\n' "$1" > "$uconf" xdg-user-dirs-update --dummy-output "$scratch/enc.dirs" >/dev/null 2>&1 echo $? } enc_msg() { # $1 = charset -> combined output printf 'enabled=True\nfilename_encoding=%s\n' "$1" > "$uconf" xdg-user-dirs-update --dummy-output "$scratch/enc.dirs" 2>&1 } # An impossible charset must be refused, on every image: this is the one iconv # assertion that does not depend on what glibc carries. check_eq xdg-user-dirs/enc-bogus-rc1 1 "$(enc_rc NOSUCHENC)" out=$(enc_msg NOSUCHENC) case "$out" in *"Can't convert from UTF-8 to NOSUCHENC"*) bst_ok xdg-user-dirs/enc-bogus-msg ;; *) bst_notok xdg-user-dirs/enc-bogus-msg "got:'$(_bst_1line "$out")'" ;; esac # The shipped setting takes the no-iconv path and must still succeed. check_eq xdg-user-dirs/enc-utf8-rc0 0 "$(enc_rc UTF-8)" # Three charsets, three different things glibc has to do for them. Asked in this # order they say how MUCH of the conversion machinery is missing, rather than # only that some of it is: # ISO-8859-2 a MODULE — /usr/lib/gconv/ISO8859-2.so, needs a dlopen # LATIN1 an ALIAS — resolved only through the gconv-modules data file # ISO-8859-1 a BUILTIN transformation — compiled into libc, needs neither enc_why_module='iconv_open("ISO-8859-2") refused. It is a loadable /usr/lib/gconv module, and this image links statically and ships no gconv directory (BRINTOS-184 established the same for CP850 through mtools).' enc_why_alias='iconv_open("LATIN1") refused. It is an ALIAS of ISO-8859-1, resolvable only from the gconv-modules data file, which this image does not ship either.' enc_why_builtin='iconv_open("ISO-8859-1") refused — and this is the one that says how far it goes: that name is a glibc BUILTIN transformation, needing no module and no dlopen. So it is the whole gconv configuration that is absent here, not just the loadable charsets, and NO charset conversion is available to this image at all. Not a defect: the tool is clear of it on the shipped UTF-8 setting.' for pair in "ISO-8859-2:module" "LATIN1:alias" "ISO-8859-1:builtin"; do enc=${pair%%:*}; kind=${pair#*:} rc=$(enc_rc "$enc") eval "why=\$enc_why_$kind" if [ "$rc" -eq 0 ]; then bst_ok "xdg-user-dirs/enc-$kind" elif [ "$rc" -eq 1 ]; then bst_skip "xdg-user-dirs/enc-$kind" "$why" else bst_notok "xdg-user-dirs/enc-$kind" "unexpected rc=$rc for $enc" fi done rm -f "$uconf" # --------------------------------- the real home, on the root filesystem -- # Everything above ran with $HOME under /tmp, which is a tmpfs on this image # (inittab mounts it at sysinit). The home this tool is actually pointed at is # on the cloud virtio-fs root, and save_user_dirs() reaches it through # mkstemp() then rename(2) -- a pair the tmpfs pass does not exercise at all. # A suite that only ever ran on tmpfs would be green on an image where the # tool cannot write a config where it matters. rootscratch="" if [ -n "$pwhome" ] && mkdir -p "$pwhome/.bst-xdg-user-dirs.$$" 2>/dev/null; then rootscratch=$pwhome/.bst-xdg-user-dirs.$$ HOME=$rootscratch; export HOME check_rc xdg-user-dirs/realhome-update-rc0 0 xdg-user-dirs-update absent="" for d in $DIRS; do [ -d "$rootscratch/$d" ] || absent="$absent $d"; done if [ -z "$absent" ]; then bst_ok xdg-user-dirs/realhome-dirs-created else bst_notok xdg-user-dirs/realhome-dirs-created "not created under $pwhome:$absent"; fi check xdg-user-dirs/realhome-config-written test -s "$rootscratch/.config/user-dirs.dirs" check_out xdg-user-dirs/realhome-config-music 'XDG_MUSIC_DIR="$HOME/Music"' \ cat "$rootscratch/.config/user-dirs.dirs" check_eq xdg-user-dirs/realhome-lookup "$rootscratch/Pictures" "$(xdg-user-dir PICTURES)" # mkstemp's temporary must be gone: rename(2) completed rather than leaving a # user-dirs.dirsXXXXXX stranded beside the real file. leftover=$(ls "$rootscratch/.config/" 2>/dev/null | grep 'user-dirs\.dirs.') if [ -z "$leftover" ]; then bst_ok xdg-user-dirs/realhome-mkstemp-renamed else bst_notok xdg-user-dirs/realhome-mkstemp-renamed "mkstemp temporary survived: $leftover"; fi HOME=$H; export HOME else for c in realhome-update-rc0 realhome-dirs-created realhome-config-written \ realhome-config-music realhome-lookup realhome-mkstemp-renamed; do bst_skip "xdg-user-dirs/$c" "no writable home for uid $uid on the root filesystem" done fi rm -rf "$scratch" ${rootscratch:+"$rootscratch"}