core/src/main.c

#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include "panic.h"
#include "curve.h"
#include "procfs.h"
#include "memory.h"
#include "font.h"
#include "menu.h"
#include "tooltip.h"

SDL_Window *window;
SDL_Renderer *renderer;

static int window_width = 640;
static int window_height = 480;

static double scale = 1; 
static double pos_x = 1024;
static double pos_y = 0;
static bool brush_bit = true;

static pid_t pid = -1;
static bool is_child = false;
static bool stopped = false;
static page_list pages = {.fd = -1, .first = NULL};
static procfs_map *mappings = NULL;
static int n_mappings = 0;

static menu *current_menu = NULL;
static tooltip current_tooltip = {0};

static double get_scale() {
	if (scale >= 1) {
		return SDL_round(scale);
	}
	return scale;
}

static void add_scale(double amount) {
	float mx, my;
	SDL_GetMouseState(&mx, &my);
	double old_scale = get_scale();
	scale += amount;
	if (scale > 50) scale = 50;
	else if (scale < 0.125) scale = 0.125;
	pos_x -= mx/get_scale() - mx/old_scale;
	pos_y -= my/get_scale() - my/old_scale;
}

static void absolute_pos(double *x, double *y) {
	*x = *x / get_scale() + pos_x;
	*y = *y / get_scale() + pos_y;
}

static void go_to(uintptr_t addr) {
	int ipos_x, ipos_y;
	to_pos(addr, &ipos_x, &ipos_y);
	pos_x = ipos_x; pos_y = ipos_y;
	pos_x -= window_width / 2 / get_scale();
	pos_y -= window_height / 2 / get_scale();
}

static void detach() {
	if (pid == -1) return;
	if (pages.fd != -1) {
		int fd = pages.fd;
		free_page_list(&pages);
		close(fd);
	}
	if (is_child) {
		kill(pid, SIGKILL);
		while (true) {
			int status;
			pid_t w = waitpid(pid, &status, 0);
			if (w == -1) {
				break;
			}
			if (w == pid && WIFEXITED(status)) {
				break;
			}
		}
	}
	pid = -1;
	is_child = false;
}

static bool attach(pid_t p) {
	detach();
	pid = p;
	int fd = procfs_open(pid);
	if (fd == -1) {
		pid = -1;
		fprintf(stderr, "error attaching to pid %d: %s\n", p, strerror(errno));
		return false;
	}
	init_page_list(&pages, fd);
	stopped = false;
	n_mappings = procfs_maps(pid, &mappings);
	for (int i = 0; i < n_mappings; i++) {
		if (strcmp("[stack]", mappings[i].name) == 0) {
			go_to(mappings[i].max);
		}
	}
	return true;
}

static bool spawn(char *const cmdline[]) {
	detach();
	int fildes[2];
	if (pipe(fildes) == -1) {
		fprintf(stderr, "error starting child: pipe: %d\n", strerror(errno));
		return false;
	}
	pid_t child = fork();
	if (child == -1) {
		fprintf(stderr, "error starting child: %d\n", strerror(errno));
		close(fildes[0]);
		close(fildes[1]);
		return false;
	}
	if (child == 0) {
		close(fildes[0]);
		if (fcntl(fildes[1], F_SETFD, FD_CLOEXEC) == -1) {
			perror("fcntl");
			exit(-1);
		}
		if (execvp(cmdline[0], cmdline) == -1) {
			const char *err = strerror(errno);
			write(fildes[1], err, strlen(err));
			exit(-1);
		}
	}
	close(fildes[1]);
	char child_err[2048] = {0};
	ssize_t n = read(fildes[0], child_err, 2047);
	close(fildes[0]);
	if (n == -1) {
		fprintf(stderr,
			"error starting child: read pipe: %d\n", strerror(errno)
		);
		goto err;
	}
	if (n != 0) {
		fprintf(stderr, "%s: %s\n", cmdline[0], child_err);
	}

	if (attach(child)) {
		is_child = true;
		return true;
	}
err:
	kill(child, SIGKILL);
	while (true) {
		int status;
		pid_t w = waitpid(child, &status, 0);
		if (w == -1) {
			break;
		}
		if (w == child && WIFEXITED(status)) {
			break;
		}
	}
	return false;
}

static void handle_child() {
	int status;
	pid_t w = waitpid(pid, &status, WNOHANG);
	if (w == -1) {
		return;
	}
	if (w == pid && WIFEXITED(status)) {
		is_child = false;
		detach();
		fprintf(
			stderr, "child exited with status %d\n", WEXITSTATUS(status)
		);
	}
}

static void replace_menu(menu *new) {
	if (current_menu) close_menu(current_menu);
	current_menu = new;
}

static void map_menu_selected(void *data) {
	go_to((uintptr_t) data);
}

static void open_map_menu() {
	menu_entry *entries = calloc(n_mappings, sizeof(menu_entry));
	for (int i = 0; i < n_mappings; i++) {
		entries[i].action = map_menu_selected;
		entries[i].data = (void *) mappings[i].base;
		snprintf(
			entries[i].name, sizeof(entries[i].name), "%p %s",
			mappings[i].base, mappings[i].name
		);
	}
	float mx, my;
	SDL_GetMouseState(&mx, &my);
	replace_menu(create_menu(entries, n_mappings, mx, my));
}

static bool held(SDL_Keycode k) {
	const bool *state = SDL_GetKeyboardState(NULL);
	return state[SDL_GetScancodeFromKey(k, NULL)];
}

static void handle_key(SDL_Keycode key) {
	switch (key) {
	case SDLK_SPACE:
		if (pid == -1) break;
		kill(pid, stopped ? SIGSTOP : SIGCONT);
		stopped = !stopped;
		break;
	case SDLK_E:
		brush_bit = !brush_bit;
		break;
	default:
	}
}

static bool handle_event(SDL_Event e) {
	if (current_menu) {
		if (!menu_handle_event(current_menu, e)) {
			close_menu(current_menu);
			current_menu = NULL;
		}
		return true;
	}
	switch (e.type) {
	case SDL_EVENT_KEY_DOWN:
		handle_key(e.key.key);
		break;
	case SDL_EVENT_MOUSE_MOTION:
		SDL_MouseButtonFlags state = e.motion.state;
		if (state & SDL_BUTTON_LMASK) {
			pos_x -= (double) e.motion.xrel / get_scale();
			pos_y -= (double) e.motion.yrel / get_scale();
		}
		break;
	case SDL_EVENT_MOUSE_BUTTON_DOWN:
		uint8_t button = e.button.button;
		if (button == SDL_BUTTON_MIDDLE) {
			open_map_menu();
		} else if (button == SDL_BUTTON_RIGHT && held(SDLK_LSHIFT)) {
			open_map_menu();
		} else if (button == SDL_BUTTON_RIGHT && held(SDLK_RSHIFT)) {
			open_map_menu();
		}
		break;
	case SDL_EVENT_MOUSE_WHEEL:
		add_scale(e.wheel.y * get_scale() / 10);
		break;
	case SDL_EVENT_QUIT:
		return false;
		break;
	default:
	}
	return true;
}

static void update() {
	SDL_GetWindowSize(window, &window_width, &window_height);
	if (pid != -1) {
		n_mappings = procfs_maps(pid, &mappings);
		if (is_child) handle_child();
	}

	if (current_menu) return;

	if (held(SDLK_W)) {
		pos_y -= 5 / get_scale();
	}
	if (held(SDLK_S)) {
		pos_y += 5 / get_scale();
	}
	if (held(SDLK_A)) {
		pos_x -= 5 / get_scale();
	}
	if (held(SDLK_D)) {
		pos_x += 5 / get_scale();
	}
	float mx, my;
	static bool mouse_held = false;
	static double px, py;
	SDL_MouseButtonFlags state = SDL_GetMouseState(&mx, &my);
	if (state & SDL_BUTTON_RMASK) {
		double x2 = mx, y2 = my;
		absolute_pos(&x2, &y2);
		double x1, y1;
		if (mouse_held) {
			x1 = px; y1 = py;
		} else {
			x1 = x2; y1 = y2;
		}
		px = x2; py = y2;
		mouse_held = true;
		draw_line(&pages, x1, y1, x2, y2, brush_bit);
	} else {
		mouse_held = false;
	}

	double x = mx, y = my;
	absolute_pos(&x, &y);
	uintptr_t addr = to_addr(x, y);
	uintptr_t page_addr = addr - addr % PAGE_SIZE;
	static uint8_t page_data[PAGE_SIZE];
	if (read_mem(pages.fd, page_addr, page_data, PAGE_SIZE) == 0) {
		uint8_t hover_byte = page_data[addr % PAGE_SIZE];
		static char tooltip_buf[1024];
		int n = snprintf(tooltip_buf, sizeof(tooltip_buf),
			"%p\n0x%02x", addr, hover_byte
		);
		char *p = tooltip_buf + n;
		if (hover_byte < 128 && hover_byte != 0) {
			p = stpcpy(p, "\n\"");
			int min, max;
			for (min = addr % PAGE_SIZE; min > 0; min--) {
				uint8_t next = page_data[min - 1];
				if (next >= 128 || next == 0) break;
			}
			for (max = addr % PAGE_SIZE; max < PAGE_SIZE; max++) {
				uint8_t cur = page_data[max];
				if (cur >= 128 || cur == 0) break;
			}
			while (max - min > 64) {
				if (max - addr % PAGE_SIZE > addr % PAGE_SIZE - min) {
					max--;
				} else {
					min++;
				}
			}
			memcpy(p, page_data + min, max - min);
			p += max - min;
			p = stpcpy(p, "\"");
		}
		set_tooltip_text(&current_tooltip, tooltip_buf);
	} else {
		set_tooltip_text(&current_tooltip, NULL);
	}
}

static SDL_Texture *missing_texture();

static void show_pages() {
	double s = get_scale();

	int page_x = pos_x / PAGE_WIDTH;
	int page_y = pos_y / PAGE_HEIGHT;

	double offs_x = SDL_fmod(pos_x, PAGE_WIDTH) * s;
	double offs_y = SDL_fmod(pos_y, PAGE_HEIGHT) * s;

	double page_width = PAGE_WIDTH * s;
	double page_height = PAGE_HEIGHT * s;

	double view_width = window_width / page_width;
	double view_height = window_height / page_height;

	for (int draw_y = -1; draw_y <= view_height + 1; draw_y++) {
		for (int draw_x = -1; draw_x <= view_width + 1; draw_x++) {
			SDL_FRect src = {0, 0, PAGE_WIDTH, PAGE_HEIGHT};
			SDL_FRect dest = {
				draw_x * page_width - offs_x, draw_y * page_height - offs_y,
				page_width, page_height
			};
			SDL_Texture *tex = NULL;
			if (pid != -1) {
				uintptr_t addr = zorder(page_x + draw_x, page_y + draw_y);
				addr *= PAGE_SIZE;
				page *p = get_page(&pages, addr);
				tex = get_texture(p);
			}
			if (!tex) {
				tex = missing_texture();
			}
			if (s < 0.75) {
				SDL_SetTextureScaleMode(tex, SDL_SCALEMODE_LINEAR);
			} else {
				SDL_SetTextureScaleMode(tex, SDL_SCALEMODE_NEAREST);
			}
			SDL_RenderTexture(renderer, tex, &src, &dest);
		}
	}
	free_unused_pages(&pages);
}

static SDL_Texture *missing_texture() {
	static SDL_Texture *tex = NULL;
	if (!tex) {
		tex = must(SDL_CreateTexture(renderer,
			SDL_PIXELFORMAT_RGBA32,
			SDL_TEXTUREACCESS_STREAMING,
			PAGE_WIDTH, PAGE_HEIGHT
		));
		uint32_t *pixels; int pitch;
		SDL_Rect rect = {0, 0, PAGE_WIDTH, PAGE_HEIGHT};
		must(SDL_LockTexture(tex, &rect, (void **) &pixels, &pitch));
		for (int i = 0; i < PAGE_WIDTH * PAGE_HEIGHT; i++) {
			if (((i / 32) + i / PAGE_WIDTH / 32) % 2) {
				pixels[i] = 0xFF151515;
			} else {
				pixels[i] = 0xFF222222;
			}
		}
		SDL_UnlockTexture(tex);
	}
	return tex;
}

static void show() {
	SDL_GetWindowSize(window, &window_width, &window_height);
	SDL_RenderClear(renderer);
	show_pages();
	SDL_RenderPresent(renderer);
	if (current_menu) {
		menu_render(current_menu);
		hide_tooltip(&current_tooltip);
	} else {
		float mx, my;
		SDL_GetMouseState(&mx, &my);
		show_tooltip(&current_tooltip, mx, my);
	}
}

static void main_loop() {
	int last_time = 0, current_time = 0;
	while (true) {
		SDL_Event e;
		while (SDL_PollEvent(&e)) {
			if (!handle_event(e)) {
				return;
			}
		}
		update();
		show();

		last_time = current_time;
		current_time = SDL_GetTicks();
		int elapsed = current_time - last_time;
		if (elapsed < 5) SDL_Delay(5 - elapsed);
	}
}

static void usage(const char *name) {
	fprintf(stderr, "usage: %s -p <pid>\n", name);
	fprintf(stderr, "       %s command [arg...]\n", name);
	exit(-1);
}

int main(int argc, char *argv[]) {
	must(TTF_Init());
	load_font();
	must(SDL_Init(SDL_INIT_VIDEO));
	must(SDL_CreateWindowAndRenderer("core",
		window_width, window_height, SDL_WINDOW_RESIZABLE, &window, &renderer
	));
	SDL_EnableScreenSaver();
	SDL_SetRenderVSync(renderer, 1);

	if (argc > 1) {
		if (argv[1][0] == '-') {
			if (strcmp(argv[1], "-p") == 0) {
				if (argc < 3) {
					usage(argv[0]);
				}
				pid_t arg = strtol(argv[2], NULL, 10);
				if (arg == 0 && errno == EINVAL) {
					usage(argv[0]);
				}
				attach(arg);
			} else {
				usage(argv[0]);
			}
		} else {
			spawn(argv + 1);
		}
	}
	main_loop();
	detach();
	SDL_DestroyRenderer(renderer);
	SDL_DestroyWindow(window);
	SDL_Quit();
	return 0;
}