[GH-ISSUE #429] Empty value in json template variable will crash AP #3571

Open
opened 2026-03-20 23:06:52 +01:00 by sascha_hemi · 4 comments
Owner

Originally created by @ghoeffner on GitHub (Jan 14, 2025).
Original GitHub issue: https://github.com/OpenEPaperLink/OpenEPaperLink/issues/429

Originally assigned to: @nlimper on GitHub.

Describe the bug
This caused me a log of headache and I even reset the whole AP (and accidently lost my json templates), until I figured out what is actually going on. Seems like a empty value on the second level of a json returned in the fetch URL of a json template tag will straight out crash the AP.

To Reproduce
Tested on the following tag:
M2 2.6"
296x152 fw:40 0x28

json template

[
    {"text": [5,5,"1123","fonts/bahnschrift20",1]},
    {"box": [10,30,20,20,1]},
    {"box": [35,30,20,20,2]},
    {"triangle": [60,30,60,50,80,40,1]},
    {"text": [5,80,"Plain text glasstown_nbp_tf","glasstown_nbp_tf",1]},
    {"text": [5,95,"Plain text 7x14_tf","7x14_tf",2]},
    {"text": [5,110,"Plain text t0_14b_tf","t0_14b_tf",1]},
    {"text": [135,5,"30","fonts/bahnschrift30",2]},
    {"text": [215,5,"70","fonts/bahnschrift70",1]},
    {"text": [150,80, "{.test1.test}" ,"fonts/calibrib50",2,0]},
    {"text": [205,60,"80","fonts/calibrib80",2]},
    {"text": [90,35,"calibrib30","fonts/calibrib30",1]},
    {"line": [10,120,290,120,1]},
    {"line": [10,115,290,115,2]}
]

data returned on URL:
{ "test1": { "test": "" } }

Now fill in any string in the "test" value and you will see that it recovers from crashing and returns to expected behavior.

Expected behavior
It should not crash ;-)

Originally created by @ghoeffner on GitHub (Jan 14, 2025). Original GitHub issue: https://github.com/OpenEPaperLink/OpenEPaperLink/issues/429 Originally assigned to: @nlimper on GitHub. **Describe the bug** This caused me a log of headache and I even reset the whole AP (and accidently lost my json templates), until I figured out what is actually going on. Seems like a empty value on the second level of a json returned in the fetch URL of a json template tag will straight out crash the AP. **To Reproduce** Tested on the following tag: M2 2.6" 296x152 fw:40 0x28 json template ``` [ {"text": [5,5,"1123","fonts/bahnschrift20",1]}, {"box": [10,30,20,20,1]}, {"box": [35,30,20,20,2]}, {"triangle": [60,30,60,50,80,40,1]}, {"text": [5,80,"Plain text glasstown_nbp_tf","glasstown_nbp_tf",1]}, {"text": [5,95,"Plain text 7x14_tf","7x14_tf",2]}, {"text": [5,110,"Plain text t0_14b_tf","t0_14b_tf",1]}, {"text": [135,5,"30","fonts/bahnschrift30",2]}, {"text": [215,5,"70","fonts/bahnschrift70",1]}, {"text": [150,80, "{.test1.test}" ,"fonts/calibrib50",2,0]}, {"text": [205,60,"80","fonts/calibrib80",2]}, {"text": [90,35,"calibrib30","fonts/calibrib30",1]}, {"line": [10,120,290,120,1]}, {"line": [10,115,290,115,2]} ] ``` data returned on URL: `{ "test1": { "test": "" } }` Now fill in any string in the "test" value and you will see that it recovers from crashing and returns to expected behavior. **Expected behavior** It should not crash ;-)
sascha_hemi added the bug label 2026-03-20 23:06:52 +01:00
Author
Owner

@ghoeffner commented on GitHub (Apr 18, 2025):

Issue present on latest build.

<!-- gh-comment-id:2815082330 --> @ghoeffner commented on GitHub (Apr 18, 2025): Issue present on latest build.
Author
Owner

@nlimper commented on GitHub (Apr 18, 2025):

If you want to try to fix it: it's somewhere in this part of the code:
https://github.com/OpenEPaperLink/OpenEPaperLink/blob/master/ESP32_AP-Flasher/src/contentmanager.cpp#L2114-L2257
Probably in the extractValueFromJson function, or in the findAndReplace function that uses extractValueFromJson.

<!-- gh-comment-id:2815101818 --> @nlimper commented on GitHub (Apr 18, 2025): If you want to try to fix it: it's somewhere in this part of the code: https://github.com/OpenEPaperLink/OpenEPaperLink/blob/master/ESP32_AP-Flasher/src/contentmanager.cpp#L2114-L2257 Probably in the `extractValueFromJson` function, or in the `findAndReplace` function that uses extractValueFromJson.
Author
Owner

@NickWaterton commented on GitHub (Jan 24, 2026):

I just ran into this today - still present in the latest build.
Took me about 2 hours to figure out what the problem was. There are no error messages or anything - just a silent crash + reboot.

Doesn't have to be a variable, if you just send something like a Text line with an empty string in a template - crash.

Also, doesn't have to be an empty string, a string consisting of just white space does the same, so " " crashes just the same as "".

I believe the problem is in:

void replaceVariables(String &format) {
    size_t startIndex = 0;
    size_t openBraceIndex, closeBraceIndex;

    time_t now;
    time(&now);
    struct tm timedef;
    localtime_r(&now, &timedef);
    char timeBuffer[80];
    strftime(timeBuffer, sizeof(timeBuffer), "%H:%M:%S", &timedef);
    setVarDB("ap_time", timeBuffer, false);

    while ((openBraceIndex = format.indexOf('{', startIndex)) != -1 &&
           (closeBraceIndex = format.indexOf('}', openBraceIndex + 1)) != -1) {
        const std::string variableName = format.substring(openBraceIndex + 1, closeBraceIndex).c_str();
        const std::string varKey = "{" + variableName + "}";
        const auto var = varDB.find(variableName);
        if (var != varDB.end()) {
            format.replace(varKey.c_str(), var->second.value);
        } else {
            format.replace(varKey.c_str(), "-");
        }
        startIndex = closeBraceIndex + 1;
    }
}

If you pass this a NULL string (ie '\0') I don't think the behavior of format.indexOf('{', startIndex)) is defined.

I think a simple fix would be to just early return if the string length is 0. I can't test this, as I don't have a build environment.

Can anyone try this? it's 100% repeatable, so should be easy to test.

<!-- gh-comment-id:3795439249 --> @NickWaterton commented on GitHub (Jan 24, 2026): I just ran into this today - still present in the latest build. Took me about 2 hours to figure out what the problem was. There are no error messages or anything - just a silent crash + reboot. Doesn't have to be a variable, if you just send something like a Text line with an empty string in a template - crash. Also, doesn't have to be an empty string, a string consisting of just white space does the same, so `" "` crashes just the same as `""`. I believe the problem is in: ``` void replaceVariables(String &format) { size_t startIndex = 0; size_t openBraceIndex, closeBraceIndex; time_t now; time(&now); struct tm timedef; localtime_r(&now, &timedef); char timeBuffer[80]; strftime(timeBuffer, sizeof(timeBuffer), "%H:%M:%S", &timedef); setVarDB("ap_time", timeBuffer, false); while ((openBraceIndex = format.indexOf('{', startIndex)) != -1 && (closeBraceIndex = format.indexOf('}', openBraceIndex + 1)) != -1) { const std::string variableName = format.substring(openBraceIndex + 1, closeBraceIndex).c_str(); const std::string varKey = "{" + variableName + "}"; const auto var = varDB.find(variableName); if (var != varDB.end()) { format.replace(varKey.c_str(), var->second.value); } else { format.replace(varKey.c_str(), "-"); } startIndex = closeBraceIndex + 1; } } ``` If you pass this a `NULL` string (ie `'\0'`) I don't think the behavior of `format.indexOf('{', startIndex))` is defined. I think a simple fix would be to just early return if the string length is 0. I can't test this, as I don't have a build environment. Can anyone try this? it's 100% repeatable, so should be easy to test.
Author
Owner

@NickWaterton commented on GitHub (Feb 6, 2026):

UPDATE:

For anyone else that runs into this, I have found the problem, its in drawString(), not replaceVariables() as I thought.
Made a build environment, and tested out the fix, this is it:

void drawString(TFT_eSprite &spr, String content, int16_t posx, int16_t posy, String font, byte align, uint16_t color, uint16_t size, uint16_t bgcolor) {
    // drawString(spr,"test",100,10,"bahnschrift30",TC_DATUM,TFT_RED);
    if (content && content[0] != 0) {
        // backwards compatibility
        replaceVariables(content);
        if (font.startsWith("fonts/calibrib")) {
            String numericValueStr = font.substring(14);
            int calibriSize = numericValueStr.toInt();
            if (calibriSize != 30 && calibriSize != 16) {
                font = "Signika-SB.ttf";
                size = calibriSize;
            }
        }
        if (font == "glasstown_nbp_tf") {
            font = "tahoma9.vlw";
            posy -= 8;
        }
        if (font == "7x14_tf") {
            font = "REFSAN12.vlw";
            posy -= 10;
        }
        if (font == "t0_14b_tf") {
            font = "calibrib16.vlw";
            posy -= 11;
        }

        switch (processFontPath(font)) {
            case 2: {
                // truetype
                time_t t = millis();
                truetypeClass truetype = truetypeClass();
                void *framebuffer = spr.getPointer();
                truetype.setFramebuffer(spr.width(), spr.height(), spr.getColorDepth(), static_cast<uint8_t *>(framebuffer));
                File fontFile = contentFS->open(font, "r");
                if (!truetype.setTtfFile(fontFile)) {
                    Serial.println("read ttf failed");
                    return;
                }

                truetype.setCharacterSize(size);
                truetype.setCharacterSpacing(0);
                if (align == TC_DATUM) {
                    posx -= truetype.getStringWidth(content) / 2;
                }
                if (align == TR_DATUM) {
                    posx -= truetype.getStringWidth(content);
                }
                truetype.setTextBoundary(posx, spr.width(), spr.height());
                if (spr.getColorDepth() == 8) {
                    truetype.setTextColor(spr.color16to8(color), spr.color16to8(color));
                } else {
                    truetype.setTextColor(color, color);
                }
                truetype.textDraw(posx, posy, content);
                truetype.end();
            } break;
            case 3: {
                // vlw bitmap font
                spr.setTextDatum(align);
                if (font != "") spr.loadFont(font.substring(1), *contentFS);
                spr.setTextColor(color, bgcolor);
                spr.setTextWrap(false, false);
                spr.drawString(content, posx, posy);
                if (font != "") spr.unloadFont();
            }
        }
    }
}

It's pretty simple, just add a check for a NULL string at the top:

if (content && content[0] != 0)

This will then skip drawing the text if it's empty, the same as displaying "". Prevents the stack corruption reboot that you get otherwise.

My fork with the fix is here https://github.com/NickWaterton/OpenEPaperLink/tree/master
But be warned, it has a lot of changes in it, along with replacing the old Bluedroid BLE library with NimBLE, which makes it a lot more reliable - ie it doesn't crash evey hour or so.

<!-- gh-comment-id:3861714428 --> @NickWaterton commented on GitHub (Feb 6, 2026): UPDATE: For anyone else that runs into this, I have found the problem, its in `drawString()`, not `replaceVariables()` as I thought. Made a build environment, and tested out the fix, this is it: ``` void drawString(TFT_eSprite &spr, String content, int16_t posx, int16_t posy, String font, byte align, uint16_t color, uint16_t size, uint16_t bgcolor) { // drawString(spr,"test",100,10,"bahnschrift30",TC_DATUM,TFT_RED); if (content && content[0] != 0) { // backwards compatibility replaceVariables(content); if (font.startsWith("fonts/calibrib")) { String numericValueStr = font.substring(14); int calibriSize = numericValueStr.toInt(); if (calibriSize != 30 && calibriSize != 16) { font = "Signika-SB.ttf"; size = calibriSize; } } if (font == "glasstown_nbp_tf") { font = "tahoma9.vlw"; posy -= 8; } if (font == "7x14_tf") { font = "REFSAN12.vlw"; posy -= 10; } if (font == "t0_14b_tf") { font = "calibrib16.vlw"; posy -= 11; } switch (processFontPath(font)) { case 2: { // truetype time_t t = millis(); truetypeClass truetype = truetypeClass(); void *framebuffer = spr.getPointer(); truetype.setFramebuffer(spr.width(), spr.height(), spr.getColorDepth(), static_cast<uint8_t *>(framebuffer)); File fontFile = contentFS->open(font, "r"); if (!truetype.setTtfFile(fontFile)) { Serial.println("read ttf failed"); return; } truetype.setCharacterSize(size); truetype.setCharacterSpacing(0); if (align == TC_DATUM) { posx -= truetype.getStringWidth(content) / 2; } if (align == TR_DATUM) { posx -= truetype.getStringWidth(content); } truetype.setTextBoundary(posx, spr.width(), spr.height()); if (spr.getColorDepth() == 8) { truetype.setTextColor(spr.color16to8(color), spr.color16to8(color)); } else { truetype.setTextColor(color, color); } truetype.textDraw(posx, posy, content); truetype.end(); } break; case 3: { // vlw bitmap font spr.setTextDatum(align); if (font != "") spr.loadFont(font.substring(1), *contentFS); spr.setTextColor(color, bgcolor); spr.setTextWrap(false, false); spr.drawString(content, posx, posy); if (font != "") spr.unloadFont(); } } } } ``` It's pretty simple, just add a check for a NULL string at the top: ``` if (content && content[0] != 0) ``` This will then skip drawing the text if it's empty, the same as displaying `""`. Prevents the stack corruption reboot that you get otherwise. My fork with the fix is here https://github.com/NickWaterton/OpenEPaperLink/tree/master But be warned, it has a lot of changes in it, along with replacing the old Bluedroid BLE library with NimBLE, which makes it a lot more reliable - ie it doesn't crash evey hour or so.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/OpenEPaperLink#3571