fix(memory): Deep clone en Row + CellValue para evitar dangling pointers

- CellValue.clone(): duplica strings en lugar de copiar punteros
- CellValue.deinit(): libera strings clonados
- Row.owns_data: flag para indicar propiedad de memoria
- Row.clone(): ahora hace deep copy completo
- Row.deinit(): libera strings si owns_data=true

Soluciona crash al guardar cliente sin población/provincia/país,
donde loadWhoList() liberaba strings que Row clonada seguía referenciando.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
R.Eugenio 2025-12-22 13:16:59 +01:00
parent 3f442bf8b9
commit 59d102315d
2 changed files with 92 additions and 6 deletions

View file

@ -158,6 +158,22 @@ pub const CellValue = union(enum) {
.boolean => |b| if (b) "Yes" else "No", .boolean => |b| if (b) "Yes" else "No",
}; };
} }
/// Clone value with deep copy of strings
pub fn clone(self: CellValue, allocator: std.mem.Allocator) !CellValue {
return switch (self) {
.string => |s| .{ .string = if (s.len > 0) try allocator.dupe(u8, s) else "" },
else => self, // Other types are value types, no allocation needed
};
}
/// Free allocated string (only call for cloned values)
pub fn deinit(self: CellValue, allocator: std.mem.Allocator) void {
switch (self) {
.string => |s| if (s.len > 0) allocator.free(s),
else => {},
}
}
}; };
// ============================================================================= // =============================================================================
@ -452,15 +468,26 @@ pub const OnActiveRowChangedFn = *const fn (old_row: ?usize, new_row: usize, row
pub const Row = struct { pub const Row = struct {
data: std.StringHashMap(CellValue), data: std.StringHashMap(CellValue),
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
/// True if this Row owns the string data (was created via clone)
/// When true, deinit() will free the strings
owns_data: bool = false,
pub fn init(allocator: std.mem.Allocator) Row { pub fn init(allocator: std.mem.Allocator) Row {
return .{ return .{
.data = std.StringHashMap(CellValue).init(allocator), .data = std.StringHashMap(CellValue).init(allocator),
.allocator = allocator, .allocator = allocator,
.owns_data = false,
}; };
} }
pub fn deinit(self: *Row) void { pub fn deinit(self: *Row) void {
// Free cloned string values if we own them
if (self.owns_data) {
var iter = self.data.iterator();
while (iter.next()) |entry| {
entry.value_ptr.*.deinit(self.allocator);
}
}
self.data.deinit(); self.data.deinit();
} }
@ -472,11 +499,22 @@ pub const Row = struct {
try self.data.put(column, value); try self.data.put(column, value);
} }
/// Deep clone: duplicates all string values
/// The cloned Row owns its data and will free it on deinit()
pub fn clone(self: *const Row, allocator: std.mem.Allocator) !Row { pub fn clone(self: *const Row, allocator: std.mem.Allocator) !Row {
var new_row = Row.init(allocator); var new_row = Row{
.data = std.StringHashMap(CellValue).init(allocator),
.allocator = allocator,
.owns_data = true, // Cloned row owns its string data
};
errdefer new_row.deinit();
var iter = self.data.iterator(); var iter = self.data.iterator();
while (iter.next()) |entry| { while (iter.next()) |entry| {
try new_row.data.put(entry.key_ptr.*, entry.value_ptr.*); // Keys are typically string literals, no need to clone
// Values need deep clone for strings
const cloned_value = try entry.value_ptr.*.clone(allocator);
try new_row.data.put(entry.key_ptr.*, cloned_value);
} }
return new_row; return new_row;
} }

View file

@ -40,6 +40,10 @@ pub const AutoCompleteState = struct {
was_focused: bool = false, was_focused: bool = false,
/// First frame flag to avoid false focus detection /// First frame flag to avoid false focus detection
first_frame: bool = true, first_frame: bool = true,
/// Texto original guardado al abrir dropdown (para restaurar con Escape)
saved_text: [256]u8 = [_]u8{0} ** 256,
saved_text_len: usize = 0,
saved_cursor: usize = 0,
const Self = @This(); const Self = @This();
@ -164,8 +168,29 @@ pub const AutoCompleteState = struct {
} }
} }
/// Open the dropdown /// Guarda el texto actual (para restaurar con Escape)
pub fn saveText(self: *Self) void {
@memcpy(self.saved_text[0..self.len], self.buffer[0..self.len]);
self.saved_text_len = self.len;
self.saved_cursor = self.cursor;
}
/// Restaura el texto guardado
pub fn restoreText(self: *Self) void {
@memcpy(self.buffer[0..self.saved_text_len], self.saved_text[0..self.saved_text_len]);
self.len = self.saved_text_len;
self.cursor = self.saved_cursor;
// Sync filter
@memcpy(self.last_filter[0..self.saved_text_len], self.saved_text[0..self.saved_text_len]);
self.last_filter_len = self.saved_text_len;
}
/// Open the dropdown (guarda texto para posible restauración con Escape)
pub fn openDropdown(self: *Self) void { pub fn openDropdown(self: *Self) void {
if (!self.open) {
// Solo guardar si estamos abriendo (no si ya está abierto)
self.saveText();
}
self.open = true; self.open = true;
self.highlighted = if (self.selected >= 0) self.selected else 0; self.highlighted = if (self.selected >= 0) self.selected else 0;
} }
@ -175,6 +200,12 @@ pub const AutoCompleteState = struct {
self.open = false; self.open = false;
self.highlighted = -1; self.highlighted = -1;
} }
/// Close dropdown y restaurar texto original (para Escape)
pub fn closeDropdownAndRestore(self: *Self) void {
self.restoreText();
self.closeDropdown();
}
}; };
// ============================================================================= // =============================================================================
@ -460,7 +491,10 @@ pub fn autocompleteRect(
switch (event.key) { switch (event.key) {
.escape => { .escape => {
state.closeDropdown(); // Escape: Cierra dropdown y RESTAURA texto original
if (state.open) {
state.closeDropdownAndRestore();
}
}, },
.enter => { .enter => {
if (state.open and state.highlighted >= 0 and state.highlighted < @as(i32, @intCast(filtered_count))) { if (state.open and state.highlighted >= 0 and state.highlighted < @as(i32, @intCast(filtered_count))) {
@ -480,7 +514,13 @@ pub fn autocompleteRect(
} }
}, },
.up => { .up => {
if (state.open) { // Ctrl+Up: Cierra dropdown (sin restaurar - confirma el estado actual)
if (ctx.input.modifiers.ctrl) {
if (state.open) {
state.closeDropdown();
}
} else if (state.open) {
// Navegar hacia arriba en la lista
if (state.highlighted > 0) { if (state.highlighted > 0) {
state.highlighted -= 1; state.highlighted -= 1;
// Scroll if needed // Scroll if needed
@ -489,11 +529,18 @@ pub fn autocompleteRect(
} }
} }
} else { } else {
// Abrir dropdown si está cerrado
state.openDropdown(); state.openDropdown();
} }
}, },
.down => { .down => {
if (state.open) { // Ctrl+Down: Forzar apertura dropdown (SIN limpiar texto)
if (ctx.input.modifiers.ctrl) {
state.highlighted = 0;
state.scroll_offset = 0;
state.openDropdown();
} else if (state.open) {
// Navegar hacia abajo en la lista
if (state.highlighted < @as(i32, @intCast(filtered_count)) - 1) { if (state.highlighted < @as(i32, @intCast(filtered_count)) - 1) {
state.highlighted += 1; state.highlighted += 1;
// Scroll if needed // Scroll if needed
@ -503,6 +550,7 @@ pub fn autocompleteRect(
} }
} }
} else { } else {
// Abrir dropdown si está cerrado
state.openDropdown(); state.openDropdown();
} }
}, },