This patch extends libgdiagnostics to provide a way to capture
the pp tokens making up a message string, so that SARIF and
HTML sinks can retain information such as event IDs and URLs.
As well as richer output, this improves the round-tripping of such
information through sarif-replay.

This also allows diagnostic messages to be built up in pieces,
with a drop-in replacement for fprintf, which I've found useful when
attempting to port "ld" to use libgdiagnostics.

Successfully bootstrapped & regrtested on x86_64-pc-linux-gnu.
Pushed to trunk as r16-2274-g2a521eee58da7c.

gcc/ChangeLog:
        PR sarif-replay/120792
        * auto-obstack.h: New file, based on material taken from
        pretty-print.cc.
        * diagnostic-digraphs.h
        (diagnostics::digraphs::digraph::set_description): New.
        (diagnostics::digraphs::node::set_label): New.
        * doc/libgdiagnostics/topics/compatibility.rst: Add
        LIBGDIAGNOSTICS_ABI_4.
        * doc/libgdiagnostics/topics/diagnostics.rst
        (diagnostic_finish_via_msg_buf): Document new entrypoint.
        * doc/libgdiagnostics/topics/execution-paths.rst
        (diagnostic_execution_path_add_event_via_msg_buf): Document new
        entrypoint.
        * doc/libgdiagnostics/topics/index.rst: Add message-buffers.rst.
        * doc/libgdiagnostics/topics/message-buffers.rst: New file.
        * doc/libgdiagnostics/topics/message-formatting.rst: Add note
        about message buffers.
        * doc/libgdiagnostics/topics/physical-locations.rst
        (diagnostic_add_location_with_label_via_msg_buf): Add.
        * doc/libgdiagnostics/tutorial/07-execution-paths.rst: Link to
        next section.
        * doc/libgdiagnostics/tutorial/08-message-buffers.rst: New file.
        * doc/libgdiagnostics/tutorial/index.rst: Add
        08-message-buffers.rst.
        * libgdiagnostics++.h (libgdiagnostics::message_buffer): New
        class.
        (libgdiagnostics::execution_path::add_event_via_msg_buf): New.
        (libgdiagnostics::diagnostic::add_location_with_label): New.
        (libgdiagnostics::diagnostic::finish_via_msg_buf): New.
        (libgdiagnostics::graph::set_description): New overload.
        (libgdiagnostics::graph::add_edge): New overload.
        (libgdiagnostics::node::set_label): New overload.
        * libgdiagnostics-private.h
        (private_diagnostic_execution_path_add_event_2): Drop decl.
        (private_diagnostic_execution_path_add_event_3): New decl.
        * libgdiagnostics.cc:  Include "pretty-print-format-impl.h",
        "pretty-print-markup.h", and "auto-obstack.h".
        (class copying_token_printer): New.
        (struct diagnostic_message_buffer): New.
        (class pp_element_message_buffer): New.
        (libgdiagnostics_path_event::libgdiagnostics_path_event): Replace
        params "gmsgid" and "args" with "msg_buf".
        (libgdiagnostics_path_event::print_desc): Reimplement using
        pp_element_message_buffer to replay m_msg_buf into "pp".
        (libgdiagnostics_path_event::m_desc_uncolored): Drop field.
        (libgdiagnostics_path_event::m_desc_colored): Drop field.
        (libgdiagnostics_path_event::msg_buf): New field.
        (diagnostic_execution_path::add_event_va): Reimplement.
        (diagnostic_execution_path::add_event_via_msg_buf): New.
        (diagnostic::add_location_with_label): New overload, using
        msg_buf.
        (diagnostic_manager::emit): Reimplement with...
        (diagnostic_manager::emit_va): ...this.
        (diagnostic_manager::emit_msg_buf): New.
        (FAIL_IF_NULL): Rename "p" to "ptr_arg".
        (diagnostic_finish_va): Update to use diagnostic_manager::emit_va.
        (diagnostic_graph::add_node_with_id): Rename "id" to "node_id".
        (diagnostic_graph_add_node): Likewise.
        (diagnostic_graph_add_edge): Rename "id" to "edge_id".
        (diagnostic_graph_get_node_by_id): Rename "id" to "node_id".
        (diagnostic_graph_get_edge_by_id): Rename "id" to "edge_id".
        (private_diagnostic_execution_path_add_event_2): Delete.
        (diagnostic_message_buffer_new): New public entrypoint.
        (diagnostic_message_buffer_release): Likewise.
        (diagnostic_message_buffer_append_str): Likewise.
        (diagnostic_message_buffer_append_text): Likewise.
        (diagnostic_message_buffer_append_byte): Likewise.
        (diagnostic_message_buffer_append_printf): Likewise.
        (diagnostic_message_buffer_append_event_id): Likewise.
        (diagnostic_message_buffer_begin_url): Likewise.
        (diagnostic_message_buffer_end_url): Likewise.
        (diagnostic_message_buffer_begin_quote): Likewise.
        (diagnostic_message_buffer_end_quote): Likewise.
        (diagnostic_message_buffer_begin_color): Likewise.
        (diagnostic_message_buffer_end_color): Likewise.
        (diagnostic_message_buffer_dump): Likewise.
        (diagnostic_finish_via_msg_buf): Likewise.
        (diagnostic_add_location_with_label_via_msg_buf): Likewise.
        (diagnostic_execution_path_add_event_via_msg_buf): Likewise.
        (diagnostic_graph_set_description_via_msg_buf): Likewise.
        (diagnostic_graph_add_edge_via_msg_buf): Likewise.
        (diagnostic_node_set_label_via_msg_buf): Likewise.
        (private_diagnostic_execution_path_add_event_3): New private
        entrypoint.
        * libgdiagnostics.h (LIBGDIAGNOSTICS_PARAM_FORMAT_STRING): New macro.
        (LIBGDIAGNOSTICS_PARAM_PRINTF_FORMAT_STRING): New macro.
        (diagnostic_message_buffer): New typedef.
        (LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer): New define.
        (diagnostic_message_buffer_new): New decl.
        (diagnostic_message_buffer_release): New decl.
        (diagnostic_message_buffer_append_str): New decl.
        (diagnostic_message_buffer_append_text): New decl.
        (diagnostic_message_buffer_append_byte): New decl.
        (diagnostic_message_buffer_append_printf): New decl.
        (diagnostic_message_buffer_append_event_id): New decl.
        (diagnostic_message_buffer_begin_url): New decl.
        (diagnostic_message_buffer_end_url): New decl.
        (diagnostic_message_buffer_begin_quote): New decl.
        (diagnostic_message_buffer_end_quote): New decl.
        (diagnostic_message_buffer_begin_color): New decl.
        (diagnostic_message_buffer_end_color): New decl.
        (diagnostic_message_buffer_dump): New decl.
        (diagnostic_finish_via_msg_buf
        (diagnostic_add_location_with_label_via_msg_buf): New decl.
        (diagnostic_execution_path_add_event_via_msg_buf): New decl.
        (diagnostic_graph_set_description_via_msg_buf): New decl.
        (diagnostic_graph_add_edge_via_msg_buf): New decl.
        (diagnostic_node_set_label_via_msg_buf): New decl.
        * libgdiagnostics.map (LIBGDIAGNOSTICS_ABI_3): Drop
        private_diagnostic_execution_path_add_event_2.
        (LIBGDIAGNOSTICS_ABI_4): New.
        * libsarifreplay.cc (class annotation): Use
        libgdiagnostics::message_buffer rather than label_text.
        (add_any_annotations): Likewise.
        (sarif_replayer::handle_result_obj): Likewise.
        (make_plain_text_within_result_message): Likewise.
        (handle_thread_flow_location_object): Likewise.
        (handle_location_object): Likewise.
        (sarif_replayer::handle_graph_object): Likewise.
        (sarif_replayer::handle_node_object): Likewise.
        (sarif_replayer::handle_edge_object): Likewise.
        * pretty-print-format-impl.h (pp_token_list::push_back_byte): New
        decl.
        * pretty-print-markup.h (pp_markup::context::begin_url): New decl.
        (pp_markup::context::end_url): New decl.
        (pp_markup::context::add_event_id): New decl.
        * pretty-print.cc: Include "auto-obstack.h".
        (pp_token_list::push_back_byte): New.
        (struct auto_obstack): Move to auto-obstack.h.
        (default_token_printer): Make non-static.
        (pp_markup::context::begin_url): New.
        (pp_markup::context::end_url): New.
        (pp_markup::context::add_event_id): New.

gcc/testsuite/ChangeLog:
        PR sarif-replay/120792
        * libgdiagnostics.dg/sarif.py: Delete duplicate script.
        * libgdiagnostics.dg/test-message-buffer-c.py: New test script.
        * libgdiagnostics.dg/test-message-buffer.c: New test.
        * libgdiagnostics.dg/test-warning-with-path-c.py: Update expected
        output to reflect that SARIF for event messages now contains JSON
        pointers when referring to other events by ID.
        * sarif-replay.dg/2.1.0-valid/3.11.6-embedded-links.sarif: Add
        HTML and SARIF output, and call out to Python scripts to verify
        the output.  Add example of a result with a link in its message.
        * sarif-replay.dg/2.1.0-valid/embedded-links-check-html.py: New
        test script.
        * sarif-replay.dg/2.1.0-valid/embedded-links-check-sarif-roundtrip.py:
        New test script.

Signed-off-by: David Malcolm <dmalc...@redhat.com>
---
 gcc/auto-obstack.h                            |  58 ++
 gcc/diagnostic-digraphs.h                     |  10 +
 .../libgdiagnostics/topics/compatibility.rst  |  43 ++
 .../libgdiagnostics/topics/diagnostics.rst    |  18 +
 .../topics/execution-paths.rst                |  22 +
 gcc/doc/libgdiagnostics/topics/index.rst      |   1 +
 .../topics/message-buffers.rst                | 310 ++++++++
 .../topics/message-formatting.rst             |   5 +
 .../topics/physical-locations.rst             |  20 +
 .../tutorial/07-execution-paths.rst           |   8 +-
 .../tutorial/08-message-buffers.rst           |  75 ++
 gcc/doc/libgdiagnostics/tutorial/index.rst    |   1 +
 gcc/libgdiagnostics++.h                       | 143 ++++
 gcc/libgdiagnostics-private.h                 |  29 +-
 gcc/libgdiagnostics.cc                        | 699 ++++++++++++++++--
 gcc/libgdiagnostics.h                         | 217 ++++++
 gcc/libgdiagnostics.map                       |  30 +-
 gcc/libsarifreplay.cc                         | 116 ++-
 gcc/pretty-print-format-impl.h                |   1 +
 gcc/pretty-print-markup.h                     |   5 +
 gcc/pretty-print.cc                           |  69 +-
 gcc/testsuite/libgdiagnostics.dg/sarif.py     |  23 -
 .../test-message-buffer-c.py                  |  12 +
 .../libgdiagnostics.dg/test-message-buffer.c  |  80 ++
 .../test-warning-with-path-c.py               |   2 +-
 .../2.1.0-valid/3.11.6-embedded-links.sarif   |  19 +-
 .../2.1.0-valid/embedded-links-check-html.py  |  28 +
 .../embedded-links-check-sarif-roundtrip.py   |  13 +
 28 files changed, 1852 insertions(+), 205 deletions(-)
 create mode 100644 gcc/auto-obstack.h
 create mode 100644 gcc/doc/libgdiagnostics/topics/message-buffers.rst
 create mode 100644 gcc/doc/libgdiagnostics/tutorial/08-message-buffers.rst
 delete mode 100644 gcc/testsuite/libgdiagnostics.dg/sarif.py
 create mode 100644 gcc/testsuite/libgdiagnostics.dg/test-message-buffer-c.py
 create mode 100644 gcc/testsuite/libgdiagnostics.dg/test-message-buffer.c
 create mode 100644 
gcc/testsuite/sarif-replay.dg/2.1.0-valid/embedded-links-check-html.py
 create mode 100644 
gcc/testsuite/sarif-replay.dg/2.1.0-valid/embedded-links-check-sarif-roundtrip.py

diff --git a/gcc/auto-obstack.h b/gcc/auto-obstack.h
new file mode 100644
index 000000000000..fe16dbd1068a
--- /dev/null
+++ b/gcc/auto-obstack.h
@@ -0,0 +1,58 @@
+/* RAII wrapper around obstack.
+   Copyright (C) 2024-2025 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalc...@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3, or (at your option)
+any later version.
+
+GCC is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_AUTO_OBSTACK_H
+#define GCC_AUTO_OBSTACK_H
+
+/* RAII wrapper around obstack.  */
+
+struct auto_obstack
+{
+  auto_obstack ()
+  {
+    obstack_init (&m_obstack);
+  }
+
+  ~auto_obstack ()
+  {
+    obstack_free (&m_obstack, NULL);
+  }
+
+  operator obstack & () { return m_obstack; }
+
+  void grow (const void *src, size_t length)
+  {
+    obstack_grow (&m_obstack, src, length);
+  }
+
+  void *object_base () const
+  {
+    return m_obstack.object_base;
+  }
+
+  size_t object_size () const
+  {
+    return obstack_object_size (&m_obstack);
+  }
+
+  obstack m_obstack;
+};
+
+#endif /* GCC_AUTO_OBSTACK_H */
diff --git a/gcc/diagnostic-digraphs.h b/gcc/diagnostic-digraphs.h
index 94cb76edd403..d6f8e7e11ac4 100644
--- a/gcc/diagnostic-digraphs.h
+++ b/gcc/diagnostic-digraphs.h
@@ -109,6 +109,11 @@ class digraph : public object
     else
       m_description = nullptr;
   }
+  void
+  set_description (std::string desc)
+  {
+    m_description = std::make_unique<std::string> (std::move (desc));
+  }
 
   node *
   get_node_by_id (const char *id) const
@@ -240,6 +245,11 @@ class node : public object
     else
       m_label = nullptr;
   }
+  void
+  set_label (std::string label)
+  {
+    m_label = std::make_unique<std::string> (std::move (label));
+  }
 
   size_t
   get_num_children () const { return m_children.size (); }
diff --git a/gcc/doc/libgdiagnostics/topics/compatibility.rst 
b/gcc/doc/libgdiagnostics/topics/compatibility.rst
index 0e078d0120e5..0ca41a330095 100644
--- a/gcc/doc/libgdiagnostics/topics/compatibility.rst
+++ b/gcc/doc/libgdiagnostics/topics/compatibility.rst
@@ -193,6 +193,7 @@ supporting command-line options and SARIF playback:
 
 ``LIBGDIAGNOSTICS_ABI_3``
 -------------------------
+
 ``LIBGDIAGNOSTICS_ABI_3`` covers the addition of these functions for
 working with directed graphs:
 
@@ -219,3 +220,45 @@ working with directed graphs:
   * :func:`diagnostic_node_set_location`
 
   * :func:`diagnostic_node_set_logical_location`
+
+.. _LIBGDIAGNOSTICS_ABI_4:
+
+``LIBGDIAGNOSTICS_ABI_4``
+-------------------------
+
+``LIBGDIAGNOSTICS_ABI_4`` covers the addition of these functions for
+working with :type:`diagnostic_message_buffer`.
+
+  * :func:`diagnostic_message_buffer_new`
+
+  * :func:`diagnostic_message_buffer_release`
+
+  * :func:`diagnostic_message_buffer_append_str`
+
+  * :func:`diagnostic_message_buffer_append_text`
+
+  * :func:`diagnostic_message_buffer_append_byte`
+
+  * :func:`diagnostic_message_buffer_append_printf`
+
+  * :func:`diagnostic_message_buffer_append_event_id`
+
+  * :func:`diagnostic_message_buffer_begin_url`
+
+  * :func:`diagnostic_message_buffer_end_url`
+
+  * :func:`diagnostic_message_buffer_begin_quote`
+
+  * :func:`diagnostic_message_buffer_end_quote`
+
+  * :func:`diagnostic_message_buffer_begin_color`
+
+  * :func:`diagnostic_message_buffer_end_color`
+
+  * :func:`diagnostic_message_buffer_dump`
+
+  * :func:`diagnostic_finish_via_msg_buf`
+
+  * :func:`diagnostic_add_location_with_label_via_msg_buf`
+
+  * :func:`diagnostic_execution_path_add_event_via_msg_buf`
diff --git a/gcc/doc/libgdiagnostics/topics/diagnostics.rst 
b/gcc/doc/libgdiagnostics/topics/diagnostics.rst
index 3d24da0164c4..7454c6e01748 100644
--- a/gcc/doc/libgdiagnostics/topics/diagnostics.rst
+++ b/gcc/doc/libgdiagnostics/topics/diagnostics.rst
@@ -105,6 +105,24 @@ Diagnostics are
 
    All three parameters must be non-NULL.
 
+.. function:: void diagnostic_finish_via_msg_buf (diagnostic *diag, \
+                              diagnostic_message_buffer *msg_buf)
+
+   This is equivalent to :func:`diagnostic_finish`, but using a message
+   buffer rather than a format string and variadic arguments.
+
+   ``diag`` and ``msg_buf`` must both be non-NULL.
+
+   Calling this function transfers ownership of ``msg_buf`` to the
+   diagnostic - do not call :func:`diagnostic_message_buffer_release` on
+   it.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_3`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
 
 Diagnostic groups
 *****************
diff --git a/gcc/doc/libgdiagnostics/topics/execution-paths.rst 
b/gcc/doc/libgdiagnostics/topics/execution-paths.rst
index 321503f2f6af..8381c452a0f3 100644
--- a/gcc/doc/libgdiagnostics/topics/execution-paths.rst
+++ b/gcc/doc/libgdiagnostics/topics/execution-paths.rst
@@ -88,6 +88,28 @@ cross-references between events.  In particular FIXME
    Equivalent to :func:`diagnostic_execution_path_add_event`, but using a
    :type:`va_list` rather than directly taking variadic arguments.
 
+.. function:: diagnostic_event_id 
diagnostic_execution_path_add_event_via_msg_buf (diagnostic_execution_path 
*path, \
+                                                const 
diagnostic_physical_location *physical_loc, \
+                                                const 
diagnostic_logical_location *logical_loc, \
+                                                unsigned stack_depth,
+                                                diagnostic_message_buffer 
*msg_buf)
+
+   This is equivalent to :func:`diagnostic_execution_path_add_event` but
+   using a message buffer rather than a format string and variadic
+   arguments.
+
+   ``path`` and ``msg_buf`` must both be non-NULL.
+
+   Calling this function transfers ownership of ``msg_buf`` to the
+   path - do not call :func:`diagnostic_message_buffer_release` on it.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_3`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
+
 Paths are printed to text sinks, and for SARIF sinks each path is added as
 a ``codeFlow`` object (see SARIF 2.1.0
 `§3.36 codeFlow object 
<https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/sarif-v2.1.0-errata01-os-complete.html#_Toc141790990>`_).
diff --git a/gcc/doc/libgdiagnostics/topics/index.rst 
b/gcc/doc/libgdiagnostics/topics/index.rst
index 966f5ef4e350..437ee058a170 100644
--- a/gcc/doc/libgdiagnostics/topics/index.rst
+++ b/gcc/doc/libgdiagnostics/topics/index.rst
@@ -28,6 +28,7 @@ Topic reference
    diagnostic-manager.rst
    diagnostics.rst
    message-formatting.rst
+   message-buffers.rst
    physical-locations.rst
    logical-locations.rst
    metadata.rst
diff --git a/gcc/doc/libgdiagnostics/topics/message-buffers.rst 
b/gcc/doc/libgdiagnostics/topics/message-buffers.rst
new file mode 100644
index 000000000000..c6f5851e16e9
--- /dev/null
+++ b/gcc/doc/libgdiagnostics/topics/message-buffers.rst
@@ -0,0 +1,310 @@
+.. Copyright (C) 2025 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalc...@redhat.com>
+
+   This is free software: you can redistribute it and/or modify it
+   under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful, but
+   WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Message buffers
+===============
+
+.. type:: diagnostic_message_buffer
+
+A :type:`diagnostic_message_buffer` is a buffer into which text can be
+accumulated, before being used:
+
+* as the message of a diagnostic, using :func:`diagnostic_finish_via_msg_buf`
+
+* as the text of a label for a :type:`diagnostic_physical_location` using
+  :func:`diagnostic_add_location_with_label_via_msg_buf`
+
+* as the text of an event within a :type:`diagnostic_execution_path` using
+  :func:`diagnostic_execution_path_add_event_via_msg_buf`
+
+This is to allow more flexible creation of messages than a "format string
+plus variadic arguments" API.
+
+.. function:: diagnostic_message_buffer * diagnostic_message_buffer_new (void)
+
+   This function creates a new :type:`diagnostic_message_buffer`.
+
+   The caller is responsible for cleaning it up, either by handing it off
+   to one of the API entrypoints that takes ownership of it (such as
+   :func:`diagnostic_finish_via_msg_buf`), or by calling
+   :func:`diagnostic_message_buffer_release` on it.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_4`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
+
+.. function:: void diagnostic_message_buffer_release 
(diagnostic_message_buffer *msg_buf)
+
+   This function releases ``msg_buf``.
+
+   Typically you don't need to call this, but instead will pass the
+   buffer to one of the API entrypoints that takes over ownership of
+   it (such as :func:`diagnostic_finish_via_msg_buf`); calling it
+   after this would lead to a double-free bug, as you no longer "own"
+   the buffer.
+
+   ``msg_buf`` must be non-NULL.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_4`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
+
+.. function:: void diagnostic_message_buffer_append_str 
(diagnostic_message_buffer *msg_buf, \
+                                     const char *p)
+
+   This function appends the null-terminated string ``p`` to the buffer.
+   The string is assumed to be UTF-8 encoded.
+
+   ``msg_buf`` and ``p`` must both be non-NULL.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_4`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
+
+.. function:: void diagnostic_message_buffer_append_text 
(diagnostic_message_buffer *msg_buf, \
+              const char *p, \
+              size_t len)
+
+   This function appends ``len`` bytes from ``p`` to the buffer.
+   The bytes are assumed to be UTF-8 encoded.
+
+   ``msg_buf`` and ``p`` must both be non-NULL.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_4`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
+
+.. function:: void diagnostic_message_buffer_append_byte 
(diagnostic_message_buffer *msg_buf,\
+                                      char ch)
+
+   This function appends ``ch`` to the buffer.  This should be either
+   ASCII, or part of UTF-8 encoded text.
+
+   ``msg_buf`` must be non-NULL.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_4`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
+
+.. function:: void diagnostic_message_buffer_append_printf 
(diagnostic_message_buffer *msg_buf, \
+                                        const char *fmt, ...)
+
+   This function appends a formatted string to the buffer, using the
+   formatting rules for ``printf``.
+
+   The string is assumed to be UTF-8 encoded.
+
+   ``msg_buf`` and ``fmt`` must both be non-NULL.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_4`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
+
+.. function:: void diagnostic_message_buffer_append_event_id 
(diagnostic_message_buffer *msg_buf, \
+                                          diagnostic_event_id event_id)
+
+   This function appends a :type:`diagnostic_event_id` to the buffer.
+
+   ``msg_buf`` must be non-NULL.
+
+   For text output, the event will be printed in the form ``(1)``.
+
+   This is analogous to the
+   :doc:`%@ message formatting code <message-formatting>`.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_4`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
+
+Hyperlink support
+*****************
+
+.. function:: void diagnostic_message_buffer_begin_url 
(diagnostic_message_buffer *msg_buf, \
+                                    const char *url)
+
+   This function indicates the beginning of a run of text that should be
+   associated with the given URL.  The run of text should be closed with
+   a matching call to :func:`diagnostic_message_buffer_end_url`.
+
+   ``msg_buf`` and ``url`` must both be non-NULL.
+
+   For text output in a suitably modern terminal, the run of text will
+   be emitted as a clickable hyperlink to the URL.
+
+   For SARIF sinks, the run of text will be emitted using SARIF's
+   embedded link syntax.
+
+   This is analogous to the
+   :doc:`%{ message formatting code <message-formatting>`.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_4`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
+
+.. function:: void diagnostic_message_buffer_end_url 
(diagnostic_message_buffer *msg_buf)
+
+   This function ends a run of text within the buffer started with
+   :func:`diagnostic_message_buffer_begin_url`.
+
+   ``msg_buf`` must be non-NULL.
+
+   This is analogous to the
+   :doc:`%} message formatting code <message-formatting>`.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_4`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
+
+Quoted text
+***********
+
+.. function:: void diagnostic_message_buffer_begin_quote 
(diagnostic_message_buffer *msg_buf)
+
+   This function indicates the beginning of a run of text that should be
+   printed in quotes.  The run of text should be closed with
+   a matching call to :func:`diagnostic_message_buffer_end_quote`.
+
+   ``msg_buf`` must be non-NULL.
+
+   For text output in a suitably modern terminal, the run of text will
+   appear in bold.
+   be emitted as a clickable hyperlink to the URL.
+
+   For SARIF sinks, the run of text will be emitted using SARIF's
+   embedded link syntax.
+
+   This is analogous to the
+   ``%<``:doc:`message formatting code <message-formatting>`.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_4`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
+
+.. function:: void diagnostic_message_buffer_end_url 
(diagnostic_message_buffer *msg_buf)
+
+   This function ends a run of text within the buffer started with
+   :func:`diagnostic_message_buffer_begin_url`.
+
+   ``msg_buf`` must be non-NULL.
+
+   This is analogous to the
+   :doc:`%> message formatting code <message-formatting>`.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_4`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
+
+Color
+*****
+
+.. function:: void diagnostic_message_buffer_begin_color 
(diagnostic_message_buffer *msg_buf, \
+                                    const char *color)
+
+   This function indicates the beginning of a run of text that should be
+   colorized as the given color.  The run of text should be closed with
+   a matching call to :func:`diagnostic_message_buffer_end_color`.
+
+   The precise set of available color names is currently undocumented.
+
+   ``msg_buf`` and ``color`` must both be non-NULL.
+
+   For text output in a suitable terminal, the run of text will
+   be colorized.
+
+   For SARIF sinks, the run of text will be emitted using SARIF's
+   embedded link syntax.
+
+   This is analogous to the
+   :doc:`%r message formatting code <message-formatting>`.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_4`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
+
+.. function:: void diagnostic_message_buffer_end_color 
(diagnostic_message_buffer *msg_buf)
+
+   This function ends a run of text within the buffer started with
+   :func:`diagnostic_message_buffer_begin_color`.
+
+   ``msg_buf`` must be non-NULL.
+
+   This is analogous to the
+   :doc:`%R message formatting code <message-formatting>`.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_4`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
+
+Debugging a message buffer
+**************************
+
+.. function:: void diagnostic_message_buffer_dump (const 
diagnostic_message_buffer *msg_buf, \
+                               FILE *outf)
+
+   This function writes a representation of the contents of ``msg_buf``
+   to ``outf``, for debugging.
+
+   ``msg_buf`` can be NULL or non-NULL.
+   ``outf`` must be non-NULL.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_4`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
diff --git a/gcc/doc/libgdiagnostics/topics/message-formatting.rst 
b/gcc/doc/libgdiagnostics/topics/message-formatting.rst
index 7064b702be21..803feba13cc9 100644
--- a/gcc/doc/libgdiagnostics/topics/message-formatting.rst
+++ b/gcc/doc/libgdiagnostics/topics/message-formatting.rst
@@ -23,6 +23,11 @@ Message formatting
 Various libgdiagnostics entrypoints take a format string and
 variadic arguments.
 
+.. note::
+
+   See also :type:`diagnostic_message_buffer`, which offers an
+   alternative way to build up messages.
+
 The format strings take codes prefixed by ``%``, or ``%q`` to put
 the result in quotes.  For example::
 
diff --git a/gcc/doc/libgdiagnostics/topics/physical-locations.rst 
b/gcc/doc/libgdiagnostics/topics/physical-locations.rst
index 099e27e98224..be8e7ebc5d1f 100644
--- a/gcc/doc/libgdiagnostics/topics/physical-locations.rst
+++ b/gcc/doc/libgdiagnostics/topics/physical-locations.rst
@@ -284,3 +284,23 @@ This diagnostic has three locations
             |   ~~ ^ ~~~~~
             |   |    |
             |   int  const char *
+
+.. function:: void diagnostic_add_location_with_label_via_msg_buf (diagnostic 
*diag, \
+                                               const 
diagnostic_physical_location *loc, \
+                                               diagnostic_message_buffer 
*msg_buf)
+
+   This is equivalent to :func:`diagnostic_add_location_with_label` but
+   using a message buffer rather than a text string.
+
+   ``diag`` and ``msg_buf`` must both be non-NULL.
+
+   Calling this function transfers ownership of ``msg_buf`` to the
+   diagnostic - do not call :func:`diagnostic_message_buffer_release` on
+   it.
+
+   This function was added in :ref:`LIBGDIAGNOSTICS_ABI_3`; you can
+   test for its presence using
+
+   .. code-block:: c
+
+      #ifdef LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
diff --git a/gcc/doc/libgdiagnostics/tutorial/07-execution-paths.rst 
b/gcc/doc/libgdiagnostics/tutorial/07-execution-paths.rst
index 0ac8bf07b44e..9147171ad6f5 100644
--- a/gcc/doc/libgdiagnostics/tutorial/07-execution-paths.rst
+++ b/gcc/doc/libgdiagnostics/tutorial/07-execution-paths.rst
@@ -134,8 +134,6 @@ Here's the above example in full:
    :end-before:  end full example
 
 
-Moving on
-*********
-
-That's the end of the tutorial.  For more information on libgdiagnostics, see
-the :doc:`topic guide <../topics/index>`.
+See the :doc:`guide to execution paths <../topics/execution-paths>`
+for more information, or go on to
+:doc:`the next section of the tutorial <08-message-buffers>`.
diff --git a/gcc/doc/libgdiagnostics/tutorial/08-message-buffers.rst 
b/gcc/doc/libgdiagnostics/tutorial/08-message-buffers.rst
new file mode 100644
index 000000000000..a83c50ce91ba
--- /dev/null
+++ b/gcc/doc/libgdiagnostics/tutorial/08-message-buffers.rst
@@ -0,0 +1,75 @@
+.. Copyright (C) 2025 Free Software Foundation, Inc.
+   Originally contributed by David Malcolm <dmalc...@redhat.com>
+
+   This is free software: you can redistribute it and/or modify it
+   under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful, but
+   WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see
+   <https://www.gnu.org/licenses/>.
+
+.. default-domain:: c
+
+Tutorial part 8: message buffers
+================================
+
+In previous examples, we finished a diagnostic with a call to
+:func:`diagnostic_finish`, which takes a format string and arguments
+to determine the text message of the diagnostic.
+
+Sometimes this approach is inconvenient, such as where you might want to
+build up a message programatically from a series of components.
+Additionally, you might have existing code that uses ``fprintf``, whereas
+:func:`diagnostic_finish` has its
+:doc:`own formatting conventions <../topics/message-formatting>` which are
+:strong:`not` the same as printf.
+
+For this reason libgdiagnostics (from ``LIBGDIAGNOSTICS_ABI_3`` onwards)
+supports :type:`diagnostic_message_buffer`, which can be used to accumulate a
+message before using it.
+
+You create a :type:`diagnostic_message_buffer` using
+:func:`diagnostic_message_buffer_new`.
+
+There are various API entrypoints for accumulating text into the buffer.
+
+For example:
+
+.. literalinclude:: ../../../testsuite/libgdiagnostics.dg/test-message-buffer.c
+   :language: c
+   :start-after: /* begin quoted source */
+   :end-before:  /* end quoted source */
+
+Running this will produce this text output::
+
+.. code-block:: console
+
+  $ ./test-message-buffer.c.exe
+  ./test-message-buffer.c.exe: error: this is a string; foo; int: 42 str: 
mostly harmless; this is a link  'this is quoted' highlight A highlight B (1).
+
+where in a suitably-capable terminal if a text sink is directly
+connected to a tty:
+
+* the ``this is a link`` will be a clickable hyperlink
+  (and the URL will be captured in SARIF output).
+
+* the quoted text will be in bold
+
+* the ``highlight A`` and ``highlight B`` text will be colorized
+
+* the event ID will be colorized (and will be a URL in SARIF output
+  if used within a :type:`diagnostic_execution_path`).
+
+
+Moving on
+*********
+
+That's the end of the tutorial.  For more information on libgdiagnostics, see
+the :doc:`topic guide <../topics/index>`.
diff --git a/gcc/doc/libgdiagnostics/tutorial/index.rst 
b/gcc/doc/libgdiagnostics/tutorial/index.rst
index 172a28c6c772..09a15e9c209e 100644
--- a/gcc/doc/libgdiagnostics/tutorial/index.rst
+++ b/gcc/doc/libgdiagnostics/tutorial/index.rst
@@ -30,3 +30,4 @@ The following tutorial gives an overview of how to use 
libgdiagnostics.
    05-warnings.rst
    06-fix-it-hints.rst
    07-execution-paths.rst
+   08-message-buffers.rst
diff --git a/gcc/libgdiagnostics++.h b/gcc/libgdiagnostics++.h
index 4beee446d8b4..c955d56db5b3 100644
--- a/gcc/libgdiagnostics++.h
+++ b/gcc/libgdiagnostics++.h
@@ -37,6 +37,7 @@ class diagnostic;
 class graph;
 class node;
 class edge;
+class message_buffer;
 
 /* Wrapper around a borrowed diagnostic_text_sink *.  */
 
@@ -134,6 +135,65 @@ public:
   const diagnostic_logical_location *m_inner;
 };
 
+/* Wrapper around a diagnostic_message_buffer *, with ownership.  */
+
+class message_buffer
+{
+public:
+  message_buffer () : m_inner (nullptr) {}
+  message_buffer (diagnostic_message_buffer *inner) : m_inner (inner) {}
+  ~message_buffer ()
+  {
+    if (m_inner)
+      diagnostic_message_buffer_release (m_inner);
+  }
+  message_buffer (const message_buffer &) = delete;
+  message_buffer (message_buffer &&other)
+  {
+    m_inner = other.m_inner;
+    other.m_inner = nullptr;
+  }
+  message_buffer& operator= (const message_buffer &) = delete;
+  message_buffer& operator= (message_buffer &&other)
+  {
+    if (m_inner)
+      diagnostic_message_buffer_release (m_inner);
+    m_inner = other.m_inner;
+    other.m_inner = nullptr;
+    return *this;
+  }
+
+  message_buffer&
+  operator+= (const char *str)
+  {
+    diagnostic_message_buffer_append_str (m_inner, str);
+    return *this;
+  }
+
+  message_buffer&
+  operator+= (char ch)
+  {
+    diagnostic_message_buffer_append_byte (m_inner, ch);
+    return *this;
+  }
+
+  message_buffer &
+  begin_url (const char *url)
+  {
+    diagnostic_message_buffer_begin_url (m_inner, url);
+    return *this;
+  }
+
+  message_buffer &
+  end_url ()
+  {
+    diagnostic_message_buffer_end_url (m_inner);
+    return *this;
+  }
+
+  diagnostic_message_buffer *m_inner;
+};
+
 /* RAII class around a diagnostic_execution_path *.  */
 
 class execution_path
@@ -191,6 +251,12 @@ public:
                va_list *args)
     LIBGDIAGNOSTICS_PARAM_GCC_FORMAT_STRING (5, 0);
 
+  diagnostic_event_id
+  add_event_via_msg_buf (physical_location physical_loc,
+                        logical_location logical_loc,
+                        unsigned stack_depth,
+                        message_buffer &&msg_buf);
+
   diagnostic_execution_path *m_inner;
   bool m_owned;
 };
@@ -230,6 +296,10 @@ public:
   add_location_with_label (physical_location loc,
                           const char *text);
 
+  void
+  add_location_with_label (physical_location loc,
+                          message_buffer &&text);
+
   void
   set_logical_location (logical_location loc);
 
@@ -261,6 +331,9 @@ public:
     LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2)
     LIBGDIAGNOSTICS_PARAM_GCC_FORMAT_STRING (2, 0);
 
+  void
+  finish_via_msg_buf (message_buffer &&msg_buf);
+
   ::diagnostic * const m_inner;
 };
 
@@ -450,6 +523,8 @@ public:
 
   void
   set_description (const char *);
+  void
+  set_description (message_buffer &&);
 
   node
   get_node_by_id (const char *id) const;
@@ -459,6 +534,8 @@ public:
 
   edge
   add_edge (const char *id, node src_node, node dst_node, const char *label);
+  edge
+  add_edge (const char *id, node src_node, node dst_node, message_buffer 
&&label);
 
   diagnostic_graph *m_inner;
   bool m_owned;
@@ -474,6 +551,8 @@ public:
 
   void
   set_label (const char *);
+  void
+  set_label (message_buffer &&);
 
   void
   set_location (physical_location loc);
@@ -591,6 +670,21 @@ execution_path::add_event_va (physical_location 
physical_loc,
                                                 args);
 }
 
+inline diagnostic_event_id
+execution_path::add_event_via_msg_buf (physical_location physical_loc,
+                                      logical_location logical_loc,
+                                      unsigned stack_depth,
+                                      message_buffer &&msg_buf)
+{
+  diagnostic_message_buffer *inner_msg_buf = msg_buf.m_inner;
+  msg_buf.m_inner = nullptr;
+  return diagnostic_execution_path_add_event_via_msg_buf (m_inner,
+                                                         physical_loc.m_inner,
+                                                         logical_loc.m_inner,
+                                                         stack_depth,
+                                                         inner_msg_buf);
+}
+
 // class group
 
 inline
@@ -633,6 +727,17 @@ diagnostic::add_location_with_label (physical_location loc,
   diagnostic_add_location_with_label (m_inner, loc.m_inner, text);
 }
 
+inline void
+diagnostic::add_location_with_label (physical_location loc,
+                                    message_buffer &&msg_buf)
+{
+  diagnostic_message_buffer *inner_msg_buf = msg_buf.m_inner;
+  msg_buf.m_inner = nullptr;
+  diagnostic_add_location_with_label_via_msg_buf (m_inner,
+                                                 loc.m_inner,
+                                                 inner_msg_buf);
+}
+
 inline void
 diagnostic::add_location (physical_location loc)
 {
@@ -710,6 +815,14 @@ diagnostic::finish_va (const char *fmt, va_list *args)
   diagnostic_finish_va (m_inner, fmt, args);
 }
 
+inline void
+diagnostic::finish_via_msg_buf (message_buffer &&msg_buf)
+{
+  diagnostic_message_buffer *inner_msg_buf = msg_buf.m_inner;
+  msg_buf.m_inner = nullptr;
+  diagnostic_finish_via_msg_buf (m_inner, inner_msg_buf);
+}
+
 // class manager
 
 inline file
@@ -821,6 +934,14 @@ graph::set_description (const char *desc)
   diagnostic_graph_set_description (m_inner, desc);
 }
 
+inline void
+graph::set_description (message_buffer &&msg_buf)
+{
+  diagnostic_message_buffer *inner_msg_buf = msg_buf.m_inner;
+  msg_buf.m_inner = nullptr;
+  diagnostic_graph_set_description_via_msg_buf (m_inner, inner_msg_buf);
+}
+
 inline node
 graph::get_node_by_id (const char *id) const
 {
@@ -845,6 +966,20 @@ graph::add_edge (const char *id,
                                          label));
 }
 
+inline edge
+graph::add_edge (const char *id,
+                node src_node, node dst_node,
+                message_buffer &&label)
+{
+  diagnostic_message_buffer *inner_label = label.m_inner;
+  label.m_inner = nullptr;
+  return edge (diagnostic_graph_add_edge_via_msg_buf (m_inner,
+                                                     id,
+                                                     src_node.m_inner,
+                                                     dst_node.m_inner,
+                                                     inner_label));
+}
+
 // class node
 
 inline void
@@ -853,6 +988,14 @@ node::set_label (const char *label)
   diagnostic_node_set_label (m_inner, label);
 }
 
+inline void
+node::set_label (message_buffer &&label)
+{
+  diagnostic_message_buffer *inner_label = label.m_inner;
+  label.m_inner = nullptr;
+  diagnostic_node_set_label_via_msg_buf (m_inner, inner_label);
+}
+
 inline void
 node::set_location (physical_location loc)
 {
diff --git a/gcc/libgdiagnostics-private.h b/gcc/libgdiagnostics-private.h
index 0f628e429f93..0e90f8720079 100644
--- a/gcc/libgdiagnostics-private.h
+++ b/gcc/libgdiagnostics-private.h
@@ -31,20 +31,6 @@ extern "C" {
 
 /* Entrypoints added in LIBGDIAGNOSTICS_ABI_3.  */
 
-extern diagnostic_event_id
-private_diagnostic_execution_path_add_event_2 (diagnostic_execution_path *path,
-                                              const 
diagnostic_physical_location *physical_loc,
-                                              const 
diagnostic_logical_location *logical_loc,
-                                              unsigned stack_depth,
-                                              diagnostic_graph *state_graph,
-                                              const char *fmt, ...)
-  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
-  LIBGDIAGNOSTICS_PARAM_CAN_BE_NULL (2)
-  LIBGDIAGNOSTICS_PARAM_CAN_BE_NULL (3)
-  LIBGDIAGNOSTICS_PARAM_CAN_BE_NULL (5)
-  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (6)
-  LIBGDIAGNOSTICS_PARAM_GCC_FORMAT_STRING (6, 7);
-
 extern void
 private_diagnostic_graph_set_property_bag (diagnostic_graph &graph,
                                           std::unique_ptr<json::object> 
properties);
@@ -57,6 +43,21 @@ extern void
 private_diagnostic_edge_set_property_bag (diagnostic_edge &edge,
                                          std::unique_ptr<json::object> 
properties);
 
+/* Entrypoint added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern diagnostic_event_id
+private_diagnostic_execution_path_add_event_3 (diagnostic_execution_path *path,
+                                              const 
diagnostic_physical_location *physical_loc,
+                                              const 
diagnostic_logical_location *logical_loc,
+                                              unsigned stack_depth,
+                                              diagnostic_graph *state_graph,
+                                              diagnostic_message_buffer 
*msg_buf)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBGDIAGNOSTICS_PARAM_CAN_BE_NULL (2)
+  LIBGDIAGNOSTICS_PARAM_CAN_BE_NULL (3)
+  LIBGDIAGNOSTICS_PARAM_CAN_BE_NULL (5)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (6);
+
 } // extern "C"
 
 #endif  /* LIBGDIAGNOSTICS_PRIVATE_H  */
diff --git a/gcc/libgdiagnostics.cc b/gcc/libgdiagnostics.cc
index 04812dce4fe6..172f19c59bd6 100644
--- a/gcc/libgdiagnostics.cc
+++ b/gcc/libgdiagnostics.cc
@@ -39,6 +39,9 @@ along with GCC; see the file COPYING3.  If not see
 #include "edit-context.h"
 #include "libgdiagnostics.h"
 #include "libgdiagnostics-private.h"
+#include "pretty-print-format-impl.h"
+#include "pretty-print-markup.h"
+#include "auto-obstack.h"
 
 class owned_nullable_string
 {
@@ -246,6 +249,97 @@ private:
   diagnostic_source_printing_options m_source_printing;
 };
 
+/* A token_printer that makes a deep copy of the pp_token_list
+   into another obstack.  */
+
+class copying_token_printer : public token_printer
+{
+public:
+  copying_token_printer (obstack &dst_obstack,
+                        pp_token_list &dst_token_list)
+  : m_dst_obstack (dst_obstack),
+    m_dst_token_list (dst_token_list)
+  {
+  }
+
+  void
+  print_tokens (pretty_printer *,
+               const pp_token_list &tokens) final override
+  {
+    for (auto iter = tokens.m_first; iter; iter = iter->m_next)
+      switch (iter->m_kind)
+       {
+       default:
+         gcc_unreachable ();
+
+       case pp_token::kind::text:
+         {
+           const pp_token_text *sub = as_a <const pp_token_text *> (iter);
+           /* Copy the text, with null terminator.  */
+           obstack_grow (&m_dst_obstack, sub->m_value.get (),
+                         strlen (sub->m_value.get ()) + 1);
+           m_dst_token_list.push_back_text
+             (label_text::borrow (XOBFINISH (&m_dst_obstack,
+                                             const char *)));
+         }
+         break;
+
+       case pp_token::kind::begin_color:
+         {
+           pp_token_begin_color *sub = as_a <pp_token_begin_color *> (iter);
+           /* Copy the color, with null terminator.  */
+           obstack_grow (&m_dst_obstack, sub->m_value.get (),
+                         strlen (sub->m_value.get ()) + 1);
+           m_dst_token_list.push_back<pp_token_begin_color>
+             (label_text::borrow (XOBFINISH (&m_dst_obstack,
+                                             const char *)));
+         }
+         break;
+       case pp_token::kind::end_color:
+         m_dst_token_list.push_back<pp_token_end_color> ();
+         break;
+
+       case pp_token::kind::begin_quote:
+         m_dst_token_list.push_back<pp_token_begin_quote> ();
+         break;
+       case pp_token::kind::end_quote:
+         m_dst_token_list.push_back<pp_token_end_quote> ();
+         break;
+
+       case pp_token::kind::begin_url:
+         {
+           pp_token_begin_url *sub = as_a <pp_token_begin_url *> (iter);
+           /* Copy the URL, with null terminator.  */
+           obstack_grow (&m_dst_obstack, sub->m_value.get (),
+                         strlen (sub->m_value.get ()) + 1);
+           m_dst_token_list.push_back<pp_token_begin_url>
+             (label_text::borrow (XOBFINISH (&m_dst_obstack,
+                                             const char *)));
+         }
+         break;
+       case pp_token::kind::end_url:
+         m_dst_token_list.push_back<pp_token_end_url> ();
+         break;
+
+       case pp_token::kind::event_id:
+         {
+           pp_token_event_id *sub = as_a <pp_token_event_id *> (iter);
+           m_dst_token_list.push_back<pp_token_event_id> (sub->m_event_id);
+         }
+         break;
+
+       case pp_token::kind::custom_data:
+         /* These should have been eliminated by replace_custom_tokens.  */
+         gcc_unreachable ();
+         break;
+       }
+  }
+
+private:
+  obstack &m_dst_obstack;
+  pp_token_list &m_dst_token_list;
+};
+
 class sarif_sink : public sink
 {
 public:
@@ -255,6 +349,107 @@ public:
              const sarif_generation_options &sarif_gen_opts);
 };
 
+struct diagnostic_message_buffer
+{
+  diagnostic_message_buffer ()
+  : m_tokens (m_obstack)
+  {
+  }
+
+  diagnostic_message_buffer (const char *gmsgid,
+                            va_list *args)
+  : m_tokens (m_obstack)
+  {
+    text_info text (gmsgid, args, errno);
+    pretty_printer pp;
+    pp.set_output_stream (nullptr);
+    copying_token_printer tok_printer (m_obstack, m_tokens);
+    pp.set_token_printer (&tok_printer);
+    pp_format (&pp, &text);
+    pp_output_formatted_text (&pp, nullptr);
+  }
+
+
+  std::string to_string () const;
+
+  auto_obstack m_obstack;
+  pp_token_list m_tokens;
+};
+
+/* A pp_element subclass that replays the saved tokens in a
+   diagnostic_message_buffer.  */
+
+class pp_element_message_buffer : public pp_element
+{
+public:
+  pp_element_message_buffer (diagnostic_message_buffer &msg_buf)
+    : m_msg_buf (msg_buf)
+  {
+  }
+
+  void add_to_phase_2 (pp_markup::context &ctxt) final override
+  {
+    /* Convert to text, possibly with colorization, URLs, etc.  */
+    for (auto iter = m_msg_buf.m_tokens.m_first; iter; iter = iter->m_next)
+      switch (iter->m_kind)
+       {
+       default:
+         gcc_unreachable ();
+
+       case pp_token::kind::text:
+         {
+           pp_token_text *sub = as_a <pp_token_text *> (iter);
+           pp_string (&ctxt.m_pp, sub->m_value.get ());
+           ctxt.push_back_any_text ();
+         }
+         break;
+
+       case pp_token::kind::begin_color:
+         {
+           pp_token_begin_color *sub = as_a <pp_token_begin_color *> (iter);
+           ctxt.begin_highlight_color (sub->m_value.get ());
+         }
+         break;
+       case pp_token::kind::end_color:
+         ctxt.end_highlight_color ();
+         break;
+
+       case pp_token::kind::begin_quote:
+         ctxt.begin_quote ();
+         break;
+       case pp_token::kind::end_quote:
+         ctxt.end_quote ();
+         break;
+
+       case pp_token::kind::begin_url:
+         {
+           pp_token_begin_url *sub = as_a <pp_token_begin_url *> (iter);
+           ctxt.begin_url (sub->m_value.get ());
+         }
+         break;
+       case pp_token::kind::end_url:
+         ctxt.end_url ();
+         break;
+
+       case pp_token::kind::event_id:
+         {
+           pp_token_event_id *sub = as_a <pp_token_event_id *> (iter);
+           gcc_assert (sub->m_event_id.known_p ());
+           ctxt.add_event_id (sub->m_event_id);
+         }
+         break;
+
+       case pp_token::kind::custom_data:
+         /* We don't have a way of handling custom_data tokens here.  */
+         gcc_unreachable ();
+         break;
+       }
+  }
+
+private:
+  diagnostic_message_buffer &m_msg_buf;
+};
+
 /* Helper for the linemap code.  */
 
 static size_t
@@ -508,9 +703,15 @@ public:
     m_sinks.push_back (std::move (sink));
   }
 
-  void emit (diagnostic &diag, const char *msgid, va_list *args)
+  void emit_va (diagnostic &diag, const char *msgid, va_list *args)
     LIBGDIAGNOSTICS_PARAM_GCC_FORMAT_STRING(3, 0);
 
+  void emit (diagnostic &diag, const char *msgid, ...)
+    LIBGDIAGNOSTICS_PARAM_GCC_FORMAT_STRING(3, 4);
+
+  void emit_msg_buf (diagnostic &diag,
+                    diagnostic_message_buffer &msg_buf);
+
   diagnostic_file *
   new_file (const char *name,
            const char *sarif_source_language)
@@ -755,10 +956,10 @@ struct diagnostic_graph : public 
diagnostics::digraphs::digraph
   diagnostic_graph (diagnostic_manager &) {}
 
   diagnostic_node *
-  add_node_with_id (std::string id,
+  add_node_with_id (std::string node_id,
                    diagnostic_node *parent_node);
   diagnostic_edge *
-  add_edge_with_label (const char *id,
+  add_edge_with_label (const char *edge_id,
                       diagnostic_node &src_node,
                       diagnostic_node &dst_node,
                       const char *label);
@@ -791,15 +992,14 @@ public:
                              const diagnostic_logical_location *logical_loc,
                              unsigned stack_depth,
                              std::unique_ptr<diagnostic_graph> state_graph,
-                             const char *gmsgid,
-                             va_list *args)
+                             std::unique_ptr<diagnostic_message_buffer> 
msg_buf)
   : m_physical_loc (physical_loc),
     m_logical_loc (logical_loc),
     m_stack_depth (stack_depth),
-    m_state_graph (std::move (state_graph))
+    m_state_graph (std::move (state_graph)),
+    m_msg_buf (std::move (msg_buf))
   {
-    m_desc_uncolored = make_desc (gmsgid, args, false);
-    m_desc_colored = make_desc (gmsgid, args, true);
+    gcc_assert (m_msg_buf);
   }
 
   /* diagnostic_event vfunc implementations.  */
@@ -816,10 +1016,11 @@ public:
 
   void print_desc (pretty_printer &pp) const final override
   {
-    if (pp_show_color (&pp))
-      pp_string (&pp, m_desc_colored.get ());
-    else
-      pp_string (&pp, m_desc_uncolored.get ());
+    if (m_msg_buf)
+      {
+       pp_element_message_buffer e_msg_buf (*m_msg_buf);
+       pp_printf (&pp, "%e", &e_msg_buf);
+      }
   }
 
   logical_location get_logical_location () const final override
@@ -877,8 +1078,7 @@ private:
   const diagnostic_logical_location *m_logical_loc;
   unsigned m_stack_depth;
   std::unique_ptr<diagnostic_graph> m_state_graph;
-  label_text m_desc_uncolored;
-  label_text m_desc_colored;
+  std::unique_ptr<diagnostic_message_buffer> m_msg_buf;
 };
 
 class libgdiagnostics_path_thread : public diagnostic_thread
@@ -911,14 +1111,31 @@ struct diagnostic_execution_path : public diagnostic_path
                std::unique_ptr<diagnostic_graph> state_graph,
                const char *gmsgid,
                va_list *args)
+  {
+    auto msg_buf = std::make_unique<diagnostic_message_buffer> (gmsgid, args);
+
+    m_events.push_back
+      (std::make_unique<libgdiagnostics_path_event> (physical_loc,
+                                                    logical_loc,
+                                                    stack_depth,
+                                                    std::move (state_graph),
+                                                    std::move (msg_buf)));
+    return m_events.size () - 1;
+  }
+
+  diagnostic_event_id_t
+  add_event_via_msg_buf (const diagnostic_physical_location *physical_loc,
+                        const diagnostic_logical_location *logical_loc,
+                        unsigned stack_depth,
+                        std::unique_ptr<diagnostic_graph> state_graph,
+                        std::unique_ptr<diagnostic_message_buffer> msg_buf)
   {
     m_events.push_back
       (std::make_unique<libgdiagnostics_path_event> (physical_loc,
                                                     logical_loc,
                                                     stack_depth,
                                                     std::move (state_graph),
-                                                    gmsgid,
-                                                    args));
+                                                    std::move (msg_buf)));
     return m_events.size () - 1;
   }
 
@@ -1041,6 +1258,19 @@ public:
     m_labels.push_back (std::move (label));
   }
 
+  void
+  add_location_with_label (const diagnostic_physical_location *loc,
+                          std::unique_ptr<diagnostic_message_buffer> msg_buf)
+  {
+    std::string str = msg_buf->to_string ();
+    std::unique_ptr<range_label> label
+      = std::make_unique <impl_range_label> (str.c_str ());
+    m_rich_loc.add_range (as_location_t (loc),
+                         SHOW_RANGE_WITHOUT_CARET,
+                         label.get ());
+    m_labels.push_back (std::move (label));
+  }
+
   void
   set_logical_location (const diagnostic_logical_location *logical_loc)
   {
@@ -1290,6 +1520,64 @@ sarif_sink::sarif_sink (diagnostic_manager &mgr,
   mgr.get_dc ().add_sink (std::move (inner_sink));
 }
 
+// struct diagnostic_message_buffer
+
+std::string
+diagnostic_message_buffer::to_string () const
+{
+  std::string result;
+
+  /* Convert to text, dropping colorization, URLs, etc.  */
+  for (auto iter = m_tokens.m_first; iter; iter = iter->m_next)
+    switch (iter->m_kind)
+      {
+      default:
+       gcc_unreachable ();
+
+      case pp_token::kind::text:
+       {
+         pp_token_text *sub = as_a <pp_token_text *> (iter);
+         result += sub->m_value.get ();
+       }
+       break;
+
+      case pp_token::kind::begin_color:
+      case pp_token::kind::end_color:
+       // Skip
+       break;
+
+      case pp_token::kind::begin_quote:
+       result += open_quote;
+       break;
+
+      case pp_token::kind::end_quote:
+       result += close_quote;
+       break;
+
+      case pp_token::kind::begin_url:
+      case pp_token::kind::end_url:
+       // Skip
+       break;
+
+      case pp_token::kind::event_id:
+       {
+         pp_token_event_id *sub = as_a <pp_token_event_id *> (iter);
+         gcc_assert (sub->m_event_id.known_p ());
+         result += '(';
+         result += std::to_string (sub->m_event_id.one_based ());
+         result += ')';
+       }
+       break;
+
+      case pp_token::kind::custom_data:
+       /* We don't have a way of handling custom_data tokens here.  */
+       gcc_unreachable ();
+       break;
+      }
+
+  return result;
+}
+
 /* struct diagnostic_manager.  */
 
 void
@@ -1302,7 +1590,7 @@ diagnostic_manager::write_patch (FILE *dst_stream)
 }
 
 void
-diagnostic_manager::emit (diagnostic &diag, const char *msgid, va_list *args)
+diagnostic_manager::emit_va (diagnostic &diag, const char *msgid, va_list 
*args)
 {
   set_line_table_global ();
 
@@ -1331,6 +1619,24 @@ GCC_DIAGNOSTIC_POP
   m_current_diag = nullptr;
 }
 
+void
+diagnostic_manager::emit (diagnostic &diag, const char *msgid, ...)
+{
+  va_list args;
+  va_start (args, msgid);
+  emit_va (diag, msgid, &args);
+  va_end (args);
+}
+
+void
+diagnostic_manager::emit_msg_buf (diagnostic &diag,
+                                 diagnostic_message_buffer &msg_buf)
+{
+
+  pp_element_message_buffer e_msg_buf (msg_buf);
+  emit (diag, "%e", &e_msg_buf);
+}
+
 diagnostic_execution_path *
 diagnostic_manager::new_execution_path ()
 {
@@ -1365,10 +1671,10 @@ diagnostic_manager::take_global_graph 
(std::unique_ptr<diagnostic_graph> graph)
 /* Error-checking at the API boundary.  */
 
 #define FAIL_IF_NULL(PTR_ARG) \
-  do {                                             \
-    volatile const void *p = (PTR_ARG);                    \
-    if (!p) {                                      \
-      fprintf (stderr, "%s: %s must be non-NULL\n",   \
+  do {                                                     \
+    volatile const void *ptr_arg = (PTR_ARG);              \
+    if (!ptr_arg) {                                        \
+      fprintf (stderr, "%s: %s must be non-NULL\n",        \
               __func__, #PTR_ARG);                   \
       abort ();                                              \
     }                                              \
@@ -2044,7 +2350,7 @@ diagnostic_finish_va (diagnostic *diag, const char 
*gmsgid, va_list *args)
   else
     progname = "progname";
   auto_diagnostic_group d;
-  diag->get_manager ().emit (*diag, gmsgid, args);
+  diag->get_manager ().emit_va (*diag, gmsgid, args);
   delete diag;
 }
 
@@ -2165,13 +2471,14 @@ diagnostic_manager_set_analysis_target 
(diagnostic_manager *mgr,
   mgr->get_dc ().set_main_input_filename (file->get_name ());
 }
 
-// struct diagnostic_graph : public diagnostics::digraphs::graph<foo_traits>
+/* Public entrypoint.  */
 
 diagnostic_node *
-diagnostic_graph::add_node_with_id (std::string id,
+diagnostic_graph::add_node_with_id (std::string node_id,
                                    diagnostic_node *parent_node)
 {
-  auto node_up = std::make_unique<diagnostic_node> (*this, std::move (id));
+  auto node_up
+    = std::make_unique<diagnostic_node> (*this, std::move (node_id));
   diagnostic_node *new_node = node_up.get ();
   if (parent_node)
     parent_node->add_child (std::move (node_up));
@@ -2180,14 +2487,16 @@ diagnostic_graph::add_node_with_id (std::string id,
   return new_node;
 }
 
+/* Public entrypoint.  */
+
 diagnostic_edge *
-diagnostic_graph::add_edge_with_label (const char *id,
+diagnostic_graph::add_edge_with_label (const char *edge_id,
                                       diagnostic_node &src_node,
                                       diagnostic_node &dst_node,
                                       const char *label)
 {
   auto edge_up
-    = std::make_unique<diagnostic_edge> (*this, id,
+    = std::make_unique<diagnostic_edge> (*this, edge_id,
                                         src_node, dst_node);
   diagnostic_edge *new_edge = edge_up.get ();
   if (label)
@@ -2247,22 +2556,24 @@ diagnostic_graph_set_description (diagnostic_graph 
*graph,
   graph->set_description (desc);
 }
 
+/* Public entrypoint.  */
+
 diagnostic_node *
 diagnostic_graph_add_node (diagnostic_graph *graph,
-                          const char *id,
+                          const char *node_id,
                           diagnostic_node *parent_node)
 {
   FAIL_IF_NULL (graph);
-  FAIL_IF_NULL (id);
+  FAIL_IF_NULL (node_id);
 
-  return graph->add_node_with_id (id, parent_node);
+  return graph->add_node_with_id (node_id, parent_node);
 }
 
 /* Public entrypoint.  */
 
 diagnostic_edge *
 diagnostic_graph_add_edge (diagnostic_graph *graph,
-                          const char *id,
+                          const char *edge_id,
                           diagnostic_node *src_node,
                           diagnostic_node *dst_node,
                           const char *label)
@@ -2271,31 +2582,31 @@ diagnostic_graph_add_edge (diagnostic_graph *graph,
   FAIL_IF_NULL (src_node);
   FAIL_IF_NULL (dst_node);
 
-  return graph->add_edge_with_label (id, *src_node, *dst_node, label);
+  return graph->add_edge_with_label (edge_id, *src_node, *dst_node, label);
 }
 
 /* Public entrypoint.  */
 
 diagnostic_node *
 diagnostic_graph_get_node_by_id (diagnostic_graph *graph,
-                                const char *id)
+                                const char *node_id)
 {
   FAIL_IF_NULL (graph);
-  FAIL_IF_NULL (id);
+  FAIL_IF_NULL (node_id);
 
-  return static_cast<diagnostic_node *> (graph->get_node_by_id (id));
+  return static_cast<diagnostic_node *> (graph->get_node_by_id (node_id));
 }
 
 /* Public entrypoint.  */
 
 diagnostic_edge *
 diagnostic_graph_get_edge_by_id (diagnostic_graph *graph,
-                                const char *id)
+                                const char *edge_id)
 {
   FAIL_IF_NULL (graph);
-  FAIL_IF_NULL (id);
+  FAIL_IF_NULL (edge_id);
 
-  return static_cast<diagnostic_edge *> (graph->get_edge_by_id (id));
+  return static_cast<diagnostic_edge *> (graph->get_edge_by_id (edge_id));
 }
 
 /* Public entrypoint.  */
@@ -2320,6 +2631,8 @@ diagnostic_node_set_label (diagnostic_node *node,
   node->set_label (label);
 }
 
+/* Public entrypoint.  */
+
 void
 diagnostic_node_set_logical_location (diagnostic_node *node,
                                      const diagnostic_logical_location 
*logical_loc)
@@ -2332,34 +2645,6 @@ diagnostic_node_set_logical_location (diagnostic_node 
*node,
 
 /* Private entrypoint.  */
 
-diagnostic_event_id
-private_diagnostic_execution_path_add_event_2 (diagnostic_execution_path *path,
-                                              const 
diagnostic_physical_location *physical_loc,
-                                              const 
diagnostic_logical_location *logical_loc,
-                                              unsigned stack_depth,
-                                              diagnostic_graph *state_graph,
-                                              const char *gmsgid, ...)
-
-{
-  FAIL_IF_NULL (path);
-  FAIL_IF_NULL (gmsgid);
-
-  va_list args;
-  va_start (args, gmsgid);
-  diagnostic_event_id_t result
-    = path->add_event_va (physical_loc,
-                         logical_loc,
-                         stack_depth,
-                         std::unique_ptr <diagnostic_graph> (state_graph),
-                         gmsgid, &args);
-  va_end (args);
-
-  return as_diagnostic_event_id (result);
-
-}
-
-/* Private entrypoint.  */
-
 void
 private_diagnostic_graph_set_property_bag (diagnostic_graph &graph,
                                          std::unique_ptr<json::object> 
properties)
@@ -2384,3 +2669,285 @@ private_diagnostic_edge_set_property_bag 
(diagnostic_edge &edge,
 {
   edge.set_property_bag (std::move (properties));
 }
+
+/* Public entrypoint.  */
+
+diagnostic_message_buffer *
+diagnostic_message_buffer_new ()
+{
+  return new diagnostic_message_buffer ();
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_message_buffer_release (diagnostic_message_buffer *msg_buf)
+{
+  FAIL_IF_NULL (msg_buf);
+  delete msg_buf;
+}
+
+void
+diagnostic_message_buffer_append_str (diagnostic_message_buffer *msg_buf,
+                                     const char *p)
+{
+  FAIL_IF_NULL (msg_buf);
+  FAIL_IF_NULL (p);
+  msg_buf->m_tokens.push_back_text (label_text::take (xstrdup (p)));
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_message_buffer_append_text (diagnostic_message_buffer *msg_buf,
+                                      const char *p,
+                                      size_t len)
+{
+  FAIL_IF_NULL (msg_buf);
+  FAIL_IF_NULL (p);
+  msg_buf->m_tokens.push_back_text (label_text::take (xstrndup (p, len)));
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_message_buffer_append_byte (diagnostic_message_buffer *msg_buf,
+                                      char ch)
+{
+  FAIL_IF_NULL (msg_buf);
+  msg_buf->m_tokens.push_back_byte (ch);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_message_buffer_append_printf (diagnostic_message_buffer *msg_buf,
+                                        const char *fmt, ...)
+{
+  FAIL_IF_NULL (msg_buf);
+  FAIL_IF_NULL (fmt);
+
+  va_list args;
+  va_start (args, fmt);
+
+  char *formatted_buf = xvasprintf (fmt, args);
+
+  va_end (args);
+
+  msg_buf->m_tokens.push_back_text (label_text::take (formatted_buf));
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_message_buffer_append_event_id (diagnostic_message_buffer *msg_buf,
+                                          diagnostic_event_id event_id)
+{
+  FAIL_IF_NULL (msg_buf);
+  msg_buf->m_tokens.push_back<pp_token_event_id> (event_id);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_message_buffer_begin_url (diagnostic_message_buffer *msg_buf,
+                                    const char *url)
+{
+  FAIL_IF_NULL (msg_buf);
+  FAIL_IF_NULL (url);
+  msg_buf->m_tokens.push_back<pp_token_begin_url>
+    (label_text::take (xstrdup (url)));
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_message_buffer_end_url (diagnostic_message_buffer *msg_buf)
+{
+  FAIL_IF_NULL (msg_buf);
+  msg_buf->m_tokens.push_back<pp_token_end_url> ();
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_message_buffer_begin_quote (diagnostic_message_buffer *msg_buf)
+{
+  FAIL_IF_NULL (msg_buf);
+  msg_buf->m_tokens.push_back<pp_token_begin_quote> ();
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_message_buffer_end_quote (diagnostic_message_buffer *msg_buf)
+{
+  FAIL_IF_NULL (msg_buf);
+  msg_buf->m_tokens.push_back<pp_token_end_quote> ();
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_message_buffer_begin_color (diagnostic_message_buffer *msg_buf,
+                                      const char *color)
+{
+  FAIL_IF_NULL (msg_buf);
+  FAIL_IF_NULL (color);
+  msg_buf->m_tokens.push_back<pp_token_begin_color>
+    (label_text::take (xstrdup (color)));
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_message_buffer_end_color (diagnostic_message_buffer *msg_buf)
+{
+  FAIL_IF_NULL (msg_buf);
+  msg_buf->m_tokens.push_back<pp_token_end_color> ();
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_message_buffer_dump (const diagnostic_message_buffer *msg_buf,
+                               FILE *outf)
+{
+  FAIL_IF_NULL (msg_buf);
+  FAIL_IF_NULL (outf);
+
+  msg_buf->m_tokens.dump (outf);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_finish_via_msg_buf (diagnostic *diag,
+                              diagnostic_message_buffer *msg_buf)
+{
+  FAIL_IF_NULL (diag);
+  FAIL_IF_NULL (msg_buf);
+
+  if (const char *tool_name
+      = diag->get_manager ().get_client_version_info ()->m_name.get_str ())
+    progname = tool_name;
+  else
+    progname = "progname";
+  auto_diagnostic_group d;
+  diag->get_manager ().emit_msg_buf (*diag, *msg_buf);
+  delete diag;
+  delete msg_buf;
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_add_location_with_label_via_msg_buf (diagnostic *diag,
+                                               const 
diagnostic_physical_location *loc,
+                                               diagnostic_message_buffer 
*msg_buf)
+{
+  FAIL_IF_NULL (diag);
+  diag->get_manager ().assert_valid_diagnostic_physical_location (loc);
+  FAIL_IF_NULL (msg_buf);
+
+  std::unique_ptr<diagnostic_message_buffer> msg_buf_up (msg_buf);
+  diag->add_location_with_label (loc, std::move (msg_buf_up));
+}
+
+/* Public entrypoint.  */
+
+diagnostic_event_id
+diagnostic_execution_path_add_event_via_msg_buf (diagnostic_execution_path 
*path,
+                                                const 
diagnostic_physical_location *physical_loc,
+                                                const 
diagnostic_logical_location *logical_loc,
+                                                unsigned stack_depth,
+                                                diagnostic_message_buffer 
*msg_buf)
+{
+  FAIL_IF_NULL (path);
+  FAIL_IF_NULL (msg_buf);
+
+  std::unique_ptr<diagnostic_message_buffer> msg_buf_up (msg_buf);
+  diagnostic_event_id_t result
+    = path->add_event_via_msg_buf (physical_loc,
+                                  logical_loc,
+                                  stack_depth,
+                                  nullptr,
+                                  std::move (msg_buf_up));
+  return as_diagnostic_event_id (result);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_graph_set_description_via_msg_buf (diagnostic_graph *graph,
+                                             diagnostic_message_buffer *desc)
+{
+  FAIL_IF_NULL (graph);
+
+  if (desc)
+    graph->set_description (desc->to_string ());
+  else
+    graph->set_description (nullptr);
+}
+
+/* Public entrypoint.  */
+
+diagnostic_edge *
+diagnostic_graph_add_edge_via_msg_buf (diagnostic_graph *graph,
+                                      const char *edge_id,
+                                      diagnostic_node *src_node,
+                                      diagnostic_node *dst_node,
+                                      diagnostic_message_buffer *label)
+{
+  FAIL_IF_NULL (graph);
+  FAIL_IF_NULL (src_node);
+  FAIL_IF_NULL (dst_node);
+
+  if (label)
+    {
+      std::string label_str (label->to_string ());
+      return graph->add_edge_with_label (edge_id, *src_node, *dst_node,
+                                        label_str.c_str ());
+    }
+  else
+    return graph->add_edge_with_label (edge_id, *src_node, *dst_node,
+                                      nullptr);
+}
+
+/* Public entrypoint.  */
+
+void
+diagnostic_node_set_label_via_msg_buf (diagnostic_node *node,
+                                      diagnostic_message_buffer *label)
+{
+  FAIL_IF_NULL (node);
+
+  if (label)
+    node->set_label (label->to_string ());
+  else
+    node->set_label (nullptr);
+}
+
+/* Private entrypoint.  */
+
+diagnostic_event_id
+private_diagnostic_execution_path_add_event_3 (diagnostic_execution_path *path,
+                                              const 
diagnostic_physical_location *physical_loc,
+                                              const 
diagnostic_logical_location *logical_loc,
+                                              unsigned stack_depth,
+                                              diagnostic_graph *state_graph,
+                                              diagnostic_message_buffer 
*msg_buf)
+{
+  FAIL_IF_NULL (path);
+  FAIL_IF_NULL (msg_buf);
+
+  diagnostic_event_id_t result
+    = path->add_event_via_msg_buf
+       (physical_loc,
+        logical_loc,
+        stack_depth,
+        std::unique_ptr <diagnostic_graph> (state_graph),
+        std::unique_ptr <diagnostic_message_buffer> (msg_buf));
+
+  return as_diagnostic_event_id (result);
+}
diff --git a/gcc/libgdiagnostics.h b/gcc/libgdiagnostics.h
index f79790a6cf7f..c202feb1cae3 100644
--- a/gcc/libgdiagnostics.h
+++ b/gcc/libgdiagnostics.h
@@ -50,6 +50,13 @@ extern "C" {
 #define LIBGDIAGNOSTICS_PARAM_CAN_BE_NULL(ARG_NUM)
   /* empty; for the human reader */
 
+# if (LIBGDIAGNOSTICS_GCC_VERSION >= 4001)
+#  define LIBGDIAGNOSTICS_PARAM_FORMAT_STRING(FMT_KIND, FMT_ARG_NUM, 
ARGS_ARG_NUM) \
+     __attribute__ ((__format__ (FMT_KIND, FMT_ARG_NUM, ARGS_ARG_NUM)))
+# else
+#  define LIBGDIAGNOSTICS_PARAM_FORMAT_STRING(FMT_KIND, FMT_ARG_NUM, 
ARGS_ARG_NUM)
+# endif /* GNUC >= 4.1 */
+
 #define LIBGDIAGNOSTICS_PARAM_GCC_FORMAT_STRING(FMT_ARG_NUM, ARGS_ARG_NUM) \
   LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (FMT_ARG_NUM)
   /* In theory we'd also add
@@ -59,6 +66,10 @@ extern "C" {
      of -Wall but undocumented, and much fussier than I'd want to inflict
      on users of libgdiagnostics.  */
 
+#define LIBGDIAGNOSTICS_PARAM_PRINTF_FORMAT_STRING(FMT_ARG_NUM, ARGS_ARG_NUM) \
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (FMT_ARG_NUM) \
+  LIBGDIAGNOSTICS_PARAM_FORMAT_STRING(gnu_printf, FMT_ARG_NUM, ARGS_ARG_NUM)
+
 /**********************************************************************
  Data structures and types.
  All structs within the API are opaque.
@@ -230,6 +241,8 @@ enum diagnostic_level
 typedef struct diagnostic_execution_path diagnostic_execution_path;
 typedef int diagnostic_event_id;
 
+typedef struct diagnostic_message_buffer diagnostic_message_buffer;
+
 /**********************************************************************
  API entrypoints.
  **********************************************************************/
@@ -899,6 +912,209 @@ diagnostic_node_set_logical_location (diagnostic_node 
*node,
   LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
   LIBGDIAGNOSTICS_PARAM_CAN_BE_NULL (2);
 
+/* Message buffers.  */
+
+#define LIBDIAGNOSTICS_HAVE_diagnostic_message_buffer
+
+/* Create a new diagnostic_message_buffer.
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern diagnostic_message_buffer *
+diagnostic_message_buffer_new (void);
+
+/* Release a diagnostic_message_buffer that hasn't been used.
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern void
+diagnostic_message_buffer_release (diagnostic_message_buffer *msg_buf)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* Append a UTF-8 encoded null-terminated string to the buffer.
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern void
+diagnostic_message_buffer_append_str (diagnostic_message_buffer *msg_buf,
+                                     const char *p)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2);
+
+/* Append a UTF-8 encoded run of bytes to the buffer.
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern void
+diagnostic_message_buffer_append_text (diagnostic_message_buffer *msg_buf,
+                                      const char *p,
+                                      size_t len)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2);
+
+/* Append a byte to to the buffer.  This should be either
+   ASCII, or part of UTF-8 encoded text.
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern void
+diagnostic_message_buffer_append_byte (diagnostic_message_buffer *msg_buf,
+                                      char ch)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* Append a formatted string to the buffer, using the formatting rules
+   for "printf".
+   The string is assumed to be UTF-8 encoded.
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern void
+diagnostic_message_buffer_append_printf (diagnostic_message_buffer *msg_buf,
+                                        const char *fmt, ...)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBGDIAGNOSTICS_PARAM_PRINTF_FORMAT_STRING (2, 3);
+
+/* Append a diagnostic_event_id to the buffer in the form "(1)".
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern void
+diagnostic_message_buffer_append_event_id (diagnostic_message_buffer *msg_buf,
+                                          diagnostic_event_id event_id)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* Begin a run of text associated with the given URL.
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern void
+diagnostic_message_buffer_begin_url (diagnostic_message_buffer *msg_buf,
+                                    const char *url)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2);
+
+/* End a run of text started with diagnostic_message_buffer_begin_url.
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern void
+diagnostic_message_buffer_end_url (diagnostic_message_buffer *msg_buf)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* Begin a run of text to be printed in quotes.
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern void
+diagnostic_message_buffer_begin_quote (diagnostic_message_buffer *msg_buf)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* End a run of text started with diagnostic_message_buffer_begin_quote.
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern void
+diagnostic_message_buffer_end_quote (diagnostic_message_buffer *msg_buf)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* Begin a run of text to be printed with color.
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern void
+diagnostic_message_buffer_begin_color (diagnostic_message_buffer *msg_buf,
+                                      const char *color)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2);
+
+/* End a run of text started with diagnostic_message_buffer_begin_color.
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern void
+diagnostic_message_buffer_end_color (diagnostic_message_buffer *msg_buf)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1);
+
+/* Write a debugging representation of MSG_BUG to OUTF.
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern void
+diagnostic_message_buffer_dump (const diagnostic_message_buffer *msg_buf,
+                               FILE *outf)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2);
+
+/* As diagnostic_finish, but takes ownership of MSG_BUF.
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern void
+diagnostic_finish_via_msg_buf (diagnostic *diag,
+                              diagnostic_message_buffer *msg_buf)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2);
+
+/* As diagnostic_add_location_with_label but takes ownership of MSG_BUF.
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern void
+diagnostic_add_location_with_label_via_msg_buf (diagnostic *diag,
+                                               const 
diagnostic_physical_location *loc,
+                                               diagnostic_message_buffer 
*msg_buf)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBGDIAGNOSTICS_PARAM_CAN_BE_NULL (2)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (3);
+
+/* As diagnostic_execution_path_add_event but takes ownership of MSG_BUF.
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern diagnostic_event_id
+diagnostic_execution_path_add_event_via_msg_buf (diagnostic_execution_path 
*path,
+                                                const 
diagnostic_physical_location *physical_loc,
+                                                const 
diagnostic_logical_location *logical_loc,
+                                                unsigned stack_depth,
+                                                diagnostic_message_buffer 
*msg_buf)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBGDIAGNOSTICS_PARAM_CAN_BE_NULL (2)
+  LIBGDIAGNOSTICS_PARAM_CAN_BE_NULL (3)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (5);
+
+/* Set the description of GRAPH for use
+   in the value of the SARIF "description" property
+   (SARIF v2.1.0 section 3.39.2).
+
+   Takes ownership of DESC, if non-null.
+
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern void
+diagnostic_graph_set_description_via_msg_buf (diagnostic_graph *graph,
+                                             diagnostic_message_buffer *desc)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBGDIAGNOSTICS_PARAM_CAN_BE_NULL (2);
+
+/* Create and add a new edge within GRAPH.
+
+   If non-null, then EDGE_ID must be unique within edges in GRAPH;
+   if EDGE_ID is null then a unique id of the form "edge0", "edge1", etc
+   will be used automatically.
+
+   Takes ownership of LABEL, if non-null.
+
+   The new edge is owned by GRAPH.
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern diagnostic_edge *
+diagnostic_graph_add_edge_via_msg_buf (diagnostic_graph *graph,
+                                      const char *edge_id,
+                                      diagnostic_node *src_node,
+                                      diagnostic_node *dst_node,
+                                      diagnostic_message_buffer *label)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBGDIAGNOSTICS_PARAM_CAN_BE_NULL (2)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (3)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (4)
+  LIBGDIAGNOSTICS_PARAM_CAN_BE_NULL (5);
+
+/* Set the label of NODE for use
+   in the value of the SARIF "label" property
+   (SARIF v2.1.0 section 3.40.3).
+
+   Takes ownership of LABEL, if non-null.
+
+   Added in LIBGDIAGNOSTICS_ABI_4.  */
+
+extern void
+diagnostic_node_set_label_via_msg_buf (diagnostic_node *node,
+                                      diagnostic_message_buffer *label)
+  LIBGDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (1)
+  LIBGDIAGNOSTICS_PARAM_CAN_BE_NULL (2);
+
 /* DEFERRED:
    - thread-safety
    - plural forms
@@ -906,6 +1122,7 @@ diagnostic_node_set_logical_location (diagnostic_node 
*node,
    - locations within binary files
    - options and URLs for warnings
    - enable/disable of warnings by kind
+   - command-line arguments
    - plugin metadata.  */
 
 #ifdef __cplusplus
diff --git a/gcc/libgdiagnostics.map b/gcc/libgdiagnostics.map
index cae28d14425a..91f3951a35b4 100644
--- a/gcc/libgdiagnostics.map
+++ b/gcc/libgdiagnostics.map
@@ -69,6 +69,7 @@ LIBGDIAGNOSTICS_ABI_0
     diagnostic_finish;
     diagnostic_finish_va;
 
+
     diagnostic_physical_location_get_file;
 
   local: *;
@@ -108,8 +109,35 @@ LIBGDIAGNOSTICS_ABI_3 {
     diagnostic_node_set_logical_location;
 
     # Private hooks used by sarif-replay
-    private_diagnostic_execution_path_add_event_2;
     private_diagnostic_graph_set_property_bag;
     private_diagnostic_node_set_property_bag;
     private_diagnostic_edge_set_property_bag;
 } LIBGDIAGNOSTICS_ABI_2;
+
+# Add diagnostic_message_buffer
+LIBGDIAGNOSTICS_ABI_4 {
+  global:
+    diagnostic_message_buffer_new;
+    diagnostic_message_buffer_release;
+    diagnostic_message_buffer_append_str;
+    diagnostic_message_buffer_append_text;
+    diagnostic_message_buffer_append_byte;
+    diagnostic_message_buffer_append_printf;
+    diagnostic_message_buffer_append_event_id;
+    diagnostic_message_buffer_begin_url;
+    diagnostic_message_buffer_end_url;
+    diagnostic_message_buffer_begin_quote;
+    diagnostic_message_buffer_end_quote;
+    diagnostic_message_buffer_begin_color;
+    diagnostic_message_buffer_end_color;
+    diagnostic_message_buffer_dump;
+    diagnostic_finish_via_msg_buf;
+    diagnostic_add_location_with_label_via_msg_buf;
+    diagnostic_execution_path_add_event_via_msg_buf;
+    diagnostic_graph_set_description_via_msg_buf;
+    diagnostic_graph_add_edge_via_msg_buf;
+    diagnostic_node_set_label_via_msg_buf;
+
+    # Private hook used by sarif-replay
+    private_diagnostic_execution_path_add_event_3;
+} LIBGDIAGNOSTICS_ABI_3;
diff --git a/gcc/libsarifreplay.cc b/gcc/libsarifreplay.cc
index cad535bc5287..815869886013 100644
--- a/gcc/libsarifreplay.cc
+++ b/gcc/libsarifreplay.cc
@@ -282,14 +282,14 @@ class annotation
 {
 public:
   annotation (libgdiagnostics::physical_location phys_loc,
-             label_text label)
+             libgdiagnostics::message_buffer label)
   : m_phys_loc (phys_loc),
     m_label (std::move (label))
   {
   }
 
   libgdiagnostics::physical_location m_phys_loc;
-  label_text m_label;
+  libgdiagnostics::message_buffer m_label;
 };
 
 using id_map = std::map<std::string, const json::string *>;
@@ -333,7 +333,7 @@ private:
 
   enum status emit_sarif_as_diagnostics (const json::value &jv);
 
-  label_text
+  libgdiagnostics::message_buffer
   make_plain_text_within_result_message (const json::object 
*tool_component_obj,
                                         const json::object &message_obj,
                                         const json::object *rule_obj);
@@ -1181,12 +1181,12 @@ sarif_replayer::get_level_from_level_str (const 
json::string &level_str)
 
 static void
 add_any_annotations (libgdiagnostics::diagnostic &diag,
-                    const std::vector<annotation> &annotations)
+                    std::vector<annotation> &annotations)
 {
   for (auto &annotation : annotations)
-    if (annotation.m_label.get ())
+    if (annotation.m_label.m_inner)
       diag.add_location_with_label (annotation.m_phys_loc,
-                                   annotation.m_label.get ());
+                                   std::move (annotation.m_label));
     else
       diag.add_location (annotation.m_phys_loc);
 }
@@ -1249,13 +1249,13 @@ sarif_replayer::handle_result_obj (const json::object 
&result_obj,
     }
 
   // §3.27.11 "message" property
-  label_text text;
+  libgdiagnostics::message_buffer msg_buf;
   if (auto message_obj
        = get_optional_property<json::object> (result_obj, PROP_result_message))
-    text = make_plain_text_within_result_message (nullptr, // TODO: 
tool_component_obj,
-                                                 *message_obj,
-                                                 rule_obj);
-  if (!text.get ())
+    msg_buf = make_plain_text_within_result_message (nullptr, // TODO: 
tool_component_obj,
+                                                    *message_obj,
+                                                    rule_obj);
+  if (!msg_buf.m_inner)
     return status::err_invalid_sarif;
 
   // §3.27.12 "locations" property
@@ -1368,7 +1368,8 @@ sarif_replayer::handle_result_obj (const json::object 
&result_obj,
     }
 
   // §3.27.22 relatedLocations property
-  std::vector<std::pair<libgdiagnostics::diagnostic, label_text>> notes;
+  std::vector<std::pair<libgdiagnostics::diagnostic,
+                       libgdiagnostics::message_buffer>> notes;
   const property_spec_ref prop_related_locations
     ("result", "relatedLocations", "3.27.22");
   if (auto related_locations_arr
@@ -1400,18 +1401,18 @@ sarif_replayer::handle_result_obj (const json::object 
&result_obj,
                                                     prop_message))
            {
              /* Treat related locations with a message as a "note".  */
-             label_text text
+             libgdiagnostics::message_buffer msg_buf
                (make_plain_text_within_result_message
                 (tool_component_obj,
                  *message_obj,
                  rule_obj));
-             if (!text.get ())
+             if (!msg_buf.m_inner)
                return status::err_invalid_sarif;
              auto note (m_output_mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_NOTE));
              note.set_location (physical_loc);
              note.set_logical_location (logical_loc);
              add_any_annotations (note, annotations);
-             notes.push_back ({std::move (note), std::move (text)});
+             notes.push_back ({std::move (note), std::move (msg_buf)});
            }
          else
            {
@@ -1434,14 +1435,14 @@ sarif_replayer::handle_result_obj (const json::object 
&result_obj,
          handle_fix_object (err, *fix_obj);
     }
 
-  err.finish ("%s", text.get ());
+  err.finish_via_msg_buf (std::move (msg_buf));
 
   // Flush any notes
   for (auto &iter : notes)
     {
       auto &note = iter.first;
-      auto &text = iter.second;
-      note.finish ("%s", text.get ());
+      auto &msg_buf = iter.second;
+      note.finish_via_msg_buf (std::move (msg_buf));
     }
 
   return status::ok;
@@ -1569,13 +1570,10 @@ maybe_consume_embedded_link (const char *&iter_src)
    and substitute for any placeholders (§3.11.5) and handle any
    embedded links (§3.11.6).
 
-   Limitations:
-   - we don't preserve destinations within embedded links
-
    MESSAGE_OBJ is "theMessage"
    RULE_OBJ is "theRule".  */
 
-label_text
+libgdiagnostics::message_buffer
 sarif_replayer::
 make_plain_text_within_result_message (const json::object *tool_component_obj,
                                       const json::object &message_obj,
@@ -1588,7 +1586,7 @@ make_plain_text_within_result_message (const json::object 
*tool_component_obj,
                                               rule_obj,
                                               js_str);
   if (!original_text)
-    return label_text::borrow (nullptr);
+    return libgdiagnostics::message_buffer ();
 
   gcc_assert (js_str);
 
@@ -1598,7 +1596,7 @@ make_plain_text_within_result_message (const json::object 
*tool_component_obj,
     = get_optional_property<json::array> (message_obj, arguments_prop);
 
   /* Duplicate original_text, substituting any placeholders.  */
-  std::string accum;
+  libgdiagnostics::message_buffer result (diagnostic_message_buffer_new ());
 
   const char *iter_src = original_text;
   while (char ch = *iter_src)
@@ -1614,7 +1612,7 @@ make_plain_text_within_result_message (const json::object 
*tool_component_obj,
                 " but message object has no %qs property",
                 (int)arg_idx,
                 arguments_prop.get_property_name ());
-             return label_text::borrow (nullptr);
+             return libgdiagnostics::message_buffer ();
            }
          if (arg_idx >= arguments->length ())
            {
@@ -1625,20 +1623,20 @@ make_plain_text_within_result_message (const 
json::object *tool_component_obj,
                 arguments_prop.get_property_name (),
                 (int)arg_idx);
              // TODO: might be nice to add a note showing the args
-             return label_text::borrow (nullptr);
+             return libgdiagnostics::message_buffer ();
            }
          auto replacement_jstr
            = require_string (*arguments->get (arg_idx), arguments_prop);
          if (!replacement_jstr)
-           return label_text::borrow (nullptr);
-         accum += replacement_jstr->get_string ();
+           return libgdiagnostics::message_buffer ();
+         result += replacement_jstr->get_string ();
        }
       else if (ch == '{' || ch == '}')
        {
          /* '{' and '}' are escaped by repeating them.  */
          if (iter_src[1] == ch)
            {
-             accum += ch;
+             result += ch;
              iter_src += 2;
            }
          else
@@ -1648,24 +1646,25 @@ make_plain_text_within_result_message (const 
json::object *tool_component_obj,
              report_invalid_sarif (*js_str, msgs_with_placeholders,
                                    "unescaped '%c' within message string",
                                    ch);
-             return label_text::borrow (nullptr);
+             return libgdiagnostics::message_buffer ();
            }
        }
       else if (auto link = maybe_consume_embedded_link (iter_src))
        {
-         accum += link->text;
-         /* TODO: use the destination.  */
+         result.begin_url (link->destination.c_str ());
+         result += link->text.c_str ();
+         result.end_url ();
          /* TODO: potentially could try to convert
             intra-sarif links into event ids.  */
        }
       else
        {
-         accum += ch;
+         result += ch;
          iter_src++;
        }
     }
 
-  return label_text::take (xstrdup (accum.c_str ()));
+  return result;
 }
 
 /* Handle a value that should be a multiformatMessageString object (§3.12).
@@ -1799,7 +1798,7 @@ handle_thread_flow_location_object (const json::object 
&tflow_loc_obj,
 {
   libgdiagnostics::physical_location physical_loc;
   libgdiagnostics::logical_location logical_loc;
-  label_text message;
+  libgdiagnostics::message_buffer msg_buf;
   int stack_depth = 0;
 
   const property_spec_ref location_prop
@@ -1821,7 +1820,7 @@ handle_thread_flow_location_object (const json::object 
&tflow_loc_obj,
          = get_optional_property<json::object> (*location_obj,
                                                 location_message))
        {
-         message = make_plain_text_within_result_message
+         msg_buf = make_plain_text_within_result_message
            (nullptr,
             *message_obj,
             nullptr/* TODO.  */);
@@ -1874,21 +1873,18 @@ handle_thread_flow_location_object (const json::object 
&tflow_loc_obj,
        return s;
     }
 
-  if (message.get ())
-    private_diagnostic_execution_path_add_event_2 (path.m_inner,
-                                                  physical_loc.m_inner,
-                                                  logical_loc.m_inner,
-                                                  stack_depth,
-                                                  state_graph.m_inner,
-                                                  "%s", message.get ());
-  else
-    private_diagnostic_execution_path_add_event_2 (path.m_inner,
-                                                  physical_loc.m_inner,
-                                                  logical_loc.m_inner,
-                                                  stack_depth,
-                                                  state_graph.m_inner,
-                                                  "");
+  if (!msg_buf.m_inner)
+    msg_buf.m_inner = diagnostic_message_buffer_new ();
+
+  private_diagnostic_execution_path_add_event_3 (path.m_inner,
+                                                physical_loc.m_inner,
+                                                logical_loc.m_inner,
+                                                stack_depth,
+                                                state_graph.m_inner,
+                                                msg_buf.m_inner);
+
   state_graph.m_owned = false;
+  msg_buf.m_inner = nullptr;
 
   return status::ok;
 }
@@ -1967,7 +1963,7 @@ handle_location_object (const json::object &location_obj,
          if (s != status::ok)
            return s;
 
-         label_text label;
+         libgdiagnostics::message_buffer label;
 
          // §3.30.14 message property
          {
@@ -2304,13 +2300,13 @@ sarif_replayer::handle_graph_object (const json::object 
&graph_json_obj,
   if (auto description_obj
       = get_optional_property<json::object> (graph_json_obj, description_prop))
     {
-      label_text text
+      auto msg_buf
        = make_plain_text_within_result_message (&run_obj,
                                                 *description_obj,
                                                 nullptr);
-      if (!text.get ())
+      if (!msg_buf.m_inner)
        return status::err_invalid_sarif;
-      out_graph.set_description (text.get ());
+      out_graph.set_description (std::move (msg_buf));
     }
 
   // §3.39.3: MAY contain a "nodes" property
@@ -2403,13 +2399,13 @@ sarif_replayer::handle_node_object (const json::object 
&node_json_obj,
   if (auto label_obj
       = get_optional_property<json::object> (node_json_obj, label_prop))
     {
-      label_text text
+      auto msg_buf
        = make_plain_text_within_result_message (&run_obj,
                                                 *label_obj,
                                                 nullptr);
-      if (!text.get ())
+      if (!msg_buf.m_inner)
        return nullptr;
-      new_node.set_label (text.get ());
+      new_node.set_label (std::move (msg_buf));
     }
 
   // §3.40.4 "location" property
@@ -2484,7 +2480,7 @@ sarif_replayer::handle_edge_object (const json::object 
&edge_json_obj,
   edge_id_map[id] = id_str;
 
   // §3.41.3 "label" property
-  label_text label;
+  libgdiagnostics::message_buffer label;
   const property_spec_ref label_prop
     ("edge", "label", "3.41.3");
   if (auto label_obj
@@ -2493,7 +2489,7 @@ sarif_replayer::handle_edge_object (const json::object 
&edge_json_obj,
       label = make_plain_text_within_result_message (nullptr,
                                                     *label_obj,
                                                     nullptr);
-      if (!label.get ())
+      if (!label.m_inner)
        return nullptr;
     }
 
@@ -2513,7 +2509,7 @@ sarif_replayer::handle_edge_object (const json::object 
&edge_json_obj,
   if (!dst_node.m_inner)
     return nullptr;
 
-  auto result = graph.add_edge (id, src_node, dst_node, label.get ());
+  auto result = graph.add_edge (id, src_node, dst_node, std::move (label));
 
   if (auto properties = maybe_get_property_bag (edge_json_obj))
     private_diagnostic_edge_set_property_bag (*result.m_inner,
diff --git a/gcc/pretty-print-format-impl.h b/gcc/pretty-print-format-impl.h
index cbbd21f57179..90692c80c7e8 100644
--- a/gcc/pretty-print-format-impl.h
+++ b/gcc/pretty-print-format-impl.h
@@ -334,6 +334,7 @@ public:
     push_back (std::move (tok));
   }
   void push_back_text (label_text &&text);
+  void push_back_byte (char ch);
   void push_back (std::unique_ptr<pp_token> tok);
   void push_back_list (pp_token_list &&list);
 
diff --git a/gcc/pretty-print-markup.h b/gcc/pretty-print-markup.h
index 18e298cb183d..6c0719d3e922 100644
--- a/gcc/pretty-print-markup.h
+++ b/gcc/pretty-print-markup.h
@@ -45,6 +45,11 @@ public:
   void begin_highlight_color (const char *color_name);
   void end_highlight_color ();
 
+  void begin_url (const char *url);
+  void end_url ();
+
+  void add_event_id (diagnostic_event_id_t event_id);
+
   void push_back_any_text ();
 
   pretty_printer &m_pp;
diff --git a/gcc/pretty-print.cc b/gcc/pretty-print.cc
index 6ecfcb26c43c..76bbf2b8cd9a 100644
--- a/gcc/pretty-print.cc
+++ b/gcc/pretty-print.cc
@@ -30,6 +30,7 @@ along with GCC; see the file COPYING3.  If not see
 #include "diagnostic-color.h"
 #include "diagnostic-event-id.h"
 #include "diagnostic-highlight-colors.h"
+#include "auto-obstack.h"
 #include "selftest.h"
 
 #if HAVE_ICONV
@@ -714,7 +715,7 @@ static int
 decode_utf8_char (const unsigned char *, size_t len, unsigned int *);
 static void pp_quoted_string (pretty_printer *, const char *, size_t = -1);
 
-static void
+extern void
 default_token_printer (pretty_printer *pp,
                       const pp_token_list &tokens);
 
@@ -1326,6 +1327,15 @@ pp_token_list::push_back_text (label_text &&text)
   push_back<pp_token_text> (std::move (text));
 }
 
+void
+pp_token_list::push_back_byte (char ch)
+{
+  char buf[2];
+  buf[0] = ch;
+  buf[1] = '\0';
+  push_back_text (label_text::take (xstrdup (buf)));
+}
+
 void
 pp_token_list::push_back (std::unique_ptr<pp_token> tok)
 {
@@ -2177,38 +2187,6 @@ format_phase_2 (pretty_printer *pp,
       gcc_assert (!formatters[argno]);
 }
 
-struct auto_obstack
-{
-  auto_obstack ()
-  {
-    obstack_init (&m_obstack);
-  }
-
-  ~auto_obstack ()
-  {
-    obstack_free (&m_obstack, NULL);
-  }
-
-  operator obstack & () { return m_obstack; }
-
-  void grow (const void *src, size_t length)
-  {
-    obstack_grow (&m_obstack, src, length);
-  }
-
-  void *object_base () const
-  {
-    return m_obstack.object_base;
-  }
-
-  size_t object_size () const
-  {
-    return obstack_object_size (&m_obstack);
-  }
-
-  obstack m_obstack;
-};
-
 /* Phase 3 of formatting a message (phases 1 and 2 done by pp_format).
 
    Pop a pp_formatted_chunks from chunk_obstack, collecting all the tokens from
@@ -2261,7 +2239,7 @@ pp_output_formatted_text (pretty_printer *pp,
 
 /* Default implementation of token printing.  */
 
-static void
+void
 default_token_printer (pretty_printer *pp,
                       const pp_token_list &tokens)
 {
@@ -3188,6 +3166,29 @@ pp_markup::context::end_highlight_color ()
   m_formatted_token_list->push_back<pp_token_end_color> ();
 }
 
+void
+pp_markup::context::begin_url (const char *url)
+{
+  push_back_any_text ();
+  m_formatted_token_list->push_back<pp_token_begin_url>
+    (label_text::take (xstrdup (url)));
+}
+
+void
+pp_markup::context::end_url ()
+{
+  push_back_any_text ();
+  m_formatted_token_list->push_back<pp_token_end_url> ();
+}
+
+void
+pp_markup::context::add_event_id (diagnostic_event_id_t event_id)
+{
+  gcc_assert (event_id.known_p ());
+  push_back_any_text ();
+  m_formatted_token_list->push_back<pp_token_event_id> (event_id);
+}
+
 void
 pp_markup::context::push_back_any_text ()
 {
diff --git a/gcc/testsuite/libgdiagnostics.dg/sarif.py 
b/gcc/testsuite/libgdiagnostics.dg/sarif.py
deleted file mode 100644
index 7daf35b58190..000000000000
--- a/gcc/testsuite/libgdiagnostics.dg/sarif.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import json
-import os
-
-def sarif_from_env():
-    # return parsed JSON content a SARIF_PATH file
-    json_filename = os.environ['SARIF_PATH']
-    json_filename += '.sarif'
-    print('json_filename: %r' % json_filename)
-    with open(json_filename) as f:
-        json_data = f.read()
-    return json.loads(json_data)
-
-def get_location_artifact_uri(location):
-    return location['physicalLocation']['artifactLocation']['uri']
-
-def get_location_physical_region(location):
-    return location['physicalLocation']['region']
-
-def get_location_snippet_text(location):
-    return location['physicalLocation']['contextRegion']['snippet']['text']
-
-def get_location_relationships(location):
-    return location['relationships']
diff --git a/gcc/testsuite/libgdiagnostics.dg/test-message-buffer-c.py 
b/gcc/testsuite/libgdiagnostics.dg/test-message-buffer-c.py
new file mode 100644
index 000000000000..9d14b9a7bda9
--- /dev/null
+++ b/gcc/testsuite/libgdiagnostics.dg/test-message-buffer-c.py
@@ -0,0 +1,12 @@
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+def test_message_in_generated_sarif(sarif):
+    result = get_result_by_index(sarif, 0)
+    assert result['level'] == 'error'
+    assert result['message']['text'] == "this is a string; foo; int: 42 str: 
mostly harmless; [this is a link](https://example.com/) 'this is quoted' 
highlight A highlight B (1)."
diff --git a/gcc/testsuite/libgdiagnostics.dg/test-message-buffer.c 
b/gcc/testsuite/libgdiagnostics.dg/test-message-buffer.c
new file mode 100644
index 000000000000..a958fc577036
--- /dev/null
+++ b/gcc/testsuite/libgdiagnostics.dg/test-message-buffer.c
@@ -0,0 +1,80 @@
+/* Example of using a message buffer to build the text of a diagnostic
+   in pieces before emitting it.  */
+
+#include "libgdiagnostics.h"
+#include "test-helpers.h"
+
+int
+main ()
+{
+  begin_test ("test-message-buffer.c.exe",
+             "test-message-buffer.c.sarif",
+             __FILE__, "c");
+
+  diagnostic_event_id event_id = 0;
+
+  /* begin quoted source */
+  diagnostic *d = diagnostic_begin (diag_mgr,
+                                   DIAGNOSTIC_LEVEL_ERROR);
+
+  diagnostic_message_buffer *msg_buf = diagnostic_message_buffer_new ();
+
+  /* Add a null-terminated string.  */
+  diagnostic_message_buffer_append_str (msg_buf, "this is a string; ");
+
+  /* Add a length-specified string.  */
+  diagnostic_message_buffer_append_text (msg_buf, "foobar", 3);
+
+  /* "printf"-formatting.  */
+  diagnostic_message_buffer_append_printf (msg_buf,
+                                          "; int: %i str: %s; ",
+                                          42, "mostly harmless");
+
+  /* Adding a URL.  */
+  diagnostic_message_buffer_begin_url (msg_buf, "https://example.com/";);
+  diagnostic_message_buffer_append_str (msg_buf, "this is a link");  
+  diagnostic_message_buffer_end_url (msg_buf);
+
+  diagnostic_message_buffer_append_str (msg_buf, " ");  
+
+  /* Add quoted text.  */
+  diagnostic_message_buffer_begin_quote (msg_buf);
+  diagnostic_message_buffer_append_str (msg_buf, "this is quoted");  
+  diagnostic_message_buffer_end_quote (msg_buf);
+  
+  diagnostic_message_buffer_append_str (msg_buf, " ");  
+
+  /* Add colorized text.  */
+  diagnostic_message_buffer_begin_color (msg_buf, "highlight-a");
+  diagnostic_message_buffer_append_str (msg_buf, "highlight A");  
+  diagnostic_message_buffer_end_color (msg_buf);
+
+  diagnostic_message_buffer_append_str (msg_buf, " ");
+  
+  diagnostic_message_buffer_begin_color (msg_buf, "highlight-b");
+  diagnostic_message_buffer_append_str (msg_buf, "highlight B");  
+  diagnostic_message_buffer_end_color (msg_buf);
+  
+  diagnostic_message_buffer_append_str (msg_buf, " ");
+
+  /* Add an event ID.  This will be printed as "(1)".  */
+  diagnostic_message_buffer_append_event_id (msg_buf, event_id);
+  
+  /* Add an ASCII char.  */
+  diagnostic_message_buffer_append_byte (msg_buf, '.');
+
+  diagnostic_finish_via_msg_buf (d, msg_buf);
+  /* end quoted source */
+
+  return end_test ();
+};
+
+/* Verify the output from the text sink.
+   { dg-regexp "test-message-buffer.c.exe: error: this is a string; foo; int: 
42 str: mostly harmless; this is a link 'this is quoted' highlight A highlight 
B \\(1\\)." } */
+
+/* Verify that some JSON was written to a file with the expected name:
+   { dg-final { verify-sarif-file } } */
+
+/* Use a Python script to verify various properties about the generated
+   .sarif file:
+   { dg-final { run-sarif-pytest test-message-buffer.c 
"test-message-buffer-c.py" } } */
diff --git a/gcc/testsuite/libgdiagnostics.dg/test-warning-with-path-c.py 
b/gcc/testsuite/libgdiagnostics.dg/test-warning-with-path-c.py
index af1e7b980fa9..61ccb93336a9 100644
--- a/gcc/testsuite/libgdiagnostics.dg/test-warning-with-path-c.py
+++ b/gcc/testsuite/libgdiagnostics.dg/test-warning-with-path-c.py
@@ -101,7 +101,7 @@ def test_sarif_output_for_warning_with_path(sarif):
         == '    PyList_Append(list, item);\n'
     assert tfl_2_loc['logicalLocations'] == location['logicalLocations']
     assert tfl_2_loc['message']['text'] \
-        == "when calling 'PyList_Append', passing NULL from (1) as argument 1"
+        == "when calling 'PyList_Append', passing NULL from 
[(1)](sarif:/runs/0/results/0/codeFlows/0/threadFlows/0/locations/0) as 
argument 1"
     assert tfl_2['nestingLevel'] == 0
     assert tfl_2['executionOrder'] == 3
 
diff --git 
a/gcc/testsuite/sarif-replay.dg/2.1.0-valid/3.11.6-embedded-links.sarif 
b/gcc/testsuite/sarif-replay.dg/2.1.0-valid/3.11.6-embedded-links.sarif
index bc64521716c6..cd7b8228742f 100644
--- a/gcc/testsuite/sarif-replay.dg/2.1.0-valid/3.11.6-embedded-links.sarif
+++ b/gcc/testsuite/sarif-replay.dg/2.1.0-valid/3.11.6-embedded-links.sarif
@@ -1,3 +1,6 @@
+/* { dg-additional-options 
"-fdiagnostics-add-output=experimental-html:file=3.11.6-embedded-links.sarif.html,javascript=no"
 } */
+/* { dg-additional-options 
"-fdiagnostics-add-output=sarif:file=3.11.6-embedded-links.sarif.roundtrip.sarif"
 } */
+
 {"$schema": 
"https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json";,
  "version": "2.1.0",
  "runs": [{"tool": {"driver": {"name": "hand-written"}},
@@ -16,10 +19,24 @@ hand-written: warning: 002: Prohibited term used in 
[para\[0\]\\spans\[2\](1).
                        
                        /* With the fix from 
https://github.com/oasis-tcs/sarif-spec/issues/656 */
                        {"message": {"text": "003: Prohibited term used in 
[para\\[0\\]\\\\spans\\[2\\]](1)."},
-                       "locations": []}
+                       "locations": []},
 /* { dg-begin-multiline-output "" }
 hand-written: warning: 003: Prohibited term used in para[0]\spans[2].
+   { dg-end-multiline-output "" } */
+
+                       {"message": {"text": "004: This is a 
[link](http://www.example.com)."},
+                       "locations": []}
+/* { dg-begin-multiline-output "" }
+hand-written: warning: 004: This is a link.
    { dg-end-multiline-output "" } */
 
 ]}]}
 
+/* Use a Python script to verify various properties about the generated
+   .html file:
+   { dg-final { run-html-pytest 3.11.6-embedded-links.sarif 
"2.1.0-valid/embedded-links-check-html.py" } } */
+
+/* Use a Python script to verify various properties about the *generated*
+   .sarif file:
+   { dg-final { run-sarif-pytest 3.11.6-embedded-links.sarif.roundtrip 
"2.1.0-valid/embedded-links-check-sarif-roundtrip.py" } } */
+
diff --git 
a/gcc/testsuite/sarif-replay.dg/2.1.0-valid/embedded-links-check-html.py 
b/gcc/testsuite/sarif-replay.dg/2.1.0-valid/embedded-links-check-html.py
new file mode 100644
index 000000000000..ff1c2f261853
--- /dev/null
+++ b/gcc/testsuite/sarif-replay.dg/2.1.0-valid/embedded-links-check-html.py
@@ -0,0 +1,28 @@
+from htmltest import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def html_tree():
+    return html_tree_from_env()
+
+def test_generated_html(html_tree):
+    root = html_tree.getroot ()
+    assert root.tag == make_tag('html')
+
+    head = root.find('xhtml:head', ns)
+    assert head is not None
+
+    # Get "warning: 004: This is a link."
+    diag = get_diag_by_index(html_tree, 3)
+
+    msg = get_message_within_diag(diag)
+    assert msg is not None
+    
+    assert_tag(msg[0], 'strong')
+    assert msg[0].text == 'warning: '
+    assert msg[0].tail == ' 004: This is a '
+    assert_tag(msg[1], 'a')
+    assert msg[1].text == 'link'
+    assert msg[1].get('href') == 'http://www.example.com'
+    assert msg[1].tail == '. '
diff --git 
a/gcc/testsuite/sarif-replay.dg/2.1.0-valid/embedded-links-check-sarif-roundtrip.py
 
b/gcc/testsuite/sarif-replay.dg/2.1.0-valid/embedded-links-check-sarif-roundtrip.py
new file mode 100644
index 000000000000..171339e37571
--- /dev/null
+++ 
b/gcc/testsuite/sarif-replay.dg/2.1.0-valid/embedded-links-check-sarif-roundtrip.py
@@ -0,0 +1,13 @@
+from sarif import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def sarif():
+    return sarif_from_env()
+
+def test_roundtrip_of_url_in_generated_sarif(sarif):
+    # Get "warning: 004: This is a link."
+    result = get_result_by_index(sarif, 3)
+    assert result['level'] == 'warning'
+    assert result['message']['text'] == "004: This is a 
[link](http://www.example.com)."
-- 
2.26.3

Reply via email to