//! Reads memory from any address of the current location using OS-specific
//! syscalls, bypassing memory page protection. Useful for stack unwinding.

const builtin = @import("builtin");
const native_os = builtin.os.tag;

const std = @import("../std.zig");
const posix = std.posix;
const File = std.fs.File;
const page_size_min = std.heap.page_size_min;

const MemoryAccessor = @This();

var cached_pid: posix.pid_t = -1;

mem: switch (native_os) {
    .linux => File,
    else => void,
},

pub const init: MemoryAccessor = .{
    .mem = switch (native_os) {
        .linux => .{ .handle = -1 },
        else => {},
    },
};

pub fn deinit(ma: *MemoryAccessor) void {
    switch (native_os) {
        .linux => switch (ma.mem.handle) {
            -2, -1 => {},
            else => ma.mem.close(),
        },
        else => {},
    }
    ma.* = undefined;
}

fn read(ma: *MemoryAccessor, address: usize, buf: []u8) bool {
    switch (native_os) {
        .linux => while (true) switch (ma.mem.handle) {
            -2 => break,
            -1 => {
                const linux = std.os.linux;
                const pid = switch (@atomicLoad(posix.pid_t, &cached_pid, .monotonic)) {
                    -1 => pid: {
                        const pid = linux.getpid();
                        @atomicStore(posix.pid_t, &cached_pid, pid, .monotonic);
                        break :pid pid;
                    },
                    else => |pid| pid,
                };
                const bytes_read = linux.process_vm_readv(
                    pid,
                    &.{.{ .base = buf.ptr, .len = buf.len }},
                    &.{.{ .base = @ptrFromInt(address), .len = buf.len }},
                    0,
                );
                switch (linux.E.init(bytes_read)) {
                    .SUCCESS => return bytes_read == buf.len,
                    .FAULT => return false,
                    .INVAL, .SRCH => unreachable, // own pid is always valid
                    .PERM => {}, // Known to happen in containers.
                    .NOMEM => {},
                    .NOSYS => {}, // QEMU is known not to implement this syscall.
                    else => unreachable, // unexpected
                }
                var path_buf: [
                    std.fmt.count("/proc/{d}/mem", .{std.math.minInt(posix.pid_t)})
                ]u8 = undefined;
                const path = std.fmt.bufPrint(&path_buf, "/proc/{d}/mem", .{pid}) catch
                    unreachable;
                ma.mem = std.fs.openFileAbsolute(path, .{}) catch {
                    ma.mem.handle = -2;
                    break;
                };
            },
            else => return (ma.mem.pread(buf, address) catch return false) == buf.len,
        },
        else => {},
    }
    if (!isValidMemory(address)) return false;
    @memcpy(buf, @as([*]const u8, @ptrFromInt(address)));
    return true;
}

pub fn load(ma: *MemoryAccessor, comptime Type: type, address: usize) ?Type {
    var result: Type = undefined;
    return if (ma.read(address, std.mem.asBytes(&result))) result else null;
}

pub fn isValidMemory(address: usize) bool {
    // We are unable to determine validity of memory for freestanding targets
    if (native_os == .freestanding or native_os == .other or native_os == .uefi) return true;

    const page_size = std.heap.pageSize();
    const aligned_address = address & ~(page_size - 1);
    if (aligned_address == 0) return false;
    const aligned_memory = @as([*]align(page_size_min) u8, @ptrFromInt(aligned_address))[0..page_size];

    if (native_os == .windows) {
        const windows = std.os.windows;

        var memory_info: windows.MEMORY_BASIC_INFORMATION = undefined;

        // The only error this function can throw is ERROR_INVALID_PARAMETER.
        // supply an address that invalid i'll be thrown.
        const rc = windows.VirtualQuery(@ptrCast(aligned_memory), &memory_info, aligned_memory.len) catch {
            return false;
        };

        // Result code has to be bigger than zero (number of bytes written)
        if (rc == 0) {
            return false;
        }

        // Free pages cannot be read, they are unmapped
        if (memory_info.State == windows.MEM_FREE) {
            return false;
        }

        return true;
    } else if (have_msync) {
        posix.msync(aligned_memory, posix.MSF.ASYNC) catch |err| {
            switch (err) {
                error.UnmappedMemory => return false,
                else => unreachable,
            }
        };

        return true;
    } else {
        // We are unable to determine validity of memory on this target.
        return true;
    }
}

const have_msync = switch (native_os) {
    .wasi, .emscripten, .windows => false,
    else => true,
};
