1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
5 #include <linux/loop.h>
10 #include "architecture.h"
12 #include "dissect-image.h"
15 #include "format-table.h"
16 #include "format-util.h"
18 #include "hexdecoct.h"
20 #include "loop-util.h"
21 #include "main-func.h"
23 #include "mount-util.h"
24 #include "namespace-util.h"
25 #include "parse-util.h"
26 #include "path-util.h"
27 #include "pretty-print.h"
28 #include "stat-util.h"
29 #include "string-util.h"
31 #include "terminal-util.h"
32 #include "tmpfile-util.h"
33 #include "user-util.h"
41 } arg_action
= ACTION_DISSECT
;
42 static const char *arg_image
= NULL
;
43 static const char *arg_path
= NULL
;
44 static const char *arg_source
= NULL
;
45 static const char *arg_target
= NULL
;
46 static DissectImageFlags arg_flags
= DISSECT_IMAGE_REQUIRE_ROOT
|DISSECT_IMAGE_DISCARD_ON_LOOP
|DISSECT_IMAGE_RELAX_VAR_CHECK
|DISSECT_IMAGE_FSCK
;
47 static VeritySettings arg_verity_settings
= VERITY_SETTINGS_DEFAULT
;
48 static bool arg_json
= false;
49 static JsonFormatFlags arg_json_format_flags
= 0;
51 STATIC_DESTRUCTOR_REGISTER(arg_verity_settings
, verity_settings_done
);
53 static int help(void) {
54 _cleanup_free_
char *link
= NULL
;
57 r
= terminal_urlify_man("systemd-dissect", "1", &link
);
61 printf("%1$s [OPTIONS...] IMAGE\n"
62 "%1$s [OPTIONS...] --mount IMAGE PATH\n"
63 "%1$s [OPTIONS...] --copy-from IMAGE PATH [TARGET]\n"
64 "%1$s [OPTIONS...] --copy-to IMAGE [SOURCE] PATH\n\n"
65 "%5$sDissect a file system OS image.%6$s\n\n"
67 " -r --read-only Mount read-only\n"
68 " --fsck=BOOL Run fsck before mounting\n"
69 " --mkdir Make mount directory before mounting, if missing\n"
70 " --discard=MODE Choose 'discard' mode (disabled, loop, all, crypto)\n"
71 " --root-hash=HASH Specify root hash for verity\n"
72 " --root-hash-sig=SIG Specify pkcs7 signature of root hash for verity\n"
73 " as a DER encoded PKCS7, either as a path to a file\n"
74 " or as an ASCII base64 encoded string prefixed by\n"
76 " --verity-data=PATH Specify data file with hash tree for verity if it is\n"
77 " not embedded in IMAGE\n"
78 " --json=pretty|short|off\n"
79 " Generate JSON output\n"
80 "\n%3$sCommands:%4$s\n"
81 " -h --help Show this help\n"
82 " --version Show package version\n"
83 " -m --mount Mount the image to the specified directory\n"
84 " -M Shortcut for --mount --mkdir\n"
85 " -x --copy-from Copy files from image to host\n"
86 " -a --copy-to Copy files from host to image\n"
87 "\nSee the %2$s for details.\n"
88 , program_invocation_short_name
90 , ansi_underline(), ansi_normal()
91 , ansi_highlight(), ansi_normal());
96 static int parse_argv(int argc
, char *argv
[]) {
109 static const struct option options
[] = {
110 { "help", no_argument
, NULL
, 'h' },
111 { "version", no_argument
, NULL
, ARG_VERSION
},
112 { "mount", no_argument
, NULL
, 'm' },
113 { "read-only", no_argument
, NULL
, 'r' },
114 { "discard", required_argument
, NULL
, ARG_DISCARD
},
115 { "fsck", required_argument
, NULL
, ARG_FSCK
},
116 { "root-hash", required_argument
, NULL
, ARG_ROOT_HASH
},
117 { "root-hash-sig", required_argument
, NULL
, ARG_ROOT_HASH_SIG
},
118 { "verity-data", required_argument
, NULL
, ARG_VERITY_DATA
},
119 { "mkdir", no_argument
, NULL
, ARG_MKDIR
},
120 { "copy-from", no_argument
, NULL
, 'x' },
121 { "copy-to", no_argument
, NULL
, 'a' },
122 { "json", required_argument
, NULL
, ARG_JSON
},
131 while ((c
= getopt_long(argc
, argv
, "hmrMxa", options
, NULL
)) >= 0) {
142 arg_action
= ACTION_MOUNT
;
146 arg_flags
|= DISSECT_IMAGE_MKDIR
;
150 /* Shortcut combination of the above two */
151 arg_action
= ACTION_MOUNT
;
152 arg_flags
|= DISSECT_IMAGE_MKDIR
;
156 arg_action
= ACTION_COPY_FROM
;
157 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
161 arg_action
= ACTION_COPY_TO
;
165 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
169 DissectImageFlags flags
;
171 if (streq(optarg
, "disabled"))
173 else if (streq(optarg
, "loop"))
174 flags
= DISSECT_IMAGE_DISCARD_ON_LOOP
;
175 else if (streq(optarg
, "all"))
176 flags
= DISSECT_IMAGE_DISCARD_ON_LOOP
| DISSECT_IMAGE_DISCARD
;
177 else if (streq(optarg
, "crypt"))
178 flags
= DISSECT_IMAGE_DISCARD_ANY
;
179 else if (streq(optarg
, "list")) {
186 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
187 "Unknown --discard= parameter: %s",
189 arg_flags
= (arg_flags
& ~DISSECT_IMAGE_DISCARD_ANY
) | flags
;
194 case ARG_ROOT_HASH
: {
195 _cleanup_free_
void *p
= NULL
;
198 r
= unhexmem(optarg
, strlen(optarg
), &p
, &l
);
200 return log_error_errno(r
, "Failed to parse root hash '%s': %m", optarg
);
201 if (l
< sizeof(sd_id128_t
))
202 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
203 "Root hash must be at least 128bit long: %s", optarg
);
205 free_and_replace(arg_verity_settings
.root_hash
, p
);
206 arg_verity_settings
.root_hash_size
= l
;
210 case ARG_ROOT_HASH_SIG
: {
215 if ((value
= startswith(optarg
, "base64:"))) {
216 r
= unbase64mem(value
, strlen(value
), &p
, &l
);
218 return log_error_errno(r
, "Failed to parse root hash signature '%s': %m", optarg
);
220 r
= read_full_file(optarg
, (char**) &p
, &l
);
222 return log_error_errno(r
, "Failed to read root hash signature file '%s': %m", optarg
);
225 free_and_replace(arg_verity_settings
.root_hash_sig
, p
);
226 arg_verity_settings
.root_hash_sig_size
= l
;
230 case ARG_VERITY_DATA
:
231 r
= parse_path_argument_and_warn(optarg
, false, &arg_verity_settings
.data_path
);
237 r
= parse_boolean(optarg
);
239 return log_error_errno(r
, "Failed to parse --fsck= parameter: %s", optarg
);
241 SET_FLAG(arg_flags
, DISSECT_IMAGE_FSCK
, r
);
245 if (streq(optarg
, "pretty")) {
247 arg_json_format_flags
= JSON_FORMAT_PRETTY
|JSON_FORMAT_COLOR_AUTO
;
248 } else if (streq(optarg
, "short")) {
250 arg_json_format_flags
= JSON_FORMAT_NEWLINE
;
251 } else if (streq(optarg
, "off")) {
253 arg_json_format_flags
= 0;
254 } else if (streq(optarg
, "help")) {
260 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
), "Unknown argument to --json=: %s", optarg
);
268 assert_not_reached("Unhandled option");
273 switch (arg_action
) {
276 if (optind
+ 1 != argc
)
277 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
278 "Expected an image file path as only argument.");
280 arg_image
= argv
[optind
];
281 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
285 if (optind
+ 2 != argc
)
286 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
287 "Expected an image file path and mount point path as only arguments.");
289 arg_image
= argv
[optind
];
290 arg_path
= argv
[optind
+ 1];
293 case ACTION_COPY_FROM
:
294 if (argc
< optind
+ 2 || argc
> optind
+ 3)
295 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
296 "Expected an image file path, a source path and an optional destination path as only arguments.");
298 arg_image
= argv
[optind
];
299 arg_source
= argv
[optind
+ 1];
300 arg_target
= argc
> optind
+ 2 ? argv
[optind
+ 2] : "-" /* this means stdout */ ;
302 arg_flags
|= DISSECT_IMAGE_READ_ONLY
;
306 if (argc
< optind
+ 2 || argc
> optind
+ 3)
307 return log_error_errno(SYNTHETIC_ERRNO(EINVAL
),
308 "Expected an image file path, an optional source path and a destination path as only arguments.");
310 arg_image
= argv
[optind
];
312 if (argc
> optind
+ 2) {
313 arg_source
= argv
[optind
+ 1];
314 arg_target
= argv
[optind
+ 2];
316 arg_source
= "-"; /* this means stdin */
317 arg_target
= argv
[optind
+ 1];
323 assert_not_reached("Unknown action.");
329 static int strv_pair_to_json(char **l
, JsonVariant
**ret
) {
330 _cleanup_strv_free_
char **jl
= NULL
;
333 STRV_FOREACH_PAIR(a
, b
, l
) {
336 j
= strjoin(*a
, "=", *b
);
340 if (strv_consume(&jl
, j
) < 0)
344 return json_variant_new_array_strv(ret
, jl
);
347 static int action_dissect(DissectedImage
*m
, LoopDevice
*d
) {
348 _cleanup_(json_variant_unrefp
) JsonVariant
*v
= NULL
;
349 _cleanup_(table_unrefp
) Table
*t
= NULL
;
350 uint64_t size
= UINT64_MAX
;
357 printf(" Name: %s\n", basename(arg_image
));
359 if (ioctl(d
->fd
, BLKGETSIZE64
, &size
) < 0)
360 log_debug_errno(errno
, "Failed to query size of loopback device: %m");
361 else if (!arg_json
) {
362 char s
[FORMAT_BYTES_MAX
];
363 printf(" Size: %s\n", format_bytes(s
, sizeof(s
), size
));
369 r
= dissected_image_acquire_metadata(m
);
371 return log_error_errno(r
, "No root partition discovered.");
373 return log_error_errno(r
, "File system check of image failed.");
374 if (r
== -EMEDIUMTYPE
)
375 log_warning_errno(r
, "Not a valid OS image, no os-release file included. Proceeding anyway.");
376 else if (r
== -EUNATCH
)
377 log_warning_errno(r
, "OS image is encrypted, proceeding without showing OS image metadata.");
378 else if (r
== -EBUSY
)
379 log_warning_errno(r
, "OS image is currently in use, proceeding without showing OS image metadata.");
381 return log_error_errno(r
, "Failed to acquire image metadata: %m");
382 else if (!arg_json
) {
384 printf(" Hostname: %s\n", m
->hostname
);
386 if (!sd_id128_is_null(m
->machine_id
))
387 printf("Machine ID: " SD_ID128_FORMAT_STR
"\n", SD_ID128_FORMAT_VAL(m
->machine_id
));
389 if (!strv_isempty(m
->machine_info
)) {
392 STRV_FOREACH_PAIR(p
, q
, m
->machine_info
)
394 p
== m
->machine_info
? "Mach. Info:" : " ",
398 if (!strv_isempty(m
->os_release
)) {
401 STRV_FOREACH_PAIR(p
, q
, m
->os_release
)
403 p
== m
->os_release
? "OS Release:" : " ",
408 !sd_id128_is_null(m
->machine_id
) ||
409 !strv_isempty(m
->machine_info
) ||
410 !strv_isempty(m
->os_release
))
413 _cleanup_(json_variant_unrefp
) JsonVariant
*mi
= NULL
, *osr
= NULL
;
415 if (!strv_isempty(m
->machine_info
)) {
416 r
= strv_pair_to_json(m
->machine_info
, &mi
);
421 if (!strv_isempty(m
->os_release
)) {
422 r
= strv_pair_to_json(m
->os_release
, &osr
);
427 r
= json_build(&v
, JSON_BUILD_OBJECT(
428 JSON_BUILD_PAIR("name", JSON_BUILD_STRING(basename(arg_image
))),
429 JSON_BUILD_PAIR("size", JSON_BUILD_INTEGER(size
)),
430 JSON_BUILD_PAIR_CONDITION(m
->hostname
, "hostname", JSON_BUILD_STRING(m
->hostname
)),
431 JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(m
->machine_id
), "machineId", JSON_BUILD_ID128(m
->machine_id
)),
432 JSON_BUILD_PAIR_CONDITION(mi
, "machineInfo", JSON_BUILD_VARIANT(mi
)),
433 JSON_BUILD_PAIR_CONDITION(osr
, "osRelease", JSON_BUILD_VARIANT(osr
))));
438 t
= table_new("rw", "designator", "partition uuid", "fstype", "architecture", "verity", "node", "partno");
442 (void) table_set_empty_string(t
, "-");
443 (void) table_set_align_percent(t
, table_get_cell(t
, 0, 7), 100);
445 for (PartitionDesignator i
= 0; i
< _PARTITION_DESIGNATOR_MAX
; i
++) {
446 DissectedPartition
*p
= m
->partitions
+ i
;
453 TABLE_STRING
, p
->rw
? "rw" : "ro",
454 TABLE_STRING
, partition_designator_to_string(i
));
456 return table_log_add_error(r
);
458 if (sd_id128_is_null(p
->uuid
))
459 r
= table_add_cell(t
, NULL
, TABLE_EMPTY
, NULL
);
461 r
= table_add_cell(t
, NULL
, TABLE_UUID
, &p
->uuid
);
463 return table_log_add_error(r
);
467 TABLE_STRING
, p
->fstype
,
468 TABLE_STRING
, architecture_to_string(p
->architecture
));
470 return table_log_add_error(r
);
472 if (arg_verity_settings
.data_path
)
473 r
= table_add_cell(t
, NULL
, TABLE_STRING
, "external");
474 else if (dissected_image_can_do_verity(m
, i
))
475 r
= table_add_cell(t
, NULL
, TABLE_STRING
, yes_no(dissected_image_has_verity(m
, i
)));
477 r
= table_add_cell(t
, NULL
, TABLE_EMPTY
, NULL
);
479 return table_log_add_error(r
);
481 if (p
->partno
< 0) /* no partition table, naked file system */ {
482 r
= table_add_cell(t
, NULL
, TABLE_STRING
, arg_image
);
484 return table_log_add_error(r
);
486 r
= table_add_cell(t
, NULL
, TABLE_EMPTY
, NULL
);
488 r
= table_add_cell(t
, NULL
, TABLE_STRING
, p
->node
);
490 return table_log_add_error(r
);
492 r
= table_add_cell(t
, NULL
, TABLE_INT
, &p
->partno
);
495 return table_log_add_error(r
);
499 _cleanup_(json_variant_unrefp
) JsonVariant
*jt
= NULL
;
501 r
= table_to_json(t
, &jt
);
503 return log_error_errno(r
, "Failed to convert table to JSON: %m");
505 r
= json_variant_set_field(&v
, "mounts", jt
);
509 json_variant_dump(v
, arg_json_format_flags
, stdout
, NULL
);
511 r
= table_print(t
, stdout
);
513 return log_error_errno(r
, "Failed to dump table: %m");
519 static int action_mount(DissectedImage
*m
, LoopDevice
*d
) {
520 _cleanup_(decrypted_image_unrefp
) DecryptedImage
*di
= NULL
;
526 r
= dissected_image_decrypt_interactively(
528 &arg_verity_settings
,
534 r
= dissected_image_mount_and_warn(m
, arg_path
, UID_INVALID
, arg_flags
);
539 r
= decrypted_image_relinquish(di
);
541 return log_error_errno(r
, "Failed to relinquish DM devices: %m");
544 loop_device_relinquish(d
);
548 static int action_copy(DissectedImage
*m
, LoopDevice
*d
) {
549 _cleanup_(umount_and_rmdir_and_freep
) char *mounted_dir
= NULL
;
550 _cleanup_(decrypted_image_unrefp
) DecryptedImage
*di
= NULL
;
551 _cleanup_(rmdir_and_freep
) char *created_dir
= NULL
;
552 _cleanup_free_
char *temp
= NULL
;
558 r
= dissected_image_decrypt_interactively(
560 &arg_verity_settings
,
566 r
= detach_mount_namespace();
568 return log_error_errno(r
, "Failed to detach mount namespace: %m");
570 r
= tempfn_random_child(NULL
, program_invocation_short_name
, &temp
);
572 return log_error_errno(r
, "Failed to generate temporary mount directory: %m");
574 r
= mkdir_p(temp
, 0700);
576 return log_error_errno(r
, "Failed to create mount point: %m");
578 created_dir
= TAKE_PTR(temp
);
580 r
= dissected_image_mount_and_warn(m
, created_dir
, UID_INVALID
, arg_flags
);
584 mounted_dir
= TAKE_PTR(created_dir
);
587 r
= decrypted_image_relinquish(di
);
589 return log_error_errno(r
, "Failed to relinquish DM devices: %m");
592 loop_device_relinquish(d
);
594 if (arg_action
== ACTION_COPY_FROM
) {
595 _cleanup_close_
int source_fd
= -1, target_fd
= -1;
597 source_fd
= chase_symlinks_and_open(arg_source
, mounted_dir
, CHASE_PREFIX_ROOT
|CHASE_WARN
, O_RDONLY
|O_CLOEXEC
|O_NOCTTY
, NULL
);
599 return log_error_errno(source_fd
, "Failed to open source path '%s' in image '%s': %m", arg_source
, arg_image
);
601 /* Copying to stdout? */
602 if (streq(arg_target
, "-")) {
603 r
= copy_bytes(source_fd
, STDOUT_FILENO
, (uint64_t) -1, COPY_REFLINK
);
605 return log_error_errno(r
, "Failed to copy bytes from %s in mage '%s' to stdout: %m", arg_source
, arg_image
);
607 /* When we copy to stdou we don't copy any attributes (i.e. no access mode, no ownership, no xattr, no times) */
611 /* Try to copy as directory? */
612 r
= copy_directory_fd(source_fd
, arg_target
, COPY_REFLINK
|COPY_MERGE_EMPTY
|COPY_SIGINT
|COPY_HARDLINKS
);
616 return log_error_errno(r
, "Failed to copy %s in image '%s' to '%s': %m", arg_source
, arg_image
, arg_target
);
618 r
= fd_verify_regular(source_fd
);
620 return log_error_errno(r
, "Target '%s' exists already and is not a directory.", arg_target
);
622 return log_error_errno(r
, "Source path %s in image '%s' is neither regular file nor directory, refusing: %m", arg_source
, arg_image
);
624 /* Nah, it's a plain file! */
625 target_fd
= open(arg_target
, O_WRONLY
|O_CREAT
|O_EXCL
|O_CLOEXEC
|O_NOCTTY
|O_NOFOLLOW
, 0600);
627 return log_error_errno(errno
, "Failed to create regular file at target path '%s': %m", arg_target
);
629 r
= copy_bytes(source_fd
, target_fd
, (uint64_t) -1, COPY_REFLINK
);
631 return log_error_errno(r
, "Failed to copy bytes from %s in mage '%s' to '%s': %m", arg_source
, arg_image
, arg_target
);
633 (void) copy_xattr(source_fd
, target_fd
);
634 (void) copy_access(source_fd
, target_fd
);
635 (void) copy_times(source_fd
, target_fd
, 0);
637 /* When this is a regular file we don't copy ownership! */
640 _cleanup_close_
int source_fd
= -1, target_fd
= -1;
641 _cleanup_close_
int dfd
= -1;
642 _cleanup_free_
char *dn
= NULL
;
644 assert(arg_action
== ACTION_COPY_TO
);
646 dn
= dirname_malloc(arg_target
);
650 r
= chase_symlinks(dn
, mounted_dir
, CHASE_PREFIX_ROOT
|CHASE_WARN
, NULL
, &dfd
);
652 return log_error_errno(r
, "Failed to open '%s': %m", dn
);
654 /* Are we reading from stdin? */
655 if (streq(arg_source
, "-")) {
656 target_fd
= openat(dfd
, basename(arg_target
), O_WRONLY
|O_CREAT
|O_CLOEXEC
|O_NOCTTY
|O_EXCL
, 0644);
658 return log_error_errno(errno
, "Failed to open target file '%s': %m", arg_target
);
660 r
= copy_bytes(STDIN_FILENO
, target_fd
, (uint64_t) -1, COPY_REFLINK
);
662 return log_error_errno(r
, "Failed to copy bytes from stdin to '%s' in image '%s': %m", arg_target
, arg_image
);
664 /* When we copy from stdin we don't copy any attributes (i.e. no access mode, no ownership, no xattr, no times) */
668 source_fd
= open(arg_source
, O_RDONLY
|O_CLOEXEC
|O_NOCTTY
);
670 return log_error_errno(source_fd
, "Failed to open source path '%s': %m", arg_source
);
672 r
= fd_verify_regular(source_fd
);
675 return log_error_errno(r
, "Source '%s' is neither regular file nor directory: %m", arg_source
);
677 /* We are looking at a directory. */
679 target_fd
= openat(dfd
, basename(arg_target
), O_RDONLY
|O_DIRECTORY
|O_CLOEXEC
);
682 return log_error_errno(errno
, "Failed to open destination '%s': %m", arg_target
);
684 r
= copy_tree_at(source_fd
, ".", dfd
, basename(arg_target
), UID_INVALID
, GID_INVALID
, COPY_REFLINK
|COPY_REPLACE
|COPY_SIGINT
|COPY_HARDLINKS
);
686 r
= copy_tree_at(source_fd
, ".", target_fd
, ".", UID_INVALID
, GID_INVALID
, COPY_REFLINK
|COPY_REPLACE
|COPY_SIGINT
|COPY_HARDLINKS
);
688 return log_error_errno(r
, "Failed to copy '%s' to '%s' in image '%s': %m", arg_source
, arg_target
, arg_image
);
693 /* We area looking at a regular file */
694 target_fd
= openat(dfd
, basename(arg_target
), O_WRONLY
|O_CREAT
|O_CLOEXEC
|O_NOCTTY
|O_EXCL
, 0600);
696 return log_error_errno(errno
, "Failed to open target file '%s': %m", arg_target
);
698 r
= copy_bytes(source_fd
, target_fd
, (uint64_t) -1, COPY_REFLINK
);
700 return log_error_errno(r
, "Failed to copy bytes from '%s' to '%s' in image '%s': %m", arg_source
, arg_target
, arg_image
);
702 (void) copy_xattr(source_fd
, target_fd
);
703 (void) copy_access(source_fd
, target_fd
);
704 (void) copy_times(source_fd
, target_fd
, 0);
706 /* When this is a regular file we don't copy ownership! */
712 static int run(int argc
, char *argv
[]) {
713 _cleanup_(dissected_image_unrefp
) DissectedImage
*m
= NULL
;
714 _cleanup_(loop_device_unrefp
) LoopDevice
*d
= NULL
;
717 log_parse_environment();
720 r
= parse_argv(argc
, argv
);
724 r
= verity_settings_load(
725 &arg_verity_settings
,
726 arg_image
, NULL
, NULL
);
728 return log_error_errno(r
, "Failed to read verity artifacts for %s: %m", arg_image
);
730 if (arg_verity_settings
.data_path
)
731 arg_flags
|= DISSECT_IMAGE_NO_PARTITION_TABLE
; /* We only support Verity per file system,
732 * hence if there's external Verity data
733 * available we turn off partition table
736 r
= loop_device_make_by_path(
738 FLAGS_SET(arg_flags
, DISSECT_IMAGE_READ_ONLY
) ? O_RDONLY
: O_RDWR
,
739 FLAGS_SET(arg_flags
, DISSECT_IMAGE_NO_PARTITION_TABLE
) ? 0 : LO_FLAGS_PARTSCAN
,
742 return log_error_errno(r
, "Failed to set up loopback device: %m");
744 r
= dissect_image_and_warn(
747 &arg_verity_settings
,
754 switch (arg_action
) {
757 r
= action_dissect(m
, d
);
761 r
= action_mount(m
, d
);
764 case ACTION_COPY_FROM
:
766 r
= action_copy(m
, d
);
770 assert_not_reached("Unknown action.");
776 DEFINE_MAIN_FUNCTION(run
);