|
|
package functions |
|
|
|
|
|
import ( |
|
|
"encoding/json" |
|
|
"errors" |
|
|
"regexp" |
|
|
"strings" |
|
|
"unicode" |
|
|
) |
|
|
|
|
|
|
|
|
type JSONStackElementType int |
|
|
|
|
|
const ( |
|
|
JSONStackElementObject JSONStackElementType = iota |
|
|
JSONStackElementKey |
|
|
JSONStackElementArray |
|
|
) |
|
|
|
|
|
|
|
|
type JSONStackElement struct { |
|
|
Type JSONStackElementType |
|
|
Key string |
|
|
} |
|
|
|
|
|
|
|
|
type JSONErrorLocator struct { |
|
|
position int |
|
|
foundError bool |
|
|
lastToken string |
|
|
exceptionMessage string |
|
|
stack []JSONStackElement |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func parseJSONWithStack(input string, healingMarker string) (any, bool, string, error) { |
|
|
if healingMarker == "" { |
|
|
|
|
|
var result any |
|
|
if err := json.Unmarshal([]byte(input), &result); err != nil { |
|
|
return nil, false, "", err |
|
|
} |
|
|
return result, false, "", nil |
|
|
} |
|
|
|
|
|
|
|
|
var result any |
|
|
if err := json.Unmarshal([]byte(input), &result); err == nil { |
|
|
return result, false, "", nil |
|
|
} |
|
|
|
|
|
|
|
|
errLoc := &JSONErrorLocator{ |
|
|
position: 0, |
|
|
foundError: false, |
|
|
stack: make([]JSONStackElement, 0), |
|
|
} |
|
|
|
|
|
|
|
|
errorPos, err := parseJSONWithStackTracking(input, errLoc) |
|
|
if err == nil && !errLoc.foundError { |
|
|
|
|
|
var result any |
|
|
if err := json.Unmarshal([]byte(input), &result); err != nil { |
|
|
return nil, false, "", err |
|
|
} |
|
|
return result, false, "", nil |
|
|
} |
|
|
|
|
|
if !errLoc.foundError || len(errLoc.stack) == 0 { |
|
|
|
|
|
return nil, false, "", errors.New("incomplete JSON") |
|
|
} |
|
|
|
|
|
|
|
|
closing := "" |
|
|
for i := len(errLoc.stack) - 1; i >= 0; i-- { |
|
|
el := errLoc.stack[i] |
|
|
if el.Type == JSONStackElementObject { |
|
|
closing += "}" |
|
|
} else if el.Type == JSONStackElementArray { |
|
|
closing += "]" |
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
partialInput := input |
|
|
if errorPos > 0 && errorPos < len(input) { |
|
|
partialInput = input[:errorPos] |
|
|
} |
|
|
|
|
|
|
|
|
lastNonSpacePos := strings.LastIndexFunc(partialInput, func(r rune) bool { |
|
|
return !unicode.IsSpace(r) |
|
|
}) |
|
|
if lastNonSpacePos == -1 { |
|
|
return nil, false, "", errors.New("cannot heal a truncated JSON that stopped in an unknown location") |
|
|
} |
|
|
lastNonSpaceChar := rune(partialInput[lastNonSpacePos]) |
|
|
|
|
|
|
|
|
wasMaybeNumber := func() bool { |
|
|
if len(partialInput) > 0 && unicode.IsSpace(rune(partialInput[len(partialInput)-1])) { |
|
|
return false |
|
|
} |
|
|
return unicode.IsDigit(lastNonSpaceChar) || |
|
|
lastNonSpaceChar == '.' || |
|
|
lastNonSpaceChar == 'e' || |
|
|
lastNonSpaceChar == 'E' || |
|
|
lastNonSpaceChar == '-' |
|
|
} |
|
|
|
|
|
|
|
|
partialUnicodeRegex := regexp.MustCompile(`\\u(?:[0-9a-fA-F](?:[0-9a-fA-F](?:[0-9a-fA-F](?:[0-9a-fA-F])?)?)?)?$`) |
|
|
unicodeMarkerPadding := "udc00" |
|
|
lastUnicodeMatch := partialUnicodeRegex.FindStringSubmatch(partialInput) |
|
|
if lastUnicodeMatch != nil { |
|
|
|
|
|
unicodeMarkerPadding = strings.Repeat("0", 6-len(lastUnicodeMatch[0])) |
|
|
|
|
|
if len(lastUnicodeMatch[0]) >= 4 { |
|
|
seq := lastUnicodeMatch[0] |
|
|
if seq[0] == '\\' && seq[1] == 'u' { |
|
|
third := strings.ToLower(string(seq[2])) |
|
|
if third == "d" { |
|
|
fourth := strings.ToLower(string(seq[3])) |
|
|
if fourth == "8" || fourth == "9" || fourth == "a" || fourth == "b" { |
|
|
|
|
|
unicodeMarkerPadding += "\\udc00" |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
canParse := func(str string) bool { |
|
|
var test any |
|
|
return json.Unmarshal([]byte(str), &test) == nil |
|
|
} |
|
|
|
|
|
|
|
|
healedJSON := partialInput |
|
|
jsonDumpMarker := "" |
|
|
topElement := errLoc.stack[len(errLoc.stack)-1] |
|
|
|
|
|
if topElement.Type == JSONStackElementKey { |
|
|
|
|
|
if lastNonSpaceChar == ':' && canParse(healedJSON+"1"+closing) { |
|
|
jsonDumpMarker = "\"" + healingMarker |
|
|
healedJSON += jsonDumpMarker + "\"" + closing |
|
|
} else if canParse(healedJSON + ": 1" + closing) { |
|
|
jsonDumpMarker = ":\"" + healingMarker |
|
|
healedJSON += jsonDumpMarker + "\"" + closing |
|
|
} else if lastNonSpaceChar == '{' && canParse(healedJSON+closing) { |
|
|
jsonDumpMarker = "\"" + healingMarker |
|
|
healedJSON += jsonDumpMarker + "\": 1" + closing |
|
|
} else if canParse(healedJSON + "\"" + closing) { |
|
|
jsonDumpMarker = healingMarker |
|
|
healedJSON += jsonDumpMarker + "\"" + closing |
|
|
} else if len(healedJSON) > 0 && healedJSON[len(healedJSON)-1] == '\\' && canParse(healedJSON+"\\\""+closing) { |
|
|
jsonDumpMarker = "\\" + healingMarker |
|
|
healedJSON += jsonDumpMarker + "\"" + closing |
|
|
} else if canParse(healedJSON + unicodeMarkerPadding + "\"" + closing) { |
|
|
jsonDumpMarker = unicodeMarkerPadding + healingMarker |
|
|
healedJSON += jsonDumpMarker + "\"" + closing |
|
|
} else { |
|
|
|
|
|
lastColon := strings.LastIndex(healedJSON, ":") |
|
|
if lastColon == -1 { |
|
|
return nil, false, "", errors.New("cannot heal a truncated JSON that stopped in an unknown location") |
|
|
} |
|
|
jsonDumpMarker = "\"" + healingMarker |
|
|
healedJSON = healedJSON[:lastColon+1] + jsonDumpMarker + "\"" + closing |
|
|
} |
|
|
} else if topElement.Type == JSONStackElementArray { |
|
|
|
|
|
if (lastNonSpaceChar == ',' || lastNonSpaceChar == '[') && canParse(healedJSON+"1"+closing) { |
|
|
jsonDumpMarker = "\"" + healingMarker |
|
|
healedJSON += jsonDumpMarker + "\"" + closing |
|
|
} else if canParse(healedJSON + "\"" + closing) { |
|
|
jsonDumpMarker = healingMarker |
|
|
healedJSON += jsonDumpMarker + "\"" + closing |
|
|
} else if len(healedJSON) > 0 && healedJSON[len(healedJSON)-1] == '\\' && canParse(healedJSON+"\\\""+closing) { |
|
|
jsonDumpMarker = "\\" + healingMarker |
|
|
healedJSON += jsonDumpMarker + "\"" + closing |
|
|
} else if canParse(healedJSON + unicodeMarkerPadding + "\"" + closing) { |
|
|
jsonDumpMarker = unicodeMarkerPadding + healingMarker |
|
|
healedJSON += jsonDumpMarker + "\"" + closing |
|
|
} else if !wasMaybeNumber() && canParse(healedJSON+", 1"+closing) { |
|
|
jsonDumpMarker = ",\"" + healingMarker |
|
|
healedJSON += jsonDumpMarker + "\"" + closing |
|
|
} else { |
|
|
lastBracketOrComma := strings.LastIndexAny(healedJSON, "[,") |
|
|
if lastBracketOrComma == -1 { |
|
|
return nil, false, "", errors.New("cannot heal a truncated JSON array stopped in an unknown location") |
|
|
} |
|
|
jsonDumpMarker = "\"" + healingMarker |
|
|
healedJSON = healedJSON[:lastBracketOrComma+1] + jsonDumpMarker + "\"" + closing |
|
|
} |
|
|
} else if topElement.Type == JSONStackElementObject { |
|
|
|
|
|
if (lastNonSpaceChar == '{' && canParse(healedJSON+closing)) || |
|
|
(lastNonSpaceChar == ',' && canParse(healedJSON+"\"\": 1"+closing)) { |
|
|
jsonDumpMarker = "\"" + healingMarker |
|
|
healedJSON += jsonDumpMarker + "\": 1" + closing |
|
|
} else if !wasMaybeNumber() && canParse(healedJSON+",\"\": 1"+closing) { |
|
|
jsonDumpMarker = ",\"" + healingMarker |
|
|
healedJSON += jsonDumpMarker + "\": 1" + closing |
|
|
} else if canParse(healedJSON + "\": 1" + closing) { |
|
|
jsonDumpMarker = healingMarker |
|
|
healedJSON += jsonDumpMarker + "\": 1" + closing |
|
|
} else if len(healedJSON) > 0 && healedJSON[len(healedJSON)-1] == '\\' && canParse(healedJSON+"\\\": 1"+closing) { |
|
|
jsonDumpMarker = "\\" + healingMarker |
|
|
healedJSON += jsonDumpMarker + "\": 1" + closing |
|
|
} else if canParse(healedJSON + unicodeMarkerPadding + "\": 1" + closing) { |
|
|
jsonDumpMarker = unicodeMarkerPadding + healingMarker |
|
|
healedJSON += jsonDumpMarker + "\": 1" + closing |
|
|
} else { |
|
|
lastColon := strings.LastIndex(healedJSON, ":") |
|
|
if lastColon == -1 { |
|
|
return nil, false, "", errors.New("cannot heal a truncated JSON object stopped in an unknown location") |
|
|
} |
|
|
jsonDumpMarker = "\"" + healingMarker |
|
|
healedJSON = healedJSON[:lastColon+1] + jsonDumpMarker + "\"" + closing |
|
|
} |
|
|
} else { |
|
|
return nil, false, "", errors.New("cannot heal a truncated JSON object stopped in an unknown location") |
|
|
} |
|
|
|
|
|
|
|
|
var healedValue any |
|
|
if err := json.Unmarshal([]byte(healedJSON), &healedValue); err != nil { |
|
|
return nil, false, "", err |
|
|
} |
|
|
|
|
|
|
|
|
cleaned := removeHealingMarkerFromJSONAny(healedValue, healingMarker) |
|
|
return cleaned, true, jsonDumpMarker, nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func parseJSONWithStackTracking(input string, errLoc *JSONErrorLocator) (int, error) { |
|
|
|
|
|
decoder := json.NewDecoder(strings.NewReader(input)) |
|
|
var test any |
|
|
err := decoder.Decode(&test) |
|
|
if err != nil { |
|
|
errLoc.foundError = true |
|
|
errLoc.exceptionMessage = err.Error() |
|
|
|
|
|
var errorPos int |
|
|
if syntaxErr, ok := err.(*json.SyntaxError); ok { |
|
|
errorPos = int(syntaxErr.Offset) |
|
|
errLoc.position = errorPos |
|
|
} else { |
|
|
|
|
|
errorPos = len(input) |
|
|
errLoc.position = errorPos |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
partialInput := input |
|
|
if errorPos > 0 && errorPos < len(input) { |
|
|
partialInput = input[:errorPos] |
|
|
} |
|
|
|
|
|
|
|
|
pos := 0 |
|
|
inString := false |
|
|
escape := false |
|
|
keyStart := -1 |
|
|
keyEnd := -1 |
|
|
|
|
|
for pos < len(partialInput) { |
|
|
ch := partialInput[pos] |
|
|
|
|
|
if escape { |
|
|
escape = false |
|
|
pos++ |
|
|
continue |
|
|
} |
|
|
|
|
|
if ch == '\\' { |
|
|
escape = true |
|
|
pos++ |
|
|
continue |
|
|
} |
|
|
|
|
|
if ch == '"' { |
|
|
if !inString { |
|
|
|
|
|
inString = true |
|
|
|
|
|
if len(errLoc.stack) > 0 { |
|
|
top := errLoc.stack[len(errLoc.stack)-1] |
|
|
if top.Type == JSONStackElementObject { |
|
|
|
|
|
keyStart = pos + 1 |
|
|
} |
|
|
} |
|
|
} else { |
|
|
|
|
|
inString = false |
|
|
if keyStart != -1 { |
|
|
|
|
|
keyEnd = pos |
|
|
key := partialInput[keyStart:keyEnd] |
|
|
|
|
|
|
|
|
nextPos := pos + 1 |
|
|
for nextPos < len(partialInput) && unicode.IsSpace(rune(partialInput[nextPos])) { |
|
|
nextPos++ |
|
|
} |
|
|
if nextPos < len(partialInput) && partialInput[nextPos] == ':' { |
|
|
|
|
|
errLoc.stack = append(errLoc.stack, JSONStackElement{Type: JSONStackElementKey, Key: key}) |
|
|
} |
|
|
keyStart = -1 |
|
|
keyEnd = -1 |
|
|
} |
|
|
} |
|
|
pos++ |
|
|
continue |
|
|
} |
|
|
|
|
|
if inString { |
|
|
pos++ |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
if ch == '{' { |
|
|
errLoc.stack = append(errLoc.stack, JSONStackElement{Type: JSONStackElementObject}) |
|
|
} else if ch == '}' { |
|
|
|
|
|
for len(errLoc.stack) > 0 { |
|
|
top := errLoc.stack[len(errLoc.stack)-1] |
|
|
errLoc.stack = errLoc.stack[:len(errLoc.stack)-1] |
|
|
if top.Type == JSONStackElementObject { |
|
|
break |
|
|
} |
|
|
} |
|
|
} else if ch == '[' { |
|
|
errLoc.stack = append(errLoc.stack, JSONStackElement{Type: JSONStackElementArray}) |
|
|
} else if ch == ']' { |
|
|
|
|
|
for len(errLoc.stack) > 0 { |
|
|
top := errLoc.stack[len(errLoc.stack)-1] |
|
|
errLoc.stack = errLoc.stack[:len(errLoc.stack)-1] |
|
|
if top.Type == JSONStackElementArray { |
|
|
break |
|
|
} |
|
|
} |
|
|
} else if ch == ':' { |
|
|
|
|
|
if len(errLoc.stack) > 0 && errLoc.stack[len(errLoc.stack)-1].Type == JSONStackElementKey { |
|
|
errLoc.stack = errLoc.stack[:len(errLoc.stack)-1] |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
pos++ |
|
|
} |
|
|
|
|
|
return errorPos, err |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
pos := 0 |
|
|
inString := false |
|
|
escape := false |
|
|
|
|
|
for pos < len(input) { |
|
|
ch := input[pos] |
|
|
|
|
|
if escape { |
|
|
escape = false |
|
|
pos++ |
|
|
continue |
|
|
} |
|
|
|
|
|
if ch == '\\' { |
|
|
escape = true |
|
|
pos++ |
|
|
continue |
|
|
} |
|
|
|
|
|
if ch == '"' { |
|
|
inString = !inString |
|
|
pos++ |
|
|
continue |
|
|
} |
|
|
|
|
|
if inString { |
|
|
pos++ |
|
|
continue |
|
|
} |
|
|
|
|
|
if ch == '{' { |
|
|
errLoc.stack = append(errLoc.stack, JSONStackElement{Type: JSONStackElementObject}) |
|
|
} else if ch == '}' { |
|
|
for len(errLoc.stack) > 0 { |
|
|
top := errLoc.stack[len(errLoc.stack)-1] |
|
|
errLoc.stack = errLoc.stack[:len(errLoc.stack)-1] |
|
|
if top.Type == JSONStackElementObject { |
|
|
break |
|
|
} |
|
|
} |
|
|
} else if ch == '[' { |
|
|
errLoc.stack = append(errLoc.stack, JSONStackElement{Type: JSONStackElementArray}) |
|
|
} else if ch == ']' { |
|
|
for len(errLoc.stack) > 0 { |
|
|
top := errLoc.stack[len(errLoc.stack)-1] |
|
|
errLoc.stack = errLoc.stack[:len(errLoc.stack)-1] |
|
|
if top.Type == JSONStackElementArray { |
|
|
break |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
pos++ |
|
|
} |
|
|
|
|
|
return len(input), nil |
|
|
} |
|
|
|