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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
15 changes: 15 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,18 @@ jobs:
- uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}

# Exercise the pure GAP fallback on its own, by not building the kernel
# extension at all.
test-without-kernel-extension:
name: no kernel extension
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6
- uses: gap-actions/setup-gap@v3
- uses: gap-actions/run-pkg-tests@v4
- uses: gap-actions/process-coverage@v3
- uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
10 changes: 0 additions & 10 deletions PackageInfo.g
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,6 @@ Dependencies := rec(
ExternalConditions := [ ],
),

AvailabilityTest := function()
if IsKernelExtensionAvailable("json") = false then
LogPackageLoadingMessage( PACKAGE_WARNING,
[ "kernel functions for json are not available." ] );
return false;
else
return true;
fi;
end,

TestFile := "tst/testall.g",

#Keywords := [ "TODO" ],
Expand Down
24 changes: 19 additions & 5 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,32 @@ Note that this library will not map any other GAP types, such as groups,
permutations, to or from JSON. If you wish to map between more complex
types, look at the openmath package, or IO_Pickle in the IO package.

The package ships a JSON parser written in C and an equivalent one written
in GAP. The C one needs the package's kernel extension to have been compiled
and is used automatically whenever it is present; otherwise the GAP one takes
over, so the package works either way. See JsonAvailableImplementations,
JsonImplementation and SetJsonImplementation in the manual.


Updates
=======

unreleased
======

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.
- 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.

- The kernel extension is now optional: without it the package falls back to a
JSON parser written in GAP, which accepts and rejects the same texts, produces
the same values and reports the same errors. SetJsonImplementation switches
between the two at runtime.

- The test suite now includes JSONTestSuite (https://github.com/nst/JSONTestSuite)
and runs everything through both implementations.

v2.3.0
======
Expand Down
62 changes: 62 additions & 0 deletions gap/impl.gi
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#############################################################################
##
#W impl.gi json Package
##
## The registry of JSON implementations, and the selection of the one to use.
## This file must be read last: it refers to functions installed by all the
## other files.
##

# The kernel functions are looked up by name rather than referenced directly,
# so that reading this file produces no warnings when the kernel extension is
# absent and the globals are therefore unbound.
BindGlobal( "_JSON_IMPLEMENTATIONS", rec(
gap := rec(
name := "gap",
StringToGap := _JSON_PureStringToGap,
StreamToGap := _JSON_PureStreamToGap,
EscapeString := _JSON_PureEscapeString,
ListToString := fail, # kernel-only optimisation, see gap/json.gi
) ) );

if JSON_KERNEL_AVAILABLE then
_JSON_IMPLEMENTATIONS.kernel := rec(
name := "kernel",
StringToGap := ValueGlobal("JSON_STRING_TO_GAP"),
StreamToGap := ValueGlobal("JSON_STREAM_TO_GAP"),
EscapeString := ValueGlobal("JSON_ESCAPE_STRING"),
ListToString := ValueGlobal("GAP_LIST_TO_JSON_STRING"),
);
fi;

InstallGlobalFunction(JsonAvailableImplementations,
function()
if JSON_KERNEL_AVAILABLE then
return [ "kernel", "gap" ];
fi;
return [ "gap" ];
end );

InstallGlobalFunction(JsonImplementation,
function()
return _JSON_ACTIVE.name;
end );

InstallGlobalFunction(SetJsonImplementation,
function(name)
local old, impl, comp;
if not name in JsonAvailableImplementations() then
# the text deliberately does not list the implementations, which depend
# on whether the kernel extension was built
ErrorNoReturn("<name> must be one of JsonAvailableImplementations()");
fi;
old := _JSON_ACTIVE.name;
impl := _JSON_IMPLEMENTATIONS.(name);
# mutate in place; other code holds on to _JSON_ACTIVE itself
for comp in RecNames(impl) do
_JSON_ACTIVE.(comp) := impl.(comp);
od;
return old;
end );

SetJsonImplementation( JsonAvailableImplementations()[1] );
35 changes: 34 additions & 1 deletion gap/json.gd
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,37 @@ DeclareGlobalFunction("JsonStringToGap");
#! @Returns value
#! @Description
#! Reads a single JSON object from a <A>stream</A> and converts it to a GAP value.
DeclareGlobalFunction("JsonStreamToGap");
DeclareGlobalFunction("JsonStreamToGap");

#! @Section Implementations
#! The json package parses JSON and escapes strings either with a fast
#! implementation written in C, which is only available if the package's
#! kernel extension was compiled, or with a pure &GAP; one, which is always
#! available. Both produce the same results; the kernel implementation is used
#! whenever it is present, so switching is only of interest for testing.

#! @Arguments
#! @Returns list of strings
#! @Description
#! The names of the available implementations, most preferred first: either
#! <C>[ "kernel", "gap" ]</C> or <C>[ "gap" ]</C>.
DeclareGlobalFunction("JsonAvailableImplementations");

#! @Arguments
#! @Returns string
#! @Description
#! The name of the implementation currently in use.
DeclareGlobalFunction("JsonImplementation");

#! @Arguments name
#! @Returns string
#! @Description
#! Selects the implementation <A>name</A>, which must be one of the strings
#! returned by <Ref Func="JsonAvailableImplementations"/>, and returns the name
#! of the previously selected one.
DeclareGlobalFunction("SetJsonImplementation");

# The active implementation, as a record with components StringToGap,
# StreamToGap, EscapeString and ListToString. Bound here, in the declaration
# part, so that gap/json.gi can refer to it before gap/impl.gi fills it in.
_JSON_ACTIVE := rec( name := fail );
13 changes: 8 additions & 5 deletions gap/json.gi
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,19 @@ function(o, s)
fi;
else
WriteAll(o, "\"");
WriteAll(o, JSON_ESCAPE_STRING(s));
WriteAll(o, _JSON_ACTIVE.EscapeString(s));
WriteAll(o, "\"");
fi;
end );

InstallMethod(_GapToJsonStreamInternal, [IsOutputStream, IsList],
function(o, l)
local i, first;
if IsOutputTextStringRep(o) and IsStringRep(o![1]) then
GAP_LIST_TO_JSON_STRING(o![1], o, l);
# the kernel fast path is a pure optimisation producing byte-identical
# output, so the pure GAP implementation just falls through to the loop
if _JSON_ACTIVE.ListToString <> fail
and IsOutputTextStringRep(o) and IsStringRep(o![1]) then
_JSON_ACTIVE.ListToString(o![1], o, l);
else
first := true;
WriteAll(o, "[");
Expand Down Expand Up @@ -124,10 +127,10 @@ end );

InstallGlobalFunction(JsonStringToGap,
function(str)
return JSON_STRING_TO_GAP(str);
return _JSON_ACTIVE.StringToGap(str);
end );

InstallGlobalFunction(JsonStreamToGap,
function(str)
return JSON_STREAM_TO_GAP(str);
return _JSON_ACTIVE.StreamToGap(str);
end );
Loading
Loading