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