freebsd-update.sh revision 197618
1#!/bin/sh
2
3#-
4# Copyright 2004-2007 Colin Percival
5# All rights reserved
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted providing that the following conditions 
9# are met:
10# 1. Redistributions of source code must retain the above copyright
11#    notice, this list of conditions and the following disclaimer.
12# 2. Redistributions in binary form must reproduce the above copyright
13#    notice, this list of conditions and the following disclaimer in the
14#    documentation and/or other materials provided with the distribution.
15#
16# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
17# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
20# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
24# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
25# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26# POSSIBILITY OF SUCH DAMAGE.
27
28# $FreeBSD: head/usr.sbin/freebsd-update/freebsd-update.sh 197618 2009-09-29 16:26:09Z cperciva $
29
30#### Usage function -- called from command-line handling code.
31
32# Usage instructions.  Options not listed:
33# --debug	-- don't filter output from utilities
34# --no-stats	-- don't show progress statistics while fetching files
35usage () {
36	cat <<EOF
37usage: `basename $0` [options] command ... [path]
38
39Options:
40  -b basedir   -- Operate on a system mounted at basedir
41                  (default: /)
42  -d workdir   -- Store working files in workdir
43                  (default: /var/db/freebsd-update/)
44  -f conffile  -- Read configuration options from conffile
45                  (default: /etc/freebsd-update.conf)
46  -k KEY       -- Trust an RSA key with SHA256 hash of KEY
47  -r release   -- Target for upgrade (e.g., 6.2-RELEASE)
48  -s server    -- Server from which to fetch updates
49                  (default: update.FreeBSD.org)
50  -t address   -- Mail output of cron command, if any, to address
51                  (default: root)
52Commands:
53  fetch        -- Fetch updates from server
54  cron         -- Sleep rand(3600) seconds, fetch updates, and send an
55                  email if updates were found
56  upgrade      -- Fetch upgrades to FreeBSD version specified via -r option
57  install      -- Install downloaded updates or upgrades
58  rollback     -- Uninstall most recently installed updates
59  IDS          -- Compare the system against an index of "known good" files.
60EOF
61	exit 0
62}
63
64#### Configuration processing functions
65
66#-
67# Configuration options are set in the following order of priority:
68# 1. Command line options
69# 2. Configuration file options
70# 3. Default options
71# In addition, certain options (e.g., IgnorePaths) can be specified multiple
72# times and (as long as these are all in the same place, e.g., inside the
73# configuration file) they will accumulate.  Finally, because the path to the
74# configuration file can be specified at the command line, the entire command
75# line must be processed before we start reading the configuration file.
76#
77# Sound like a mess?  It is.  Here's how we handle this:
78# 1. Initialize CONFFILE and all the options to "".
79# 2. Process the command line.  Throw an error if a non-accumulating option
80#    is specified twice.
81# 3. If CONFFILE is "", set CONFFILE to /etc/freebsd-update.conf .
82# 4. For all the configuration options X, set X_saved to X.
83# 5. Initialize all the options to "".
84# 6. Read CONFFILE line by line, parsing options.
85# 7. For each configuration option X, set X to X_saved iff X_saved is not "".
86# 8. Repeat steps 4-7, except setting options to their default values at (6).
87
88CONFIGOPTIONS="KEYPRINT WORKDIR SERVERNAME MAILTO ALLOWADD ALLOWDELETE
89    KEEPMODIFIEDMETADATA COMPONENTS IGNOREPATHS UPDATEIFUNMODIFIED
90    BASEDIR VERBOSELEVEL TARGETRELEASE STRICTCOMPONENTS MERGECHANGES
91    IDSIGNOREPATHS BACKUPKERNEL BACKUPKERNELDIR BACKUPKERNELSYMBOLFILES"
92
93# Set all the configuration options to "".
94nullconfig () {
95	for X in ${CONFIGOPTIONS}; do
96		eval ${X}=""
97	done
98}
99
100# For each configuration option X, set X_saved to X.
101saveconfig () {
102	for X in ${CONFIGOPTIONS}; do
103		eval ${X}_saved=\$${X}
104	done
105}
106
107# For each configuration option X, set X to X_saved if X_saved is not "".
108mergeconfig () {
109	for X in ${CONFIGOPTIONS}; do
110		eval _=\$${X}_saved
111		if ! [ -z "${_}" ]; then
112			eval ${X}=\$${X}_saved
113		fi
114	done
115}
116
117# Set the trusted keyprint.
118config_KeyPrint () {
119	if [ -z ${KEYPRINT} ]; then
120		KEYPRINT=$1
121	else
122		return 1
123	fi
124}
125
126# Set the working directory.
127config_WorkDir () {
128	if [ -z ${WORKDIR} ]; then
129		WORKDIR=$1
130	else
131		return 1
132	fi
133}
134
135# Set the name of the server (pool) from which to fetch updates
136config_ServerName () {
137	if [ -z ${SERVERNAME} ]; then
138		SERVERNAME=$1
139	else
140		return 1
141	fi
142}
143
144# Set the address to which 'cron' output will be mailed.
145config_MailTo () {
146	if [ -z ${MAILTO} ]; then
147		MAILTO=$1
148	else
149		return 1
150	fi
151}
152
153# Set whether FreeBSD Update is allowed to add files (or directories, or
154# symlinks) which did not previously exist.
155config_AllowAdd () {
156	if [ -z ${ALLOWADD} ]; then
157		case $1 in
158		[Yy][Ee][Ss])
159			ALLOWADD=yes
160			;;
161		[Nn][Oo])
162			ALLOWADD=no
163			;;
164		*)
165			return 1
166			;;
167		esac
168	else
169		return 1
170	fi
171}
172
173# Set whether FreeBSD Update is allowed to remove files/directories/symlinks.
174config_AllowDelete () {
175	if [ -z ${ALLOWDELETE} ]; then
176		case $1 in
177		[Yy][Ee][Ss])
178			ALLOWDELETE=yes
179			;;
180		[Nn][Oo])
181			ALLOWDELETE=no
182			;;
183		*)
184			return 1
185			;;
186		esac
187	else
188		return 1
189	fi
190}
191
192# Set whether FreeBSD Update should keep existing inode ownership,
193# permissions, and flags, in the event that they have been modified locally
194# after the release.
195config_KeepModifiedMetadata () {
196	if [ -z ${KEEPMODIFIEDMETADATA} ]; then
197		case $1 in
198		[Yy][Ee][Ss])
199			KEEPMODIFIEDMETADATA=yes
200			;;
201		[Nn][Oo])
202			KEEPMODIFIEDMETADATA=no
203			;;
204		*)
205			return 1
206			;;
207		esac
208	else
209		return 1
210	fi
211}
212
213# Add to the list of components which should be kept updated.
214config_Components () {
215	for C in $@; do
216		COMPONENTS="${COMPONENTS} ${C}"
217	done
218}
219
220# Add to the list of paths under which updates will be ignored.
221config_IgnorePaths () {
222	for C in $@; do
223		IGNOREPATHS="${IGNOREPATHS} ${C}"
224	done
225}
226
227# Add to the list of paths which IDS should ignore.
228config_IDSIgnorePaths () {
229	for C in $@; do
230		IDSIGNOREPATHS="${IDSIGNOREPATHS} ${C}"
231	done
232}
233
234# Add to the list of paths within which updates will be performed only if the
235# file on disk has not been modified locally.
236config_UpdateIfUnmodified () {
237	for C in $@; do
238		UPDATEIFUNMODIFIED="${UPDATEIFUNMODIFIED} ${C}"
239	done
240}
241
242# Add to the list of paths within which updates to text files will be merged
243# instead of overwritten.
244config_MergeChanges () {
245	for C in $@; do
246		MERGECHANGES="${MERGECHANGES} ${C}"
247	done
248}
249
250# Work on a FreeBSD installation mounted under $1
251config_BaseDir () {
252	if [ -z ${BASEDIR} ]; then
253		BASEDIR=$1
254	else
255		return 1
256	fi
257}
258
259# When fetching upgrades, should we assume the user wants exactly the
260# components listed in COMPONENTS, rather than trying to guess based on
261# what's currently installed?
262config_StrictComponents () {
263	if [ -z ${STRICTCOMPONENTS} ]; then
264		case $1 in
265		[Yy][Ee][Ss])
266			STRICTCOMPONENTS=yes
267			;;
268		[Nn][Oo])
269			STRICTCOMPONENTS=no
270			;;
271		*)
272			return 1
273			;;
274		esac
275	else
276		return 1
277	fi
278}
279
280# Upgrade to FreeBSD $1
281config_TargetRelease () {
282	if [ -z ${TARGETRELEASE} ]; then
283		TARGETRELEASE=$1
284	else
285		return 1
286	fi
287	if echo ${TARGETRELEASE} | grep -qE '^[0-9.]+$'; then
288		TARGETRELEASE="${TARGETRELEASE}-RELEASE"
289	fi
290}
291
292# Define what happens to output of utilities
293config_VerboseLevel () {
294	if [ -z ${VERBOSELEVEL} ]; then
295		case $1 in
296		[Dd][Ee][Bb][Uu][Gg])
297			VERBOSELEVEL=debug
298			;;
299		[Nn][Oo][Ss][Tt][Aa][Tt][Ss])
300			VERBOSELEVEL=nostats
301			;;
302		[Ss][Tt][Aa][Tt][Ss])
303			VERBOSELEVEL=stats
304			;;
305		*)
306			return 1
307			;;
308		esac
309	else
310		return 1
311	fi
312}
313
314config_BackupKernel () {
315	if [ -z ${BACKUPKERNEL} ]; then
316		case $1 in
317		[Yy][Ee][Ss])
318			BACKUPKERNEL=yes
319			;;
320		[Nn][Oo])
321			BACKUPKERNEL=no
322			;;
323		*)
324			return 1
325			;;
326		esac
327	else
328		return 1
329	fi
330}
331
332config_BackupKernelDir () {
333	if [ -z ${BACKUPKERNELDIR} ]; then
334		if [ -z "$1" ]; then
335			echo "BackupKernelDir set to empty dir"
336			return 1
337		fi
338
339		# We check for some paths which would be extremely odd
340		# to use, but which could cause a lot of problems if
341		# used.
342		case $1 in
343		/|/bin|/boot|/etc|/lib|/libexec|/sbin|/usr|/var)
344			echo "BackupKernelDir set to invalid path $1"
345			return 1
346			;;
347		/*)
348			BACKUPKERNELDIR=$1
349			;;
350		*)
351			echo "BackupKernelDir ($1) is not an absolute path"
352			return 1
353			;;
354		esac
355	else
356		return 1
357	fi
358}
359
360config_BackupKernelSymbolFiles () {
361	if [ -z ${BACKUPKERNELSYMBOLFILES} ]; then
362		case $1 in
363		[Yy][Ee][Ss])
364			BACKUPKERNELSYMBOLFILES=yes
365			;;
366		[Nn][Oo])
367			BACKUPKERNELSYMBOLFILES=no
368			;;
369		*)
370			return 1
371			;;
372		esac
373	else
374		return 1
375	fi
376}
377
378# Handle one line of configuration
379configline () {
380	if [ $# -eq 0 ]; then
381		return
382	fi
383
384	OPT=$1
385	shift
386	config_${OPT} $@
387}
388
389#### Parameter handling functions.
390
391# Initialize parameters to null, just in case they're
392# set in the environment.
393init_params () {
394	# Configration settings
395	nullconfig
396
397	# No configuration file set yet
398	CONFFILE=""
399
400	# No commands specified yet
401	COMMANDS=""
402}
403
404# Parse the command line
405parse_cmdline () {
406	while [ $# -gt 0 ]; do
407		case "$1" in
408		# Location of configuration file
409		-f)
410			if [ $# -eq 1 ]; then usage; fi
411			if [ ! -z "${CONFFILE}" ]; then usage; fi
412			shift; CONFFILE="$1"
413			;;
414
415		# Configuration file equivalents
416		-b)
417			if [ $# -eq 1 ]; then usage; fi; shift
418			config_BaseDir $1 || usage
419			;;
420		-d)
421			if [ $# -eq 1 ]; then usage; fi; shift
422			config_WorkDir $1 || usage
423			;;
424		-k)
425			if [ $# -eq 1 ]; then usage; fi; shift
426			config_KeyPrint $1 || usage
427			;;
428		-s)
429			if [ $# -eq 1 ]; then usage; fi; shift
430			config_ServerName $1 || usage
431			;;
432		-r)
433			if [ $# -eq 1 ]; then usage; fi; shift
434			config_TargetRelease $1 || usage
435			;;
436		-t)
437			if [ $# -eq 1 ]; then usage; fi; shift
438			config_MailTo $1 || usage
439			;;
440		-v)
441			if [ $# -eq 1 ]; then usage; fi; shift
442			config_VerboseLevel $1 || usage
443			;;
444
445		# Aliases for "-v debug" and "-v nostats"
446		--debug)
447			config_VerboseLevel debug || usage
448			;;
449		--no-stats)
450			config_VerboseLevel nostats || usage
451			;;
452
453		# Commands
454		cron | fetch | upgrade | install | rollback | IDS)
455			COMMANDS="${COMMANDS} $1"
456			;;
457
458		# Anything else is an error
459		*)
460			usage
461			;;
462		esac
463		shift
464	done
465
466	# Make sure we have at least one command
467	if [ -z "${COMMANDS}" ]; then
468		usage
469	fi
470}
471
472# Parse the configuration file
473parse_conffile () {
474	# If a configuration file was specified on the command line, check
475	# that it exists and is readable.
476	if [ ! -z "${CONFFILE}" ] && [ ! -r "${CONFFILE}" ]; then
477		echo -n "File does not exist "
478		echo -n "or is not readable: "
479		echo ${CONFFILE}
480		exit 1
481	fi
482
483	# If a configuration file was not specified on the command line,
484	# use the default configuration file path.  If that default does
485	# not exist, give up looking for any configuration.
486	if [ -z "${CONFFILE}" ]; then
487		CONFFILE="/etc/freebsd-update.conf"
488		if [ ! -r "${CONFFILE}" ]; then
489			return
490		fi
491	fi
492
493	# Save the configuration options specified on the command line, and
494	# clear all the options in preparation for reading the config file.
495	saveconfig
496	nullconfig
497
498	# Read the configuration file.  Anything after the first '#' is
499	# ignored, and any blank lines are ignored.
500	L=0
501	while read LINE; do
502		L=$(($L + 1))
503		LINEX=`echo "${LINE}" | cut -f 1 -d '#'`
504		if ! configline ${LINEX}; then
505			echo "Error processing configuration file, line $L:"
506			echo "==> ${LINE}"
507			exit 1
508		fi
509	done < ${CONFFILE}
510
511	# Merge the settings read from the configuration file with those
512	# provided at the command line.
513	mergeconfig
514}
515
516# Provide some default parameters
517default_params () {
518	# Save any parameters already configured, and clear the slate
519	saveconfig
520	nullconfig
521
522	# Default configurations
523	config_WorkDir /var/db/freebsd-update
524	config_MailTo root
525	config_AllowAdd yes
526	config_AllowDelete yes
527	config_KeepModifiedMetadata yes
528	config_BaseDir /
529	config_VerboseLevel stats
530	config_StrictComponents no
531	config_BackupKernel yes
532	config_BackupKernelDir /boot/kernel.old
533	config_BackupKernelSymbolFiles no
534
535	# Merge these defaults into the earlier-configured settings
536	mergeconfig
537}
538
539# Set utility output filtering options, based on ${VERBOSELEVEL}
540fetch_setup_verboselevel () {
541	case ${VERBOSELEVEL} in
542	debug)
543		QUIETREDIR="/dev/stderr"
544		QUIETFLAG=" "
545		STATSREDIR="/dev/stderr"
546		DDSTATS=".."
547		XARGST="-t"
548		NDEBUG=" "
549		;;
550	nostats)
551		QUIETREDIR=""
552		QUIETFLAG=""
553		STATSREDIR="/dev/null"
554		DDSTATS=".."
555		XARGST=""
556		NDEBUG=""
557		;;
558	stats)
559		QUIETREDIR="/dev/null"
560		QUIETFLAG="-q"
561		STATSREDIR="/dev/stdout"
562		DDSTATS=""
563		XARGST=""
564		NDEBUG="-n"
565		;;
566	esac
567}
568
569# Perform sanity checks and set some final parameters
570# in preparation for fetching files.  Figure out which
571# set of updates should be downloaded: If the user is
572# running *-p[0-9]+, strip off the last part; if the
573# user is running -SECURITY, call it -RELEASE.  Chdir
574# into the working directory.
575fetch_check_params () {
576	export HTTP_USER_AGENT="freebsd-update (${COMMAND}, `uname -r`)"
577
578	_SERVERNAME_z=\
579"SERVERNAME must be given via command line or configuration file."
580	_KEYPRINT_z="Key must be given via -k option or configuration file."
581	_KEYPRINT_bad="Invalid key fingerprint: "
582	_WORKDIR_bad="Directory does not exist or is not writable: "
583
584	if [ -z "${SERVERNAME}" ]; then
585		echo -n "`basename $0`: "
586		echo "${_SERVERNAME_z}"
587		exit 1
588	fi
589	if [ -z "${KEYPRINT}" ]; then
590		echo -n "`basename $0`: "
591		echo "${_KEYPRINT_z}"
592		exit 1
593	fi
594	if ! echo "${KEYPRINT}" | grep -qE "^[0-9a-f]{64}$"; then
595		echo -n "`basename $0`: "
596		echo -n "${_KEYPRINT_bad}"
597		echo ${KEYPRINT}
598		exit 1
599	fi
600	if ! [ -d "${WORKDIR}" -a -w "${WORKDIR}" ]; then
601		echo -n "`basename $0`: "
602		echo -n "${_WORKDIR_bad}"
603		echo ${WORKDIR}
604		exit 1
605	fi
606	cd ${WORKDIR} || exit 1
607
608	# Generate release number.  The s/SECURITY/RELEASE/ bit exists
609	# to provide an upgrade path for FreeBSD Update 1.x users, since
610	# the kernels provided by FreeBSD Update 1.x are always labelled
611	# as X.Y-SECURITY.
612	RELNUM=`uname -r |
613	    sed -E 's,-p[0-9]+,,' |
614	    sed -E 's,-SECURITY,-RELEASE,'`
615	ARCH=`uname -m`
616	FETCHDIR=${RELNUM}/${ARCH}
617	PATCHDIR=${RELNUM}/${ARCH}/bp
618
619	# Figure out what directory contains the running kernel
620	BOOTFILE=`sysctl -n kern.bootfile`
621	KERNELDIR=${BOOTFILE%/kernel}
622	if ! [ -d ${KERNELDIR} ]; then
623		echo "Cannot identify running kernel"
624		exit 1
625	fi
626
627	# Figure out what kernel configuration is running.  We start with
628	# the output of `uname -i`, and then make the following adjustments:
629	# 1. Replace "SMP-GENERIC" with "SMP".  Why the SMP kernel config
630	# file says "ident SMP-GENERIC", I don't know...
631	# 2. If the kernel claims to be GENERIC _and_ ${ARCH} is "amd64"
632	# _and_ `sysctl kern.version` contains a line which ends "/SMP", then
633	# we're running an SMP kernel.  This mis-identification is a bug
634	# which was fixed in 6.2-STABLE.
635	KERNCONF=`uname -i`
636	if [ ${KERNCONF} = "SMP-GENERIC" ]; then
637		KERNCONF=SMP
638	fi
639	if [ ${KERNCONF} = "GENERIC" ] && [ ${ARCH} = "amd64" ]; then
640		if sysctl kern.version | grep -qE '/SMP$'; then
641			KERNCONF=SMP
642		fi
643	fi
644
645	# Define some paths
646	BSPATCH=/usr/bin/bspatch
647	SHA256=/sbin/sha256
648	PHTTPGET=/usr/libexec/phttpget
649
650	# Set up variables relating to VERBOSELEVEL
651	fetch_setup_verboselevel
652
653	# Construct a unique name from ${BASEDIR}
654	BDHASH=`echo ${BASEDIR} | sha256 -q`
655}
656
657# Perform sanity checks etc. before fetching upgrades.
658upgrade_check_params () {
659	fetch_check_params
660
661	# Unless set otherwise, we're upgrading to the same kernel config.
662	NKERNCONF=${KERNCONF}
663
664	# We need TARGETRELEASE set
665	_TARGETRELEASE_z="Release target must be specified via -r option."
666	if [ -z "${TARGETRELEASE}" ]; then
667		echo -n "`basename $0`: "
668		echo "${_TARGETRELEASE_z}"
669		exit 1
670	fi
671
672	# The target release should be != the current release.
673	if [ "${TARGETRELEASE}" = "${RELNUM}" ]; then
674		echo -n "`basename $0`: "
675		echo "Cannot upgrade from ${RELNUM} to itself"
676		exit 1
677	fi
678
679	# Turning off AllowAdd or AllowDelete is a bad idea for upgrades.
680	if [ "${ALLOWADD}" = "no" ]; then
681		echo -n "`basename $0`: "
682		echo -n "WARNING: \"AllowAdd no\" is a bad idea "
683		echo "when upgrading between releases."
684		echo
685	fi
686	if [ "${ALLOWDELETE}" = "no" ]; then
687		echo -n "`basename $0`: "
688		echo -n "WARNING: \"AllowDelete no\" is a bad idea "
689		echo "when upgrading between releases."
690		echo
691	fi
692
693	# Set EDITOR to /usr/bin/vi if it isn't already set
694	: ${EDITOR:='/usr/bin/vi'}
695}
696
697# Perform sanity checks and set some final parameters in
698# preparation for installing updates.
699install_check_params () {
700	# Check that we are root.  All sorts of things won't work otherwise.
701	if [ `id -u` != 0 ]; then
702		echo "You must be root to run this."
703		exit 1
704	fi
705
706	# Check that securelevel <= 0.  Otherwise we can't update schg files.
707	if [ `sysctl -n kern.securelevel` -gt 0 ]; then
708		echo "Updates cannot be installed when the system securelevel"
709		echo "is greater than zero."
710		exit 1
711	fi
712
713	# Check that we have a working directory
714	_WORKDIR_bad="Directory does not exist or is not writable: "
715	if ! [ -d "${WORKDIR}" -a -w "${WORKDIR}" ]; then
716		echo -n "`basename $0`: "
717		echo -n "${_WORKDIR_bad}"
718		echo ${WORKDIR}
719		exit 1
720	fi
721	cd ${WORKDIR} || exit 1
722
723	# Construct a unique name from ${BASEDIR}
724	BDHASH=`echo ${BASEDIR} | sha256 -q`
725
726	# Check that we have updates ready to install
727	if ! [ -L ${BDHASH}-install ]; then
728		echo "No updates are available to install."
729		echo "Run '$0 fetch' first."
730		exit 1
731	fi
732	if ! [ -f ${BDHASH}-install/INDEX-OLD ] ||
733	    ! [ -f ${BDHASH}-install/INDEX-NEW ]; then
734		echo "Update manifest is corrupt -- this should never happen."
735		echo "Re-run '$0 fetch'."
736		exit 1
737	fi
738
739	# Figure out what directory contains the running kernel
740	BOOTFILE=`sysctl -n kern.bootfile`
741	KERNELDIR=${BOOTFILE%/kernel}
742	if ! [ -d ${KERNELDIR} ]; then
743		echo "Cannot identify running kernel"
744		exit 1
745	fi
746}
747
748# Perform sanity checks and set some final parameters in
749# preparation for UNinstalling updates.
750rollback_check_params () {
751	# Check that we are root.  All sorts of things won't work otherwise.
752	if [ `id -u` != 0 ]; then
753		echo "You must be root to run this."
754		exit 1
755	fi
756
757	# Check that we have a working directory
758	_WORKDIR_bad="Directory does not exist or is not writable: "
759	if ! [ -d "${WORKDIR}" -a -w "${WORKDIR}" ]; then
760		echo -n "`basename $0`: "
761		echo -n "${_WORKDIR_bad}"
762		echo ${WORKDIR}
763		exit 1
764	fi
765	cd ${WORKDIR} || exit 1
766
767	# Construct a unique name from ${BASEDIR}
768	BDHASH=`echo ${BASEDIR} | sha256 -q`
769
770	# Check that we have updates ready to rollback
771	if ! [ -L ${BDHASH}-rollback ]; then
772		echo "No rollback directory found."
773		exit 1
774	fi
775	if ! [ -f ${BDHASH}-rollback/INDEX-OLD ] ||
776	    ! [ -f ${BDHASH}-rollback/INDEX-NEW ]; then
777		echo "Update manifest is corrupt -- this should never happen."
778		exit 1
779	fi
780}
781
782# Perform sanity checks and set some final parameters
783# in preparation for comparing the system against the
784# published index.  Figure out which index we should
785# compare against: If the user is running *-p[0-9]+,
786# strip off the last part; if the user is running
787# -SECURITY, call it -RELEASE.  Chdir into the working
788# directory.
789IDS_check_params () {
790	export HTTP_USER_AGENT="freebsd-update (${COMMAND}, `uname -r`)"
791
792	_SERVERNAME_z=\
793"SERVERNAME must be given via command line or configuration file."
794	_KEYPRINT_z="Key must be given via -k option or configuration file."
795	_KEYPRINT_bad="Invalid key fingerprint: "
796	_WORKDIR_bad="Directory does not exist or is not writable: "
797
798	if [ -z "${SERVERNAME}" ]; then
799		echo -n "`basename $0`: "
800		echo "${_SERVERNAME_z}"
801		exit 1
802	fi
803	if [ -z "${KEYPRINT}" ]; then
804		echo -n "`basename $0`: "
805		echo "${_KEYPRINT_z}"
806		exit 1
807	fi
808	if ! echo "${KEYPRINT}" | grep -qE "^[0-9a-f]{64}$"; then
809		echo -n "`basename $0`: "
810		echo -n "${_KEYPRINT_bad}"
811		echo ${KEYPRINT}
812		exit 1
813	fi
814	if ! [ -d "${WORKDIR}" -a -w "${WORKDIR}" ]; then
815		echo -n "`basename $0`: "
816		echo -n "${_WORKDIR_bad}"
817		echo ${WORKDIR}
818		exit 1
819	fi
820	cd ${WORKDIR} || exit 1
821
822	# Generate release number.  The s/SECURITY/RELEASE/ bit exists
823	# to provide an upgrade path for FreeBSD Update 1.x users, since
824	# the kernels provided by FreeBSD Update 1.x are always labelled
825	# as X.Y-SECURITY.
826	RELNUM=`uname -r |
827	    sed -E 's,-p[0-9]+,,' |
828	    sed -E 's,-SECURITY,-RELEASE,'`
829	ARCH=`uname -m`
830	FETCHDIR=${RELNUM}/${ARCH}
831	PATCHDIR=${RELNUM}/${ARCH}/bp
832
833	# Figure out what directory contains the running kernel
834	BOOTFILE=`sysctl -n kern.bootfile`
835	KERNELDIR=${BOOTFILE%/kernel}
836	if ! [ -d ${KERNELDIR} ]; then
837		echo "Cannot identify running kernel"
838		exit 1
839	fi
840
841	# Figure out what kernel configuration is running.  We start with
842	# the output of `uname -i`, and then make the following adjustments:
843	# 1. Replace "SMP-GENERIC" with "SMP".  Why the SMP kernel config
844	# file says "ident SMP-GENERIC", I don't know...
845	# 2. If the kernel claims to be GENERIC _and_ ${ARCH} is "amd64"
846	# _and_ `sysctl kern.version` contains a line which ends "/SMP", then
847	# we're running an SMP kernel.  This mis-identification is a bug
848	# which was fixed in 6.2-STABLE.
849	KERNCONF=`uname -i`
850	if [ ${KERNCONF} = "SMP-GENERIC" ]; then
851		KERNCONF=SMP
852	fi
853	if [ ${KERNCONF} = "GENERIC" ] && [ ${ARCH} = "amd64" ]; then
854		if sysctl kern.version | grep -qE '/SMP$'; then
855			KERNCONF=SMP
856		fi
857	fi
858
859	# Define some paths
860	SHA256=/sbin/sha256
861	PHTTPGET=/usr/libexec/phttpget
862
863	# Set up variables relating to VERBOSELEVEL
864	fetch_setup_verboselevel
865}
866
867#### Core functionality -- the actual work gets done here
868
869# Use an SRV query to pick a server.  If the SRV query doesn't provide
870# a useful answer, use the server name specified by the user.
871# Put another way... look up _http._tcp.${SERVERNAME} and pick a server
872# from that; or if no servers are returned, use ${SERVERNAME}.
873# This allows a user to specify "portsnap.freebsd.org" (in which case
874# portsnap will select one of the mirrors) or "portsnap5.tld.freebsd.org"
875# (in which case portsnap will use that particular server, since there
876# won't be an SRV entry for that name).
877#
878# We ignore the Port field, since we are always going to use port 80.
879
880# Fetch the mirror list, but do not pick a mirror yet.  Returns 1 if
881# no mirrors are available for any reason.
882fetch_pick_server_init () {
883	: > serverlist_tried
884
885# Check that host(1) exists (i.e., that the system wasn't built with the
886# WITHOUT_BIND set) and don't try to find a mirror if it doesn't exist.
887	if ! which -s host; then
888		: > serverlist_full
889		return 1
890	fi
891
892	echo -n "Looking up ${SERVERNAME} mirrors... "
893
894# Issue the SRV query and pull out the Priority, Weight, and Target fields.
895# BIND 9 prints "$name has SRV record ..." while BIND 8 prints
896# "$name server selection ..."; we allow either format.
897	MLIST="_http._tcp.${SERVERNAME}"
898	host -t srv "${MLIST}" |
899	    sed -nE "s/${MLIST} (has SRV record|server selection) //p" |
900	    cut -f 1,2,4 -d ' ' |
901	    sed -e 's/\.$//' |
902	    sort > serverlist_full
903
904# If no records, give up -- we'll just use the server name we were given.
905	if [ `wc -l < serverlist_full` -eq 0 ]; then
906		echo "none found."
907		return 1
908	fi
909
910# Report how many mirrors we found.
911	echo `wc -l < serverlist_full` "mirrors found."
912
913# Generate a random seed for use in picking mirrors.  If HTTP_PROXY
914# is set, this will be used to generate the seed; otherwise, the seed
915# will be random.
916	if [ -n "${HTTP_PROXY}${http_proxy}" ]; then
917		RANDVALUE=`sha256 -qs "${HTTP_PROXY}${http_proxy}" |
918		    tr -d 'a-f' |
919		    cut -c 1-9`
920	else
921		RANDVALUE=`jot -r 1 0 999999999`
922	fi
923}
924
925# Pick a mirror.  Returns 1 if we have run out of mirrors to try.
926fetch_pick_server () {
927# Generate a list of not-yet-tried mirrors
928	sort serverlist_tried |
929	    comm -23 serverlist_full - > serverlist
930
931# Have we run out of mirrors?
932	if [ `wc -l < serverlist` -eq 0 ]; then
933		echo "No mirrors remaining, giving up."
934		return 1
935	fi
936
937# Find the highest priority level (lowest numeric value).
938	SRV_PRIORITY=`cut -f 1 -d ' ' serverlist | sort -n | head -1`
939
940# Add up the weights of the response lines at that priority level.
941	SRV_WSUM=0;
942	while read X; do
943		case "$X" in
944		${SRV_PRIORITY}\ *)
945			SRV_W=`echo $X | cut -f 2 -d ' '`
946			SRV_WSUM=$(($SRV_WSUM + $SRV_W))
947			;;
948		esac
949	done < serverlist
950
951# If all the weights are 0, pretend that they are all 1 instead.
952	if [ ${SRV_WSUM} -eq 0 ]; then
953		SRV_WSUM=`grep -E "^${SRV_PRIORITY} " serverlist | wc -l`
954		SRV_W_ADD=1
955	else
956		SRV_W_ADD=0
957	fi
958
959# Pick a value between 0 and the sum of the weights - 1
960	SRV_RND=`expr ${RANDVALUE} % ${SRV_WSUM}`
961
962# Read through the list of mirrors and set SERVERNAME.  Write the line
963# corresponding to the mirror we selected into serverlist_tried so that
964# we won't try it again.
965	while read X; do
966		case "$X" in
967		${SRV_PRIORITY}\ *)
968			SRV_W=`echo $X | cut -f 2 -d ' '`
969			SRV_W=$(($SRV_W + $SRV_W_ADD))
970			if [ $SRV_RND -lt $SRV_W ]; then
971				SERVERNAME=`echo $X | cut -f 3 -d ' '`
972				echo "$X" >> serverlist_tried
973				break
974			else
975				SRV_RND=$(($SRV_RND - $SRV_W))
976			fi
977			;;
978		esac
979	done < serverlist
980}
981
982# Take a list of ${oldhash}|${newhash} and output a list of needed patches,
983# i.e., those for which we have ${oldhash} and don't have ${newhash}.
984fetch_make_patchlist () {
985	grep -vE "^([0-9a-f]{64})\|\1$" |
986	    tr '|' ' ' |
987		while read X Y; do
988			if [ -f "files/${Y}.gz" ] ||
989			    [ ! -f "files/${X}.gz" ]; then
990				continue
991			fi
992			echo "${X}|${Y}"
993		done | uniq
994}
995
996# Print user-friendly progress statistics
997fetch_progress () {
998	LNC=0
999	while read x; do
1000		LNC=$(($LNC + 1))
1001		if [ $(($LNC % 10)) = 0 ]; then
1002			echo -n $LNC
1003		elif [ $(($LNC % 2)) = 0 ]; then
1004			echo -n .
1005		fi
1006	done
1007	echo -n " "
1008}
1009
1010# Function for asking the user if everything is ok
1011continuep () {
1012	while read -p "Does this look reasonable (y/n)? " CONTINUE; do
1013		case "${CONTINUE}" in
1014		y*)
1015			return 0
1016			;;
1017		n*)
1018			return 1
1019			;;
1020		esac
1021	done
1022}
1023
1024# Initialize the working directory
1025workdir_init () {
1026	mkdir -p files
1027	touch tINDEX.present
1028}
1029
1030# Check that we have a public key with an appropriate hash, or
1031# fetch the key if it doesn't exist.  Returns 1 if the key has
1032# not yet been fetched.
1033fetch_key () {
1034	if [ -r pub.ssl ] && [ `${SHA256} -q pub.ssl` = ${KEYPRINT} ]; then
1035		return 0
1036	fi
1037
1038	echo -n "Fetching public key from ${SERVERNAME}... "
1039	rm -f pub.ssl
1040	fetch ${QUIETFLAG} http://${SERVERNAME}/${FETCHDIR}/pub.ssl \
1041	    2>${QUIETREDIR} || true
1042	if ! [ -r pub.ssl ]; then
1043		echo "failed."
1044		return 1
1045	fi
1046	if ! [ `${SHA256} -q pub.ssl` = ${KEYPRINT} ]; then
1047		echo "key has incorrect hash."
1048		rm -f pub.ssl
1049		return 1
1050	fi
1051	echo "done."
1052}
1053
1054# Fetch metadata signature, aka "tag".
1055fetch_tag () {
1056	echo -n "Fetching metadata signature "
1057	echo ${NDEBUG} "for ${RELNUM} from ${SERVERNAME}... "
1058	rm -f latest.ssl
1059	fetch ${QUIETFLAG} http://${SERVERNAME}/${FETCHDIR}/latest.ssl	\
1060	    2>${QUIETREDIR} || true
1061	if ! [ -r latest.ssl ]; then
1062		echo "failed."
1063		return 1
1064	fi
1065
1066	openssl rsautl -pubin -inkey pub.ssl -verify		\
1067	    < latest.ssl > tag.new 2>${QUIETREDIR} || true
1068	rm latest.ssl
1069
1070	if ! [ `wc -l < tag.new` = 1 ] ||
1071	    ! grep -qE	\
1072    "^freebsd-update\|${ARCH}\|${RELNUM}\|[0-9]+\|[0-9a-f]{64}\|[0-9]{10}" \
1073		tag.new; then
1074		echo "invalid signature."
1075		return 1
1076	fi
1077
1078	echo "done."
1079
1080	RELPATCHNUM=`cut -f 4 -d '|' < tag.new`
1081	TINDEXHASH=`cut -f 5 -d '|' < tag.new`
1082	EOLTIME=`cut -f 6 -d '|' < tag.new`
1083}
1084
1085# Sanity-check the patch number in a tag, to make sure that we're not
1086# going to "update" backwards and to prevent replay attacks.
1087fetch_tagsanity () {
1088	# Check that we're not going to move from -pX to -pY with Y < X.
1089	RELPX=`uname -r | sed -E 's,.*-,,'`
1090	if echo ${RELPX} | grep -qE '^p[0-9]+$'; then
1091		RELPX=`echo ${RELPX} | cut -c 2-`
1092	else
1093		RELPX=0
1094	fi
1095	if [ "${RELPATCHNUM}" -lt "${RELPX}" ]; then
1096		echo
1097		echo -n "Files on mirror (${RELNUM}-p${RELPATCHNUM})"
1098		echo " appear older than what"
1099		echo "we are currently running (`uname -r`)!"
1100		echo "Cowardly refusing to proceed any further."
1101		return 1
1102	fi
1103
1104	# If "tag" exists and corresponds to ${RELNUM}, make sure that
1105	# it contains a patch number <= RELPATCHNUM, in order to protect
1106	# against rollback (replay) attacks.
1107	if [ -f tag ] &&
1108	    grep -qE	\
1109    "^freebsd-update\|${ARCH}\|${RELNUM}\|[0-9]+\|[0-9a-f]{64}\|[0-9]{10}" \
1110		tag; then
1111		LASTRELPATCHNUM=`cut -f 4 -d '|' < tag`
1112
1113		if [ "${RELPATCHNUM}" -lt "${LASTRELPATCHNUM}" ]; then
1114			echo
1115			echo -n "Files on mirror (${RELNUM}-p${RELPATCHNUM})"
1116			echo " are older than the"
1117			echo -n "most recently seen updates"
1118			echo " (${RELNUM}-p${LASTRELPATCHNUM})."
1119			echo "Cowardly refusing to proceed any further."
1120			return 1
1121		fi
1122	fi
1123}
1124
1125# Fetch metadata index file
1126fetch_metadata_index () {
1127	echo ${NDEBUG} "Fetching metadata index... "
1128	rm -f ${TINDEXHASH}
1129	fetch ${QUIETFLAG} http://${SERVERNAME}/${FETCHDIR}/t/${TINDEXHASH}
1130	    2>${QUIETREDIR}
1131	if ! [ -f ${TINDEXHASH} ]; then
1132		echo "failed."
1133		return 1
1134	fi
1135	if [ `${SHA256} -q ${TINDEXHASH}` != ${TINDEXHASH} ]; then
1136		echo "update metadata index corrupt."
1137		return 1
1138	fi
1139	echo "done."
1140}
1141
1142# Print an error message about signed metadata being bogus.
1143fetch_metadata_bogus () {
1144	echo
1145	echo "The update metadata$1 is correctly signed, but"
1146	echo "failed an integrity check."
1147	echo "Cowardly refusing to proceed any further."
1148	return 1
1149}
1150
1151# Construct tINDEX.new by merging the lines named in $1 from ${TINDEXHASH}
1152# with the lines not named in $@ from tINDEX.present (if that file exists).
1153fetch_metadata_index_merge () {
1154	for METAFILE in $@; do
1155		if [ `grep -E "^${METAFILE}\|" ${TINDEXHASH} | wc -l`	\
1156		    -ne 1 ]; then
1157			fetch_metadata_bogus " index"
1158			return 1
1159		fi
1160
1161		grep -E "${METAFILE}\|" ${TINDEXHASH}
1162	done |
1163	    sort > tINDEX.wanted
1164
1165	if [ -f tINDEX.present ]; then
1166		join -t '|' -v 2 tINDEX.wanted tINDEX.present |
1167		    sort -m - tINDEX.wanted > tINDEX.new
1168		rm tINDEX.wanted
1169	else
1170		mv tINDEX.wanted tINDEX.new
1171	fi
1172}
1173
1174# Sanity check all the lines of tINDEX.new.  Even if more metadata lines
1175# are added by future versions of the server, this won't cause problems,
1176# since the only lines which appear in tINDEX.new are the ones which we
1177# specifically grepped out of ${TINDEXHASH}.
1178fetch_metadata_index_sanity () {
1179	if grep -qvE '^[0-9A-Z.-]+\|[0-9a-f]{64}$' tINDEX.new; then
1180		fetch_metadata_bogus " index"
1181		return 1
1182	fi
1183}
1184
1185# Sanity check the metadata file $1.
1186fetch_metadata_sanity () {
1187	# Some aliases to save space later: ${P} is a character which can
1188	# appear in a path; ${M} is the four numeric metadata fields; and
1189	# ${H} is a sha256 hash.
1190	P="[-+./:=_[[:alnum:]]"
1191	M="[0-9]+\|[0-9]+\|[0-9]+\|[0-9]+"
1192	H="[0-9a-f]{64}"
1193
1194	# Check that the first four fields make sense.
1195	if gunzip -c < files/$1.gz |
1196	    grep -qvE "^[a-z]+\|[0-9a-z]+\|${P}+\|[fdL-]\|"; then
1197		fetch_metadata_bogus ""
1198		return 1
1199	fi
1200
1201	# Remove the first three fields.
1202	gunzip -c < files/$1.gz |
1203	    cut -f 4- -d '|' > sanitycheck.tmp
1204
1205	# Sanity check entries with type 'f'
1206	if grep -E '^f' sanitycheck.tmp |
1207	    grep -qvE "^f\|${M}\|${H}\|${P}*\$"; then
1208		fetch_metadata_bogus ""
1209		return 1
1210	fi
1211
1212	# Sanity check entries with type 'd'
1213	if grep -E '^d' sanitycheck.tmp |
1214	    grep -qvE "^d\|${M}\|\|\$"; then
1215		fetch_metadata_bogus ""
1216		return 1
1217	fi
1218
1219	# Sanity check entries with type 'L'
1220	if grep -E '^L' sanitycheck.tmp |
1221	    grep -qvE "^L\|${M}\|${P}*\|\$"; then
1222		fetch_metadata_bogus ""
1223		return 1
1224	fi
1225
1226	# Sanity check entries with type '-'
1227	if grep -E '^-' sanitycheck.tmp |
1228	    grep -qvE "^-\|\|\|\|\|\|"; then
1229		fetch_metadata_bogus ""
1230		return 1
1231	fi
1232
1233	# Clean up
1234	rm sanitycheck.tmp
1235}
1236
1237# Fetch the metadata index and metadata files listed in $@,
1238# taking advantage of metadata patches where possible.
1239fetch_metadata () {
1240	fetch_metadata_index || return 1
1241	fetch_metadata_index_merge $@ || return 1
1242	fetch_metadata_index_sanity || return 1
1243
1244	# Generate a list of wanted metadata patches
1245	join -t '|' -o 1.2,2.2 tINDEX.present tINDEX.new |
1246	    fetch_make_patchlist > patchlist
1247
1248	if [ -s patchlist ]; then
1249		# Attempt to fetch metadata patches
1250		echo -n "Fetching `wc -l < patchlist | tr -d ' '` "
1251		echo ${NDEBUG} "metadata patches.${DDSTATS}"
1252		tr '|' '-' < patchlist |
1253		    lam -s "${FETCHDIR}/tp/" - -s ".gz" |
1254		    xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}	\
1255			2>${STATSREDIR} | fetch_progress
1256		echo "done."
1257
1258		# Attempt to apply metadata patches
1259		echo -n "Applying metadata patches... "
1260		tr '|' ' ' < patchlist |
1261		    while read X Y; do
1262			if [ ! -f "${X}-${Y}.gz" ]; then continue; fi
1263			gunzip -c < ${X}-${Y}.gz > diff
1264			gunzip -c < files/${X}.gz > diff-OLD
1265
1266			# Figure out which lines are being added and removed
1267			grep -E '^-' diff |
1268			    cut -c 2- |
1269			    while read PREFIX; do
1270				look "${PREFIX}" diff-OLD
1271			    done |
1272			    sort > diff-rm
1273			grep -E '^\+' diff |
1274			    cut -c 2- > diff-add
1275
1276			# Generate the new file
1277			comm -23 diff-OLD diff-rm |
1278			    sort - diff-add > diff-NEW
1279
1280			if [ `${SHA256} -q diff-NEW` = ${Y} ]; then
1281				mv diff-NEW files/${Y}
1282				gzip -n files/${Y}
1283			else
1284				mv diff-NEW ${Y}.bad
1285			fi
1286			rm -f ${X}-${Y}.gz diff
1287			rm -f diff-OLD diff-NEW diff-add diff-rm
1288		done 2>${QUIETREDIR}
1289		echo "done."
1290	fi
1291
1292	# Update metadata without patches
1293	cut -f 2 -d '|' < tINDEX.new |
1294	    while read Y; do
1295		if [ ! -f "files/${Y}.gz" ]; then
1296			echo ${Y};
1297		fi
1298	    done |
1299	    sort -u > filelist
1300
1301	if [ -s filelist ]; then
1302		echo -n "Fetching `wc -l < filelist | tr -d ' '` "
1303		echo ${NDEBUG} "metadata files... "
1304		lam -s "${FETCHDIR}/m/" - -s ".gz" < filelist |
1305		    xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}	\
1306		    2>${QUIETREDIR}
1307
1308		while read Y; do
1309			if ! [ -f ${Y}.gz ]; then
1310				echo "failed."
1311				return 1
1312			fi
1313			if [ `gunzip -c < ${Y}.gz |
1314			    ${SHA256} -q` = ${Y} ]; then
1315				mv ${Y}.gz files/${Y}.gz
1316			else
1317				echo "metadata is corrupt."
1318				return 1
1319			fi
1320		done < filelist
1321		echo "done."
1322	fi
1323
1324# Sanity-check the metadata files.
1325	cut -f 2 -d '|' tINDEX.new > filelist
1326	while read X; do
1327		fetch_metadata_sanity ${X} || return 1
1328	done < filelist
1329
1330# Remove files which are no longer needed
1331	cut -f 2 -d '|' tINDEX.present |
1332	    sort > oldfiles
1333	cut -f 2 -d '|' tINDEX.new |
1334	    sort |
1335	    comm -13 - oldfiles |
1336	    lam -s "files/" - -s ".gz" |
1337	    xargs rm -f
1338	rm patchlist filelist oldfiles
1339	rm ${TINDEXHASH}
1340
1341# We're done!
1342	mv tINDEX.new tINDEX.present
1343	mv tag.new tag
1344
1345	return 0
1346}
1347
1348# Extract a subset of a downloaded metadata file containing only the parts
1349# which are listed in COMPONENTS.
1350fetch_filter_metadata_components () {
1351	METAHASH=`look "$1|" tINDEX.present | cut -f 2 -d '|'`
1352	gunzip -c < files/${METAHASH}.gz > $1.all
1353
1354	# Fish out the lines belonging to components we care about.
1355	for C in ${COMPONENTS}; do
1356		look "`echo ${C} | tr '/' '|'`|" $1.all
1357	done > $1
1358
1359	# Remove temporary file.
1360	rm $1.all
1361}
1362
1363# Generate a filtered version of the metadata file $1 from the downloaded
1364# file, by fishing out the lines corresponding to components we're trying
1365# to keep updated, and then removing lines corresponding to paths we want
1366# to ignore.
1367fetch_filter_metadata () {
1368	# Fish out the lines belonging to components we care about.
1369	fetch_filter_metadata_components $1
1370
1371	# Canonicalize directory names by removing any trailing / in
1372	# order to avoid listing directories multiple times if they
1373	# belong to multiple components.  Turning "/" into "" doesn't
1374	# matter, since we add a leading "/" when we use paths later.
1375	cut -f 3- -d '|' $1 |
1376	    sed -e 's,/|d|,|d|,' |
1377	    sort -u > $1.tmp
1378
1379	# Figure out which lines to ignore and remove them.
1380	for X in ${IGNOREPATHS}; do
1381		grep -E "^${X}" $1.tmp
1382	done |
1383	    sort -u |
1384	    comm -13 - $1.tmp > $1
1385
1386	# Remove temporary files.
1387	rm $1.tmp
1388}
1389
1390# Filter the metadata file $1 by adding lines with "/boot/$2"
1391# replaced by ${KERNELDIR} (which is `sysctl -n kern.bootfile` minus the
1392# trailing "/kernel"); and if "/boot/$2" does not exist, remove
1393# the original lines which start with that.
1394# Put another way: Deal with the fact that the FOO kernel is sometimes
1395# installed in /boot/FOO/ and is sometimes installed elsewhere.
1396fetch_filter_kernel_names () {
1397	grep ^/boot/$2 $1 |
1398	    sed -e "s,/boot/$2,${KERNELDIR},g" |
1399	    sort - $1 > $1.tmp
1400	mv $1.tmp $1
1401
1402	if ! [ -d /boot/$2 ]; then
1403		grep -v ^/boot/$2 $1 > $1.tmp
1404		mv $1.tmp $1
1405	fi
1406}
1407
1408# For all paths appearing in $1 or $3, inspect the system
1409# and generate $2 describing what is currently installed.
1410fetch_inspect_system () {
1411	# No errors yet...
1412	rm -f .err
1413
1414	# Tell the user why his disk is suddenly making lots of noise
1415	echo -n "Inspecting system... "
1416
1417	# Generate list of files to inspect
1418	cat $1 $3 |
1419	    cut -f 1 -d '|' |
1420	    sort -u > filelist
1421
1422	# Examine each file and output lines of the form
1423	# /path/to/file|type|device-inum|user|group|perm|flags|value
1424	# sorted by device and inode number.
1425	while read F; do
1426		# If the symlink/file/directory does not exist, record this.
1427		if ! [ -e ${BASEDIR}/${F} ]; then
1428			echo "${F}|-||||||"
1429			continue
1430		fi
1431		if ! [ -r ${BASEDIR}/${F} ]; then
1432			echo "Cannot read file: ${BASEDIR}/${F}"	\
1433			    >/dev/stderr
1434			touch .err
1435			return 1
1436		fi
1437
1438		# Otherwise, output an index line.
1439		if [ -L ${BASEDIR}/${F} ]; then
1440			echo -n "${F}|L|"
1441			stat -n -f '%d-%i|%u|%g|%Mp%Lp|%Of|' ${BASEDIR}/${F};
1442			readlink ${BASEDIR}/${F};
1443		elif [ -f ${BASEDIR}/${F} ]; then
1444			echo -n "${F}|f|"
1445			stat -n -f '%d-%i|%u|%g|%Mp%Lp|%Of|' ${BASEDIR}/${F};
1446			sha256 -q ${BASEDIR}/${F};
1447		elif [ -d ${BASEDIR}/${F} ]; then
1448			echo -n "${F}|d|"
1449			stat -f '%d-%i|%u|%g|%Mp%Lp|%Of|' ${BASEDIR}/${F};
1450		else
1451			echo "Unknown file type: ${BASEDIR}/${F}"	\
1452			    >/dev/stderr
1453			touch .err
1454			return 1
1455		fi
1456	done < filelist |
1457	    sort -k 3,3 -t '|' > $2.tmp
1458	rm filelist
1459
1460	# Check if an error occured during system inspection
1461	if [ -f .err ]; then
1462		return 1
1463	fi
1464
1465	# Convert to the form
1466	# /path/to/file|type|user|group|perm|flags|value|hlink
1467	# by resolving identical device and inode numbers into hard links.
1468	cut -f 1,3 -d '|' $2.tmp |
1469	    sort -k 1,1 -t '|' |
1470	    sort -s -u -k 2,2 -t '|' |
1471	    join -1 2 -2 3 -t '|' - $2.tmp |
1472	    awk -F \| -v OFS=\|		\
1473		'{
1474		    if (($2 == $3) || ($4 == "-"))
1475			print $3,$4,$5,$6,$7,$8,$9,""
1476		    else
1477			print $3,$4,$5,$6,$7,$8,$9,$2
1478		}' |
1479	    sort > $2
1480	rm $2.tmp
1481
1482	# We're finished looking around
1483	echo "done."
1484}
1485
1486# For any paths matching ${MERGECHANGES}, compare $1 and $2 and find any
1487# files which differ; generate $3 containing these paths and the old hashes.
1488fetch_filter_mergechanges () {
1489	# Pull out the paths and hashes of the files matching ${MERGECHANGES}.
1490	for F in $1 $2; do
1491		for X in ${MERGECHANGES}; do
1492			grep -E "^${X}" ${F}
1493		done |
1494		    cut -f 1,2,7 -d '|' |
1495		    sort > ${F}-values
1496	done
1497
1498	# Any line in $2-values which doesn't appear in $1-values and is a
1499	# file means that we should list the path in $3.
1500	comm -13 $1-values $2-values |
1501	    fgrep '|f|' |
1502	    cut -f 1 -d '|' > $2-paths
1503
1504	# For each path, pull out one (and only one!) entry from $1-values.
1505	# Note that we cannot distinguish which "old" version the user made
1506	# changes to; but hopefully any changes which occur due to security
1507	# updates will exist in both the "new" version and the version which
1508	# the user has installed, so the merging will still work.
1509	while read X; do
1510		look "${X}|" $1-values |
1511		    head -1
1512	done < $2-paths > $3
1513
1514	# Clean up
1515	rm $1-values $2-values $2-paths
1516}
1517
1518# For any paths matching ${UPDATEIFUNMODIFIED}, remove lines from $[123]
1519# which correspond to lines in $2 with hashes not matching $1 or $3, unless
1520# the paths are listed in $4.  For entries in $2 marked "not present"
1521# (aka. type -), remove lines from $[123] unless there is a corresponding
1522# entry in $1.
1523fetch_filter_unmodified_notpresent () {
1524	# Figure out which lines of $1 and $3 correspond to bits which
1525	# should only be updated if they haven't changed, and fish out
1526	# the (path, type, value) tuples.
1527	# NOTE: We don't consider a file to be "modified" if it matches
1528	# the hash from $3.
1529	for X in ${UPDATEIFUNMODIFIED}; do
1530		grep -E "^${X}" $1
1531		grep -E "^${X}" $3
1532	done |
1533	    cut -f 1,2,7 -d '|' |
1534	    sort > $1-values
1535
1536	# Do the same for $2.
1537	for X in ${UPDATEIFUNMODIFIED}; do
1538		grep -E "^${X}" $2
1539	done |
1540	    cut -f 1,2,7 -d '|' |
1541	    sort > $2-values
1542
1543	# Any entry in $2-values which is not in $1-values corresponds to
1544	# a path which we need to remove from $1, $2, and $3, unless it
1545	# that path appears in $4.
1546	comm -13 $1-values $2-values |
1547	    sort -t '|' -k 1,1 > mlines.tmp
1548	cut -f 1 -d '|' $4 |
1549	    sort |
1550	    join -v 2 -t '|' - mlines.tmp |
1551	    sort > mlines
1552	rm $1-values $2-values mlines.tmp
1553
1554	# Any lines in $2 which are not in $1 AND are "not present" lines
1555	# also belong in mlines.
1556	comm -13 $1 $2 |
1557	    cut -f 1,2,7 -d '|' |
1558	    fgrep '|-|' >> mlines
1559
1560	# Remove lines from $1, $2, and $3
1561	for X in $1 $2 $3; do
1562		sort -t '|' -k 1,1 ${X} > ${X}.tmp
1563		cut -f 1 -d '|' < mlines |
1564		    sort |
1565		    join -v 2 -t '|' - ${X}.tmp |
1566		    sort > ${X}
1567		rm ${X}.tmp
1568	done
1569
1570	# Store a list of the modified files, for future reference
1571	fgrep -v '|-|' mlines |
1572	    cut -f 1 -d '|' > modifiedfiles
1573	rm mlines
1574}
1575
1576# For each entry in $1 of type -, remove any corresponding
1577# entry from $2 if ${ALLOWADD} != "yes".  Remove all entries
1578# of type - from $1.
1579fetch_filter_allowadd () {
1580	cut -f 1,2 -d '|' < $1 |
1581	    fgrep '|-' |
1582	    cut -f 1 -d '|' > filesnotpresent
1583
1584	if [ ${ALLOWADD} != "yes" ]; then
1585		sort < $2 |
1586		    join -v 1 -t '|' - filesnotpresent |
1587		    sort > $2.tmp
1588		mv $2.tmp $2
1589	fi
1590
1591	sort < $1 |
1592	    join -v 1 -t '|' - filesnotpresent |
1593	    sort > $1.tmp
1594	mv $1.tmp $1
1595	rm filesnotpresent
1596}
1597
1598# If ${ALLOWDELETE} != "yes", then remove any entries from $1
1599# which don't correspond to entries in $2.
1600fetch_filter_allowdelete () {
1601	# Produce a lists ${PATH}|${TYPE}
1602	for X in $1 $2; do
1603		cut -f 1-2 -d '|' < ${X} |
1604		    sort -u > ${X}.nodes
1605	done
1606
1607	# Figure out which lines need to be removed from $1.
1608	if [ ${ALLOWDELETE} != "yes" ]; then
1609		comm -23 $1.nodes $2.nodes > $1.badnodes
1610	else
1611		: > $1.badnodes
1612	fi
1613
1614	# Remove the relevant lines from $1
1615	while read X; do
1616		look "${X}|" $1
1617	done < $1.badnodes |
1618	    comm -13 - $1 > $1.tmp
1619	mv $1.tmp $1
1620
1621	rm $1.badnodes $1.nodes $2.nodes
1622}
1623
1624# If ${KEEPMODIFIEDMETADATA} == "yes", then for each entry in $2
1625# with metadata not matching any entry in $1, replace the corresponding
1626# line of $3 with one having the same metadata as the entry in $2.
1627fetch_filter_modified_metadata () {
1628	# Fish out the metadata from $1 and $2
1629	for X in $1 $2; do
1630		cut -f 1-6 -d '|' < ${X} > ${X}.metadata
1631	done
1632
1633	# Find the metadata we need to keep
1634	if [ ${KEEPMODIFIEDMETADATA} = "yes" ]; then
1635		comm -13 $1.metadata $2.metadata > keepmeta
1636	else
1637		: > keepmeta
1638	fi
1639
1640	# Extract the lines which we need to remove from $3, and
1641	# construct the lines which we need to add to $3.
1642	: > $3.remove
1643	: > $3.add
1644	while read LINE; do
1645		NODE=`echo "${LINE}" | cut -f 1-2 -d '|'`
1646		look "${NODE}|" $3 >> $3.remove
1647		look "${NODE}|" $3 |
1648		    cut -f 7- -d '|' |
1649		    lam -s "${LINE}|" - >> $3.add
1650	done < keepmeta
1651
1652	# Remove the specified lines and add the new lines.
1653	sort $3.remove |
1654	    comm -13 - $3 |
1655	    sort -u - $3.add > $3.tmp
1656	mv $3.tmp $3
1657
1658	rm keepmeta $1.metadata $2.metadata $3.add $3.remove
1659}
1660
1661# Remove lines from $1 and $2 which are identical;
1662# no need to update a file if it isn't changing.
1663fetch_filter_uptodate () {
1664	comm -23 $1 $2 > $1.tmp
1665	comm -13 $1 $2 > $2.tmp
1666
1667	mv $1.tmp $1
1668	mv $2.tmp $2
1669}
1670
1671# Fetch any "clean" old versions of files we need for merging changes.
1672fetch_files_premerge () {
1673	# We only need to do anything if $1 is non-empty.
1674	if [ -s $1 ]; then
1675		# Tell the user what we're doing
1676		echo -n "Fetching files from ${OLDRELNUM} for merging... "
1677
1678		# List of files wanted
1679		fgrep '|f|' < $1 |
1680		    cut -f 3 -d '|' |
1681		    sort -u > files.wanted
1682
1683		# Only fetch the files we don't already have
1684		while read Y; do
1685			if [ ! -f "files/${Y}.gz" ]; then
1686				echo ${Y};
1687			fi
1688		done < files.wanted > filelist
1689
1690		# Actually fetch them
1691		lam -s "${OLDFETCHDIR}/f/" - -s ".gz" < filelist |
1692		    xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}	\
1693		    2>${QUIETREDIR}
1694
1695		# Make sure we got them all, and move them into /files/
1696		while read Y; do
1697			if ! [ -f ${Y}.gz ]; then
1698				echo "failed."
1699				return 1
1700			fi
1701			if [ `gunzip -c < ${Y}.gz |
1702			    ${SHA256} -q` = ${Y} ]; then
1703				mv ${Y}.gz files/${Y}.gz
1704			else
1705				echo "${Y} has incorrect hash."
1706				return 1
1707			fi
1708		done < filelist
1709		echo "done."
1710
1711		# Clean up
1712		rm filelist files.wanted
1713	fi
1714}
1715
1716# Prepare to fetch files: Generate a list of the files we need,
1717# copy the unmodified files we have into /files/, and generate
1718# a list of patches to download.
1719fetch_files_prepare () {
1720	# Tell the user why his disk is suddenly making lots of noise
1721	echo -n "Preparing to download files... "
1722
1723	# Reduce indices to ${PATH}|${HASH} pairs
1724	for X in $1 $2 $3; do
1725		cut -f 1,2,7 -d '|' < ${X} |
1726		    fgrep '|f|' |
1727		    cut -f 1,3 -d '|' |
1728		    sort > ${X}.hashes
1729	done
1730
1731	# List of files wanted
1732	cut -f 2 -d '|' < $3.hashes |
1733	    sort -u |
1734	    while read HASH; do
1735		if ! [ -f files/${HASH}.gz ]; then
1736			echo ${HASH}
1737		fi
1738	done > files.wanted
1739
1740	# Generate a list of unmodified files
1741	comm -12 $1.hashes $2.hashes |
1742	    sort -k 1,1 -t '|' > unmodified.files
1743
1744	# Copy all files into /files/.  We only need the unmodified files
1745	# for use in patching; but we'll want all of them if the user asks
1746	# to rollback the updates later.
1747	while read LINE; do
1748		F=`echo "${LINE}" | cut -f 1 -d '|'`
1749		HASH=`echo "${LINE}" | cut -f 2 -d '|'`
1750
1751		# Skip files we already have.
1752		if [ -f files/${HASH}.gz ]; then
1753			continue
1754		fi
1755
1756		# Make sure the file hasn't changed.
1757		cp "${BASEDIR}/${F}" tmpfile
1758		if [ `sha256 -q tmpfile` != ${HASH} ]; then
1759			echo
1760			echo "File changed while FreeBSD Update running: ${F}"
1761			return 1
1762		fi
1763
1764		# Place the file into storage.
1765		gzip -c < tmpfile > files/${HASH}.gz
1766		rm tmpfile
1767	done < $2.hashes
1768
1769	# Produce a list of patches to download
1770	sort -k 1,1 -t '|' $3.hashes |
1771	    join -t '|' -o 2.2,1.2 - unmodified.files |
1772	    fetch_make_patchlist > patchlist
1773
1774	# Garbage collect
1775	rm unmodified.files $1.hashes $2.hashes $3.hashes
1776
1777	# We don't need the list of possible old files any more.
1778	rm $1
1779
1780	# We're finished making noise
1781	echo "done."
1782}
1783
1784# Fetch files.
1785fetch_files () {
1786	# Attempt to fetch patches
1787	if [ -s patchlist ]; then
1788		echo -n "Fetching `wc -l < patchlist | tr -d ' '` "
1789		echo ${NDEBUG} "patches.${DDSTATS}"
1790		tr '|' '-' < patchlist |
1791		    lam -s "${PATCHDIR}/" - |
1792		    xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}	\
1793			2>${STATSREDIR} | fetch_progress
1794		echo "done."
1795
1796		# Attempt to apply patches
1797		echo -n "Applying patches... "
1798		tr '|' ' ' < patchlist |
1799		    while read X Y; do
1800			if [ ! -f "${X}-${Y}" ]; then continue; fi
1801			gunzip -c < files/${X}.gz > OLD
1802
1803			bspatch OLD NEW ${X}-${Y}
1804
1805			if [ `${SHA256} -q NEW` = ${Y} ]; then
1806				mv NEW files/${Y}
1807				gzip -n files/${Y}
1808			fi
1809			rm -f diff OLD NEW ${X}-${Y}
1810		done 2>${QUIETREDIR}
1811		echo "done."
1812	fi
1813
1814	# Download files which couldn't be generate via patching
1815	while read Y; do
1816		if [ ! -f "files/${Y}.gz" ]; then
1817			echo ${Y};
1818		fi
1819	done < files.wanted > filelist
1820
1821	if [ -s filelist ]; then
1822		echo -n "Fetching `wc -l < filelist | tr -d ' '` "
1823		echo ${NDEBUG} "files... "
1824		lam -s "${FETCHDIR}/f/" - -s ".gz" < filelist |
1825		    xargs ${XARGST} ${PHTTPGET} ${SERVERNAME}	\
1826		    2>${QUIETREDIR}
1827
1828		while read Y; do
1829			if ! [ -f ${Y}.gz ]; then
1830				echo "failed."
1831				return 1
1832			fi
1833			if [ `gunzip -c < ${Y}.gz |
1834			    ${SHA256} -q` = ${Y} ]; then
1835				mv ${Y}.gz files/${Y}.gz
1836			else
1837				echo "${Y} has incorrect hash."
1838				return 1
1839			fi
1840		done < filelist
1841		echo "done."
1842	fi
1843
1844	# Clean up
1845	rm files.wanted filelist patchlist
1846}
1847
1848# Create and populate install manifest directory; and report what updates
1849# are available.
1850fetch_create_manifest () {
1851	# If we have an existing install manifest, nuke it.
1852	if [ -L "${BDHASH}-install" ]; then
1853		rm -r ${BDHASH}-install/
1854		rm ${BDHASH}-install
1855	fi
1856
1857	# Report to the user if any updates were avoided due to local changes
1858	if [ -s modifiedfiles ]; then
1859		echo
1860		echo -n "The following files are affected by updates, "
1861		echo "but no changes have"
1862		echo -n "been downloaded because the files have been "
1863		echo "modified locally:"
1864		cat modifiedfiles
1865	fi | more
1866	rm modifiedfiles
1867
1868	# If no files will be updated, tell the user and exit
1869	if ! [ -s INDEX-PRESENT ] &&
1870	    ! [ -s INDEX-NEW ]; then
1871		rm INDEX-PRESENT INDEX-NEW
1872		echo
1873		echo -n "No updates needed to update system to "
1874		echo "${RELNUM}-p${RELPATCHNUM}."
1875		return
1876	fi
1877
1878	# Divide files into (a) removed files, (b) added files, and
1879	# (c) updated files.
1880	cut -f 1 -d '|' < INDEX-PRESENT |
1881	    sort > INDEX-PRESENT.flist
1882	cut -f 1 -d '|' < INDEX-NEW |
1883	    sort > INDEX-NEW.flist
1884	comm -23 INDEX-PRESENT.flist INDEX-NEW.flist > files.removed
1885	comm -13 INDEX-PRESENT.flist INDEX-NEW.flist > files.added
1886	comm -12 INDEX-PRESENT.flist INDEX-NEW.flist > files.updated
1887	rm INDEX-PRESENT.flist INDEX-NEW.flist
1888
1889	# Report removed files, if any
1890	if [ -s files.removed ]; then
1891		echo
1892		echo -n "The following files will be removed "
1893		echo "as part of updating to ${RELNUM}-p${RELPATCHNUM}:"
1894		cat files.removed
1895	fi | more
1896	rm files.removed
1897
1898	# Report added files, if any
1899	if [ -s files.added ]; then
1900		echo
1901		echo -n "The following files will be added "
1902		echo "as part of updating to ${RELNUM}-p${RELPATCHNUM}:"
1903		cat files.added
1904	fi | more
1905	rm files.added
1906
1907	# Report updated files, if any
1908	if [ -s files.updated ]; then
1909		echo
1910		echo -n "The following files will be updated "
1911		echo "as part of updating to ${RELNUM}-p${RELPATCHNUM}:"
1912
1913		cat files.updated
1914	fi | more
1915	rm files.updated
1916
1917	# Create a directory for the install manifest.
1918	MDIR=`mktemp -d install.XXXXXX` || return 1
1919
1920	# Populate it
1921	mv INDEX-PRESENT ${MDIR}/INDEX-OLD
1922	mv INDEX-NEW ${MDIR}/INDEX-NEW
1923
1924	# Link it into place
1925	ln -s ${MDIR} ${BDHASH}-install
1926}
1927
1928# Warn about any upcoming EoL
1929fetch_warn_eol () {
1930	# What's the current time?
1931	NOWTIME=`date "+%s"`
1932
1933	# When did we last warn about the EoL date?
1934	if [ -f lasteolwarn ]; then
1935		LASTWARN=`cat lasteolwarn`
1936	else
1937		LASTWARN=`expr ${NOWTIME} - 63072000`
1938	fi
1939
1940	# If the EoL time is past, warn.
1941	if [ ${EOLTIME} -lt ${NOWTIME} ]; then
1942		echo
1943		cat <<-EOF
1944		WARNING: `uname -sr` HAS PASSED ITS END-OF-LIFE DATE.
1945		Any security issues discovered after `date -r ${EOLTIME}`
1946		will not have been corrected.
1947		EOF
1948		return 1
1949	fi
1950
1951	# Figure out how long it has been since we last warned about the
1952	# upcoming EoL, and how much longer we have left.
1953	SINCEWARN=`expr ${NOWTIME} - ${LASTWARN}`
1954	TIMELEFT=`expr ${EOLTIME} - ${NOWTIME}`
1955
1956	# Don't warn if the EoL is more than 3 months away
1957	if [ ${TIMELEFT} -gt 7884000 ]; then
1958		return 0
1959	fi
1960
1961	# Don't warn if the time remaining is more than 3 times the time
1962	# since the last warning.
1963	if [ ${TIMELEFT} -gt `expr ${SINCEWARN} \* 3` ]; then
1964		return 0
1965	fi
1966
1967	# Figure out what time units to use.
1968	if [ ${TIMELEFT} -lt 604800 ]; then
1969		UNIT="day"
1970		SIZE=86400
1971	elif [ ${TIMELEFT} -lt 2678400 ]; then
1972		UNIT="week"
1973		SIZE=604800
1974	else
1975		UNIT="month"
1976		SIZE=2678400
1977	fi
1978
1979	# Compute the right number of units
1980	NUM=`expr ${TIMELEFT} / ${SIZE}`
1981	if [ ${NUM} != 1 ]; then
1982		UNIT="${UNIT}s"
1983	fi
1984
1985	# Print the warning
1986	echo
1987	cat <<-EOF
1988		WARNING: `uname -sr` is approaching its End-of-Life date.
1989		It is strongly recommended that you upgrade to a newer
1990		release within the next ${NUM} ${UNIT}.
1991	EOF
1992
1993	# Update the stored time of last warning
1994	echo ${NOWTIME} > lasteolwarn
1995}
1996
1997# Do the actual work involved in "fetch" / "cron".
1998fetch_run () {
1999	workdir_init || return 1
2000
2001	# Prepare the mirror list.
2002	fetch_pick_server_init && fetch_pick_server
2003
2004	# Try to fetch the public key until we run out of servers.
2005	while ! fetch_key; do
2006		fetch_pick_server || return 1
2007	done
2008
2009	# Try to fetch the metadata index signature ("tag") until we run
2010	# out of available servers; and sanity check the downloaded tag.
2011	while ! fetch_tag; do
2012		fetch_pick_server || return 1
2013	done
2014	fetch_tagsanity || return 1
2015
2016	# Fetch the latest INDEX-NEW and INDEX-OLD files.
2017	fetch_metadata INDEX-NEW INDEX-OLD || return 1
2018
2019	# Generate filtered INDEX-NEW and INDEX-OLD files containing only
2020	# the lines which (a) belong to components we care about, and (b)
2021	# don't correspond to paths we're explicitly ignoring.
2022	fetch_filter_metadata INDEX-NEW || return 1
2023	fetch_filter_metadata INDEX-OLD || return 1
2024
2025	# Translate /boot/${KERNCONF} into ${KERNELDIR}
2026	fetch_filter_kernel_names INDEX-NEW ${KERNCONF}
2027	fetch_filter_kernel_names INDEX-OLD ${KERNCONF}
2028
2029	# For all paths appearing in INDEX-OLD or INDEX-NEW, inspect the
2030	# system and generate an INDEX-PRESENT file.
2031	fetch_inspect_system INDEX-OLD INDEX-PRESENT INDEX-NEW || return 1
2032
2033	# Based on ${UPDATEIFUNMODIFIED}, remove lines from INDEX-* which
2034	# correspond to lines in INDEX-PRESENT with hashes not appearing
2035	# in INDEX-OLD or INDEX-NEW.  Also remove lines where the entry in
2036	# INDEX-PRESENT has type - and there isn't a corresponding entry in
2037	# INDEX-OLD with type -.
2038	fetch_filter_unmodified_notpresent	\
2039	    INDEX-OLD INDEX-PRESENT INDEX-NEW /dev/null
2040
2041	# For each entry in INDEX-PRESENT of type -, remove any corresponding
2042	# entry from INDEX-NEW if ${ALLOWADD} != "yes".  Remove all entries
2043	# of type - from INDEX-PRESENT.
2044	fetch_filter_allowadd INDEX-PRESENT INDEX-NEW
2045
2046	# If ${ALLOWDELETE} != "yes", then remove any entries from
2047	# INDEX-PRESENT which don't correspond to entries in INDEX-NEW.
2048	fetch_filter_allowdelete INDEX-PRESENT INDEX-NEW
2049
2050	# If ${KEEPMODIFIEDMETADATA} == "yes", then for each entry in
2051	# INDEX-PRESENT with metadata not matching any entry in INDEX-OLD,
2052	# replace the corresponding line of INDEX-NEW with one having the
2053	# same metadata as the entry in INDEX-PRESENT.
2054	fetch_filter_modified_metadata INDEX-OLD INDEX-PRESENT INDEX-NEW
2055
2056	# Remove lines from INDEX-PRESENT and INDEX-NEW which are identical;
2057	# no need to update a file if it isn't changing.
2058	fetch_filter_uptodate INDEX-PRESENT INDEX-NEW
2059
2060	# Prepare to fetch files: Generate a list of the files we need,
2061	# copy the unmodified files we have into /files/, and generate
2062	# a list of patches to download.
2063	fetch_files_prepare INDEX-OLD INDEX-PRESENT INDEX-NEW || return 1
2064
2065	# Fetch files.
2066	fetch_files || return 1
2067
2068	# Create and populate install manifest directory; and report what
2069	# updates are available.
2070	fetch_create_manifest || return 1
2071
2072	# Warn about any upcoming EoL
2073	fetch_warn_eol || return 1
2074}
2075
2076# If StrictComponents is not "yes", generate a new components list
2077# with only the components which appear to be installed.
2078upgrade_guess_components () {
2079	if [ "${STRICTCOMPONENTS}" = "no" ]; then
2080		# Generate filtered INDEX-ALL with only the components listed
2081		# in COMPONENTS.
2082		fetch_filter_metadata_components $1 || return 1
2083
2084		# Tell the user why his disk is suddenly making lots of noise
2085		echo -n "Inspecting system... "
2086
2087		# Look at the files on disk, and assume that a component is
2088		# supposed to be present if it is more than half-present.
2089		cut -f 1-3 -d '|' < INDEX-ALL |
2090		    tr '|' ' ' |
2091		    while read C S F; do
2092			if [ -e ${BASEDIR}/${F} ]; then
2093				echo "+ ${C}|${S}"
2094			fi
2095			echo "= ${C}|${S}"
2096		    done |
2097		    sort |
2098		    uniq -c |
2099		    sed -E 's,^ +,,' > compfreq
2100		grep ' = ' compfreq |
2101		    cut -f 1,3 -d ' ' |
2102		    sort -k 2,2 -t ' ' > compfreq.total
2103		grep ' + ' compfreq |
2104		    cut -f 1,3 -d ' ' |
2105		    sort -k 2,2 -t ' ' > compfreq.present
2106		join -t ' ' -1 2 -2 2 compfreq.present compfreq.total |
2107		    while read S P T; do
2108			if [ ${P} -gt `expr ${T} / 2` ]; then
2109				echo ${S}
2110			fi
2111		    done > comp.present
2112		cut -f 2 -d ' ' < compfreq.total > comp.total
2113		rm INDEX-ALL compfreq compfreq.total compfreq.present
2114
2115		# We're done making noise.
2116		echo "done."
2117
2118		# Sometimes the kernel isn't installed where INDEX-ALL
2119		# thinks that it should be: In particular, it is often in
2120		# /boot/kernel instead of /boot/GENERIC or /boot/SMP.  To
2121		# deal with this, if "kernel|X" is listed in comp.total
2122		# (i.e., is a component which would be upgraded if it is
2123		# found to be present) we will add it to comp.present.
2124		# If "kernel|<anything>" is in comp.total but "kernel|X" is
2125		# not, we print a warning -- the user is running a kernel
2126		# which isn't part of the release.
2127		KCOMP=`echo ${KERNCONF} | tr 'A-Z' 'a-z'`
2128		grep -E "^kernel\|${KCOMP}\$" comp.total >> comp.present
2129
2130		if grep -qE "^kernel\|" comp.total &&
2131		    ! grep -qE "^kernel\|${KCOMP}\$" comp.total; then
2132			cat <<-EOF
2133
2134WARNING: This system is running a "${KCOMP}" kernel, which is not a
2135kernel configuration distributed as part of FreeBSD ${RELNUM}.
2136This kernel will not be updated: you MUST update the kernel manually
2137before running "$0 install".
2138			EOF
2139		fi
2140
2141		# Re-sort the list of installed components and generate
2142		# the list of non-installed components.
2143		sort -u < comp.present > comp.present.tmp
2144		mv comp.present.tmp comp.present
2145		comm -13 comp.present comp.total > comp.absent
2146
2147		# Ask the user to confirm that what we have is correct.  To
2148		# reduce user confusion, translate "X|Y" back to "X/Y" (as
2149		# subcomponents must be listed in the configuration file).
2150		echo
2151		echo -n "The following components of FreeBSD "
2152		echo "seem to be installed:"
2153		tr '|' '/' < comp.present |
2154		    fmt -72
2155		echo
2156		echo -n "The following components of FreeBSD "
2157		echo "do not seem to be installed:"
2158		tr '|' '/' < comp.absent |
2159		    fmt -72
2160		echo
2161		continuep || return 1
2162		echo
2163
2164		# Suck the generated list of components into ${COMPONENTS}.
2165		# Note that comp.present.tmp is used due to issues with
2166		# pipelines and setting variables.
2167		COMPONENTS=""
2168		tr '|' '/' < comp.present > comp.present.tmp
2169		while read C; do
2170			COMPONENTS="${COMPONENTS} ${C}"
2171		done < comp.present.tmp
2172
2173		# Delete temporary files
2174		rm comp.present comp.present.tmp comp.absent comp.total
2175	fi
2176}
2177
2178# If StrictComponents is not "yes", COMPONENTS contains an entry
2179# corresponding to the currently running kernel, and said kernel
2180# does not exist in the new release, add "kernel/generic" to the
2181# list of components.
2182upgrade_guess_new_kernel () {
2183	if [ "${STRICTCOMPONENTS}" = "no" ]; then
2184		# Grab the unfiltered metadata file.
2185		METAHASH=`look "$1|" tINDEX.present | cut -f 2 -d '|'`
2186		gunzip -c < files/${METAHASH}.gz > $1.all
2187
2188		# If "kernel/${KCOMP}" is in ${COMPONENTS} and that component
2189		# isn't in $1.all, we need to add kernel/generic.
2190		for C in ${COMPONENTS}; do
2191			if [ ${C} = "kernel/${KCOMP}" ] &&
2192			    ! grep -qE "^kernel\|${KCOMP}\|" $1.all; then
2193				COMPONENTS="${COMPONENTS} kernel/generic"
2194				NKERNCONF="GENERIC"
2195				cat <<-EOF
2196
2197WARNING: This system is running a "${KCOMP}" kernel, which is not a
2198kernel configuration distributed as part of FreeBSD ${RELNUM}.
2199As part of upgrading to FreeBSD ${RELNUM}, this kernel will be
2200replaced with a "generic" kernel.
2201				EOF
2202				continuep || return 1
2203			fi
2204		done
2205
2206		# Don't need this any more...
2207		rm $1.all
2208	fi
2209}
2210
2211# Convert INDEX-OLD (last release) and INDEX-ALL (new release) into
2212# INDEX-OLD and INDEX-NEW files (in the sense of normal upgrades).
2213upgrade_oldall_to_oldnew () {
2214	# For each ${F}|... which appears in INDEX-ALL but does not appear
2215	# in INDEX-OLD, add ${F}|-|||||| to INDEX-OLD.
2216	cut -f 1 -d '|' < $1 |
2217	    sort -u > $1.paths
2218	cut -f 1 -d '|' < $2 |
2219	    sort -u |
2220	    comm -13 $1.paths - |
2221	    lam - -s "|-||||||" |
2222	    sort - $1 > $1.tmp
2223	mv $1.tmp $1
2224
2225	# Remove lines from INDEX-OLD which also appear in INDEX-ALL
2226	comm -23 $1 $2 > $1.tmp
2227	mv $1.tmp $1
2228
2229	# Remove lines from INDEX-ALL which have a file name not appearing
2230	# anywhere in INDEX-OLD (since these must be files which haven't
2231	# changed -- if they were new, there would be an entry of type "-").
2232	cut -f 1 -d '|' < $1 |
2233	    sort -u > $1.paths
2234	sort -k 1,1 -t '|' < $2 |
2235	    join -t '|' - $1.paths |
2236	    sort > $2.tmp
2237	rm $1.paths
2238	mv $2.tmp $2
2239
2240	# Rename INDEX-ALL to INDEX-NEW.
2241	mv $2 $3
2242}
2243
2244# From the list of "old" files in $1, merge changes in $2 with those in $3,
2245# and update $3 to reflect the hashes of merged files.
2246upgrade_merge () {
2247	# We only need to do anything if $1 is non-empty.
2248	if [ -s $1 ]; then
2249		cut -f 1 -d '|' $1 |
2250		    sort > $1-paths
2251
2252		# Create staging area for merging files
2253		rm -rf merge/
2254		while read F; do
2255			D=`dirname ${F}`
2256			mkdir -p merge/old/${D}
2257			mkdir -p merge/${OLDRELNUM}/${D}
2258			mkdir -p merge/${RELNUM}/${D}
2259			mkdir -p merge/new/${D}
2260		done < $1-paths
2261
2262		# Copy in files
2263		while read F; do
2264			# Currently installed file
2265			V=`look "${F}|" $2 | cut -f 7 -d '|'`
2266			gunzip < files/${V}.gz > merge/old/${F}
2267
2268			# Old release
2269			if look "${F}|" $1 | fgrep -q "|f|"; then
2270				V=`look "${F}|" $1 | cut -f 3 -d '|'`
2271				gunzip < files/${V}.gz		\
2272				    > merge/${OLDRELNUM}/${F}
2273			fi
2274
2275			# New release
2276			if look "${F}|" $3 | cut -f 1,2,7 -d '|' |
2277			    fgrep -q "|f|"; then
2278				V=`look "${F}|" $3 | cut -f 7 -d '|'`
2279				gunzip < files/${V}.gz		\
2280				    > merge/${RELNUM}/${F}
2281			fi
2282		done < $1-paths
2283
2284		# Attempt to automatically merge changes
2285		echo -n "Attempting to automatically merge "
2286		echo -n "changes in files..."
2287		: > failed.merges
2288		while read F; do
2289			# If the file doesn't exist in the new release,
2290			# the result of "merging changes" is having the file
2291			# not exist.
2292			if ! [ -f merge/${RELNUM}/${F} ]; then
2293				continue
2294			fi
2295
2296			# If the file didn't exist in the old release, we're
2297			# going to throw away the existing file and hope that
2298			# the version from the new release is what we want.
2299			if ! [ -f merge/${OLDRELNUM}/${F} ]; then
2300				cp merge/${RELNUM}/${F} merge/new/${F}
2301				continue
2302			fi
2303
2304			# Some files need special treatment.
2305			case ${F} in
2306			/etc/spwd.db | /etc/pwd.db | /etc/login.conf.db)
2307				# Don't merge these -- we're rebuild them
2308				# after updates are installed.
2309				cp merge/old/${F} merge/new/${F}
2310				;;
2311			*)
2312				if ! merge -p -L "current version"	\
2313				    -L "${OLDRELNUM}" -L "${RELNUM}"	\
2314				    merge/old/${F}			\
2315				    merge/${OLDRELNUM}/${F}		\
2316				    merge/${RELNUM}/${F}		\
2317				    > merge/new/${F} 2>/dev/null; then
2318					echo ${F} >> failed.merges
2319				fi
2320				;;
2321			esac
2322		done < $1-paths
2323		echo " done."
2324
2325		# Ask the user to handle any files which didn't merge.
2326		while read F; do
2327			cat <<-EOF
2328
2329The following file could not be merged automatically: ${F}
2330Press Enter to edit this file in ${EDITOR} and resolve the conflicts
2331manually...
2332			EOF
2333			read dummy </dev/tty
2334			${EDITOR} `pwd`/merge/new/${F} < /dev/tty
2335		done < failed.merges
2336		rm failed.merges
2337
2338		# Ask the user to confirm that he likes how the result
2339		# of merging files.
2340		while read F; do
2341			# Skip files which haven't changed.
2342			if [ -f merge/new/${F} ] &&
2343			    cmp -s merge/old/${F} merge/new/${F}; then
2344				continue
2345			fi
2346
2347			# Warn about files which are ceasing to exist.
2348			if ! [ -f merge/new/${F} ]; then
2349				cat <<-EOF
2350
2351The following file will be removed, as it no longer exists in
2352FreeBSD ${RELNUM}: ${F}
2353				EOF
2354				continuep < /dev/tty || return 1
2355				continue
2356			fi
2357
2358			# Print changes for the user's approval.
2359			cat <<-EOF
2360
2361The following changes, which occurred between FreeBSD ${OLDRELNUM} and
2362FreeBSD ${RELNUM} have been merged into ${F}:
2363EOF
2364			diff -U 5 -L "current version" -L "new version"	\
2365			    merge/old/${F} merge/new/${F} || true
2366			continuep < /dev/tty || return 1
2367		done < $1-paths
2368
2369		# Store merged files.
2370		while read F; do
2371			if [ -f merge/new/${F} ]; then
2372				V=`${SHA256} -q merge/new/${F}`
2373
2374				gzip -c < merge/new/${F} > files/${V}.gz
2375				echo "${F}|${V}"
2376			fi
2377		done < $1-paths > newhashes
2378
2379		# Pull lines out from $3 which need to be updated to
2380		# reflect merged files.
2381		while read F; do
2382			look "${F}|" $3
2383		done < $1-paths > $3-oldlines
2384
2385		# Update lines to reflect merged files
2386		join -t '|' -o 1.1,1.2,1.3,1.4,1.5,1.6,2.2,1.8		\
2387		    $3-oldlines newhashes > $3-newlines
2388
2389		# Remove old lines from $3 and add new lines.
2390		sort $3-oldlines |
2391		    comm -13 - $3 |
2392		    sort - $3-newlines > $3.tmp
2393		mv $3.tmp $3
2394
2395		# Clean up
2396		rm $1-paths newhashes $3-oldlines $3-newlines
2397		rm -rf merge/
2398	fi
2399
2400	# We're done with merging files.
2401	rm $1
2402}
2403
2404# Do the work involved in fetching upgrades to a new release
2405upgrade_run () {
2406	workdir_init || return 1
2407
2408	# Prepare the mirror list.
2409	fetch_pick_server_init && fetch_pick_server
2410
2411	# Try to fetch the public key until we run out of servers.
2412	while ! fetch_key; do
2413		fetch_pick_server || return 1
2414	done
2415 
2416	# Try to fetch the metadata index signature ("tag") until we run
2417	# out of available servers; and sanity check the downloaded tag.
2418	while ! fetch_tag; do
2419		fetch_pick_server || return 1
2420	done
2421	fetch_tagsanity || return 1
2422
2423	# Fetch the INDEX-OLD and INDEX-ALL.
2424	fetch_metadata INDEX-OLD INDEX-ALL || return 1
2425
2426	# If StrictComponents is not "yes", generate a new components list
2427	# with only the components which appear to be installed.
2428	upgrade_guess_components INDEX-ALL || return 1
2429
2430	# Generate filtered INDEX-OLD and INDEX-ALL files containing only
2431	# the components we want and without anything marked as "Ignore".
2432	fetch_filter_metadata INDEX-OLD || return 1
2433	fetch_filter_metadata INDEX-ALL || return 1
2434
2435	# Merge the INDEX-OLD and INDEX-ALL files into INDEX-OLD.
2436	sort INDEX-OLD INDEX-ALL > INDEX-OLD.tmp
2437	mv INDEX-OLD.tmp INDEX-OLD
2438	rm INDEX-ALL
2439
2440	# Adjust variables for fetching files from the new release.
2441	OLDRELNUM=${RELNUM}
2442	RELNUM=${TARGETRELEASE}
2443	OLDFETCHDIR=${FETCHDIR}
2444	FETCHDIR=${RELNUM}/${ARCH}
2445
2446	# Try to fetch the NEW metadata index signature ("tag") until we run
2447	# out of available servers; and sanity check the downloaded tag.
2448	while ! fetch_tag; do
2449		fetch_pick_server || return 1
2450	done
2451
2452	# Fetch the new INDEX-ALL.
2453	fetch_metadata INDEX-ALL || return 1
2454
2455	# If StrictComponents is not "yes", COMPONENTS contains an entry
2456	# corresponding to the currently running kernel, and said kernel
2457	# does not exist in the new release, add "kernel/generic" to the
2458	# list of components.
2459	upgrade_guess_new_kernel INDEX-ALL || return 1
2460
2461	# Filter INDEX-ALL to contain only the components we want and without
2462	# anything marked as "Ignore".
2463	fetch_filter_metadata INDEX-ALL || return 1
2464
2465	# Convert INDEX-OLD (last release) and INDEX-ALL (new release) into
2466	# INDEX-OLD and INDEX-NEW files (in the sense of normal upgrades).
2467	upgrade_oldall_to_oldnew INDEX-OLD INDEX-ALL INDEX-NEW
2468
2469	# Translate /boot/${KERNCONF} or /boot/${NKERNCONF} into ${KERNELDIR}
2470	fetch_filter_kernel_names INDEX-NEW ${NKERNCONF}
2471	fetch_filter_kernel_names INDEX-OLD ${KERNCONF}
2472
2473	# For all paths appearing in INDEX-OLD or INDEX-NEW, inspect the
2474	# system and generate an INDEX-PRESENT file.
2475	fetch_inspect_system INDEX-OLD INDEX-PRESENT INDEX-NEW || return 1
2476
2477	# Based on ${MERGECHANGES}, generate a file tomerge-old with the
2478	# paths and hashes of old versions of files to merge.
2479	fetch_filter_mergechanges INDEX-OLD INDEX-PRESENT tomerge-old
2480
2481	# Based on ${UPDATEIFUNMODIFIED}, remove lines from INDEX-* which
2482	# correspond to lines in INDEX-PRESENT with hashes not appearing
2483	# in INDEX-OLD or INDEX-NEW.  Also remove lines where the entry in
2484	# INDEX-PRESENT has type - and there isn't a corresponding entry in
2485	# INDEX-OLD with type -.
2486	fetch_filter_unmodified_notpresent	\
2487	    INDEX-OLD INDEX-PRESENT INDEX-NEW tomerge-old
2488
2489	# For each entry in INDEX-PRESENT of type -, remove any corresponding
2490	# entry from INDEX-NEW if ${ALLOWADD} != "yes".  Remove all entries
2491	# of type - from INDEX-PRESENT.
2492	fetch_filter_allowadd INDEX-PRESENT INDEX-NEW
2493
2494	# If ${ALLOWDELETE} != "yes", then remove any entries from
2495	# INDEX-PRESENT which don't correspond to entries in INDEX-NEW.
2496	fetch_filter_allowdelete INDEX-PRESENT INDEX-NEW
2497
2498	# If ${KEEPMODIFIEDMETADATA} == "yes", then for each entry in
2499	# INDEX-PRESENT with metadata not matching any entry in INDEX-OLD,
2500	# replace the corresponding line of INDEX-NEW with one having the
2501	# same metadata as the entry in INDEX-PRESENT.
2502	fetch_filter_modified_metadata INDEX-OLD INDEX-PRESENT INDEX-NEW
2503
2504	# Remove lines from INDEX-PRESENT and INDEX-NEW which are identical;
2505	# no need to update a file if it isn't changing.
2506	fetch_filter_uptodate INDEX-PRESENT INDEX-NEW
2507
2508	# Fetch "clean" files from the old release for merging changes.
2509	fetch_files_premerge tomerge-old
2510
2511	# Prepare to fetch files: Generate a list of the files we need,
2512	# copy the unmodified files we have into /files/, and generate
2513	# a list of patches to download.
2514	fetch_files_prepare INDEX-OLD INDEX-PRESENT INDEX-NEW || return 1
2515
2516	# Fetch patches from to-${RELNUM}/${ARCH}/bp/
2517	PATCHDIR=to-${RELNUM}/${ARCH}/bp
2518	fetch_files || return 1
2519
2520	# Merge configuration file changes.
2521	upgrade_merge tomerge-old INDEX-PRESENT INDEX-NEW || return 1
2522
2523	# Create and populate install manifest directory; and report what
2524	# updates are available.
2525	fetch_create_manifest || return 1
2526
2527	# Leave a note behind to tell the "install" command that the kernel
2528	# needs to be installed before the world.
2529	touch ${BDHASH}-install/kernelfirst
2530}
2531
2532# Make sure that all the file hashes mentioned in $@ have corresponding
2533# gzipped files stored in /files/.
2534install_verify () {
2535	# Generate a list of hashes
2536	cat $@ |
2537	    cut -f 2,7 -d '|' |
2538	    grep -E '^f' |
2539	    cut -f 2 -d '|' |
2540	    sort -u > filelist
2541
2542	# Make sure all the hashes exist
2543	while read HASH; do
2544		if ! [ -f files/${HASH}.gz ]; then
2545			echo -n "Update files missing -- "
2546			echo "this should never happen."
2547			echo "Re-run '$0 fetch'."
2548			return 1
2549		fi
2550	done < filelist
2551
2552	# Clean up
2553	rm filelist
2554}
2555
2556# Remove the system immutable flag from files
2557install_unschg () {
2558	# Generate file list
2559	cat $@ |
2560	    cut -f 1 -d '|' > filelist
2561
2562	# Remove flags
2563	while read F; do
2564		if ! [ -e ${BASEDIR}/${F} ]; then
2565			continue
2566		fi
2567
2568		chflags noschg ${BASEDIR}/${F} || return 1
2569	done < filelist
2570
2571	# Clean up
2572	rm filelist
2573}
2574
2575# Decide which directory name to use for kernel backups.
2576backup_kernel_finddir () {
2577	CNT=0
2578	while true ; do
2579		# Pathname does not exist, so it is OK use that name
2580		# for backup directory.
2581		if [ ! -e $BACKUPKERNELDIR ]; then
2582			return 0
2583		fi
2584
2585		# If directory do exist, we only use if it has our
2586		# marker file.
2587		if [ -d $BACKUPKERNELDIR -a \
2588			-e $BACKUPKERNELDIR/.freebsd-update ]; then
2589			return 0
2590		fi
2591
2592		# We could not use current directory name, so add counter to
2593		# the end and try again.
2594		CNT=$((CNT + 1))
2595		if [ $CNT -gt 9 ]; then
2596			echo "Could not find valid backup dir ($BACKUPKERNELDIR)"
2597			exit 1
2598		fi
2599		BACKUPKERNELDIR="`echo $BACKUPKERNELDIR | sed -Ee 's/[0-9]\$//'`"
2600		BACKUPKERNELDIR="${BACKUPKERNELDIR}${CNT}"
2601	done
2602}
2603
2604# Backup the current kernel using hardlinks, if not disabled by user.
2605# Since we delete all files in the directory used for previous backups
2606# we create a marker file called ".freebsd-update" in the directory so
2607# we can determine on the next run that the directory was created by
2608# freebsd-update and we then do not accidentally remove user files in
2609# the unlikely case that the user has created a directory with a
2610# conflicting name.
2611backup_kernel () {
2612	# Only make kernel backup is so configured.
2613	if [ $BACKUPKERNEL != yes ]; then
2614		return 0
2615	fi
2616
2617	# Decide which directory name to use for kernel backups.
2618	backup_kernel_finddir
2619
2620	# Remove old kernel backup files.  If $BACKUPKERNELDIR was
2621	# "not ours", backup_kernel_finddir would have exited, so
2622	# deleting the directory content is as safe as we can make it.
2623	if [ -d $BACKUPKERNELDIR ]; then
2624		rm -f $BACKUPKERNELDIR/*
2625	fi
2626
2627	# Create directory for backup if it doesn't exist.
2628	mkdir -p $BACKUPKERNELDIR
2629
2630	# Mark the directory as having been created by freebsd-update.
2631	touch $BACKUPKERNELDIR/.freebsd-update
2632	if [ $? -ne 0 ]; then
2633		echo "Could not create kernel backup directory"
2634		exit 1
2635	fi
2636
2637	# Disable pathname expansion to be sure *.symbols is not
2638	# expanded.
2639	set -f
2640
2641	# Use find to ignore symbol files, unless disabled by user.
2642	if [ $BACKUPKERNELSYMBOLFILES = yes ]; then
2643		FINDFILTER=""
2644	else
2645		FINDFILTER=-"a ! -name *.symbols"
2646	fi
2647
2648	# Backup all the kernel files using hardlinks.
2649	find $KERNELDIR -type f $FINDFILTER | \
2650		sed -Ee "s,($KERNELDIR)/?(.*),\1/\2 ${BACKUPKERNELDIR}/\2," | \
2651		xargs -n 2 cp -pl
2652
2653	# Re-enable patchname expansion.
2654	set +f
2655}
2656
2657# Install new files
2658install_from_index () {
2659	# First pass: Do everything apart from setting file flags.  We
2660	# can't set flags yet, because schg inhibits hard linking.
2661	sort -k 1,1 -t '|' $1 |
2662	    tr '|' ' ' |
2663	    while read FPATH TYPE OWNER GROUP PERM FLAGS HASH LINK; do
2664		case ${TYPE} in
2665		d)
2666			# Create a directory
2667			install -d -o ${OWNER} -g ${GROUP}		\
2668			    -m ${PERM} ${BASEDIR}/${FPATH}
2669			;;
2670		f)
2671			if [ -z "${LINK}" ]; then
2672				# Create a file, without setting flags.
2673				gunzip < files/${HASH}.gz > ${HASH}
2674				install -S -o ${OWNER} -g ${GROUP}	\
2675				    -m ${PERM} ${HASH} ${BASEDIR}/${FPATH}
2676				rm ${HASH}
2677			else
2678				# Create a hard link.
2679				ln -f ${BASEDIR}/${LINK} ${BASEDIR}/${FPATH}
2680			fi
2681			;;
2682		L)
2683			# Create a symlink
2684			ln -sfh ${HASH} ${BASEDIR}/${FPATH}
2685			;;
2686		esac
2687	    done
2688
2689	# Perform a second pass, adding file flags.
2690	tr '|' ' ' < $1 |
2691	    while read FPATH TYPE OWNER GROUP PERM FLAGS HASH LINK; do
2692		if [ ${TYPE} = "f" ] &&
2693		    ! [ ${FLAGS} = "0" ]; then
2694			chflags ${FLAGS} ${BASEDIR}/${FPATH}
2695		fi
2696	    done
2697}
2698
2699# Remove files which we want to delete
2700install_delete () {
2701	# Generate list of new files
2702	cut -f 1 -d '|' < $2 |
2703	    sort > newfiles
2704
2705	# Generate subindex of old files we want to nuke
2706	sort -k 1,1 -t '|' $1 |
2707	    join -t '|' -v 1 - newfiles |
2708	    sort -r -k 1,1 -t '|' |
2709	    cut -f 1,2 -d '|' |
2710	    tr '|' ' ' > killfiles
2711
2712	# Remove the offending bits
2713	while read FPATH TYPE; do
2714		case ${TYPE} in
2715		d)
2716			rmdir ${BASEDIR}/${FPATH}
2717			;;
2718		f)
2719			rm ${BASEDIR}/${FPATH}
2720			;;
2721		L)
2722			rm ${BASEDIR}/${FPATH}
2723			;;
2724		esac
2725	done < killfiles
2726
2727	# Clean up
2728	rm newfiles killfiles
2729}
2730
2731# Install new files, delete old files, and update linker.hints
2732install_files () {
2733	# If we haven't already dealt with the kernel, deal with it.
2734	if ! [ -f $1/kerneldone ]; then
2735		grep -E '^/boot/' $1/INDEX-OLD > INDEX-OLD
2736		grep -E '^/boot/' $1/INDEX-NEW > INDEX-NEW
2737
2738		# Backup current kernel before installing a new one
2739		backup_kernel || return 1
2740
2741		# Install new files
2742		install_from_index INDEX-NEW || return 1
2743
2744		# Remove files which need to be deleted
2745		install_delete INDEX-OLD INDEX-NEW || return 1
2746
2747		# Update linker.hints if necessary
2748		if [ -s INDEX-OLD -o -s INDEX-NEW ]; then
2749			kldxref -R /boot/ 2>/dev/null
2750		fi
2751
2752		# We've finished updating the kernel.
2753		touch $1/kerneldone
2754
2755		# Do we need to ask for a reboot now?
2756		if [ -f $1/kernelfirst ] &&
2757		    [ -s INDEX-OLD -o -s INDEX-NEW ]; then
2758			cat <<-EOF
2759
2760Kernel updates have been installed.  Please reboot and run
2761"$0 install" again to finish installing updates.
2762			EOF
2763			exit 0
2764		fi
2765	fi
2766
2767	# If we haven't already dealt with the world, deal with it.
2768	if ! [ -f $1/worlddone ]; then
2769		# Install new shared libraries next
2770		grep -vE '^/boot/' $1/INDEX-NEW |
2771		    grep -E '/lib/.*\.so\.[0-9]+\|' > INDEX-NEW
2772		install_from_index INDEX-NEW || return 1
2773
2774		# Deal with everything else
2775		grep -vE '^/boot/' $1/INDEX-OLD |
2776		    grep -vE '/lib/.*\.so\.[0-9]+\|' > INDEX-OLD
2777		grep -vE '^/boot/' $1/INDEX-NEW |
2778		    grep -vE '/lib/.*\.so\.[0-9]+\|' > INDEX-NEW
2779		install_from_index INDEX-NEW || return 1
2780		install_delete INDEX-OLD INDEX-NEW || return 1
2781
2782		# Rebuild /etc/spwd.db and /etc/pwd.db if necessary.
2783		if [ /etc/master.passwd -nt /etc/spwd.db ] ||
2784		    [ /etc/master.passwd -nt /etc/pwd.db ]; then
2785			pwd_mkdb /etc/master.passwd
2786		fi
2787
2788		# Rebuild /etc/login.conf.db if necessary.
2789		if [ /etc/login.conf -nt /etc/login.conf.db ]; then
2790			cap_mkdb /etc/login.conf
2791		fi
2792
2793		# We've finished installing the world and deleting old files
2794		# which are not shared libraries.
2795		touch $1/worlddone
2796
2797		# Do we need to ask the user to portupgrade now?
2798		grep -vE '^/boot/' $1/INDEX-NEW |
2799		    grep -E '/lib/.*\.so\.[0-9]+\|' |
2800		    cut -f 1 -d '|' |
2801		    sort > newfiles
2802		if grep -vE '^/boot/' $1/INDEX-OLD |
2803		    grep -E '/lib/.*\.so\.[0-9]+\|' |
2804		    cut -f 1 -d '|' |
2805		    sort |
2806		    join -v 1 - newfiles |
2807		    grep -q .; then
2808			cat <<-EOF
2809
2810Completing this upgrade requires removing old shared object files.
2811Please rebuild all installed 3rd party software (e.g., programs
2812installed from the ports tree) and then run "$0 install"
2813again to finish installing updates.
2814			EOF
2815			rm newfiles
2816			exit 0
2817		fi
2818		rm newfiles
2819	fi
2820
2821	# Remove old shared libraries
2822	grep -vE '^/boot/' $1/INDEX-NEW |
2823	    grep -E '/lib/.*\.so\.[0-9]+\|' > INDEX-NEW
2824	grep -vE '^/boot/' $1/INDEX-OLD |
2825	    grep -E '/lib/.*\.so\.[0-9]+\|' > INDEX-OLD
2826	install_delete INDEX-OLD INDEX-NEW || return 1
2827
2828	# Remove temporary files
2829	rm INDEX-OLD INDEX-NEW
2830}
2831
2832# Rearrange bits to allow the installed updates to be rolled back
2833install_setup_rollback () {
2834	# Remove the "reboot after installing kernel", "kernel updated", and
2835	# "finished installing the world" flags if present -- they are
2836	# irrelevant when rolling back updates.
2837	if [ -f ${BDHASH}-install/kernelfirst ]; then
2838		rm ${BDHASH}-install/kernelfirst
2839		rm ${BDHASH}-install/kerneldone
2840	fi
2841	if [ -f ${BDHASH}-install/worlddone ]; then
2842		rm ${BDHASH}-install/worlddone
2843	fi
2844
2845	if [ -L ${BDHASH}-rollback ]; then
2846		mv ${BDHASH}-rollback ${BDHASH}-install/rollback
2847	fi
2848
2849	mv ${BDHASH}-install ${BDHASH}-rollback
2850}
2851
2852# Actually install updates
2853install_run () {
2854	echo -n "Installing updates..."
2855
2856	# Make sure we have all the files we should have
2857	install_verify ${BDHASH}-install/INDEX-OLD	\
2858	    ${BDHASH}-install/INDEX-NEW || return 1
2859
2860	# Remove system immutable flag from files
2861	install_unschg ${BDHASH}-install/INDEX-OLD	\
2862	    ${BDHASH}-install/INDEX-NEW || return 1
2863
2864	# Install new files, delete old files, and update linker.hints
2865	install_files ${BDHASH}-install || return 1
2866
2867	# Rearrange bits to allow the installed updates to be rolled back
2868	install_setup_rollback
2869
2870	echo " done."
2871}
2872
2873# Rearrange bits to allow the previous set of updates to be rolled back next.
2874rollback_setup_rollback () {
2875	if [ -L ${BDHASH}-rollback/rollback ]; then
2876		mv ${BDHASH}-rollback/rollback rollback-tmp
2877		rm -r ${BDHASH}-rollback/
2878		rm ${BDHASH}-rollback
2879		mv rollback-tmp ${BDHASH}-rollback
2880	else
2881		rm -r ${BDHASH}-rollback/
2882		rm ${BDHASH}-rollback
2883	fi
2884}
2885
2886# Install old files, delete new files, and update linker.hints
2887rollback_files () {
2888	# Install old shared library files which don't have the same path as
2889	# a new shared library file.
2890	grep -vE '^/boot/' $1/INDEX-NEW |
2891	    grep -E '/lib/.*\.so\.[0-9]+\|' |
2892	    cut -f 1 -d '|' |
2893	    sort > INDEX-NEW.libs.flist
2894	grep -vE '^/boot/' $1/INDEX-OLD |
2895	    grep -E '/lib/.*\.so\.[0-9]+\|' |
2896	    sort -k 1,1 -t '|' - |
2897	    join -t '|' -v 1 - INDEX-NEW.libs.flist > INDEX-OLD
2898	install_from_index INDEX-OLD || return 1
2899
2900	# Deal with files which are neither kernel nor shared library
2901	grep -vE '^/boot/' $1/INDEX-OLD |
2902	    grep -vE '/lib/.*\.so\.[0-9]+\|' > INDEX-OLD
2903	grep -vE '^/boot/' $1/INDEX-NEW |
2904	    grep -vE '/lib/.*\.so\.[0-9]+\|' > INDEX-NEW
2905	install_from_index INDEX-OLD || return 1
2906	install_delete INDEX-NEW INDEX-OLD || return 1
2907
2908	# Install any old shared library files which we didn't install above.
2909	grep -vE '^/boot/' $1/INDEX-OLD |
2910	    grep -E '/lib/.*\.so\.[0-9]+\|' |
2911	    sort -k 1,1 -t '|' - |
2912	    join -t '|' - INDEX-NEW.libs.flist > INDEX-OLD
2913	install_from_index INDEX-OLD || return 1
2914
2915	# Delete unneeded shared library files
2916	grep -vE '^/boot/' $1/INDEX-OLD |
2917	    grep -E '/lib/.*\.so\.[0-9]+\|' > INDEX-OLD
2918	grep -vE '^/boot/' $1/INDEX-NEW |
2919	    grep -E '/lib/.*\.so\.[0-9]+\|' > INDEX-NEW
2920	install_delete INDEX-NEW INDEX-OLD || return 1
2921
2922	# Deal with kernel files
2923	grep -E '^/boot/' $1/INDEX-OLD > INDEX-OLD
2924	grep -E '^/boot/' $1/INDEX-NEW > INDEX-NEW
2925	install_from_index INDEX-OLD || return 1
2926	install_delete INDEX-NEW INDEX-OLD || return 1
2927	if [ -s INDEX-OLD -o -s INDEX-NEW ]; then
2928		kldxref -R /boot/ 2>/dev/null
2929	fi
2930
2931	# Remove temporary files
2932	rm INDEX-OLD INDEX-NEW INDEX-NEW.libs.flist
2933}
2934
2935# Actually rollback updates
2936rollback_run () {
2937	echo -n "Uninstalling updates..."
2938
2939	# If there are updates waiting to be installed, remove them; we
2940	# want the user to re-run 'fetch' after rolling back updates.
2941	if [ -L ${BDHASH}-install ]; then
2942		rm -r ${BDHASH}-install/
2943		rm ${BDHASH}-install
2944	fi
2945
2946	# Make sure we have all the files we should have
2947	install_verify ${BDHASH}-rollback/INDEX-NEW	\
2948	    ${BDHASH}-rollback/INDEX-OLD || return 1
2949
2950	# Remove system immutable flag from files
2951	install_unschg ${BDHASH}-rollback/INDEX-NEW	\
2952	    ${BDHASH}-rollback/INDEX-OLD || return 1
2953
2954	# Install old files, delete new files, and update linker.hints
2955	rollback_files ${BDHASH}-rollback || return 1
2956
2957	# Remove the rollback directory and the symlink pointing to it; and
2958	# rearrange bits to allow the previous set of updates to be rolled
2959	# back next.
2960	rollback_setup_rollback
2961
2962	echo " done."
2963}
2964
2965# Compare INDEX-ALL and INDEX-PRESENT and print warnings about differences.
2966IDS_compare () {
2967	# Get all the lines which mismatch in something other than file
2968	# flags.  We ignore file flags because sysinstall doesn't seem to
2969	# set them when it installs FreeBSD; warning about these adds a
2970	# very large amount of noise.
2971	cut -f 1-5,7-8 -d '|' $1 > $1.noflags
2972	sort -k 1,1 -t '|' $1.noflags > $1.sorted
2973	cut -f 1-5,7-8 -d '|' $2 |
2974	    comm -13 $1.noflags - |
2975	    fgrep -v '|-|||||' |
2976	    sort -k 1,1 -t '|' |
2977	    join -t '|' $1.sorted - > INDEX-NOTMATCHING
2978
2979	# Ignore files which match IDSIGNOREPATHS.
2980	for X in ${IDSIGNOREPATHS}; do
2981		grep -E "^${X}" INDEX-NOTMATCHING
2982	done |
2983	    sort -u |
2984	    comm -13 - INDEX-NOTMATCHING > INDEX-NOTMATCHING.tmp
2985	mv INDEX-NOTMATCHING.tmp INDEX-NOTMATCHING
2986
2987	# Go through the lines and print warnings.
2988	while read LINE; do
2989		FPATH=`echo "${LINE}" | cut -f 1 -d '|'`
2990		TYPE=`echo "${LINE}" | cut -f 2 -d '|'`
2991		OWNER=`echo "${LINE}" | cut -f 3 -d '|'`
2992		GROUP=`echo "${LINE}" | cut -f 4 -d '|'`
2993		PERM=`echo "${LINE}" | cut -f 5 -d '|'`
2994		HASH=`echo "${LINE}" | cut -f 6 -d '|'`
2995		LINK=`echo "${LINE}" | cut -f 7 -d '|'`
2996		P_TYPE=`echo "${LINE}" | cut -f 8 -d '|'`
2997		P_OWNER=`echo "${LINE}" | cut -f 9 -d '|'`
2998		P_GROUP=`echo "${LINE}" | cut -f 10 -d '|'`
2999		P_PERM=`echo "${LINE}" | cut -f 11 -d '|'`
3000		P_HASH=`echo "${LINE}" | cut -f 12 -d '|'`
3001		P_LINK=`echo "${LINE}" | cut -f 13 -d '|'`
3002
3003		# Warn about different object types.
3004		if ! [ "${TYPE}" = "${P_TYPE}" ]; then
3005			echo -n "${FPATH} is a "
3006			case "${P_TYPE}" in
3007			f)	echo -n "regular file, "
3008				;;
3009			d)	echo -n "directory, "
3010				;;
3011			L)	echo -n "symlink, "
3012				;;
3013			esac
3014			echo -n "but should be a "
3015			case "${TYPE}" in
3016			f)	echo -n "regular file."
3017				;;
3018			d)	echo -n "directory."
3019				;;
3020			L)	echo -n "symlink."
3021				;;
3022			esac
3023			echo
3024
3025			# Skip other tests, since they don't make sense if
3026			# we're comparing different object types.
3027			continue
3028		fi
3029
3030		# Warn about different owners.
3031		if ! [ "${OWNER}" = "${P_OWNER}" ]; then
3032			echo -n "${FPATH} is owned by user id ${P_OWNER}, "
3033			echo "but should be owned by user id ${OWNER}."
3034		fi
3035
3036		# Warn about different groups.
3037		if ! [ "${GROUP}" = "${P_GROUP}" ]; then
3038			echo -n "${FPATH} is owned by group id ${P_GROUP}, "
3039			echo "but should be owned by group id ${GROUP}."
3040		fi
3041
3042		# Warn about different permissions.  We do not warn about
3043		# different permissions on symlinks, since some archivers
3044		# don't extract symlink permissions correctly and they are
3045		# ignored anyway.
3046		if ! [ "${PERM}" = "${P_PERM}" ] &&
3047		    ! [ "${TYPE}" = "L" ]; then
3048			echo -n "${FPATH} has ${P_PERM} permissions, "
3049			echo "but should have ${PERM} permissions."
3050		fi
3051
3052		# Warn about different file hashes / symlink destinations.
3053		if ! [ "${HASH}" = "${P_HASH}" ]; then
3054			if [ "${TYPE}" = "L" ]; then
3055				echo -n "${FPATH} is a symlink to ${P_HASH}, "
3056				echo "but should be a symlink to ${HASH}."
3057			fi
3058			if [ "${TYPE}" = "f" ]; then
3059				echo -n "${FPATH} has SHA256 hash ${P_HASH}, "
3060				echo "but should have SHA256 hash ${HASH}."
3061			fi
3062		fi
3063
3064		# We don't warn about different hard links, since some
3065		# some archivers break hard links, and as long as the
3066		# underlying data is correct they really don't matter.
3067	done < INDEX-NOTMATCHING
3068
3069	# Clean up
3070	rm $1 $1.noflags $1.sorted $2 INDEX-NOTMATCHING
3071}
3072
3073# Do the work involved in comparing the system to a "known good" index
3074IDS_run () {
3075	workdir_init || return 1
3076
3077	# Prepare the mirror list.
3078	fetch_pick_server_init && fetch_pick_server
3079
3080	# Try to fetch the public key until we run out of servers.
3081	while ! fetch_key; do
3082		fetch_pick_server || return 1
3083	done
3084 
3085	# Try to fetch the metadata index signature ("tag") until we run
3086	# out of available servers; and sanity check the downloaded tag.
3087	while ! fetch_tag; do
3088		fetch_pick_server || return 1
3089	done
3090	fetch_tagsanity || return 1
3091
3092	# Fetch INDEX-OLD and INDEX-ALL.
3093	fetch_metadata INDEX-OLD INDEX-ALL || return 1
3094
3095	# Generate filtered INDEX-OLD and INDEX-ALL files containing only
3096	# the components we want and without anything marked as "Ignore".
3097	fetch_filter_metadata INDEX-OLD || return 1
3098	fetch_filter_metadata INDEX-ALL || return 1
3099
3100	# Merge the INDEX-OLD and INDEX-ALL files into INDEX-ALL.
3101	sort INDEX-OLD INDEX-ALL > INDEX-ALL.tmp
3102	mv INDEX-ALL.tmp INDEX-ALL
3103	rm INDEX-OLD
3104
3105	# Translate /boot/${KERNCONF} to ${KERNELDIR}
3106	fetch_filter_kernel_names INDEX-ALL ${KERNCONF}
3107
3108	# Inspect the system and generate an INDEX-PRESENT file.
3109	fetch_inspect_system INDEX-ALL INDEX-PRESENT /dev/null || return 1
3110
3111	# Compare INDEX-ALL and INDEX-PRESENT and print warnings about any
3112	# differences.
3113	IDS_compare INDEX-ALL INDEX-PRESENT
3114}
3115
3116#### Main functions -- call parameter-handling and core functions
3117
3118# Using the command line, configuration file, and defaults,
3119# set all the parameters which are needed later.
3120get_params () {
3121	init_params
3122	parse_cmdline $@
3123	parse_conffile
3124	default_params
3125}
3126
3127# Fetch command.  Make sure that we're being called
3128# interactively, then run fetch_check_params and fetch_run
3129cmd_fetch () {
3130	if [ ! -t 0 ]; then
3131		echo -n "`basename $0` fetch should not "
3132		echo "be run non-interactively."
3133		echo "Run `basename $0` cron instead."
3134		exit 1
3135	fi
3136	fetch_check_params
3137	fetch_run || exit 1
3138}
3139
3140# Cron command.  Make sure the parameters are sensible; wait
3141# rand(3600) seconds; then fetch updates.  While fetching updates,
3142# send output to a temporary file; only print that file if the
3143# fetching failed.
3144cmd_cron () {
3145	fetch_check_params
3146	sleep `jot -r 1 0 3600`
3147
3148	TMPFILE=`mktemp /tmp/freebsd-update.XXXXXX` || exit 1
3149	if ! fetch_run >> ${TMPFILE} ||
3150	    ! grep -q "No updates needed" ${TMPFILE} ||
3151	    [ ${VERBOSELEVEL} = "debug" ]; then
3152		mail -s "`hostname` security updates" ${MAILTO} < ${TMPFILE}
3153	fi
3154
3155	rm ${TMPFILE}
3156}
3157
3158# Fetch files for upgrading to a new release.
3159cmd_upgrade () {
3160	upgrade_check_params
3161	upgrade_run || exit 1
3162}
3163
3164# Install downloaded updates.
3165cmd_install () {
3166	install_check_params
3167	install_run || exit 1
3168}
3169
3170# Rollback most recently installed updates.
3171cmd_rollback () {
3172	rollback_check_params
3173	rollback_run || exit 1
3174}
3175
3176# Compare system against a "known good" index.
3177cmd_IDS () {
3178	IDS_check_params
3179	IDS_run || exit 1
3180}
3181
3182#### Entry point
3183
3184# Make sure we find utilities from the base system
3185export PATH=/sbin:/bin:/usr/sbin:/usr/bin:${PATH}
3186
3187# Set LC_ALL in order to avoid problems with character ranges like [A-Z].
3188export LC_ALL=C
3189
3190get_params $@
3191for COMMAND in ${COMMANDS}; do
3192	cmd_${COMMAND}
3193done
3194