Json-Tutorial05 数组解析

前言

本节将要学习的是第一种复合类型的解析:数组。具体的解析规则在Tutorial中已经有了,概括下简单的思想就是遇到[符号之后挨个调用lept_parse_value来解析数组的每一个元素,当然每次遇到逗号就要将已经解析的那个元素进栈,当遇到]符号时,栈中所有元素一起出栈,保存到数组Json Value中。特别需要注意的是解析失败时的内存泄露问题。

代码设计

1. 编写 test_parse_array() 单元测试

lept_parse_array大致框架已经为我们搭建好的情况下,我们只需要为其写两个单测。由于数组元素个数、类型都是不确定的,所以不能用一个宏就搞定。这里的关键在于使用lept_get_array_element将数组元素一个个取出来,挨个对其进行值、类型的校验。

static void test_parse_array() {
    lept_value v;
    lept_init(&v);
    EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "[ ]"));
    EXPECT_EQ_INT(LEPT_ARRAY, lept_get_type(&v));
    EXPECT_EQ_SIZE_T(0, lept_get_array_size(&v));
    lept_free(&v);

    lept_init(&v);
    EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "[ null , false , true , 123 , \"abc\" ]"));
    EXPECT_EQ_INT(LEPT_ARRAY, lept_get_type(&v));
    EXPECT_EQ_SIZE_T(5, lept_get_array_size(&v));
    EXPECT_EQ_INT(LEPT_NULL, lept_get_type(lept_get_array_element(&v, 0)));
    EXPECT_EQ_INT(LEPT_FALSE, lept_get_type(lept_get_array_element(&v, 1)));
    EXPECT_EQ_INT(LEPT_TRUE, lept_get_type(lept_get_array_element(&v, 2)));
    EXPECT_EQ_INT(LEPT_NUMBER, lept_get_type(lept_get_array_element(&v, 3)));
    EXPECT_EQ_DOUBLE(123.0, lept_get_number(lept_get_array_element(&v, 3)));
    EXPECT_EQ_STRING("abc", lept_get_string(lept_get_array_element(&v, 4)), 3);
    lept_free(&v);

    lept_init(&v);
    size_t i;
    EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "[ [ ] , [ 0 ] , [ 0 , 1 ] , [ 0 , 1 , 2 ] ]"));
    EXPECT_EQ_INT(LEPT_ARRAY, lept_get_type(&v));
    EXPECT_EQ_SIZE_T(4, lept_get_array_size(&v));
    for (i = 0; i < 4; i++) {
        lept_value* sub_v = lept_get_array_element(&v, i);
        EXPECT_EQ_SIZE_T(i, lept_get_array_size(sub_v));
        int j;
        for (j = 0; j < i; j++) {
            EXPECT_EQ_INT(LEPT_NUMBER, lept_get_type(lept_get_array_element(sub_v, j)));
            EXPECT_EQ_DOUBLE((double)j, lept_get_number(lept_get_array_element(sub_v, j)));
        }
    }

    lept_free(&v);
}

2. lept_parse_array() 里加入空白字符处理

按现时的 lept_parse_array() 的编写方式,需要加入 3 个 lept_parse_whitespace() 调用,分别是解析 [ 之后,元素之后,以及 , 之后:

static int lept_parse_array(lept_context* c, lept_value* v) {
    /* ... */
    EXPECT(c, '[');
    lept_parse_whitespace(c);
    /* ... */
    for (;;) {
        /* ... */
        if ((ret = lept_parse_value(c, &e)) != LEPT_PARSE_OK)
            return ret;
        /* ... */
        lept_parse_whitespace(c);
        if (*c->json == ',') {
            c->json++;
            lept_parse_whitespace(c);
        }
        /* ... */
    }
}

3. 内存泄漏

我们在遇到]解析完成后,会为解析出来的数组使用mallocmemcpy为数组的Json Value分配可变内存。那么当一个单测用例结束时,我们应该将这部分内存释放,于是顺理成章地,这部分逻辑应该实现在lept_free里面。

void lept_free(lept_value* v) {
    assert(v != NULL);
    size_t i;
    if (v->type == LEPT_STRING)
        free(v->u.s.s);
    else if (v->type == LEPT_ARRAY) {
        for (i = 0; i < v->u.a.size; i++) {
            lept_free(&v->u.a.e[i]);
        }
        free(v->u.a.e);
    }
    v->type = LEPT_NULL;
}

这里需要把v中的元素挨个递归调用lept_free释放内存,因为数组元素也可以是一个数组,是一种嵌套关系

4. 解析错误时的缓冲区临时值处理

lept_parse_array中,有两个地方可能会导致解析失败:

  • lept_parse_value解析单个值
  • 除了数值、,]以外的符号

以下是我一开始的初版实现,仅仅在上述的两个分支内加了lept_context_pop函数,想将栈内的字符弹出。但是后来发现这个函数仅仅只是改了栈顶指针,并没有实际调用free

static int lept_parse_array(lept_context* c, lept_value* v) {
    size_t size = 0;
    int ret;
    EXPECT(c, '[');
    lept_parse_whitespace(c);
    if (*c->json == ']') {
        c->json++;
        v->type = LEPT_ARRAY;
        v->u.a.size = 0;
        v->u.a.e = NULL;
        return LEPT_PARSE_OK;
    }
    for (;;) {
        lept_value e;
        lept_init(&e);
        if ((ret = lept_parse_value(c, &e)) != LEPT_PARSE_OK) {
            lept_context_pop(c, size * sizeof(lept_value));
            return ret;
        }
        memcpy(lept_context_push(c, sizeof(lept_value)), &e, sizeof(lept_value));
        size++;

        lept_parse_whitespace(c);
        if (*c->json == ',') {
            c->json++;
            lept_parse_whitespace(c);
        } else if (*c->json == ']') {
            c->json++;
            v->type = LEPT_ARRAY;
            v->u.a.size = size;
            size *= sizeof(lept_value);
            memcpy(v->u.a.e = (lept_value*)malloc(size), lept_context_pop(c, size), size);
            return LEPT_PARSE_OK;
        }
        else {
            lept_context_pop(c, size * sizeof(lept_value));
            return LEPT_PARSE_MISS_COMMA_OR_SQUARE_BRACKET;
        }
    }
}

static void* lept_context_pop(lept_context* c, size_t size) {
    assert(c->top >= size);
    return c->stack + (c->top -= size);
}

实际上正确的做法是,挨个遍历栈中的lept_value然后释放。我们的栈是按照字节(char)存储的,一个lept_value的大小是24字节,即sizeof(lept_value)=24。所以每次必须要弹出24字节并转为lept_value*。

static int lept_parse_array(lept_context* c, lept_value* v) {
    /*size must be initialized.*/
    size_t size = 0, i;
    int ret;
    EXPECT(c, '[');
    lept_parse_whitespace(c);
    if (*c->json == ']') {
        c->json++;
        v->type = LEPT_ARRAY;
        v->u.a.size = 0;
        v->u.a.e = NULL;
        return LEPT_PARSE_OK;
    }
    for (;;) {
        lept_value e;
        lept_init(&e);
        if ((ret = lept_parse_value(c, &e)) != LEPT_PARSE_OK) {
            /*Break here.*/
            break;
        }
        memcpy(lept_context_push(c, sizeof(lept_value)), &e, sizeof(lept_value));
        size++;

        lept_parse_whitespace(c);
        if (*c->json == ',') {
            c->json++;
            lept_parse_whitespace(c);
        } else if (*c->json == ']') {
            c->json++;
            v->type = LEPT_ARRAY;
            v->u.a.size = size;
            size *= sizeof(lept_value);
            memcpy(v->u.a.e = (lept_value*)malloc(size), lept_context_pop(c, size), size);
            return LEPT_PARSE_OK;
        }
        else {
            /*Break here.*/
            ret = LEPT_PARSE_MISS_COMMA_OR_SQUARE_BRACKET;
            break;
        }
    }
    
    /*Free resource here.*/
    for (i = 0; i < size; i++) {
        lept_free((lept_value*)lept_context_pop(c, sizeof(lept_value)));
    }
    return ret;
} 

在解析遇到错误时,直接保存返回值后break。在最后遍历栈释放资源。

5. 第 4 节那段代码为什么会有 bug?

见Tutorial的解释,bug来自于:如果提前获取了lept_context_push的压栈指针,在lept_parse_value中,可能会遇到realloc函数扩容导致的悬空指针。

posted @ 2023-01-09 11:34  南小小小小乔  阅读(31)  评论(0编辑  收藏  举报