@@ -34,6 +34,13 @@ struct _OstreeBootconfigParser
3434
3535 /* Additional initrds; the primary initrd is in options. */
3636 char * * overlay_initrds ;
37+
38+ /* Magic comments preserved across parse/write roundtrips.
39+ * Each entry is the comment body (without the leading "# "),
40+ * e.g. "x-ostree-options-source-tuned nohz=full isolcpus=1-3".
41+ * Only comments matching allowed_comment_prefixes are preserved.
42+ */
43+ GPtrArray * comments ;
3744};
3845
3946typedef GObjectClass OstreeBootconfigParserClass ;
@@ -57,6 +64,13 @@ ostree_bootconfig_parser_clone (OstreeBootconfigParser *self)
5764 parser -> filename = g_strdup (self -> filename );
5865 parser -> overlay_initrds = g_strdupv (self -> overlay_initrds );
5966
67+ if (self -> comments )
68+ {
69+ parser -> comments = g_ptr_array_new_with_free_func (g_free );
70+ for (guint i = 0 ; i < self -> comments -> len ; i ++ )
71+ g_ptr_array_add (parser -> comments , g_strdup (g_ptr_array_index (self -> comments , i )));
72+ }
73+
6074 return parser ;
6175}
6276
@@ -136,6 +150,25 @@ _ostree_bootconfig_parser_filename (OstreeBootconfigParser *self)
136150 return self -> filename ;
137151}
138152
153+ /* Allowlist of comment prefixes that should be preserved across parse/write
154+ * roundtrips and the staged deployment serialization. Only BLS comment lines
155+ * (starting with '#') whose body (after stripping "# ") matches one of these
156+ * prefixes are stored in self->comments. To allow a new family of magic
157+ * comments, add its prefix here.
158+ */
159+ static const char * const allowed_comment_prefixes [] = { "x-ostree-options-source-" , NULL };
160+
161+ static gboolean
162+ is_allowed_comment (const char * comment_body )
163+ {
164+ for (const char * const * p = allowed_comment_prefixes ; * p != NULL ; p ++ )
165+ {
166+ if (g_str_has_prefix (comment_body , * p ))
167+ return TRUE;
168+ }
169+ return FALSE;
170+ }
171+
139172/**
140173 * ostree_bootconfig_parser_parse_at:
141174 * @self: Parser
@@ -188,6 +221,21 @@ ostree_bootconfig_parser_parse_at (OstreeBootconfigParser *self, int dfd, const
188221 g_strfreev (items );
189222 }
190223 }
224+ else if (* line == '#' )
225+ {
226+ /* Check for magic comment lines that we preserve.
227+ * Strip the leading '#' and any whitespace to get the body.
228+ */
229+ const char * body = line + 1 ;
230+ while (* body == ' ' || * body == '\t' )
231+ body ++ ;
232+ if (is_allowed_comment (body ))
233+ {
234+ if (!self -> comments )
235+ self -> comments = g_ptr_array_new_with_free_func (g_free );
236+ g_ptr_array_add (self -> comments , g_strdup (body ));
237+ }
238+ }
191239 }
192240
193241 if (overlay_initrds )
@@ -324,6 +372,18 @@ ostree_bootconfig_parser_write_at (OstreeBootconfigParser *self, int dfd, const
324372 write_key (self , buf , k , v );
325373 }
326374
375+ /* Write preserved magic comments */
376+ if (self -> comments )
377+ {
378+ for (guint i = 0 ; i < self -> comments -> len ; i ++ )
379+ {
380+ const char * comment = g_ptr_array_index (self -> comments , i );
381+ g_string_append (buf , "# " );
382+ g_string_append (buf , comment );
383+ g_string_append_c (buf , '\n' );
384+ }
385+ }
386+
327387 if (!glnx_file_replace_contents_at (dfd , path , (guint8 * )buf -> str , buf -> len ,
328388 GLNX_FILE_REPLACE_NODATASYNC , cancellable , error ))
329389 return FALSE;
@@ -339,6 +399,136 @@ ostree_bootconfig_parser_write (OstreeBootconfigParser *self, GFile *output,
339399 cancellable , error );
340400}
341401
402+ /**
403+ * ostree_bootconfig_parser_get_comment:
404+ * @self: Parser
405+ * @comment_key: The comment key to look up (e.g. "x-ostree-options-source-tuned")
406+ *
407+ * Searches the stored magic comments for one whose first space-delimited
408+ * token matches @comment_key. Returns the value portion (everything after
409+ * the first space), or NULL if not found.
410+ *
411+ * Returns: (nullable): The value string, or NULL if no matching comment exists.
412+ * The returned string is owned by the parser; do not free.
413+ *
414+ * Since: 2025.8
415+ */
416+ const char *
417+ ostree_bootconfig_parser_get_comment (OstreeBootconfigParser * self , const char * comment_key )
418+ {
419+ if (!self -> comments )
420+ return NULL ;
421+
422+ const gsize key_len = strlen (comment_key );
423+ for (guint i = 0 ; i < self -> comments -> len ; i ++ )
424+ {
425+ const char * entry = g_ptr_array_index (self -> comments , i );
426+ if (g_str_has_prefix (entry , comment_key ))
427+ {
428+ char after = entry [key_len ];
429+ if (after == '\0' )
430+ return "" ; /* Key exists with no value */
431+ if (after == ' ' || after == '\t' )
432+ return entry + key_len + 1 ; /* Skip the separator */
433+ }
434+ }
435+ return NULL ;
436+ }
437+
438+ /**
439+ * ostree_bootconfig_parser_set_comment:
440+ * @self: Parser
441+ * @comment_key: The comment key (e.g. "x-ostree-options-source-tuned")
442+ * @value: (nullable): The value string (e.g. "nohz=full isolcpus=1-3"),
443+ * or NULL/empty to set a key-only comment
444+ *
445+ * Sets or replaces a magic comment entry. If a comment with the same key
446+ * already exists, it is replaced. Otherwise a new entry is appended.
447+ * The comment must match an allowed prefix (see allowed_comment_prefixes).
448+ *
449+ * Since: 2025.8
450+ */
451+ void
452+ ostree_bootconfig_parser_set_comment (OstreeBootconfigParser * self , const char * comment_key ,
453+ const char * value )
454+ {
455+ g_assert (is_allowed_comment (comment_key ));
456+
457+ g_autofree char * new_entry = NULL ;
458+ if (value && * value )
459+ new_entry = g_strdup_printf ("%s %s" , comment_key , value );
460+ else
461+ new_entry = g_strdup (comment_key );
462+
463+ if (!self -> comments )
464+ self -> comments = g_ptr_array_new_with_free_func (g_free );
465+
466+ /* Replace existing entry if present */
467+ const gsize key_len = strlen (comment_key );
468+ for (guint i = 0 ; i < self -> comments -> len ; i ++ )
469+ {
470+ const char * entry = g_ptr_array_index (self -> comments , i );
471+ if (g_str_has_prefix (entry , comment_key ))
472+ {
473+ char after = entry [key_len ];
474+ if (after == '\0' || after == ' ' || after == '\t' )
475+ {
476+ g_free (self -> comments -> pdata [i ]);
477+ self -> comments -> pdata [i ] = g_steal_pointer (& new_entry );
478+ return ;
479+ }
480+ }
481+ }
482+
483+ /* Not found — append new entry */
484+ g_ptr_array_add (self -> comments , g_steal_pointer (& new_entry ));
485+ }
486+
487+ /**
488+ * _ostree_bootconfig_parser_get_comments_variant:
489+ * @self: Parser
490+ *
491+ * Returns a GVariant of type "a{ss}" containing magic comment entries
492+ * whose prefix is in the allowed_comment_prefixes allowlist (currently
493+ * "x-ostree-options-source-"). Each entry is a comment key mapped to
494+ * its value string. These are metadata set by consumers like rpm-ostree
495+ * (e.g. "x-ostree-options-source-tuned" -> "nohz=full isolcpus=1-3")
496+ * that need to survive the staged deployment serialization roundtrip.
497+ *
498+ * Returns: (nullable): A new floating #GVariant of type "a{ss}", or %NULL if
499+ * there are no matching comments. The returned variant is floating;
500+ * if the caller is the definite owner, use g_variant_ref_sink().
501+ */
502+ GVariant *
503+ _ostree_bootconfig_parser_get_comments_variant (OstreeBootconfigParser * self )
504+ {
505+ if (!self -> comments || self -> comments -> len == 0 )
506+ return NULL ;
507+
508+ g_auto (GVariantBuilder ) builder = OT_VARIANT_BUILDER_INITIALIZER ;
509+ g_variant_builder_init (& builder , (GVariantType * )"a{ss}" );
510+
511+ for (guint i = 0 ; i < self -> comments -> len ; i ++ )
512+ {
513+ const char * entry = g_ptr_array_index (self -> comments , i );
514+ /* Split into key and value on the first space/tab */
515+ const char * sep = strpbrk (entry , " \t" );
516+ if (sep )
517+ {
518+ g_autofree char * key = g_strndup (entry , sep - entry );
519+ const char * val = sep + 1 ;
520+ g_variant_builder_add (& builder , "{ss}" , key , val );
521+ }
522+ else
523+ {
524+ /* Key only, no value */
525+ g_variant_builder_add (& builder , "{ss}" , entry , "" );
526+ }
527+ }
528+
529+ return g_variant_builder_end (& builder );
530+ }
531+
342532static void
343533ostree_bootconfig_parser_finalize (GObject * object )
344534{
@@ -347,6 +537,7 @@ ostree_bootconfig_parser_finalize (GObject *object)
347537 g_free (self -> filename );
348538 g_strfreev (self -> overlay_initrds );
349539 g_hash_table_unref (self -> options );
540+ g_clear_pointer (& self -> comments , g_ptr_array_unref );
350541
351542 G_OBJECT_CLASS (ostree_bootconfig_parser_parent_class )-> finalize (object );
352543}
0 commit comments