LCOV - code coverage report
Current view: top level - src - block.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 99.5 % 213 212
Test Date: 2026-10-21 16:35:02 Functions: 100.0 % 19 19

            Line data    Source code
       1              : //
       2              : //  block.c
       3              : //  cloudsync
       4              : //
       5              : //  Block-level LWW CRDT support for text/blob fields.
       6              : //
       7              : 
       8              : #include <stdio.h>
       9              : #include <string.h>
      10              : #include <stdlib.h>
      11              : #include "block.h"
      12              : #include "utils.h"
      13              : #include "fractional_indexing.h"
      14              : 
      15              : // MARK: - Col name helpers -
      16              : 
      17       204430 : bool block_is_block_colname(const char *col_name) {
      18       204430 :     if (!col_name) return false;
      19       204430 :     return strchr(col_name, BLOCK_SEPARATOR) != NULL;
      20       204430 : }
      21              : 
      22         3554 : char *block_extract_base_colname(const char *col_name) {
      23         3554 :     if (!col_name) return NULL;
      24         3554 :     const char *sep = strchr(col_name, BLOCK_SEPARATOR);
      25         3554 :     if (!sep) return cloudsync_string_dup(col_name);
      26         3554 :     return cloudsync_string_ndup(col_name, (size_t)(sep - col_name));
      27         3554 : }
      28              : 
      29         4291 : const char *block_extract_position_id(const char *col_name) {
      30         4291 :     if (!col_name) return NULL;
      31         4291 :     const char *sep = strchr(col_name, BLOCK_SEPARATOR);
      32         4291 :     if (!sep) return NULL;
      33         4291 :     return sep + 1;
      34         4291 : }
      35              : 
      36         7355 : char *block_build_colname(const char *base_col, const char *position_id) {
      37         7355 :     if (!base_col || !position_id) return NULL;
      38         7355 :     size_t blen = strlen(base_col);
      39         7355 :     size_t plen = strlen(position_id);
      40         7355 :     char *result = (char *)cloudsync_memory_alloc(blen + 1 + plen + 1);
      41         7355 :     if (!result) return NULL;
      42         7354 :     memcpy(result, base_col, blen);
      43         7354 :     result[blen] = BLOCK_SEPARATOR;
      44         7354 :     memcpy(result + blen + 1, position_id, plen);
      45         7354 :     result[blen + 1 + plen] = '\0';
      46         7354 :     return result;
      47         7355 : }
      48              : 
      49              : // MARK: - Text splitting -
      50              : 
      51         2898 : static block_list_t *block_list_create(void) {
      52         2898 :     block_list_t *list = (block_list_t *)cloudsync_memory_zeroalloc(sizeof(block_list_t));
      53         2898 :     return list;
      54              : }
      55              : 
      56         7154 : static bool block_list_append(block_list_t *list, const char *content, size_t content_len, const char *position_id) {
      57         7154 :     if (list->count >= list->capacity) {
      58         1855 :         int new_cap = list->capacity ? list->capacity * 2 : 16;
      59         1855 :         block_entry_t *new_entries = (block_entry_t *)cloudsync_memory_realloc(
      60         1855 :             list->entries, (uint64_t)(new_cap * sizeof(block_entry_t)));
      61         1855 :         if (!new_entries) return false;
      62         1851 :         list->entries = new_entries;
      63         1851 :         list->capacity = new_cap;
      64         1851 :     }
      65         7150 :     block_entry_t *e = &list->entries[list->count];
      66         7150 :     e->content = cloudsync_string_ndup(content, content_len);
      67         7150 :     e->position_id = position_id ? cloudsync_string_dup(position_id) : NULL;
      68         7150 :     if (!e->content || (position_id && !e->position_id)) {
      69            6 :         cloudsync_memory_free(e->content);
      70            6 :         cloudsync_memory_free(e->position_id);
      71            6 :         return false;
      72              :     }
      73         7144 :     list->count++;
      74         7144 :     return true;
      75         7154 : }
      76              : 
      77         2894 : void block_list_free(block_list_t *list) {
      78         2894 :     if (!list) return;
      79        10038 :     for (int i = 0; i < list->count; i++) {
      80         7144 :         if (list->entries[i].content) cloudsync_memory_free(list->entries[i].content);
      81         7144 :         if (list->entries[i].position_id) cloudsync_memory_free(list->entries[i].position_id);
      82         7144 :     }
      83         2894 :     if (list->entries) cloudsync_memory_free(list->entries);
      84         2894 :     cloudsync_memory_free(list);
      85         2894 : }
      86              : 
      87         1926 : block_list_t *block_list_create_empty(void) {
      88         1926 :     return block_list_create();
      89              : }
      90              : 
      91         2983 : bool block_list_add(block_list_t *list, const char *content, const char *position_id) {
      92         2983 :     if (!list) return false;
      93         2983 :     return block_list_append(list, content, strlen(content), position_id);
      94         2983 : }
      95              : 
      96          972 : block_list_t *block_split(const char *text, const char *delimiter) {
      97          972 :     block_list_t *list = block_list_create();
      98          972 :     if (!list) return NULL;
      99              : 
     100          969 :     if (!text || !*text) {
     101              :         // Empty text produces a single empty block
     102           48 :         if (!block_list_append(list, "", 0, NULL)) {
     103            2 :             block_list_free(list);
     104            2 :             return NULL;
     105              :         }
     106           46 :         return list;
     107              :     }
     108              : 
     109          921 :     if (!delimiter) delimiter = BLOCK_DEFAULT_DELIMITER;
     110          921 :     size_t dlen = strlen(delimiter);
     111          921 :     if (dlen == 0) {
     112              :         // No delimiter: entire text is one block
     113            3 :         if (!block_list_append(list, text, strlen(text), NULL)) {
     114            2 :             block_list_free(list);
     115            2 :             return NULL;
     116              :         }
     117            1 :         return list;
     118              :     }
     119              : 
     120          918 :     const char *start = text;
     121              :     const char *found;
     122         4120 :     while ((found = strstr(start, delimiter)) != NULL) {
     123         3204 :         size_t seg_len = (size_t)(found - start);
     124         3204 :         if (!block_list_append(list, start, seg_len, NULL)) {
     125            2 :             block_list_free(list);
     126            2 :             return NULL;
     127              :         }
     128         3202 :         start = found + dlen;
     129              :     }
     130              :     // Last segment (after last delimiter or entire string if no delimiter found)
     131          916 :     if (!block_list_append(list, start, strlen(start), NULL)) {
     132            1 :         block_list_free(list);
     133            1 :         return NULL;
     134              :     }
     135              : 
     136          915 :     return list;
     137          972 : }
     138              : 
     139              : // MARK: - Fractional indexing (via fractional-indexing submodule) -
     140              : 
     141              : // Wrapper for malloc: fractional_indexing expects size_t (32-bit on WASM) but cloudsync_memory_alloc takes uint64_t.
     142              : // A direct cast passes strict call_indirect type checking in WASM and triggers a RuntimeError.
     143         2858 : static void *fi_malloc_wrapper(size_t size) {
     144         2858 :     return cloudsync_memory_alloc((uint64_t)size);
     145              : }
     146              : 
     147              : // Wrapper for calloc: fractional_indexing expects (count, size) but cloudsync_memory_zeroalloc takes a single size.
     148          369 : static void *fi_calloc_wrapper(size_t count, size_t size) {
     149          369 :     return cloudsync_memory_zeroalloc((uint64_t)(count * size));
     150              : }
     151              : 
     152          771 : void block_init_allocator(void) {
     153          771 :     fractional_indexing_allocator alloc = {
     154              :         .malloc = fi_malloc_wrapper,
     155              :         .calloc = fi_calloc_wrapper,
     156              :         .free   = cloudsync_memory_free
     157              :     };
     158          771 :     fractional_indexing_set_allocator(&alloc);
     159          771 : }
     160              : 
     161         2492 : char *block_position_between(const char *before, const char *after) {
     162         2492 :     return generate_key_between(before, after);
     163              : }
     164              : 
     165              : // MARK: - Block diff -
     166              : 
     167          971 : static block_diff_t *block_diff_create(void) {
     168          971 :     block_diff_t *diff = (block_diff_t *)cloudsync_memory_zeroalloc(sizeof(block_diff_t));
     169          971 :     return diff;
     170              : }
     171              : 
     172         2582 : static bool block_diff_append(block_diff_t *diff, block_diff_type type, const char *position_id, const char *content) {
     173         2582 :     if (diff->count >= diff->capacity) {
     174          973 :         int new_cap = diff->capacity ? diff->capacity * 2 : 16;
     175          973 :         block_diff_entry_t *new_entries = (block_diff_entry_t *)cloudsync_memory_realloc(
     176          973 :             diff->entries, (uint64_t)(new_cap * sizeof(block_diff_entry_t)));
     177          973 :         if (!new_entries) return false;
     178          972 :         diff->entries = new_entries;
     179          972 :         diff->capacity = new_cap;
     180          972 :     }
     181         2581 :     block_diff_entry_t *e = &diff->entries[diff->count];
     182         2581 :     e->type = type;
     183         2581 :     e->position_id = cloudsync_string_dup(position_id);
     184         2581 :     e->content = content ? cloudsync_string_dup(content) : NULL;
     185         2581 :     if (!e->position_id || (content && !e->content)) {
     186            4 :         cloudsync_memory_free(e->position_id);
     187            4 :         cloudsync_memory_free(e->content);
     188            4 :         return false;
     189              :     }
     190         2577 :     diff->count++;
     191         2577 :     return true;
     192         2582 : }
     193              : 
     194          970 : void block_diff_free(block_diff_t *diff) {
     195          970 :     if (!diff) return;
     196         3547 :     for (int i = 0; i < diff->count; i++) {
     197         2577 :         if (diff->entries[i].position_id) cloudsync_memory_free(diff->entries[i].position_id);
     198         2577 :         if (diff->entries[i].content) cloudsync_memory_free(diff->entries[i].content);
     199         2577 :     }
     200          970 :     if (diff->entries) cloudsync_memory_free(diff->entries);
     201          970 :     cloudsync_memory_free(diff);
     202          970 : }
     203              : 
     204              : // Content-based matching diff algorithm:
     205              : // 1. Build a consumed-set from old blocks
     206              : // 2. For each new block, find the first unconsumed old block with matching content
     207              : // 3. Matched blocks keep their position_id (UNCHANGED)
     208              : // 4. Unmatched new blocks get new position_ids (ADDED)
     209              : // 5. Unconsumed old blocks are REMOVED
     210              : // Modified blocks are detected when content changed but position stayed (handled as MODIFIED)
     211          971 : block_diff_t *block_diff(block_entry_t *old_blocks, int old_count,
     212              :                          const char **new_parts, int new_count) {
     213          971 :     block_diff_t *diff = block_diff_create();
     214          971 :     if (!diff) return NULL;
     215              : 
     216              :     // Track which old blocks have been consumed
     217          970 :     bool *old_consumed = NULL;
     218          970 :     if (old_count > 0) {
     219          265 :         old_consumed = (bool *)cloudsync_memory_zeroalloc((uint64_t)(old_count * sizeof(bool)));
     220          265 :         if (!old_consumed) {
     221            1 :             block_diff_free(diff);
     222            1 :             return NULL;
     223              :         }
     224          264 :     }
     225              : 
     226              :     // For each new block, try to find a matching unconsumed old block
     227              :     // Use a simple forward scan to preserve ordering
     228          969 :     int old_scan = 0;
     229          969 :     char *last_position = NULL;
     230              : 
     231         5137 :     for (int ni = 0; ni < new_count; ni++) {
     232         4175 :         bool found = false;
     233              : 
     234              :         // Scan forward in old blocks for a content match
     235         4793 :         for (int oi = old_scan; oi < old_count; oi++) {
     236         2301 :             if (old_consumed[oi]) continue;
     237              : 
     238         2301 :             if (strcmp(old_blocks[oi].content, new_parts[ni]) == 0) {
     239              :                 // Exact match — mark any skipped old blocks as REMOVED
     240         1712 :                 for (int si = old_scan; si < oi; si++) {
     241           31 :                     if (!old_consumed[si]) {
     242           31 :                         if (!block_diff_append(diff, BLOCK_DIFF_REMOVED, old_blocks[si].position_id, NULL)) goto fail;
     243           29 :                         old_consumed[si] = true;
     244           29 :                     }
     245           29 :                 }
     246         1681 :                 old_consumed[oi] = true;
     247         1681 :                 old_scan = oi + 1;
     248         1681 :                 last_position = old_blocks[oi].position_id;
     249         1681 :                 found = true;
     250         1681 :                 break;
     251              :             }
     252          618 :         }
     253              : 
     254         4173 :         if (!found) {
     255              :             // New block — needs a new position_id
     256         2492 :             const char *next_pos = NULL;
     257              :             // Find the next unconsumed old block's position for the upper bound
     258         2492 :             for (int oi = old_scan; oi < old_count; oi++) {
     259          429 :                 if (!old_consumed[oi]) {
     260          429 :                     next_pos = old_blocks[oi].position_id;
     261          429 :                     break;
     262              :                 }
     263            0 :             }
     264              : 
     265         2492 :             char *new_pos = block_position_between(last_position, next_pos);
     266         2492 :             if (!new_pos) goto fail;
     267              :             {
     268         2489 :                 bool appended = block_diff_append(diff, BLOCK_DIFF_ADDED, new_pos, new_parts[ni]);
     269         2489 :                 cloudsync_memory_free(new_pos);
     270         2489 :                 if (!appended) goto fail;
     271         2487 :                 last_position = diff->entries[diff->count - 1].position_id;
     272              :             }
     273         2487 :         }
     274         4168 :     }
     275              : 
     276              :     // Mark remaining unconsumed old blocks as REMOVED
     277         1023 :     for (int oi = old_scan; oi < old_count; oi++) {
     278           62 :         if (!old_consumed[oi]) {
     279           62 :             if (!block_diff_append(diff, BLOCK_DIFF_REMOVED, old_blocks[oi].position_id, NULL)) goto fail;
     280           61 :         }
     281           61 :     }
     282              : 
     283          961 :     if (old_consumed) cloudsync_memory_free(old_consumed);
     284          961 :     return diff;
     285              : fail:
     286            8 :     cloudsync_memory_free(old_consumed);
     287            8 :     block_diff_free(diff);
     288            8 :     return NULL;
     289          971 : }
     290              : 
     291              : // MARK: - Materialization -
     292              : 
     293         4025 : char *block_materialize_text(const char **blocks, int count, const char *delimiter) {
     294         4025 :     if (count == 0) return cloudsync_string_dup("");
     295         4025 :     if (!delimiter) delimiter = BLOCK_DEFAULT_DELIMITER;
     296              : 
     297         4025 :     size_t dlen = strlen(delimiter);
     298         4025 :     size_t total = 0;
     299        36219 :     for (int i = 0; i < count; i++) {
     300        32194 :         total += strlen(blocks[i]);
     301        32194 :         if (i < count - 1) total += dlen;
     302        32194 :     }
     303              : 
     304         4025 :     char *result = (char *)cloudsync_memory_alloc(total + 1);
     305         4025 :     if (!result) return NULL;
     306              : 
     307         4024 :     size_t offset = 0;
     308        36216 :     for (int i = 0; i < count; i++) {
     309        32192 :         size_t blen = strlen(blocks[i]);
     310        32192 :         memcpy(result + offset, blocks[i], blen);
     311        32192 :         offset += blen;
     312        32192 :         if (i < count - 1) {
     313        28168 :             memcpy(result + offset, delimiter, dlen);
     314        28168 :             offset += dlen;
     315        28168 :         }
     316        32192 :     }
     317         4024 :     result[offset] = '\0';
     318              : 
     319         4024 :     return result;
     320         4025 : }
        

Generated by: LCOV version 2.5-0