Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ tests/runtime_tests
tests/cli_runtime_tests
tests/*_tests
tests/*_tests.dSYM/
tests/diff_roots_spike
tests/diff_roots_spike.dSYM/
tests/verbs_test.tmp
tests/table_operations_integration
tests/time_portability_test
Expand Down
30 changes: 30 additions & 0 deletions Common/headers/op.h
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,36 @@ extern boolean oppack (Handle *); /*oppack.c*/

extern boolean oppackoutline (hdloutlinerecord, Handle *);

/*
Volatile (save-metadata) regions of the packed-outline header, for tools
that compare packed outlines for CONTENT equality.

These fields are rewritten by the act of saving -- and ctsaves is
INCREMENTED by oppack itself -- so an equality check over raw packed bytes
must exclude them, or two semantically identical outlines compare unequal.

Exposed as a table derived with offsetof()/sizeof() on the real struct
(which is file-local to oppack_v7.c) so callers cannot drift out of
agreement with the on-disk layout. Only fields with DEMONSTRATED volatility
are listed; anything else must surface as a real difference rather than be
silently swallowed.

op_packed_header_volatile_regions() writes up to *ctregions entries into
regions[] and sets *ctregions to the number written; returns false if the
caller's array is too small. op_packed_header_size() is the total header
size, so a caller can bound-check an offset before consulting the table.
*/
typedef struct typackedheaderregion {

long offset;
long length;

} typackedheaderregion;

extern long op_packed_header_size (void);

extern boolean op_packed_header_volatile_regions (typackedheaderregion *regions, long *ctregions);

extern boolean opunpack (Handle, long *, hdloutlinerecord *);

extern boolean opunpackoutline (Handle, hdloutlinerecord *);
Expand Down
64 changes: 64 additions & 0 deletions Common/source/oppack_v7.c
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,70 @@ typedef struct typortablediskheader {
_Static_assert (sizeof (typortablediskheader) == 1068, "typortablediskheader must be 1068 bytes");


long op_packed_header_size (void) {

return ((long) sizeof (typortablediskheader));
} /*op_packed_header_size*/


boolean op_packed_header_volatile_regions (typackedheaderregion *regions, long *ctregions) {

/*
The save-metadata fields of the packed header, derived from the struct
rather than hardcoded so a layout change cannot silently desynchronize a
caller. See op.h for why callers need these.

This is an ALLOWLIST: a field earns a place only by demonstrated
volatility, with the mechanism named. Adding to it on suspicion is how a
real difference gets silently swallowed.

timecreated -- per-value creation stamp; differs between a built root
and its ancestor without the content differing
timelastsave -- rewritten by the act of saving
ctsaves -- INCREMENTED by oppack() itself (see below, where it is
bumped and written back into the outline record), so it
differs even between two packs of one resident value

outlinesignature is deliberately NOT listed, and it is not a close call:
it is SEMANTIC CONTENT, not metadata. It selects the OSA server that will
run the script (scripts.c:943, getosaserver), drives tree building
(scripts.c:1302), and gates UserTalk-specific text handling
(scripts.c:3049, `outlinesignature == typeLAND`). A difference in this
field means a different LANGUAGE, which is exactly the kind of difference
the walker exists to report. That it reads as a constant 'LAND' throughout
the shipped roots is a fact about today's data, not a license to exclude
it.
*/

const typackedheaderregion volatileregions [] = {
{(long) offsetof (typortablediskheader, timecreated),
(long) sizeof (((typortablediskheader *) 0)->timecreated)},
{(long) offsetof (typortablediskheader, timelastsave),
(long) sizeof (((typortablediskheader *) 0)->timelastsave)},
{(long) offsetof (typortablediskheader, ctsaves),
(long) sizeof (((typortablediskheader *) 0)->ctsaves)}
};

const long ctvolatile = (long) (sizeof (volatileregions) / sizeof (volatileregions [0]));
long i;

if ((regions == NULL) || (ctregions == NULL))
return (false);

if (*ctregions < ctvolatile) {
*ctregions = ctvolatile; /*tell the caller how much room it needs*/
return (false);
}

for (i = 0; i < ctvolatile; i++)
regions [i] = volatileregions [i];

*ctregions = ctvolatile;

return (true);
} /*op_packed_header_volatile_regions*/


typedef struct tyoppackinfo {

handlestream *packstream;
Expand Down
1 change: 1 addition & 0 deletions frontier-cli/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ CLI_SOURCES = \
window_registry.c \
ut_sync.c \
ut_scan.c \
diff_roots.c \
debugger_tui.c \
boxen_repl.c \
boxen_completion_popup.c \
Expand Down
81 changes: 81 additions & 0 deletions frontier-cli/cli_parser.c
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,46 @@ boolean cli_validate_options(const cli_options_t* options) {
boolean hydration_mode = options->hydrate_system_root;
boolean migrate_mode = (options->migrate_database != NULL);

/* --diff-roots mode: compare two roots read-only and exit. Both paths are
required and both must be readable; the walker never writes either. */
if (options->diff_roots_a != NULL) {
if (options->diff_roots_b == NULL) {
log_error(LOG_COMP_GENERAL, "Error: --diff-roots requires --against <other.root>");
return false;
}
if (!cli_file_exists(options->diff_roots_a) || !cli_file_readable(options->diff_roots_a)) {
log_error(LOG_COMP_GENERAL, "Error: Cannot read database: %s", options->diff_roots_a);
return false;
}
if (!cli_file_exists(options->diff_roots_b) || !cli_file_readable(options->diff_roots_b)) {
log_error(LOG_COMP_GENERAL, "Error: Cannot read database: %s", options->diff_roots_b);
return false;
}
if (migrate_mode || hydration_mode || options->system_root != NULL
|| options->script_file != NULL || options->inline_script != NULL) {
log_error(LOG_COMP_GENERAL, "Error: --diff-roots runs standalone; do not combine with --migrate, --system-root, scripts, or REPL mode");
return false;
}
/* ut-sync is a WRITE path: its import hook rewrites outlines, sets
* dirty flags, and writes .ut-sync-state. It must never run under a
* mode whose entire contract is that neither root is modified.
* Rejected loudly rather than quietly ignored, because
* FRONTIER_UT_SYNC_DIR populates this field from the environment just
* above this validation -- so it can be on without appearing on the
* command line at all. */
if (options->ut_sync_dir != NULL) {
log_error(LOG_COMP_GENERAL, "Error: --diff-roots cannot be combined with --ut-sync-dir or FRONTIER_UT_SYNC_DIR; the walker opens both roots read-only and ut-sync would write to them");
return false;
}
return true;
}

/* --against requires --diff-roots */
if (options->diff_roots_b != NULL) {
log_error(LOG_COMP_GENERAL, "Error: --against requires --diff-roots");
return false;
}

/* --migrate mode: migrate a database to v7 and exit */
if (migrate_mode) {
if (!cli_file_exists(options->migrate_database)) {
Expand Down Expand Up @@ -285,6 +325,8 @@ boolean cli_parse_arguments(int argc, char* argv[], cli_options_t* options) {
OPT_UT_SYNC_DIR,
OPT_DEBUG_TUI, /* 2026-06-06 JES Phase B.0 #691: --debug-tui */
OPT_PLAIN, /* 2026-06-25 JES C.6 #691: --plain */
OPT_DIFF_ROOTS, /* root-build Step 1: --diff-roots A */
OPT_AGAINST, /* root-build Step 1: --against B */
};

// Define long options
Expand All @@ -293,6 +335,11 @@ boolean cli_parse_arguments(int argc, char* argv[], cli_options_t* options) {
{"system-root", required_argument, 0, 'R'},
{"migrate", required_argument, 0, 'm'},
{"output", required_argument, 0, 'o'},
/* Two-path mode. getopt_long has no two-argument option form, so the
second path arrives via a paired option -- the same shape --migrate
and --output already use. */
{"diff-roots", required_argument, 0, OPT_DIFF_ROOTS},
{"against", required_argument, 0, OPT_AGAINST},
{"force", no_argument, 0, 'f'},
{"batch", no_argument, 0, 'b'},
{"non-interactive", no_argument, 0, 'b'}, /* Alias for --batch */
Expand Down Expand Up @@ -526,6 +573,30 @@ boolean cli_parse_arguments(int argc, char* argv[], cli_options_t* options) {
options->migrate_database = strdup(optarg);
break;

case OPT_DIFF_ROOTS:
if (options->diff_roots_a != NULL) {
log_error(LOG_COMP_GENERAL, "Error: Multiple --diff-roots options not allowed");
goto parse_error;
}
if (strlen(optarg) > CLI_MAX_PATH_LENGTH) {
log_error(LOG_COMP_GENERAL, "Error: --diff-roots path too long (max %d characters)", CLI_MAX_PATH_LENGTH);
goto parse_error;
}
options->diff_roots_a = strdup(optarg);
break;

case OPT_AGAINST:
if (options->diff_roots_b != NULL) {
log_error(LOG_COMP_GENERAL, "Error: Multiple --against options not allowed");
goto parse_error;
}
if (strlen(optarg) > CLI_MAX_PATH_LENGTH) {
log_error(LOG_COMP_GENERAL, "Error: --against path too long (max %d characters)", CLI_MAX_PATH_LENGTH);
goto parse_error;
}
options->diff_roots_b = strdup(optarg);
break;

case 'o':
if (options->output_path != NULL) {
log_error(LOG_COMP_GENERAL, "Error: Multiple --output options not allowed");
Expand Down Expand Up @@ -752,6 +823,16 @@ void cli_free_options(cli_options_t* options) {
options->migrate_database = NULL;
}

if (options->diff_roots_a != NULL) {
free(options->diff_roots_a);
options->diff_roots_a = NULL;
}

if (options->diff_roots_b != NULL) {
free(options->diff_roots_b);
options->diff_roots_b = NULL;
}

if (options->output_path != NULL) {
free(options->output_path);
options->output_path = NULL;
Expand Down
3 changes: 3 additions & 0 deletions frontier-cli/cli_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ typedef struct {
char* system_root; // Path to system/root database (e.g., Frontier.root)
char* migrate_database; // Path to database to migrate (--migrate)
char* output_path; // Output path for migration (--output)
char* diff_roots_a; // First root to compare (--diff-roots)
char* diff_roots_b; // Second root to compare (--against); paired with
// diff_roots_a the same way --output pairs with --migrate
char* log_spec; // Log spec string (--log comp:level,...)
boolean verbose; // Verbose output
boolean debug; // Debug output
Expand Down
Loading
Loading