StringArrayHashmap doesn't allocate for Keys?

I’m trying to list put gnupg groups into hash maps with the group names as keys and values being the Key IDs from the groups. However when returning a StringArrayHashmap from my function the key name appears as a function when iterating over the key values. Does StringArrayHashmap only allocate memory for its values and not keys? You can find the function I’ve written below:

/// Returns a HashMap of all GNUPG groups and their KeyIDs
fn getGPGGroups(allocator: std.mem.Allocator) !std.StringArrayHashMap([]const []const u8) {
    var groups = std.StringArrayHashMap([]const []const u8).init(allocator);
    const res = try std.ChildProcess.run(.{
        .allocator = allocator,
        .argv = &.{ "gpg", "--list-config", "--with-colons" },
        .cwd = null,
        .cwd_dir = null,
    });
    defer {
        allocator.free(res.stderr);
        allocator.free(res.stdout);
    }

    var stream = std.io.fixedBufferStream(res.stdout);
    const reader = stream.reader();

    var line = std.ArrayList(u8).init(allocator);
    var ids = std.ArrayList([]const u8).init(allocator);
    defer {
        line.deinit();
        ids.deinit();
    }

    const writer = line.writer();
    while (reader.streamUntilDelimiter(writer, '\n', null)) {
        // Clear the line so we can reuse it.
        defer {
            ids.clearRetainingCapacity();
            line.clearRetainingCapacity();
        }

        if (!std.mem.startsWith(u8, line.items, "cfg:group")) continue;
        if (line.items[line.items.len - 1] == '\r') _ = line.pop();

        var line_iter = std.mem.splitScalar(u8, line.items, ':');
        _ = line_iter.first(); // cfg
        _ = line_iter.next(); // group
        const group_name = line_iter.next().?;
        const keyIds = line_iter.next().?;
        var id_iter = std.mem.splitScalar(u8, keyIds, ';');
        while (id_iter.next()) |id| {
            try ids.append(id);
        }
        std.debug.print("{s}\n", .{group_name});
        try groups.put(group_name, ids.items);
    } else |err| switch (err) {
        error.EndOfStream => {}, // Continue on
        else => return err, // Propagate error
    }
    
    return groups;
}

Any help here would be greatly appreciated!
I use the test below, I have one group that I called ‘helo’ just to test.
But get a segmentation fault when performing the test outside of the function.

test "getGPGGroups" {
    const alloc = std.testing.allocator;
    var groups = try getGPGGroups(alloc);
    defer groups.deinit();

    try std.testing.expect(groups.contains("helo"));
}

Edit
I realize I didn’t include any test data, The group line for my group ‘helo’ looks like this.
cfg:group:helo:AFEC18BD8EDE74098290EB125D93694E918198B9;8071489F45AB0284A6AE258F04934E5882A67089

From the documentation:


So yeah, you need to allocate keys on your own.
The easiest solution would be to use an ArenaAllocator, then you don’t need to keep track of freeing all the strings individually, instead you just need to deinit the arena once you are done using the HashMap.

3 Likes

So if I understand correctly I should replace the testing allocator with the arena allocator and perform allocator.dupe() on the group_name variable?

Yes, allocator.dupe will allocate a new string for each key and if using the arena, it’s deinit will free them all at once. If not using the arena, you can iterate over the keys and free them before calling deinit on the hash map itself:

defer {
    const keys = map.keys();
    for (keys) |k| allocator.free(k);
    map.deinit();
}
1 Like

All the version of HashMap (except Array), so StringHashMap and AutoHashMap are one-line wrappers around the regular HashMap with a special compare function - no more no less. Anything you would need to do for HashMap you need to do with String and Auto HM.

HM will not duplicate my slice or pointer keys (I hope it doesn’t) , so String/Auto will not either.

The source code is now in the docs, so hopefully people notice from the source at the bottom that is just creates am HM with a different compare function.

Thanks for the tip!
I really want to try to keep the testing allocator as I want to get better at managing memory.

Thanks for the help everyone!
I don’t know why I missed the documentation stating that the caller handles key memory. I guess I just automatically (mistakenly) assumed that everything that takes an allocator allocates everything the implementation touches.
I’ll mark @IntegratedQuantum’s answer as the solution but really appreciate @dude_the_builder’s tip on how to free keys in a map.

1 Like