Implement various filesystem utility functions in C so they can be used in the launcher
This commit is contained in:
parent
f49dbfaa7e
commit
76ac66fc8c
3 changed files with 289 additions and 0 deletions
|
|
@ -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
194
kitty/launcher/utils.h
Normal 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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue