Skip to content

Memory leak #2

@osmianski

Description

@osmianski

A test in my project reports a memory leak in zdotenv.

Error:

[gpa] (err): memory address 0x7f5bfe2e0000 leaked:
/home/vo/.zvm/0.14.1/lib/std/mem/Allocator.zig:423:40: 0x106a787 in dupe__anon_3850 (test)
    const new_buf = try allocator.alloc(T, m.len);
                                       ^
/home/vo/.cache/zig/p/zdotenv-0.0.0-AAAAAFEzAAD075plCeAiLUlPjxTYnzVmel3KKzK_RsCp/src/parser.zig:36:50: 0x112e492 in parse (test)
            const d_val = try self.allocator.dupe(u8, value);
                                                 ^
/home/vo/.cache/zig/p/zdotenv-0.0.0-AAAAAFEzAAD075plCeAiLUlPjxTYnzVmel3KKzK_RsCp/src/dotenv.zig:87:39: 0x112f11c in parseAndLoadEnv (test)
        var env_map = try parser.parse();
                                      ^
/home/vo/.cache/zig/p/zdotenv-0.0.0-AAAAAFEzAAD075plCeAiLUlPjxTYnzVmel3KKzK_RsCp/src/dotenv.zig:80:33: 0x112fe0d in load (test)
        try self.parseAndLoadEnv();
                                ^
/home/vo/zig-blog/lib/src/env.zig:39:20: 0x11329dd in loadEnv__anon_32367 (test)
    try dotenv.load();
                   ^
/home/vo/zig-blog/lib/src/env.zig:101:28: 0x1134ceb in test.loadEnv functionality (test)
    const env = try loadEnv(Env, testing_allocator);
                           ^
/home/vo/.zvm/0.14.1/lib/compiler/test_runner.zig:126:29: 0x1104bda in mainServer (test)
                test_fn.func() catch |err| switch (err) {
                            ^
...

The use case tested is basic, here is the code:

const builtin = @import("builtin");
const std = @import("std");
const utils = @import("utils.zig");
const zdotenv = @import("zdotenv");

const Allocator = std.mem.Allocator;
const eql = std.mem.eql;
const expect = std.testing.expect;
const expectEqualStrings = std.testing.expectEqualStrings;
const getCwd = std.process.getCwd;
const getEnvVarOwned = std.process.getEnvVarOwned;
const parseFloat = std.fmt.parseFloat;
const parseInt = std.fmt.parseInt;
const printFileName = utils.printFileName;
const printTestName = utils.printTestName;
const testing_allocator = std.testing.allocator;
const Zdotenv = zdotenv.Zdotenv;

pub const Env = struct {
    STRING_VAR: []const u8 = "foo",
    INT_VAR: i32 = 123,
    BOOL_VAR: bool = true,
    FLOAT_VAR: f32 = 1.23,
    DOUBLE_VAR: f64 = 1.23,
    CHAR_VAR: u8 = 'a',
};

pub fn loadEnv(comptime T: type, allocator: Allocator) !T {
    var env = T{};  // Start with defaults
    
    var dotenv = try Zdotenv.init(allocator);
    defer dotenv.deinit();

    // Load .env file using zdotenv, print a warning if it doesn't exist
    try dotenv.load();

    // Load .env.testing if we're in a test context, print a warning if it doesn't exist
    if (builtin.is_test) {
        var path_buffer: [std.fs.max_path_bytes]u8 = undefined;
        const cwd = try getCwd(path_buffer[0..]);
        
        const absolute_path = try std.fmt.allocPrint(allocator, "{s}/.env.testing", .{cwd});
        defer allocator.free(absolute_path);

        try dotenv.loadFromFile(absolute_path);
    }
    
    inline for (@typeInfo(T).@"struct".fields) |field| {
        const value = getEnvVarOwned(allocator, field.name) catch |err| {
            if (err == error.EnvironmentVariableNotFound) {
                comptime continue;
            }
            
            return err;
        };

        // getEnvVarOwned() leaves the caller responsibility to free the value, so we do that here.
        defer allocator.free(value);

        // Parse value based on field.type and set it
        @field(env, field.name) = try parseValue(field.type, value, allocator);
    }

    return env;
}

fn parseValue(comptime T: type, value: []const u8, allocator: Allocator) !T {
    switch (T) {
        []const u8 => {
            // For strings, we need to duplicate since the env var is freed
            return try allocator.dupe(u8, value);
        },
        i32, i64, i16, i8 => {
            return try parseInt(T, value, 10);
        },
        f32, f64 => {
            return try parseFloat(T, value);
        },
        bool => {
            return eql(u8, value, "true") or eql(u8, value, "1");
        },
        u8 => {
            return if (value.len > 0) value[0] else 0;
        },
        else => @compileError("Unsupported type: " ++ @typeName(T)),
    }
}

test {
    printFileName(@src());
}

test "loadEnv functionality" {
    printTestName(@src());

    // Load environment into struct (includes ".env" and ".env.testing" file loading)
    const env = try loadEnv(Env, testing_allocator);
    
    // Verify that .env.testing values are loaded (not .env values)
    try expectEqualStrings("testing_string", env.STRING_VAR);
    try expect(env.INT_VAR == 123);
    try expect(env.BOOL_VAR == true);
    try expect(env.FLOAT_VAR == 1.23);
    try expect(env.DOUBLE_VAR == 1.234);
    try expect(env.CHAR_VAR == 'T');
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions