Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package gap-json for openSUSE:Factory 
checked in at 2026-09-28 10:44:31
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/gap-json (Old)
 and      /work/SRC/openSUSE:Factory/.gap-json.new.383539 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "gap-json"

Mon Sep 28 10:44:31 2026 rev:8 rq:1380731 version:2.5.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/gap-json/gap-json.changes        2026-05-10 
16:48:47.257916934 +0200
+++ /work/SRC/openSUSE:Factory/.gap-json.new.383539/gap-json.changes    
2026-09-28 10:44:43.509610083 +0200
@@ -1,0 +2,7 @@
+Fri Sep 25 21:45:02 UTC 2026 - Jan Engelhardt <[email protected]>
+
+- Update to release 2.5.0
+  * Avoid format-string vulnerabilities
+  * Fix buffer over-read in getUTF8Char
+
+-------------------------------------------------------------------

Old:
----
  json-2.4.0.tar.gz

New:
----
  json-2.5.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ gap-json.spec ++++++
--- /var/tmp/diff_new_pack.VUczEn/_old  2026-09-28 10:44:45.359687595 +0200
+++ /var/tmp/diff_new_pack.VUczEn/_new  2026-09-28 10:44:45.363687762 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           gap-json
-Version:        2.4.0
+Version:        2.5.0
 Release:        0
 Summary:        GAP: Package for reading and writing JSON
 License:        BSD-2-Clause
@@ -26,10 +26,9 @@
 #Git-Clone:     https://github.com/gap-packages/json
 Source:         
https://github.com/gap-packages/json/releases/download/v%version/json-%version.tar.gz
 BuildRequires:  c++_compiler
-BuildRequires:  gap-devel >= 4.12
+BuildRequires:  gap-devel >= 4.15
 BuildRequires:  gap-rpm-devel
-Requires:       gap-core >= 4.12
-Requires:       gap-gapdoc >= 1.5
+Requires:       gap-core >= 4.15
 
 %description
 Enhances GAP by the ability to read and write JSON files.

++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.VUczEn/_old  2026-09-28 10:44:45.480692664 +0200
+++ /var/tmp/diff_new_pack.VUczEn/_new  2026-09-28 10:44:45.489693041 +0200
@@ -1,5 +1,5 @@
-mtime: 1778391754
-commit: 22f29de56f045210cd72e3667bd25c1260f2dfd67d845af1d2a2e0f3e68ca30f
+mtime: 1790372720
+commit: d81a076889674b272124c1bd5011e1f6080787cd9f0bf9c7fafa0fd39d682d46
 url: https://src.opensuse.org/jengelh/gap-json
 revision: master
 

++++++ build.specials.obscpio ++++++

++++++ build.specials.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/.gitignore new/.gitignore
--- old/.gitignore      1970-01-01 01:00:00.000000000 +0100
+++ new/.gitignore      2026-09-25 23:45:20.000000000 +0200
@@ -0,0 +1 @@
+.osc

++++++ json-2.4.0.tar.gz -> json-2.5.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/PackageInfo.g new/json-2.5.0/PackageInfo.g
--- old/json-2.4.0/PackageInfo.g        2026-05-08 02:00:00.000000000 +0200
+++ new/json-2.5.0/PackageInfo.g        2026-09-25 02:00:00.000000000 +0200
@@ -10,8 +10,8 @@
 
 PackageName := "json",
 Subtitle := "Reading and Writing JSON",
-Version := "2.4.0",
-Date := "08/05/2026", # dd/mm/yyyy format
+Version := "2.5.0",
+Date := "25/09/2026", # dd/mm/yyyy format
 License := "BSD-2-Clause",
 
 Persons := [
@@ -69,8 +69,8 @@
 ),
 
 Dependencies := rec(
-  GAP := ">= 4.12",
-  NeededOtherPackages := [ [ "GAPDoc", ">= 1.5" ] ],
+  GAP := ">= 4.15",
+  NeededOtherPackages := [ ],
   SuggestedOtherPackages := [ ],
   ExternalConditions := [ ],
 ),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/README new/json-2.5.0/README
--- old/json-2.4.0/README       2026-05-08 02:00:00.000000000 +0200
+++ new/json-2.5.0/README       2026-09-25 02:00:00.000000000 +0200
@@ -19,7 +19,25 @@
 Updates
 =======
 
-v2.3.0
+v2.5.0
+======
+
+- Speed up GapToJsonString by serialising common GAP objects directly in the
+  kernel.
+
+- Fix a buffer over-read when escaping a string whose last character is a
+  truncated multi-byte UTF-8 sequence, such as [ CHAR_INT(200) ]. Reading past
+  the end of the string now yields 0 rather than whatever happened to follow
+  it, so the sequence falls back to Latin-1 like any other malformed one
+  instead of raising an error or reading out of bounds.
+
+- Avoid format-string vulnerabilities when reporting invalid JSON.
+
+- Remove the unnecessary run-time dependency on GAPDoc.
+
+- Require GAP 4.15 or later.
+
+v2.4.0
 ======
 
 Allow outputting 'fail', it maps to null (null was already read in as fail)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/doc/_Chapter_GAP-JSON_mapping.xml 
new/json-2.5.0/doc/_Chapter_GAP-JSON_mapping.xml
--- old/json-2.4.0/doc/_Chapter_GAP-JSON_mapping.xml    2026-05-08 
02:00:00.000000000 +0200
+++ new/json-2.5.0/doc/_Chapter_GAP-JSON_mapping.xml    2026-09-25 
02:00:00.000000000 +0200
@@ -41,7 +41,6 @@
  </Description>
 </ManSection>
 
-
 <ManSection>
   <Func Arg="value" Name="GapToJsonString" />
  <Returns>string
@@ -51,7 +50,6 @@
  </Description>
 </ManSection>
 
-
 <ManSection>
   <Func Arg="string" Name="JsonStringToGap" />
  <Returns>value
@@ -61,7 +59,6 @@
  </Description>
 </ManSection>
 
-
 <ManSection>
   <Func Arg="stream" Name="JsonStreamToGap" />
  <Returns>value
@@ -71,9 +68,7 @@
  </Description>
 </ManSection>
 
-
 </Section>
 
-
 </Chapter>
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/doc/_entities.xml 
new/json-2.5.0/doc/_entities.xml
--- old/json-2.4.0/doc/_entities.xml    2026-05-08 02:00:00.000000000 +0200
+++ new/json-2.5.0/doc/_entities.xml    2026-09-25 02:00:00.000000000 +0200
@@ -1 +1,4 @@
+<!ENTITY RELEASEDATE '25 September 2026'>
+<!ENTITY RELEASEYEAR '2026'>
+<!ENTITY VERSION '2.5.0'>
 <!ENTITY json '<Package>json</Package>'>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/doc/_main.tex new/json-2.5.0/doc/_main.tex
--- old/json-2.4.0/doc/_main.tex        1970-01-01 01:00:00.000000000 +0100
+++ new/json-2.5.0/doc/_main.tex        2026-09-25 02:00:00.000000000 +0200
@@ -0,0 +1,234 @@
+% generated by GAPDoc2LaTeX from XML source (Frank Luebeck)
+\documentclass[a4paper,11pt]{report}
+
+\usepackage[top=37mm,bottom=37mm,left=27mm,right=27mm]{geometry}
+\sloppy
+\pagestyle{myheadings}
+\usepackage{amssymb}
+\usepackage[utf8]{inputenc}
+\usepackage{makeidx}
+\makeindex
+\usepackage{color}
+\definecolor{FireBrick}{rgb}{0.5812,0.0074,0.0083}
+\definecolor{RoyalBlue}{rgb}{0.0236,0.0894,0.6179}
+\definecolor{RoyalGreen}{rgb}{0.0236,0.6179,0.0894}
+\definecolor{RoyalRed}{rgb}{0.6179,0.0236,0.0894}
+\definecolor{LightBlue}{rgb}{0.8544,0.9511,1.0000}
+\definecolor{Black}{rgb}{0.0,0.0,0.0}
+
+\definecolor{linkColor}{rgb}{0.0,0.0,0.554}
+\definecolor{citeColor}{rgb}{0.0,0.0,0.554}
+\definecolor{fileColor}{rgb}{0.0,0.0,0.554}
+\definecolor{urlColor}{rgb}{0.0,0.0,0.554}
+\definecolor{promptColor}{rgb}{0.0,0.0,0.589}
+\definecolor{brkpromptColor}{rgb}{0.589,0.0,0.0}
+\definecolor{gapinputColor}{rgb}{0.589,0.0,0.0}
+\definecolor{gapoutputColor}{rgb}{0.0,0.0,0.0}
+
+%%  for a long time these were red and blue by default,
+%%  now black, but keep variables to overwrite
+\definecolor{FuncColor}{rgb}{0.0,0.0,0.0}
+%% strange name because of pdflatex bug:
+\definecolor{Chapter }{rgb}{0.0,0.0,0.0}
+\definecolor{DarkOlive}{rgb}{0.1047,0.2412,0.0064}
+
+
+\usepackage{fancyvrb}
+
+\usepackage{mathptmx,helvet}
+\usepackage[T1]{fontenc}
+\usepackage{textcomp}
+
+
+\usepackage[
+            pdftex=true,
+            bookmarks=true,        
+            a4paper=true,
+            pdftitle={Written with GAPDoc},
+            pdfcreator={LaTeX with hyperref package / GAPDoc},
+            colorlinks=true,
+            backref=page,
+            breaklinks=true,
+            linkcolor=linkColor,
+            citecolor=citeColor,
+            filecolor=fileColor,
+            urlcolor=urlColor,
+            pdfpagemode={UseNone}, 
+           ]{hyperref}
+
+\newcommand{\maintitlesize}{\fontsize{50}{55}\selectfont}
+
+% write page numbers to a .pnr log file for online help
+\newwrite\pagenrlog
+\immediate\openout\pagenrlog =\jobname.pnr
+\immediate\write\pagenrlog{PAGENRS := [}
+\newcommand{\logpage}[1]{\protect\write\pagenrlog{#1, \thepage,}}
+%% were never documented, give conflicts with some additional packages
+
+\newcommand{\GAP}{\textsf{GAP}}
+
+%% nicer description environments, allows long labels
+\usepackage{enumitem}
+\setdescription{style=nextline}
+
+%% depth of toc
+\setcounter{tocdepth}{1}
+
+
+
+
+
+%% command for ColorPrompt style examples
+\newcommand{\gapprompt}[1]{\color{promptColor}{\bfseries #1}}
+\newcommand{\gapbrkprompt}[1]{\color{brkpromptColor}{\bfseries #1}}
+\newcommand{\gapinput}[1]{\color{gapinputColor}{#1}}
+
+
+\begin{document}
+
+\logpage{[ 0, 0, 0 ]}
+\begin{titlepage}
+\mbox{}\vfill
+
+\begin{center}{\maintitlesize \textbf{ json \\
+\mbox{}}}\\
+\vfill
+
+\hypersetup{pdftitle={ json }}
+\markright{\scriptsize \mbox{}\hfill  json  \hfill\mbox{}}
+{\Huge \textbf{ Reading and Writing JSON \\
+\mbox{}}}\\
+\vfill
+
+{\Huge  2.5.0 \mbox{}}\\[1cm]
+{ 25 September 2026 \mbox{}}\\[1cm]
+\mbox{}\\[2cm]
+{\Large \textbf{\strut  Christopher Jefferson     \strut\mbox{}}}\\
+\hypersetup{pdfauthor={ Christopher Jefferson    }}
+\end{center}\vfill
+
+\mbox{}\\
+{\mbox{}\\
+\small \noindent \textbf{ Christopher Jefferson    }  Email: 
\href{mailto://[email protected]} 
{\texttt{caj21@st\texttt{\symbol{45}}andrews.ac.uk}}\\
+  Homepage: \href{https://heather.cafe/} {\texttt{https://heather.cafe/}}\\
+  Address: \begin{minipage}[t]{8cm}\noindent
+ St Andrews\\
+ Scotland\\
+ UK\\
+ \end{minipage}
+}\\
+\end{titlepage}
+
+\newpage\setcounter{page}{2}
+\newpage
+
+\def\contentsname{Contents\logpage{[ 0, 0, 1 ]}}
+
+\tableofcontents
+\newpage
+
+     
+\chapter{\textcolor{Chapter }{GAP\texttt{\symbol{45}}JSON 
mapping}}\label{Chapter_GAP-JSON_mapping}
+\logpage{[ 1, 0, 0 ]}
+\hyperdef{L}{X87B639367C9770AE}{}
+{
+  This package defines a mapping between the JSON markup language and GAP. The
+built\texttt{\symbol{45}}in datatypes of GAP provide an easy mapping to and
+from JSON. This package uses the following mapping between GAP and JSON. 
+
+ 
+\begin{itemize}
+\item  JSON lists are mapped to GAP lists 
+\item  JSON dictionaries are mapped to GAP records 
+\item  JSON strings are mapped to GAP strings 
+\item  Integers are mapped to GAP integers, non\texttt{\symbol{45}}integer 
numbers
+are mapped to Floats 
+\item  true, false and null are mapped to true, false and fail respectively 
+\end{itemize}
+ 
+
+ Note that this library is *NOT* intended to provide a general purpose library
+for transmitting any GAP object. If you wish to do this, look at the openmath
+package, or IO{\textunderscore}Pickle in the IO package. 
+\section{\textcolor{Chapter 
}{Methods}}\label{Chapter_GAP-JSON_mapping_Section_Methods}
+\logpage{[ 1, 1, 0 ]}
+\hyperdef{L}{X8606FDCE878850EF}{}
+{
+  
+
+\subsection{\textcolor{Chapter }{GapToJsonStream}}
+\logpage{[ 1, 1, 1 ]}\nobreak
+\hyperdef{L}{X83B041337FB4E4D1}{}
+{\noindent\textcolor{FuncColor}{$\triangleright$\enspace\texttt{GapToJsonStream({\mdseries\slshape
 stream, value})\index{GapToJsonStream@\texttt{GapToJsonStream}}
+\label{GapToJsonStream}
+}\hfill{\scriptsize (function)}}\\
+
+
+ Converts the \mbox{\texttt{\mdseries\slshape value}} to JSON, and outputs it 
to \mbox{\texttt{\mdseries\slshape stream}}. This function disables GAP's usual 
line splitting while JSON is being
+outputted. }
+
+ 
+
+\subsection{\textcolor{Chapter }{GapToJsonString}}
+\logpage{[ 1, 1, 2 ]}\nobreak
+\hyperdef{L}{X855531F88506F3D0}{}
+{\noindent\textcolor{FuncColor}{$\triangleright$\enspace\texttt{GapToJsonString({\mdseries\slshape
 value})\index{GapToJsonString@\texttt{GapToJsonString}}
+\label{GapToJsonString}
+}\hfill{\scriptsize (function)}}\\
+\textbf{\indent Returns:\ }
+string 
+
+
+
+ Converts a GAP \mbox{\texttt{\mdseries\slshape value}} to a JSON string. }
+
+ 
+
+\subsection{\textcolor{Chapter }{JsonStringToGap}}
+\logpage{[ 1, 1, 3 ]}\nobreak
+\hyperdef{L}{X823A762684DC39C1}{}
+{\noindent\textcolor{FuncColor}{$\triangleright$\enspace\texttt{JsonStringToGap({\mdseries\slshape
 string})\index{JsonStringToGap@\texttt{JsonStringToGap}}
+\label{JsonStringToGap}
+}\hfill{\scriptsize (function)}}\\
+\textbf{\indent Returns:\ }
+value 
+
+
+
+ Converts a JSON \mbox{\texttt{\mdseries\slshape string}} into a GAP value. }
+
+ 
+
+\subsection{\textcolor{Chapter }{JsonStreamToGap}}
+\logpage{[ 1, 1, 4 ]}\nobreak
+\hyperdef{L}{X823DB24A7D2F7D64}{}
+{\noindent\textcolor{FuncColor}{$\triangleright$\enspace\texttt{JsonStreamToGap({\mdseries\slshape
 stream})\index{JsonStreamToGap@\texttt{JsonStreamToGap}}
+\label{JsonStreamToGap}
+}\hfill{\scriptsize (function)}}\\
+\textbf{\indent Returns:\ }
+value 
+
+
+
+ Reads a single JSON object from a \mbox{\texttt{\mdseries\slshape stream}} 
and converts it to a GAP value. }
+
+ }
+
+ }
+
+ \def\indexname{Index\logpage{[ "Ind", 0, 0 ]}
+\hyperdef{L}{X83A0356F839C696F}{}
+}
+
+\cleardoublepage
+\phantomsection
+\addcontentsline{toc}{chapter}{Index}
+
+
+\printindex
+
+\immediate\write\pagenrlog{["Ind", 0, 0], \arabic{page},}
+\newpage
+\immediate\write\pagenrlog{["End"], \arabic{page}];}
+\immediate\closeout\pagenrlog
+\end{document}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/doc/_main.xml new/json-2.5.0/doc/_main.xml
--- old/json-2.4.0/doc/_main.xml        1970-01-01 01:00:00.000000000 +0100
+++ new/json-2.5.0/doc/_main.xml        2026-09-25 02:00:00.000000000 +0200
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- This is an automatically generated file. -->
+<!DOCTYPE Book SYSTEM "gapdoc.dtd"
+[
+    <#Include SYSTEM "_entities.xml">
+]
+>
+<Book Name="json">
+<#Include SYSTEM "title.xml">
+<TableOfContents/>
+<Body>
+<#Include SYSTEM "_AutoDocMainFile.xml">
+</Body>
+<TheIndex/>
+</Book>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/doc/chap0.html 
new/json-2.5.0/doc/chap0.html
--- old/json-2.4.0/doc/chap0.html       2026-05-08 02:00:00.000000000 +0200
+++ new/json-2.5.0/doc/chap0.html       2026-09-25 02:00:00.000000000 +0200
@@ -29,10 +29,10 @@
 <h2>Reading and Writing JSON</h2>
 
 <p>
-    2.4.0</p>
+    2.5.0</p>
 
 <p>
-    8 May 2026
+    25 September 2026
   </p>
 
 </div>
@@ -41,7 +41,6 @@
 
 
 
-
   </b>
 <br />Email: <span class="URL"><a 
href="mailto:[email protected]";>[email protected]</a></span>
 <br />Homepage: <span class="URL"><a 
href="https://heather.cafe/";>https://heather.cafe/</a></span>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/doc/chap0.txt new/json-2.5.0/doc/chap0.txt
--- old/json-2.4.0/doc/chap0.txt        2026-05-08 02:00:00.000000000 +0200
+++ new/json-2.5.0/doc/chap0.txt        2026-09-25 02:00:00.000000000 +0200
@@ -6,10 +6,10 @@
                             Reading and Writing JSON 
   
   
-                                     2.4.0
+                                     2.5.0
   
   
-                                   8 May 2026
+                               25 September 2026
   
   
                              Christopher Jefferson
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/doc/chap0_mj.html 
new/json-2.5.0/doc/chap0_mj.html
--- old/json-2.4.0/doc/chap0_mj.html    2026-05-08 02:00:00.000000000 +0200
+++ new/json-2.5.0/doc/chap0_mj.html    2026-09-25 02:00:00.000000000 +0200
@@ -32,10 +32,10 @@
 <h2>Reading and Writing JSON</h2>
 
 <p>
-    2.4.0</p>
+    2.5.0</p>
 
 <p>
-    8 May 2026
+    25 September 2026
   </p>
 
 </div>
@@ -44,7 +44,6 @@
 
 
 
-
   </b>
 <br />Email: <span class="URL"><a 
href="mailto:[email protected]";>[email protected]</a></span>
 <br />Homepage: <span class="URL"><a 
href="https://heather.cafe/";>https://heather.cafe/</a></span>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/doc/chooser.html 
new/json-2.5.0/doc/chooser.html
--- old/json-2.4.0/doc/chooser.html     2026-05-08 02:00:00.000000000 +0200
+++ new/json-2.5.0/doc/chooser.html     2026-09-25 02:00:00.000000000 +0200
@@ -23,9 +23,18 @@
   return "";
 }
 
+<!-- load dark appearance either explicitly or via the OS preference -->
+function writeAppearanceStyle(mode) {
+  if (mode == "dark") {
+    document.writeln('<link rel="stylesheet" type="text/css" href="dark.css" 
/>');
+  } else if (mode != "light") {
+    document.writeln('<style type="text/css">@import "dark.css" 
(prefers-color-scheme: dark);</style>');
+  }
+}
+
 <!-- find style from current values in form -->
 function getstyle() {
-  var choices = ["toggle","colorprompt","tocside","font","justify"]; 
+  var choices = 
["toggle","colorprompt","tocside","font","justify","appearance"];
   var style = "";
   for (var i=0; choices.length > i; i++) {
     var a = currval(choices[i]);
@@ -55,17 +64,23 @@
 <!-- initialize form from GAPDocStyle cookie -->
 function initform() {
   var style = valueString(document.cookie, "GAPDocStyle");
+  if (style == 0 || style == "default")
+    writeAppearanceStyle("");
   if (style != 0 && style.length > 0 && style.length != "default") {
     stlist = style.split(",");
     var chform = document.forms[0].elements;
+    var appearance = "";
     for (var i=0; chform.length > i; i++) {
       if (chform[i].type == "radio") {
         for (var j=0; stlist.length > j; j++) {
-          if (chform[i].value == stlist[j]) 
+          if (stlist[j] == "dark" || stlist[j] == "light")
+            appearance = stlist[j];
+          if (chform[i].value == stlist[j])
             chform[i].checked = true;
         }
       }
     }
+    writeAppearanceStyle(appearance);
   }
 }
 
@@ -119,6 +134,15 @@
 <input type="radio" name="justify" value="ragged"
 onclick="javascript:f()"/> ragged right 
 </p>
+<p>
+Appearance:
+<input type="radio" name="appearance" value=""
+onclick="javascript:f()"/> system (default) &nbsp;&nbsp;&nbsp;
+<input type="radio" name="appearance" value="light" checked="checked"
+onclick="javascript:f()"/> light
+<input type="radio" name="appearance" value="dark"
+onclick="javascript:f()"/> dark
+</p>
 </form>
 <p>
 <a name="backLINK" href=""><strong>Apply settings to last page.</strong></a>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/doc/dark.css new/json-2.5.0/doc/dark.css
--- old/json-2.4.0/doc/dark.css 1970-01-01 01:00:00.000000000 +0100
+++ new/json-2.5.0/doc/dark.css 2026-09-25 02:00:00.000000000 +0200
@@ -0,0 +1,178 @@
+/* dark.css                                             Frank Luebeck */
+/* Initial dark theme contributed by kiryph in issue #75.            */
+
+/* colors */
+body {
+  background: #121212;
+  color: #eee;
+}
+
+a:link {
+  color: #576cad;
+}
+
+a:visited {
+  color: #576cad;
+}
+
+a:active {
+  color: #eee;
+}
+
+a:hover {
+  background: #eee;
+}
+
+pre {
+  color: black;
+}
+
+tt, code {
+  color: #eee;
+}
+
+/* layout for the definitions of functions, variables, ... */
+div.func {
+  background: #909090;
+}
+
+/* Example elements (for old converted manuals, now in div+pre */
+table.example {
+  background: #efefef;
+}
+
+/* becomes ... */
+div.example {
+  background: #efefef;
+  color: black;
+}
+
+/* Links to chapters in all files at top and bottom. */
+div.chlinktop {
+  background: #22272e;
+  border-color: #3a414a;
+  color: #d7dde5;
+}
+
+div.chlinktop a:hover {
+  background: #2f3742;
+}
+
+div.chlinkbot {
+  background: #22272e;
+  border-color: #3a414a;
+  color: #d7dde5;
+}
+
+/* and this is for the "Top", "Prev", "Next" links */
+div.chlinkprevnexttop {
+  background: #22272e;
+  border-color: #3a414a;
+  color: #d7dde5;
+}
+
+div.chlinkprevnexttop a:hover {
+  background: #2f3742;
+}
+
+div.chlinkprevnextbot {
+  background: #22272e;
+  border-color: #3a414a;
+  color: #d7dde5;
+}
+
+div.chlinkprevnextbot a:hover {
+  background: #2f3742;
+}
+
+div.ContChap div.ContSect:hover div.ContSSBlock {
+  background: #eee;
+  border-color: #666;
+  color: #000;
+}
+
+div.ContSSBlock a:hover {
+  background: #fff;
+}
+
+/* and here for the side menu of contents in the chapter files */
+div.ChapSects a:hover {
+  background: #eee;
+  color: #000;
+}
+
+div.ChapSects div.ContSect:hover div.ContSSBlock {
+  background: #b5b5b5;
+  border-color: #666;
+  color: #000;
+}
+
+div.ChapSects div.ContSect:hover div.ContSSBlock a:hover {
+  background: #828282;
+}
+
+/* Table elements */
+table.GAPDocTable {
+  border-color: black;
+}
+
+table.GAPDocTable td, table.GAPDocTable th {
+  border-color: #555;
+}
+
+table.GAPDocTablenoborder td, table.GAPDocTable th {
+  border-color: #555;
+}
+
+/* Colors and fonts can be overwritten for some types of elements. */
+/* Verb elements */
+pre.normal {
+  color: #eee;
+}
+
+/* Func-like elements and Ref to Func-like */
+code.func {
+  color: #eee;
+}
+
+/* K elements */
+code.keyw {
+  color: #983d3d;
+}
+
+/* F elements */
+code.file {
+  color: #8e4510;
+}
+
+/* Arg elements */
+var.Arg {
+  color: #060;
+}
+
+/* colors for ColorPrompt like examples */
+span.GAPprompt {
+  color: #000097;
+}
+
+span.GAPbrkprompt {
+  color: #970000;
+}
+
+span.GAPinput {
+  color: #970000;
+}
+
+/* Bib entries */
+span.BibKey {
+  color: #052;
+}
+
+/* for light and dark mode pictures */
+.only-on-dark {
+  display: block;
+}
+
+.only-on-light {
+  display: none;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/doc/json.tex new/json-2.5.0/doc/json.tex
--- old/json-2.4.0/doc/json.tex 2026-05-08 02:00:00.000000000 +0200
+++ new/json-2.5.0/doc/json.tex 1970-01-01 01:00:00.000000000 +0100
@@ -1,235 +0,0 @@
-% generated by GAPDoc2LaTeX from XML source (Frank Luebeck)
-\documentclass[a4paper,11pt]{report}
-
-\usepackage[top=37mm,bottom=37mm,left=27mm,right=27mm]{geometry}
-\sloppy
-\pagestyle{myheadings}
-\usepackage{amssymb}
-\usepackage[utf8]{inputenc}
-\usepackage{makeidx}
-\makeindex
-\usepackage{color}
-\definecolor{FireBrick}{rgb}{0.5812,0.0074,0.0083}
-\definecolor{RoyalBlue}{rgb}{0.0236,0.0894,0.6179}
-\definecolor{RoyalGreen}{rgb}{0.0236,0.6179,0.0894}
-\definecolor{RoyalRed}{rgb}{0.6179,0.0236,0.0894}
-\definecolor{LightBlue}{rgb}{0.8544,0.9511,1.0000}
-\definecolor{Black}{rgb}{0.0,0.0,0.0}
-
-\definecolor{linkColor}{rgb}{0.0,0.0,0.554}
-\definecolor{citeColor}{rgb}{0.0,0.0,0.554}
-\definecolor{fileColor}{rgb}{0.0,0.0,0.554}
-\definecolor{urlColor}{rgb}{0.0,0.0,0.554}
-\definecolor{promptColor}{rgb}{0.0,0.0,0.589}
-\definecolor{brkpromptColor}{rgb}{0.589,0.0,0.0}
-\definecolor{gapinputColor}{rgb}{0.589,0.0,0.0}
-\definecolor{gapoutputColor}{rgb}{0.0,0.0,0.0}
-
-%%  for a long time these were red and blue by default,
-%%  now black, but keep variables to overwrite
-\definecolor{FuncColor}{rgb}{0.0,0.0,0.0}
-%% strange name because of pdflatex bug:
-\definecolor{Chapter }{rgb}{0.0,0.0,0.0}
-\definecolor{DarkOlive}{rgb}{0.1047,0.2412,0.0064}
-
-
-\usepackage{fancyvrb}
-
-\usepackage{mathptmx,helvet}
-\usepackage[T1]{fontenc}
-\usepackage{textcomp}
-
-
-\usepackage[
-            pdftex=true,
-            bookmarks=true,        
-            a4paper=true,
-            pdftitle={Written with GAPDoc},
-            pdfcreator={LaTeX with hyperref package / GAPDoc},
-            colorlinks=true,
-            backref=page,
-            breaklinks=true,
-            linkcolor=linkColor,
-            citecolor=citeColor,
-            filecolor=fileColor,
-            urlcolor=urlColor,
-            pdfpagemode={UseNone}, 
-           ]{hyperref}
-
-\newcommand{\maintitlesize}{\fontsize{50}{55}\selectfont}
-
-% write page numbers to a .pnr log file for online help
-\newwrite\pagenrlog
-\immediate\openout\pagenrlog =\jobname.pnr
-\immediate\write\pagenrlog{PAGENRS := [}
-\newcommand{\logpage}[1]{\protect\write\pagenrlog{#1, \thepage,}}
-%% were never documented, give conflicts with some additional packages
-
-\newcommand{\GAP}{\textsf{GAP}}
-
-%% nicer description environments, allows long labels
-\usepackage{enumitem}
-\setdescription{style=nextline}
-
-%% depth of toc
-\setcounter{tocdepth}{1}
-
-
-
-
-
-%% command for ColorPrompt style examples
-\newcommand{\gapprompt}[1]{\color{promptColor}{\bfseries #1}}
-\newcommand{\gapbrkprompt}[1]{\color{brkpromptColor}{\bfseries #1}}
-\newcommand{\gapinput}[1]{\color{gapinputColor}{#1}}
-
-
-\begin{document}
-
-\logpage{[ 0, 0, 0 ]}
-\begin{titlepage}
-\mbox{}\vfill
-
-\begin{center}{\maintitlesize \textbf{ json \mbox{}}}\\
-\vfill
-
-\hypersetup{pdftitle= json }
-\markright{\scriptsize \mbox{}\hfill  json  \hfill\mbox{}}
-{\Huge \textbf{ Reading and Writing JSON \mbox{}}}\\
-\vfill
-
-{\Huge  2.4.0 \mbox{}}\\[1cm]
-{ 8 May 2026 \mbox{}}\\[1cm]
-\mbox{}\\[2cm]
-{\Large \textbf{ Christopher Jefferson\\
-    \mbox{}}}\\
-\hypersetup{pdfauthor= Christopher Jefferson\\
-    }
-\end{center}\vfill
-
-\mbox{}\\
-{\mbox{}\\
-\small \noindent \textbf{ Christopher Jefferson\\
-    }  Email: \href{mailto://[email protected]} 
{\texttt{caj21@st\texttt{\symbol{45}}andrews.ac.uk}}\\
-  Homepage: \href{https://heather.cafe/} {\texttt{https://heather.cafe/}}\\
-  Address: \begin{minipage}[t]{8cm}\noindent
- St Andrews\\
- Scotland\\
- UK\\
- \end{minipage}
-}\\
-\end{titlepage}
-
-\newpage\setcounter{page}{2}
-\newpage
-
-\def\contentsname{Contents\logpage{[ 0, 0, 1 ]}}
-
-\tableofcontents
-\newpage
-
-     
-\chapter{\textcolor{Chapter }{GAP\texttt{\symbol{45}}JSON 
mapping}}\label{Chapter_GAP-JSON_mapping}
-\logpage{[ 1, 0, 0 ]}
-\hyperdef{L}{X87B639367C9770AE}{}
-{
-  This package defines a mapping between the JSON markup language and GAP. The
-built\texttt{\symbol{45}}in datatypes of GAP provide an easy mapping to and
-from JSON. This package uses the following mapping between GAP and JSON. 
-
- 
-\begin{itemize}
-\item  JSON lists are mapped to GAP lists 
-\item  JSON dictionaries are mapped to GAP records 
-\item  JSON strings are mapped to GAP strings 
-\item  Integers are mapped to GAP integers, non\texttt{\symbol{45}}integer 
numbers
-are mapped to Floats 
-\item  true, false and null are mapped to true, false and fail respectively 
-\end{itemize}
- 
-
- Note that this library is *NOT* intended to provide a general purpose library
-for transmitting any GAP object. If you wish to do this, look at the openmath
-package, or IO{\textunderscore}Pickle in the IO package. 
-\section{\textcolor{Chapter 
}{Methods}}\label{Chapter_GAP-JSON_mapping_Section_Methods}
-\logpage{[ 1, 1, 0 ]}
-\hyperdef{L}{X8606FDCE878850EF}{}
-{
-  
-
-\subsection{\textcolor{Chapter }{GapToJsonStream}}
-\logpage{[ 1, 1, 1 ]}\nobreak
-\hyperdef{L}{X83B041337FB4E4D1}{}
-{\noindent\textcolor{FuncColor}{$\triangleright$\enspace\texttt{GapToJsonStream({\mdseries\slshape
 stream, value})\index{GapToJsonStream@\texttt{GapToJsonStream}}
-\label{GapToJsonStream}
-}\hfill{\scriptsize (function)}}\\
-
-
- Converts the \mbox{\texttt{\mdseries\slshape value}} to JSON, and outputs it 
to \mbox{\texttt{\mdseries\slshape stream}}. This function disables GAP's usual 
line splitting while JSON is being
-outputted. }
-
- 
-
-\subsection{\textcolor{Chapter }{GapToJsonString}}
-\logpage{[ 1, 1, 2 ]}\nobreak
-\hyperdef{L}{X855531F88506F3D0}{}
-{\noindent\textcolor{FuncColor}{$\triangleright$\enspace\texttt{GapToJsonString({\mdseries\slshape
 value})\index{GapToJsonString@\texttt{GapToJsonString}}
-\label{GapToJsonString}
-}\hfill{\scriptsize (function)}}\\
-\textbf{\indent Returns:\ }
-string 
-
-
-
- Converts a GAP \mbox{\texttt{\mdseries\slshape value}} to a JSON string. }
-
- 
-
-\subsection{\textcolor{Chapter }{JsonStringToGap}}
-\logpage{[ 1, 1, 3 ]}\nobreak
-\hyperdef{L}{X823A762684DC39C1}{}
-{\noindent\textcolor{FuncColor}{$\triangleright$\enspace\texttt{JsonStringToGap({\mdseries\slshape
 string})\index{JsonStringToGap@\texttt{JsonStringToGap}}
-\label{JsonStringToGap}
-}\hfill{\scriptsize (function)}}\\
-\textbf{\indent Returns:\ }
-value 
-
-
-
- Converts a JSON \mbox{\texttt{\mdseries\slshape string}} into a GAP value. }
-
- 
-
-\subsection{\textcolor{Chapter }{JsonStreamToGap}}
-\logpage{[ 1, 1, 4 ]}\nobreak
-\hyperdef{L}{X823DB24A7D2F7D64}{}
-{\noindent\textcolor{FuncColor}{$\triangleright$\enspace\texttt{JsonStreamToGap({\mdseries\slshape
 stream})\index{JsonStreamToGap@\texttt{JsonStreamToGap}}
-\label{JsonStreamToGap}
-}\hfill{\scriptsize (function)}}\\
-\textbf{\indent Returns:\ }
-value 
-
-
-
- Reads a single JSON object from a \mbox{\texttt{\mdseries\slshape stream}} 
and converts it to a GAP value. }
-
- }
-
- }
-
- \def\indexname{Index\logpage{[ "Ind", 0, 0 ]}
-\hyperdef{L}{X83A0356F839C696F}{}
-}
-
-\cleardoublepage
-\phantomsection
-\addcontentsline{toc}{chapter}{Index}
-
-
-\printindex
-
-\immediate\write\pagenrlog{["Ind", 0, 0], \arabic{page},}
-\newpage
-\immediate\write\pagenrlog{["End"], \arabic{page}];}
-\immediate\closeout\pagenrlog
-\end{document}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/doc/json.xml new/json-2.5.0/doc/json.xml
--- old/json-2.4.0/doc/json.xml 2026-05-08 02:00:00.000000000 +0200
+++ new/json-2.5.0/doc/json.xml 1970-01-01 01:00:00.000000000 +0100
@@ -1,16 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<!-- This is an automatically generated file. -->
-<!DOCTYPE Book SYSTEM "gapdoc.dtd"
-[
-    [<#Include SYSTEM "_entities.xml">
-]
->
-<Book Name="json">
-<#Include SYSTEM "title.xml">
-<TableOfContents/>
-<Body>
-<#Include SYSTEM "_AutoDocMainFile.xml">
-</Body>
-<TheIndex/>
-</Book>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/doc/manual.css 
new/json-2.5.0/doc/manual.css
--- old/json-2.4.0/doc/manual.css       2026-05-08 02:00:00.000000000 +0200
+++ new/json-2.5.0/doc/manual.css       2026-09-25 02:00:00.000000000 +0200
@@ -477,6 +477,10 @@
 span.BibHowpublished {
 }
 
+/* for light and dark mode pictures */
+.only-on-dark {
+  display: none;
+}
 
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/doc/manual.js new/json-2.5.0/doc/manual.js
--- old/json-2.4.0/doc/manual.js        2026-05-08 02:00:00.000000000 +0200
+++ new/json-2.5.0/doc/manual.js        2026-09-25 02:00:00.000000000 +0200
@@ -47,6 +47,20 @@
   return 0;
 }
 
+/* load dark appearance either explicitly or via the OS preference */
+function writeAppearanceStyle(mode) {
+  if (mode == "dark") {
+    document.writeln(
+      '<link rel="stylesheet" type="text/css" href="dark.css" />'
+    );
+  } else if (mode != "light") {
+    document.writeln(
+      '<style type="text/css">@import "dark.css" ' +
+      '(prefers-color-scheme: dark);</style>'
+    );
+  }
+}
+
 /* when a non-default style is chosen via URL or a cookie, then
    the cookie is reset and the styles .js and .css files are read  */
 function overwriteStyle() {
@@ -55,6 +69,8 @@
   /* otherwise check cookie */
   if (style == 0)
     style = valueString(document.cookie, "GAPDocStyle");
+  if (style == 0 || style == "default")
+    writeAppearanceStyle("");
   if (style == 0)
     return;
   if (style == "default")
@@ -70,13 +86,19 @@
     document.cookie = "GAPDocStyle="+style+";Path="+path;
     /* split into names of style files */
     var stlist = style.split(",");
+    var appearance = "";
     /* read style's css and js files */
     for (var i=0; i < stlist.length; i++) {
-      document.writeln('<link rel="stylesheet" type="text/css" href="'+
-                                                         stlist[i]+'.css" />');
-      document.writeln('<script src="'+stlist[i]+
-                                      '.js" type="text/javascript"></script>');
+      if (stlist[i] == "dark" || stlist[i] == "light") {
+        appearance = stlist[i];
+      } else {
+        document.writeln('<link rel="stylesheet" type="text/css" href="'+
+                                                           stlist[i]+'.css" 
/>');
+        document.writeln('<script src="'+stlist[i]+
+                                        '.js" 
type="text/javascript"></script>');
+      }
     }
+    writeAppearanceStyle(appearance);
   }
 }
 
@@ -110,4 +132,3 @@
   for (var i=0; i < jscontentfuncs.length; i++)
     jscontentfuncs[i]();
 }
-
Binary files old/json-2.4.0/doc/manual.pdf and new/json-2.5.0/doc/manual.pdf 
differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/doc/title.xml new/json-2.5.0/doc/title.xml
--- old/json-2.4.0/doc/title.xml        2026-05-08 02:00:00.000000000 +0200
+++ new/json-2.5.0/doc/title.xml        2026-09-25 02:00:00.000000000 +0200
@@ -9,10 +9,10 @@
     Reading and Writing JSON
   </Subtitle>
   <Version>
-    2.4.0
+    2.5.0
   </Version>
   <Author>
-    Christopher Jefferson<Alt Only="LaTeX"><Br/></Alt>
+    Christopher Jefferson
 <Address>
 St Andrews<Br/>
 Scotland<Br/>
@@ -20,9 +20,8 @@
 </Address>
 <Email>[email protected]</Email>
 <Homepage>https://heather.cafe/</Homepage>
-
   </Author>
   <Date>
-    8 May 2026
+    25 September 2026
   </Date>
   </TitlePage>
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/gap/json.gi new/json-2.5.0/gap/json.gi
--- old/json-2.4.0/gap/json.gi  2026-05-08 02:00:00.000000000 +0200
+++ new/json-2.5.0/gap/json.gi  2026-09-25 02:00:00.000000000 +0200
@@ -25,6 +25,18 @@
   _JSON_Globals := [];
 end;
 
+# Called from C by GAP_OBJ_TO_JSON_STRING to serialise objects it does not
+# handle directly (floats, large integers, component objects, and any types
+# with a user-installed _GapToJsonStreamInternal method).
+_JSON_ObjToString := function(obj)
+  local str, s;
+  str := "";
+  s := OutputTextString(str, true);
+  SetPrintFormattingStatus(s, false);
+  _GapToJsonStreamInternal(s, obj);
+  return str;
+end;
+
 InstallMethod(_GapToJsonStreamInternal, [IsOutputStream, IsInt],
 function(o, d)
   WriteAll(o, STRING_INT(d));
@@ -114,12 +126,7 @@
 
 InstallGlobalFunction(GapToJsonString,
 function(obj)
-  local str, s;
-  str := "";
-  s := OutputTextString(str, true);
-  SetPrintFormattingStatus(s, false);
-  GapToJsonStream(s, obj);
-  return str;
+  return GAP_OBJ_TO_JSON_STRING(obj);
 end );
 
 InstallGlobalFunction(JsonStringToGap,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/src/json.cc new/json-2.5.0/src/json.cc
--- old/json-2.4.0/src/json.cc  2026-05-08 02:00:00.000000000 +0200
+++ new/json-2.5.0/src/json.cc  2026-09-25 02:00:00.000000000 +0200
@@ -9,7 +9,11 @@
 #include "picojson/picojson.h"
 #include "picojson/gap-traits.h"
 
+#include <cstdlib>
+#include <cstring>
+
 static Obj _GapToJsonStreamInternal;
+static Obj _JSON_ObjToString;
 
 typedef picojson::value_t<gap_type_traits> gmp_value;
 
@@ -69,8 +73,13 @@
     return 4;
 }
 
-static Int getChar(Obj list, Int pos)
+// Reading past the end of <list> yields 0, which is not a continuation byte,
+// so a truncated multi-byte sequence at the end of the string falls back to
+// Latin-1 like any other malformed one.
+static Int getChar(Obj list, Int len, Int pos)
 {
+    if (pos > len)
+        return 0;
     Obj c = ELM_LIST(list, pos);
     if (c == NULL)
         return 0;
@@ -78,20 +87,20 @@
         return *((UChar *)ADDR_OBJ(c));
 }
 
-static Int getUTF8Char(Obj list, Int * basepos)
+static Int getUTF8Char(Obj list, Int len, Int * basepos)
 {
     Int  pos = *basepos;
-    UInt val = getChar(list, pos);
+    UInt val = getChar(list, len, pos);
     UInt singlebyte_val = val;
-    Int  len = numberOfBytes(val);
+    Int  nbytes = numberOfBytes(val);
     pos++;
 
-    if (len == 1) {
+    if (nbytes == 1) {
         *basepos = pos;
         return val;
     }
 
-    switch (len) {
+    switch (nbytes) {
     case 2:
         val = val & 0x3F;
         if (val & 0x20)
@@ -112,8 +121,8 @@
     }
     val = val & 0x3F;
 
-    for (Int i = 1; i < len; ++i) {
-        UInt c = getChar(list, pos);
+    for (Int i = 1; i < nbytes; ++i) {
+        UInt c = getChar(list, len, pos);
         if ((c & 0xC0) != 0x80)
             goto invalid;
         val = (val << 6) | (c & 0x3F);
@@ -160,31 +169,36 @@
     return s;
 }
 
-static Obj FuncJSON_ESCAPE_STRING(Obj self, Obj param)
+// Does <param> (a string, possibly not in string rep) contain any character
+// that JSON string output must escape or re-encode?
+static Int jsonNeedsEscaping(Obj param, Int lenString)
 {
-    if(!IS_STRING(param))
-    {
-        ErrorQuit("Input to JsonEscapeString must be a string", 0, 0);
-    }
-
-    Int needEscaping = 0;
-    Int lenString = LEN_LIST(param);
-    for (Int i = 1; i <= lenString && needEscaping == 0; ++i) {
+    for (Int i = 1; i <= lenString; ++i) {
         Obj gapchar = ELMW_LIST(param, i);
         UChar u = *((UChar*)ADDR_OBJ(gapchar));
         switch(u)
         {
             case '\\': case '"': case '/': case '\b':
             case '\t': case '\n': case '\f': case '\r':
-                needEscaping = 1;
-                break;
+                return 1;
             default:
                 if (u < ' ' || u >= 128)
-                    needEscaping = 1;
+                    return 1;
         }
     }
+    return 0;
+}
 
-    if (needEscaping == 0)
+static Obj FuncJSON_ESCAPE_STRING(Obj self, Obj param)
+{
+    if(!IS_STRING(param))
+    {
+        ErrorQuit("Input to JsonEscapeString must be a string", 0, 0);
+    }
+
+    Int lenString = LEN_LIST(param);
+
+    if (jsonNeedsEscaping(param, lenString) == 0)
         return param;
 
     // Massively over-long string
@@ -193,7 +207,7 @@
     UChar * out = base;
     Int     i = 1;
     while (i <= lenString) {
-        Int u = getUTF8Char(param, &i);
+        Int u = getUTF8Char(param, lenString, &i);
         switch(u)
         {
             case '\\': case '"': case '/':
@@ -255,6 +269,240 @@
     return 0;
 }
 
+/******************************************************************************
+** Direct-to-string JSON serialiser used by GapToJsonString.
+**
+** GapToJsonString owns its output string, so instead of writing through an
+** OutputTextString + AppendCStr we manage the string as a raw buffer: we
+** over-allocate, write directly, and shrink to the exact length at the end.
+**
+** GC discipline: a raw pointer into a GAP string bag is invalidated by any
+** allocation, so we never *store* one. The buffer state is the string bag
+** handle (stable across GC) plus the length and capacity as plain integers
+** (GC-immune). Every write primitive calls jbuf_ensure first, then fetches
+** CHARS_STRING(str) freshly and writes with no allocation in between. This
+** means callers may allocate/GC freely between primitives (e.g. the fallback
+** below) with nothing to re-derive.
+******************************************************************************/
+
+struct JBuf {
+    Obj    str;    // the growing GAP string bag (handle stable across GC)
+    size_t len;    // bytes written so far
+    size_t cap;    // capacity in bytes (== the length passed to GROW_STRING)
+};
+
+// Ensure room for `need` more bytes. May allocate (and GC); callers must hold
+// no raw string pointer across this call, only the JBuf.
+static inline void jbuf_ensure(JBuf * b, size_t need)
+{
+    if (b->cap - b->len < need) {
+        size_t newcap = b->cap ? b->cap : 256;
+        while (newcap - b->len < need)
+            newcap *= 2;
+        GROW_STRING(b->str, newcap);
+        b->cap = newcap;
+    }
+}
+
+static inline void jbuf_putc(JBuf * b, char c)
+{
+    jbuf_ensure(b, 1);
+    CHARS_STRING(b->str)[b->len++] = (UChar)c;
+}
+
+static inline void jbuf_putlit(JBuf * b, const char * s, size_t n)
+{
+    jbuf_ensure(b, n);
+    memcpy(CHARS_STRING(b->str) + b->len, s, n);
+    b->len += n;
+}
+
+// Append <str> (must be in string rep) as a quoted, escaped JSON string.
+static void jbuf_json_string(JBuf * b, Obj str)
+{
+    Int len = LEN_LIST(str);
+
+    if (jsonNeedsEscaping(str, len) == 0) {
+        // No escaping needed: quote + raw bytes. Fetch both string pointers
+        // *after* jbuf_ensure, in the same no-allocation region.
+        jbuf_ensure(b, (size_t)len + 2);
+        UChar * out = CHARS_STRING(b->str) + b->len;
+        *out++ = '"';
+        memcpy(out, CONST_CSTR_STRING(str), len);
+        out += len;
+        *out++ = '"';
+        b->len += (size_t)len + 2;
+        return;
+    }
+
+    // Worst case is 6 bytes per input byte (\uXXXX). Reserve once so the walk
+    // below allocates nothing; base/out stay valid throughout.
+    jbuf_ensure(b, (size_t)len * 6 + 2);
+    UChar * base = CHARS_STRING(b->str);
+    UChar * out = base + b->len;
+    *out++ = '"';
+    Int i = 1;
+    while (i <= len) {
+        Int u = getUTF8Char(str, len, &i);
+        switch(u)
+        {
+            case '\\': case '"': case '/':
+                out[0] = '\\';
+                out[1] = u;
+                out += 2;
+                break;
+#define ESCAPE_CASE(x,y) case x: out[0] = '\\'; out[1] = y; out += 2; break;
+            ESCAPE_CASE('\b', 'b');
+            ESCAPE_CASE('\t', 't');
+            ESCAPE_CASE('\n', 'n');
+            ESCAPE_CASE('\f', 'f');
+            ESCAPE_CASE('\r', 'r');
+#undef ESCAPE_CASE
+            default:
+                if(u < ' ')
+                {
+                    snprintf((char*)out, 7, "\\u%04X", (unsigned)u);
+                    out += 6;
+                }
+                else
+                {
+                    out = outputUnicodeChar(out, u);
+                }
+        }
+    }
+    *out++ = '"';
+    b->len = out - base;
+}
+
+// Record fields sorted by name, for byte-stable output (matches 
Set(RecNames)).
+struct JsonFieldRef {
+    const UChar * name;
+    UInt          len;
+    UInt          pos;
+};
+
+static int jsonFieldRefCmp(const void * a, const void * b)
+{
+    const JsonFieldRef * fa = (const JsonFieldRef *)a;
+    const JsonFieldRef * fb = (const JsonFieldRef *)b;
+    UInt m = fa->len < fb->len ? fa->len : fb->len;
+    int  c = memcmp(fa->name, fb->name, m);
+    if (c != 0)
+        return c;
+    if (fa->len != fb->len)
+        return fa->len < fb->len ? -1 : 1;
+    return 0;
+}
+
+static void serialiseJson(JBuf * b, Obj obj);
+
+static void serialiseJsonRecord(JBuf * b, Obj rec)
+{
+    // Sort (and de-duplicate) the record's components, exactly as RecNames
+    // does. Afterwards all stored rnams are negated (the "sorted" marker), so
+    // the true rnam is -GET_RNAM_PREC(...). Without this, freshly built 
records
+    // can hold positive/unsorted rnams and NAME_RNAM indexes out of range.
+    SortPRecRNam(rec);
+    UInt n = LEN_PREC(rec);
+    jbuf_putc(b, '{');
+    if (n > 0) {
+        JsonFieldRef * fields = (JsonFieldRef *)malloc(n * 
sizeof(JsonFieldRef));
+        for (UInt i = 1; i <= n; ++i) {
+            Obj nm = NAME_RNAM(-GET_RNAM_PREC(rec, i));
+            fields[i - 1].name = CONST_CHARS_STRING(nm);
+            fields[i - 1].len = GET_LEN_STRING(nm);
+            fields[i - 1].pos = i;
+        }
+        // No GAP allocation happens during the sort, so the cached name
+        // pointers stay valid here.
+        qsort(fields, n, sizeof(JsonFieldRef), jsonFieldRefCmp);
+        for (UInt k = 0; k < n; ++k) {
+            if (k != 0)
+                jbuf_putc(b, ',');
+            // Re-fetch the name Obj fresh: emitting the previous field may 
have
+            // grown the buffer (GC), invalidating cached name pointers.
+            jbuf_json_string(b, NAME_RNAM(-GET_RNAM_PREC(rec, fields[k].pos)));
+            jbuf_putlit(b, " : ", 3);
+            serialiseJson(b, GET_ELM_PREC(rec, fields[k].pos));
+        }
+        free(fields);
+    }
+    jbuf_putc(b, '}');
+}
+
+static void serialiseJson(JBuf * b, Obj obj)
+{
+    if (IS_INTOBJ(obj)) {
+        char tmp[32];
+        int  m = snprintf(tmp, sizeof(tmp), "%ld", (long)INT_INTOBJ(obj));
+        jbuf_putlit(b, tmp, m);
+        return;
+    }
+    if (obj == True)  { jbuf_putlit(b, "true", 4);  return; }
+    if (obj == False) { jbuf_putlit(b, "false", 5); return; }
+    if (obj == Fail)  { jbuf_putlit(b, "null", 4);  return; }
+
+    if (IS_STRING(obj)) {
+        Int len = LEN_LIST(obj);
+        if (len == 0) {
+            // Matches the GAP IsString method: "" for string rep, [] 
otherwise.
+            if (IS_STRING_REP(obj))
+                jbuf_putlit(b, "\"\"", 2);
+            else
+                jbuf_putlit(b, "[]", 2);
+            return;
+        }
+        if (!IS_STRING_REP(obj))
+            obj = CopyToStringRep(obj);   // allocates; obj is local, safe 
after
+        jbuf_json_string(b, obj);
+        return;
+    }
+
+    if (IS_LIST(obj)) {
+        RequireDenseList("GapToJsonString", obj);
+        Int len = LEN_LIST(obj);
+        jbuf_putc(b, '[');
+        for (Int i = 1; i <= len; ++i) {
+            if (i != 1)
+                jbuf_putc(b, ',');
+            serialiseJson(b, ELMW_LIST(obj, i));
+        }
+        jbuf_putc(b, ']');
+        return;
+    }
+
+    if (IS_PREC(obj)) {
+        serialiseJsonRecord(b, obj);
+        return;
+    }
+
+    // Fallback for everything else (floats, large integers, component objects,
+    // user-installed methods): let the GAP operation produce the JSON string,
+    // then copy it in. The call may GC, but we hold only the JBuf (bag handle
+    // + integer offsets), so there is nothing to re-derive.
+    {
+        Obj s = CALL_1ARGS(_JSON_ObjToString, obj);
+        Int slen = GET_LEN_STRING(s);
+        jbuf_ensure(b, slen);   // may move the bags; fetch both pointers after
+        memcpy(CHARS_STRING(b->str) + b->len, CONST_CSTR_STRING(s), slen);
+        b->len += slen;
+    }
+}
+
+static Obj FuncGAP_OBJ_TO_JSON_STRING(Obj self, Obj obj)
+{
+    JBuf b;
+    b.str = NEW_STRING(256);
+    b.len = 0;
+    b.cap = 256;
+
+    serialiseJson(&b, obj);
+
+    SET_LEN_STRING(b.str, b.len);
+    SHRINK_STRING(b.str);
+    return b.str;
+}
+
 // WARNING: This class is only complete enough to work with
 // picojson's iterator support.
 struct GapStreamToInputIterator
@@ -348,7 +596,7 @@
   bool ungotc_check = false;
   picojson::parse(v, GapStreamToInputIterator(stream), endGapStreamIterator(), 
&err, &ungotc_check);
   if (! err.empty()) {
-    ErrorQuit(err.c_str(), 0, 0);
+    ErrorQuit("%s", (Int)err.c_str(), 0);
     return Fail;
   }
 
@@ -429,7 +677,7 @@
 
     //    char* res = picojson::parse(v, ptr, ptrend, &err, &ungotc_check);
     if (! err.empty()) {
-      ErrorQuit(err.c_str(), 0, 0);
+      ErrorQuit("%s", (Int)err.c_str(), 0);
       return Fail;
     }
 
@@ -464,7 +712,8 @@
     GVAR_FUNC_1ARGS(JSON_ESCAPE_STRING, string),
     GVAR_FUNC_1ARGS(JSON_STREAM_TO_GAP, string),
     GVAR_FUNC_3ARGS(GAP_LIST_TO_JSON_STRING, string, stream, list),
-    
+    GVAR_FUNC_1ARGS(GAP_OBJ_TO_JSON_STRING, obj),
+
        { 0 } /* Finish with an empty entry */
 
 };
@@ -478,6 +727,7 @@
     InitHdlrFuncsFromTable( GVarFuncs );
 
     ImportGVarFromLibrary("_GapToJsonStreamInternal", 
&_GapToJsonStreamInternal);
+    ImportGVarFromLibrary("_JSON_ObjToString", &_JSON_ObjToString);
 
     /* return success                                                      */
     return 0;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/json-2.4.0/tst/test_hard.tst 
new/json-2.5.0/tst/test_hard.tst
--- old/json-2.4.0/tst/test_hard.tst    2026-05-08 02:00:00.000000000 +0200
+++ new/json-2.5.0/tst/test_hard.tst    2026-09-25 02:00:00.000000000 +0200
@@ -7,3 +7,13 @@
 gap> sout := StringFile(fout);;
 gap> json = sout;
 true
+
+# A truncated multi-byte sequence at the very end of a string used to make
+# getUTF8Char read past the end of the buffer. Like any other malformed
+# sequence, its bytes are now treated as Latin-1 and re-encoded.
+gap> List(GapToJsonString([ CHAR_INT(200) ]), IntChar);
+[ 34, 195, 136, 34 ]
+gap> List(GapToJsonString([ CHAR_INT(226), CHAR_INT(130) ]), IntChar);
+[ 34, 195, 162, 194, 130, 34 ]
+gap> ForAll([0..255], b -> IsString(GapToJsonString([ CHAR_INT(b) ])));
+true

Reply via email to