Implement various filesystem utility functions in C so they can be used in the launcher

This commit is contained in:
Kovid Goyal 2025-04-24 20:36:51 +05:30
parent f49dbfaa7e
commit 76ac66fc8c
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
3 changed files with 289 additions and 0 deletions

View file

@ -13,6 +13,7 @@
#endif
#include "char-props.h"
#include "launcher/utils.h"
#include "line.h"
#include "charsets.h"
#include "base64.h"
@ -635,13 +636,50 @@ py_char_props_for(PyObject *self UNUSED, PyObject *ch) {
#undef B
}
static PyObject*
expanduser(PyObject *self UNUSED, PyObject *path) {
if (!PyUnicode_Check(path)) { PyErr_SetString(PyExc_TypeError, "path must a string"); return NULL; }
char buf[PATH_MAX + 1];
expand_tilde(PyUnicode_AsUTF8(path), buf, arraysz(buf));
return PyUnicode_FromString(buf);
}
static PyObject*
abspath(PyObject *self UNUSED, PyObject *path) {
if (!PyUnicode_Check(path)) { PyErr_SetString(PyExc_TypeError, "path must a string"); return NULL; }
char buf[PATH_MAX + 1];
lexical_absolute_path(PyUnicode_AsUTF8(path), buf, arraysz(buf));
return PyUnicode_FromString(buf);
}
static PyObject*
py_makedirs(PyObject *self UNUSED, PyObject *args) {
int mode = 0755; const char *p; Py_ssize_t sz;
if (!PyArg_ParseTuple(args, "s#|i", &p, &sz, &mode)) return NULL;
RAII_PyObject(b, PyBytes_FromStringAndSize(p, sz));
if (!makedirs(PyBytes_AS_STRING(b), mode)) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
Py_RETURN_NONE;
}
static PyObject*
py_get_config_dir(PyObject *self UNUSED, PyObject *args UNUSED) {
char buf[PATH_MAX];
if (get_config_dir(buf, PATH_MAX)) return PyUnicode_FromString(buf);
return PyUnicode_FromString("");
}
static PyMethodDef module_methods[] = {
METHODB(replace_c0_codes_except_nl_space_tab, METH_O),
{"wcwidth", (PyCFunction)wcwidth_wrap, METH_O, ""},
{"expanduser", (PyCFunction)expanduser, METH_O, ""},
{"abspath", (PyCFunction)abspath, METH_O, ""},
{"expand_ansi_c_escapes", (PyCFunction)expand_ansi_c_escapes, METH_O, ""},
{"get_docs_ref_map", (PyCFunction)get_docs_ref_map, METH_NOARGS, ""},
{"get_config_dir", (PyCFunction)py_get_config_dir, METH_NOARGS, ""},
{"wcswidth", (PyCFunction)wcswidth_std, METH_O, ""},
{"open_tty", open_tty, METH_VARARGS, ""},
{"makedirs", py_makedirs, METH_VARARGS, ""},
{"normal_tty", normal_tty, METH_VARARGS, ""},
{"raw_tty", raw_tty, METH_VARARGS, ""},
{"close_tty", close_tty, METH_VARARGS, ""},
@ -737,6 +775,7 @@ shift_to_first_set_bit(CellAttrs x) {
return ans;
}
EXPORTED PyMODINIT_FUNC
PyInit_fast_data_types(void) {
PyObject *m;

194
kitty/launcher/utils.h Normal file
View file

@ -0,0 +1,194 @@
/*
* utils.h
* Copyright (C) 2025 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#pragma once
#include <stddef.h>
#include <stdlib.h>
#include <stdbool.h>
#include <pwd.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
static const char* home = NULL;
static void
ensure_home_path(void) {
if (home) return;
home = getenv("HOME");
if (!home || !home[0]) {
struct passwd* pw = getpwuid(geteuid());
if (pw) home = pw->pw_dir;
}
if (!home || !home[0]) {
fprintf(stderr, "Fatal error: Cannot determine home directory\n"); exit(1);
}
}
static const char*
home_path_for(const char *username) {
struct passwd* pw = getpwnam(username);
if (pw) return pw->pw_dir;
return NULL;
}
static inline void
expand_tilde(const char* path, char *ans, size_t ans_sz) {
if (path[0] != '~') {
snprintf(ans, ans_sz, "%s", path);
return;
}
const char *prefix = NULL, *sep = "";
if (path[1] == '/' || path[1] == '\0') {
// If the path is "~" or "~/something", get the current user's home directory
ensure_home_path();
prefix = home;
} else {
// If the path is "~user/something", get the specified user's home directory
char* slash = strchr(path, '/');
if (slash) {
*slash = 0; prefix = home_path_for(path + 1); *slash = '/';
} else prefix = home_path_for(path + 1);
if (prefix) path = slash ? slash - 1 : "a";
else {
prefix = "";
path--;
}
}
// Construct the expanded path
snprintf(ans, ans_sz, "%s%s%s", prefix, sep, path + 1);
}
static size_t
clean_path(char *path) {
char *write_ptr = path;
char* read_ptr = path;
while (*read_ptr) {
if (read_ptr[0] != '/') { *write_ptr++ = *read_ptr++; continue; }
// we have /
if (read_ptr[1] == '/') { read_ptr++; continue; } // skip one slash of double slash
if (read_ptr[1] != '.') { *write_ptr++ = *read_ptr++; continue; }
// we have /.
if (read_ptr[2] == '/' || !read_ptr[2]) { read_ptr += 2; continue; } // skip /./
if (read_ptr[2] != '.') { *write_ptr++ = *read_ptr++; continue; }
// we have /..
if (read_ptr[3] == '/' || !read_ptr[3]) {
read_ptr += 3;
while (write_ptr > path) {
write_ptr--;
if (*write_ptr == '/') break;
}
} else *write_ptr++ = *read_ptr++;
}
// remove trailing slashes
while (write_ptr > path + 1 && *(write_ptr - 1) == '/') write_ptr--;
// Null-terminate the normalized path
*write_ptr++ = '\0';
return write_ptr - path - 1;
}
static size_t
lexical_absolute_path(const char* relative, char *output, size_t outsz) {
size_t rlen = strlen(relative);
char *limit = output + outsz;
char* write_ptr = output; // Points to the location to write normalized characters
#define _ensure_space(n) if (write_ptr + n + 1 >= limit) { fprintf(stderr, "Out of buffer space making absolute path for: %s with cwd: %s\n", relative, output); exit(1); }
if (relative[0] != '/') {
if (!getcwd(output, outsz)) {
perror("Getting the current working directory failed with error");
exit(1);
}
size_t cwdlen = strlen(output);
write_ptr = output + cwdlen;
_ensure_space(cwdlen + rlen + 2);
if (rlen && cwdlen && *(write_ptr - 1) != '/') *(write_ptr++) = '/';
} else { _ensure_space(rlen + 2); }
#undef _ensure_space
// Append the relative path
memcpy(write_ptr, relative, rlen);
*(write_ptr + rlen) = 0;
size_t ans = clean_path(output);
// Ensure the path is not empty
if (output[0] == '\0') {
output[0] = '/'; output[1] = 0;
ans = 1;
}
return ans;
}
static bool
makedirs_cleaned(char *path, int mode, struct stat *buffer) {
if (stat(path, buffer) == 0) {
if (S_ISDIR(buffer->st_mode)) return true;
errno = ENOTDIR;
return false;
}
if (errno == ENOTDIR) return false;
char *p = strrchr(path, '/');
if (p && p > path) {
p[0] = 0;
bool parent_created = makedirs_cleaned(path, mode, buffer);
p[0] = '/';
if (!parent_created) return false;
}
// Now parent exists
return mkdir(path, mode) == 0;
}
static bool
makedirs(char *path /* path is modified by this function */, int mode) {
struct stat buffer;
clean_path(path);
return makedirs_cleaned(path, mode, &buffer);
}
static bool
is_dir_ok_for_config(char *q) {
size_t len = strlen(q);
memcpy(q + len, "/kitty", sizeof("/kitty"));
len += sizeof("/kitty") - 1;
memcpy(q + len, "/kitty.conf", sizeof("/kitty.conf"));
if (access(q, F_OK) != 0) return false;
q[len] = 0;
return access(q, W_OK) == 0;
}
static inline bool
get_config_dir(char *output, size_t outputsz) {
const char *q;
char buf1[PATH_MAX], buf2[PATH_MAX];
#define expand(x, dest, sz) { expand_tilde(x, buf1, sizeof(buf1)); lexical_absolute_path(buf1, dest, sz); }
q = getenv("KITTY_CONFIG_DIRECTORY"); if (q && q[0]) { expand(q, output, outputsz); return true; }
#define check_and_ret(x) if (x && x[0]) { expand(x, output, outputsz); if (is_dir_ok_for_config(output)) return true; }
q = getenv("XDG_CONFIG_HOME"); check_and_ret(q);
check_and_ret("~/.config");
#ifdef __APPLE__
check_and_ret("~/Library/Preferences");
#endif
q = getenv("XDG_CONFIG_DIRS");
if (q && q[0]) {
snprintf(buf2, sizeof(buf2), "%s", q);
char *s, *token = strtok_r(buf2, ":", &s);
while (token) {
check_and_ret(token);
token = strtok_r(NULL, ":", &s);
}
}
q = getenv("XDG_CONFIG_HOME");
if (!q || !q[0]) q = "~/.config";
expand(q, buf2, sizeof(buf2));
snprintf(output, outputsz, "%s/kitty", buf2);
if (makedirs(output, 0755)) return true;
return false;
#undef expand
#undef check_and_ret
}

View file

@ -3,6 +3,7 @@
import json
import os
import shutil
import sys
import tempfile
@ -11,8 +12,12 @@
Color,
HistoryBuf,
LineBuf,
abspath,
char_props_for,
expand_ansi_c_escapes,
expanduser,
get_config_dir,
makedirs,
parse_input_from_terminal,
replace_c0_codes_except_nl_space_tab,
split_into_graphemes,
@ -461,6 +466,57 @@ def on_csi(x):
self.assertTrue(is_ok_to_read_image_file(tf.name, tf.fileno()), fifo)
self.ae(sanitize_url_for_dispay_to_user(
'h://a\u0430b.com/El%20Ni%C3%B1o/'), 'h://xn--ab-7kc.com/El Niño/')
for x in ('~', '~/', '', '~root', '~root/~', '/~', '/a/b/', '~xx/a', '~~'):
self.assertEqual(os.path.expanduser(x), expanduser(x), x)
for x in (
'/', '', '/a', '/ab', '/ab/', '/ab/c', 'a', 'ab', 'ab/', 'ab///c', 'ab/././..', '.', '..', '../', './', '../..', '../.',
'/a/../..', '/a/../../', '/a/..', '/ab/../../../cd/.', '///',
):
self.assertEqual(os.path.abspath(x), abspath(x), repr(x))
self.assertEqual('/', abspath('//'))
with tempfile.TemporaryDirectory() as tdir:
for x, ex in {
'a': None, 'a/b/c': None, 'a/..': None, 'a/../a': None,
'a/f': NotADirectoryError, 'a/f/d': NotADirectoryError, 'a/b/c/f/g': NotADirectoryError,
}.items():
q = os.path.join(tdir, x)
if ex is None:
makedirs(q)
open(os.path.join(q, 'f'), 'wb').close()
else:
with self.assertRaises(ex, msg=x):
makedirs(q)
saved = {x: os.environ.get(x) for x in 'KITTY_CONFIG_DIRECTORY XDG_CONFIG_DIRS XDG_CONFIG_HOME'.split()}
try:
dot_config = os.path.expanduser('~/.config')
if os.path.exists(dot_config):
shutil.rmtree(dot_config)
with tempfile.TemporaryDirectory() as tdir:
os.makedirs(tdir + '/good/kitty')
open(tdir + '/good/kitty/kitty.conf', 'w').close()
open(tdir + '/f', 'w').close()
for x in (
(f'KITTY_CONFIG_DIRECTORY={tdir}', f'{tdir}'),
(f'XDG_CONFIG_HOME={tdir}/good', f'{tdir}/good/kitty'),
(f'XDG_CONFIG_DIRS={tdir}:{tdir}/good', f'{tdir}/good/kitty'),
(f'XDG_CONFIG_DIRS={tdir}:{tdir}/bad:{tdir}/f', f'{dot_config}/kitty'),
(f'{dot_config}/kitty',),
):
for k in saved:
os.environ.pop(k, None)
for e in x[:-1]:
k, v = e.partition('=')[::2]
os.environ[k] = v
self.assertEqual(x[-1], get_config_dir(), str(x))
finally:
if os.path.exists(dot_config):
shutil.rmtree(dot_config)
for k in saved:
os.environ.pop(k, None)
if saved[k] is not None:
os.environ[k] = saved[k]
def test_historybuf(self):
lb = filled_line_buf()