├── version ├── tests ├── lua │ ├── empty.lua │ ├── broken-syntax.lua │ ├── broken-semantics.lua │ ├── override.lua │ └── handler.lua ├── messages.c ├── counter.c ├── deref.c ├── mountinfo.c ├── config-static.c ├── bitmap.c ├── elfinterp.c ├── extension.c ├── journal.c ├── list.c ├── storepath.c ├── timestamp.c ├── main.c ├── meson.build ├── logstep.c ├── config-lua.c ├── buffer.c ├── parents.c ├── sieve.c ├── params.c ├── linq.c ├── sync.c ├── set.c ├── config.c ├── trace.c └── handler.c ├── web ├── static │ ├── robots.txt │ ├── logo.svg │ ├── demo.cast │ ├── favicon.ico │ └── _headers ├── .npmrc ├── docs │ ├── configuration.md │ ├── mounts.md │ ├── projects.md │ ├── editors.md │ ├── index.md │ ├── security.md │ ├── usage.md │ ├── cli.md │ └── installation.md ├── babel.config.js ├── .gitignore ├── custom.css ├── src │ └── asciinema.jsx ├── package.json └── docusaurus.config.js ├── misc ├── .gitignore ├── fuzz.service.ini ├── logo.svg ├── fuzz.playbook.yaml └── demo.cast ├── package.json ├── inc ├── logstep.h ├── elfinterp.h ├── extension.h ├── deref.h ├── timestamp.h ├── counter.h ├── parents.h ├── meson.build ├── mountinfo.h ├── sync.h ├── journal.h ├── bitmap.h ├── storepath.h ├── list.h ├── sieve.h ├── handler.h ├── params.h ├── linq.h ├── set.h ├── buffer.h ├── messages.h ├── trace.h └── config.h ├── meson_options.txt ├── pnpm-lock.yaml ├── README.md ├── lua ├── weave.awk ├── meson.build └── config.lua.md ├── meson.build ├── src ├── extension.c ├── timestamp.c ├── meson.build ├── logstep.c ├── deref.c ├── list.c ├── bitmap.c ├── counter.c ├── elfinterp.c ├── parents.c ├── storepath.c ├── journal.c ├── sieve.c ├── mountinfo.c ├── messages.c ├── params.c ├── trace.c ├── set.c ├── buffer.c ├── sync.c ├── main.c ├── config-static.c ├── linq.c ├── config-lua.c └── handler.c ├── .github └── workflows │ ├── build-and-test.yaml │ └── release.yaml ├── fuzz ├── meson.build └── elfinterp.c ├── SECURITY.md ├── default.nix ├── flake.lock └── flake.nix /version: -------------------------------------------------------------------------------- 1 | 1.1.4 2 | -------------------------------------------------------------------------------- /tests/lua/empty.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/static/robots.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /misc/.gitignore: -------------------------------------------------------------------------------- 1 | hosts.ini 2 | -------------------------------------------------------------------------------- /web/.npmrc: -------------------------------------------------------------------------------- 1 | node-linker = hoisted 2 | -------------------------------------------------------------------------------- /web/static/logo.svg: -------------------------------------------------------------------------------- 1 | ../../misc/logo.svg -------------------------------------------------------------------------------- /web/static/demo.cast: -------------------------------------------------------------------------------- 1 | ../../misc/demo.cast -------------------------------------------------------------------------------- /web/docs/configuration.md: -------------------------------------------------------------------------------- 1 | ../../lua/config.lua.md -------------------------------------------------------------------------------- /tests/lua/broken-syntax.lua: -------------------------------------------------------------------------------- 1 | editors = { 2 | vim = true 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "klunok", 3 | "version": "0.0.0", 4 | "private": true 5 | } 6 | -------------------------------------------------------------------------------- /tests/lua/broken-semantics.lua: -------------------------------------------------------------------------------- 1 | version_pattern = 3 2 | editors = { 3 | "vi", 4 | "vim", 5 | } 6 | -------------------------------------------------------------------------------- /tests/lua/override.lua: -------------------------------------------------------------------------------- 1 | version_pattern = "override" 2 | editors.vi = nil 3 | editors.cat = true 4 | -------------------------------------------------------------------------------- /web/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kharacternyk/klunok/HEAD/web/static/favicon.ico -------------------------------------------------------------------------------- /tests/lua/handler.lua: -------------------------------------------------------------------------------- 1 | version_pattern = "version" 2 | editors['handler.lua'] = true 3 | debounce_seconds = 0 4 | -------------------------------------------------------------------------------- /inc/logstep.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void logstep(int fd, const char *prefix, const char *message, size_t depth); 4 | -------------------------------------------------------------------------------- /web/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve("@docusaurus/core/lib/babel/preset")], 3 | }; 4 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('lua', type: 'string', value: 'lua') 2 | option('watch_nix_store', type: 'boolean', value: false) 3 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | -------------------------------------------------------------------------------- /inc/elfinterp.h: -------------------------------------------------------------------------------- 1 | struct trace; 2 | 3 | char *get_elf_interpreter(int exe_fd, struct trace *trace) 4 | __attribute__((warn_unused_result)); 5 | -------------------------------------------------------------------------------- /inc/extension.h: -------------------------------------------------------------------------------- 1 | const char *get_file_extension(const char *path) 2 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 3 | -------------------------------------------------------------------------------- /web/static/_headers: -------------------------------------------------------------------------------- 1 | /*.js 2 | cache-control: max-age=31536000, immutable 3 | 4 | /*.css 5 | cache-control: max-age=31536000, immutable 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Please visit the documentation at https://klunok.org. 2 | 3 | Klunok logotype — a blue-yellow bundle 4 | -------------------------------------------------------------------------------- /inc/deref.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct trace; 4 | 5 | char *deref_fd(int fd, size_t length_guess, struct trace *trace) 6 | __attribute__((warn_unused_result)); 7 | -------------------------------------------------------------------------------- /inc/timestamp.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | struct trace; 5 | 6 | char *get_timestamp(const char *format, size_t max_length, struct trace *trace) 7 | __attribute__((warn_unused_result)); 8 | -------------------------------------------------------------------------------- /misc/fuzz.service.ini: -------------------------------------------------------------------------------- 1 | [Service] 2 | ExecStart=ninja fuzz-elfinterp 3 | Restart=always 4 | User=admin 5 | WorkingDirectory=/home/admin/klunok/build 6 | CPUWeight=10 7 | 8 | [Install] 9 | WantedBy=default.target 10 | -------------------------------------------------------------------------------- /inc/counter.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct trace; 4 | 5 | size_t read_counter(const char *path, struct trace *trace) 6 | __attribute__((warn_unused_result)); 7 | void write_counter(const char *path, size_t counter, struct trace *trace); 8 | -------------------------------------------------------------------------------- /tests/messages.c: -------------------------------------------------------------------------------- 1 | #include "messages.h" 2 | #include 3 | #include 4 | 5 | void test_messages(void *unused) { 6 | char **messages_array = (char **)&messages; 7 | for (size_t i = 0; i < (sizeof messages / sizeof(char *)); ++i) { 8 | assert(messages_array[i]); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lua/weave.awk: -------------------------------------------------------------------------------- 1 | /^```$/ { 2 | is_pre = 0 3 | is_post = 0 4 | } 5 | { 6 | if (is_pre) { 7 | print > pre 8 | } else if (is_post) { 9 | print > post 10 | } 11 | } 12 | /^```lua title="pre-config"/ { 13 | is_pre = 1 14 | } 15 | /^```lua title="post-config"/ { 16 | is_post = 1 17 | } 18 | -------------------------------------------------------------------------------- /inc/parents.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct trace; 4 | 5 | void create_parents(const char *path, struct trace *trace); 6 | void remove_empty_parents(const char *path, struct trace *trace); 7 | size_t get_common_parent_path_length(const char *first, const char *second) 8 | __attribute__((pure, nonnull, warn_unused_result)); 9 | -------------------------------------------------------------------------------- /inc/meson.build: -------------------------------------------------------------------------------- 1 | inc = include_directories('.') 2 | 3 | constants = configuration_data() 4 | constants.set('WATCH_NIX_STORE', get_option('watch_nix_store')) 5 | constants.set_quoted('VERSION', meson.project_version()) 6 | constants.set_quoted('LUA_VERSION', lua.version()) 7 | configure_file(output: 'constants.h', configuration: constants) 8 | -------------------------------------------------------------------------------- /inc/mountinfo.h: -------------------------------------------------------------------------------- 1 | struct trace; 2 | 3 | struct mountinfo *load_mountinfo(struct trace *trace) 4 | __attribute__((warn_unused_result)); 5 | char *make_mount(const char *path, const struct mountinfo *mountinfo, 6 | struct trace *trace) __attribute__((warn_unused_result)); 7 | void free_mountinfo(struct mountinfo *mountinfo); 8 | -------------------------------------------------------------------------------- /inc/sync.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct trace; 4 | 5 | off_t sync_file(const char *destination, const char *source, 6 | off_t source_offset, struct trace *trace); 7 | void sync_shallow_tree(const char *destination, const char *source, 8 | const char *existence_filter_root, struct trace *trace); 9 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('klunok', 'c', version: files('./version')) 2 | 3 | cc = meson.get_compiler('c') 4 | dl = cc.find_library('dl') 5 | fts = dependency('musl-fts', required: false) 6 | lua = dependency(get_option('lua'), required: false) 7 | 8 | subdir('lua') 9 | subdir('inc') 10 | subdir('src') 11 | subdir('tests') 12 | subdir('fuzz') 13 | -------------------------------------------------------------------------------- /tests/counter.c: -------------------------------------------------------------------------------- 1 | #include "counter.h" 2 | #include "trace.h" 3 | #include 4 | 5 | void test_counter(struct trace *trace) { 6 | const char *path = "counter"; 7 | size_t counter = 42; 8 | 9 | assert(read_counter(path, trace) == 0); 10 | assert(ok(trace)); 11 | write_counter(path, counter, trace); 12 | assert(ok(trace)); 13 | assert(read_counter(path, trace) == counter); 14 | } 15 | -------------------------------------------------------------------------------- /inc/journal.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct trace; 4 | 5 | struct journal *open_journal(const char *path, const char *timestamp_pattern, 6 | struct trace *trace) 7 | __attribute__((warn_unused_result)); 8 | void note(const char *event, pid_t pid, const char *path, 9 | const struct journal *journal, struct trace *trace); 10 | void free_journal(struct journal *journal); 11 | -------------------------------------------------------------------------------- /tests/deref.c: -------------------------------------------------------------------------------- 1 | #include "deref.h" 2 | #include "trace.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | void test_deref(struct trace *trace) { 9 | const char *path = "/dev/null"; 10 | int fd = open(path, O_RDONLY); 11 | char *deref_path = deref_fd(fd, 0, trace); 12 | 13 | assert(ok(trace)); 14 | assert(path); 15 | assert(!strcmp(deref_path, path)); 16 | 17 | free(deref_path); 18 | } 19 | -------------------------------------------------------------------------------- /tests/mountinfo.c: -------------------------------------------------------------------------------- 1 | #include "mountinfo.h" 2 | #include "trace.h" 3 | #include 4 | #include 5 | #include 6 | 7 | void test_mountinfo(struct trace *trace) { 8 | struct mountinfo *mountinfo = load_mountinfo(trace); 9 | assert(ok(trace)); 10 | 11 | char *mount = make_mount("/", mountinfo, trace); 12 | assert(ok(trace)); 13 | assert(!strcmp(mount, "/")); 14 | 15 | free_mountinfo(mountinfo); 16 | free(mount); 17 | } 18 | -------------------------------------------------------------------------------- /inc/bitmap.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | struct trace; 5 | 6 | struct bitmap *create_bitmap(size_t size_guess, struct trace *trace) 7 | __attribute__((warn_unused_result)); 8 | void set_bit(size_t bit, struct bitmap *bitmap, struct trace *trace); 9 | void unset_bit(size_t bit, struct bitmap *bitmap) __attribute__((nonnull)); 10 | bool get_bit(size_t bit, const struct bitmap *bitmap) 11 | __attribute__((pure, nonnull, warn_unused_result)); 12 | void free_bitmap(struct bitmap *bitmap); 13 | -------------------------------------------------------------------------------- /inc/storepath.h: -------------------------------------------------------------------------------- 1 | struct trace; 2 | 3 | struct store_path *create_store_path(const char *root, 4 | const char *relative_path, 5 | const char *version, struct trace *trace); 6 | const char *get_current_path(const struct store_path *store_path) 7 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 8 | void increment(struct store_path *store_path, struct trace *trace); 9 | void free_store_path(struct store_path *store_path); 10 | -------------------------------------------------------------------------------- /tests/config-static.c: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | #include "messages.h" 3 | #include "trace.h" 4 | #include 5 | #include 6 | 7 | void check_default_config(struct config *config); 8 | 9 | void test_config_static(struct trace *trace) { 10 | struct config *config = load_config(NULL, trace); 11 | assert(ok(trace)); 12 | check_default_config(config); 13 | 14 | try(trace); 15 | config = load_config("", trace); 16 | assert(catch_static(messages.config.is_static, trace)); 17 | finally(trace); 18 | } 19 | -------------------------------------------------------------------------------- /src/extension.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | const char *get_file_extension(const char *path) { 4 | const char *base_name = strrchr(path, '/'); 5 | if (!base_name) { 6 | base_name = path; 7 | } 8 | const char *extension = strchr(base_name, '.'); 9 | if (!extension) { 10 | extension = strrchr(path, 0); 11 | } else if (extension - base_name <= 1) { 12 | extension = strchr(extension + 1, '.'); 13 | if (!extension) { 14 | extension = strrchr(path, 0); 15 | } 16 | } 17 | return extension; 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yaml: -------------------------------------------------------------------------------- 1 | name: "Build & Test" 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "misc/**" 7 | - "web/**" 8 | - version 9 | push: 10 | paths-ignore: 11 | - "misc/**" 12 | - "web/**" 13 | - version 14 | 15 | jobs: 16 | build-and-test: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: cachix/install-nix-action@v19 21 | - uses: DeterminateSystems/magic-nix-cache-action@v2 22 | - run: nix flake check 23 | - run: nix build .#static 24 | -------------------------------------------------------------------------------- /tests/bitmap.c: -------------------------------------------------------------------------------- 1 | #include "bitmap.h" 2 | #include "trace.h" 3 | #include 4 | #include 5 | 6 | void test_bitmap(struct trace *trace) { 7 | struct bitmap *bitmap = create_bitmap(0, trace); 8 | 9 | assert(ok(trace)); 10 | assert(!get_bit(42, bitmap)); 11 | 12 | set_bit(42, bitmap, trace); 13 | assert(ok(trace)); 14 | assert(get_bit(42, bitmap)); 15 | assert(!get_bit(2048, bitmap)); 16 | 17 | set_bit(2048, bitmap, trace); 18 | assert(ok(trace)); 19 | assert(get_bit(42, bitmap)); 20 | assert(get_bit(2048, bitmap)); 21 | 22 | free_bitmap(bitmap); 23 | } 24 | -------------------------------------------------------------------------------- /inc/list.h: -------------------------------------------------------------------------------- 1 | struct trace; 2 | 3 | struct list *create_list(struct trace *trace) 4 | __attribute__((warn_unused_result)); 5 | void join(const char *head, struct list *tail, struct trace *trace); 6 | const struct list_item *peek(const struct list *list) 7 | __attribute__((pure, nonnull, warn_unused_result)); 8 | const struct list_item *get_next(const struct list_item *item) 9 | __attribute__((pure, nonnull, warn_unused_result)); 10 | const char *get_value(const struct list_item *item) 11 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 12 | void free_list(struct list *list); 13 | -------------------------------------------------------------------------------- /tests/elfinterp.c: -------------------------------------------------------------------------------- 1 | #include "elfinterp.h" 2 | #include "test-constants.h" 3 | #include "trace.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | void test_elfinterp(struct trace *trace) { 10 | int fd = open(TEST_ROOT "/meson.build", O_RDONLY); 11 | assert(fd >= 0); 12 | 13 | assert(!get_elf_interpreter(fd, trace)); 14 | assert(ok(trace)); 15 | 16 | close(fd); 17 | fd = open("/proc/self/exe", O_RDONLY); 18 | assert(fd >= 0); 19 | char *interpreter = get_elf_interpreter(fd, trace); 20 | assert(interpreter); 21 | assert(ok(trace)); 22 | 23 | close(fd); 24 | free(interpreter); 25 | } 26 | -------------------------------------------------------------------------------- /inc/sieve.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct set; 4 | struct trace; 5 | 6 | struct sieved_path *sieve(const char *path, size_t relative_path_offset, 7 | const struct set **sets, size_t set_count, 8 | struct trace *trace) 9 | __attribute__((warn_unused_result)); 10 | 11 | const char *get_hiding_dot(const struct sieved_path *sieved_path) 12 | __attribute__((pure, nonnull, warn_unused_result)); 13 | const char *const *get_sieved_ends(const struct sieved_path *sieved_path) 14 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 15 | 16 | void free_sieved_path(struct sieved_path *sieved_path); 17 | -------------------------------------------------------------------------------- /fuzz/meson.build: -------------------------------------------------------------------------------- 1 | honggfuzz = find_program('honggfuzz', required: false) 2 | 3 | if honggfuzz.found() 4 | fuzz_components = [ 5 | 'elfinterp', 6 | ] 7 | 8 | foreach component: fuzz_components 9 | fuzz_executable = executable( 10 | component, 11 | sources + files(component + '.c'), 12 | dependencies: [lua, dl], 13 | include_directories: inc, 14 | ) 15 | 16 | run_target( 17 | 'fuzz-' + component, 18 | command: [ 19 | honggfuzz, 20 | '-T', 21 | '-i', 22 | '@BUILD_ROOT@/corpus/' + component, 23 | '--', 24 | fuzz_executable, 25 | ], 26 | ) 27 | endforeach 28 | endif 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | paths: 6 | - version 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: cachix/install-nix-action@v19 14 | - run: nix flake check 15 | - run: nix build .#static 16 | - run: echo tag=v$(> $GITHUB_ENV 17 | - run: git tag ${tag} 18 | - run: git tag ${tag%.*} 19 | - run: git tag ${tag%.*.*} 20 | - run: git push --tags --force 21 | - uses: softprops/action-gh-release@v1 22 | with: 23 | files: result/bin/klunok 24 | tag_name: ${{env.tag}} 25 | -------------------------------------------------------------------------------- /inc/handler.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct trace; 4 | 5 | struct handler *load_handler(const char *config_path, 6 | size_t common_parent_path_length, 7 | struct trace *trace) 8 | __attribute__((warn_unused_result)); 9 | void handle_open_exec(pid_t pid, int fd, struct handler *handler, 10 | struct trace *trace); 11 | void handle_close_write(pid_t pid, int fd, struct handler *handler, 12 | struct trace *trace); 13 | time_t handle_timeout(struct handler *handler, struct trace *trace) 14 | __attribute__((warn_unused_result)); 15 | void free_handler(struct handler *handler); 16 | -------------------------------------------------------------------------------- /src/timestamp.c: -------------------------------------------------------------------------------- 1 | #include "timestamp.h" 2 | #include "messages.h" 3 | #include "trace.h" 4 | #include 5 | #include 6 | 7 | char *get_timestamp(const char *format, size_t max_length, 8 | struct trace *trace) { 9 | time_t t = time(NULL); 10 | struct tm *tm = TNULL(localtime(&t), trace); 11 | size_t max_size = max_length + 1; 12 | char *timestamp = TNULL(malloc(max_size), trace); 13 | 14 | if (!TNEG(strftime(timestamp, max_size, format, tm), trace)) { 15 | throw_static(messages.timestamp.overflow, trace); 16 | } 17 | if (!ok(trace)) { 18 | free(timestamp); 19 | return NULL; 20 | } 21 | 22 | return timestamp; 23 | } 24 | -------------------------------------------------------------------------------- /lua/meson.build: -------------------------------------------------------------------------------- 1 | if lua.found() 2 | lua_split = custom_target( 3 | output: ['pre_config.lua', 'post_config.lua'], 4 | input: ['weave.awk', 'config.lua.md'], 5 | command: [ 6 | 'awk', '-f@INPUT0@', 7 | '-vpre=@OUTPUT0@', 8 | '-vpost=@OUTPUT1@', 9 | '@INPUT1@' 10 | ] 11 | ) 12 | lua_sources = [] 13 | foreach file: lua_split.to_list() 14 | filename = file.full_path().split('/')[-1] 15 | lua_sources += custom_target( 16 | output: filename + '.o', 17 | input: file, 18 | command: [ 19 | 'ld', '-z', 'noexecstack', '-r', '-b', 'binary', '-o', '@OUTPUT@', '@INPUT@' 20 | ] 21 | ) 22 | endforeach 23 | endif 24 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The latest minor version is always supported. 6 | Depending on the severity of a vulnerability, 7 | older minor versions may also be patched. 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Please report vulnerabilities to nazar@vinnich.uk. 12 | If you want to use a channel that is more secure than email, 13 | express this intention through email without disclosing the vulnerability, 14 | and we will setup an alternative communication channel, e.g., Signal. 15 | 16 | ## Recognition 17 | 18 | You will be credited for the vulnerability at https://klunok.org if you give permission. 19 | Also, a modest (two-figure) bounty may be offered. 20 | -------------------------------------------------------------------------------- /web/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --ifm-color-primary: #3152bc; 3 | --ifm-color-primary-dark: #2c4aa9; 4 | --ifm-color-primary-darker: #2a46a0; 5 | --ifm-color-primary-darkest: #223984; 6 | --ifm-color-primary-light: #395ccb; 7 | --ifm-color-primary-lighter: #4364ce; 8 | --ifm-color-primary-lightest: #5f7bd5; 9 | } 10 | 11 | [data-theme="dark"] { 12 | --ifm-color-primary: #f8f66d; 13 | --ifm-color-primary-dark: #f6f44b; 14 | --ifm-color-primary-darker: #f6f33a; 15 | --ifm-color-primary-darkest: #eeeb0b; 16 | --ifm-color-primary-light: #faf88f; 17 | --ifm-color-primary-lighter: #faf9a0; 18 | --ifm-color-primary-lightest: #fdfcd3; 19 | } 20 | 21 | .tabs__item { 22 | margin-top: 0; 23 | } 24 | -------------------------------------------------------------------------------- /tests/extension.c: -------------------------------------------------------------------------------- 1 | #include "extension.h" 2 | #include 3 | #include 4 | 5 | void test_extension(void *unused) { 6 | assert(!strcmp(get_file_extension("/abc/def/archive.tar.gz"), ".tar.gz")); 7 | assert(!strcmp(get_file_extension("/abc/def/archive"), "")); 8 | assert(!strcmp(get_file_extension("/abc/def/.archive.tar.gz"), ".tar.gz")); 9 | assert(!strcmp(get_file_extension("/abc/def/.archive"), "")); 10 | assert(!strcmp(get_file_extension("archive.tar.gz"), ".tar.gz")); 11 | assert(!strcmp(get_file_extension("archive"), "")); 12 | assert(!strcmp(get_file_extension(".archive.tar.gz"), ".tar.gz")); 13 | assert(!strcmp(get_file_extension(".archive"), "")); 14 | assert(!strcmp(get_file_extension("mp2.mp3"), ".mp3")); 15 | } 16 | -------------------------------------------------------------------------------- /fuzz/elfinterp.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include "elfinterp.h" 3 | #include "trace.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | int LLVMFuzzerTestOneInput(const char *input, size_t size) { 11 | struct trace *trace = create_trace(); 12 | assert(trace); 13 | 14 | int fd = memfd_create("", O_RDWR); 15 | size_t total_written = 0; 16 | while (total_written < size) { 17 | size_t iter_written = write(fd, input + total_written, size); 18 | if (iter_written < 0) { 19 | assert(0); 20 | } 21 | total_written += iter_written; 22 | } 23 | 24 | lseek(fd, 0, SEEK_SET); 25 | free(get_elf_interpreter(fd, trace)); 26 | close(fd); 27 | 28 | return 0; 29 | } 30 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | components = [ 2 | 'bitmap', 3 | 'buffer', 4 | 'counter', 5 | 'deref', 6 | 'elfinterp', 7 | 'extension', 8 | 'handler', 9 | 'journal', 10 | 'linq', 11 | 'list', 12 | 'logstep', 13 | 'messages', 14 | 'mountinfo', 15 | 'params', 16 | 'parents', 17 | 'set', 18 | 'sieve', 19 | 'storepath', 20 | 'sync', 21 | 'timestamp', 22 | 'trace', 23 | ] + ('config-' + (lua.found() ? 'lua' : 'static')) 24 | 25 | sources = lua.found() ? lua_sources : [] 26 | foreach component: components 27 | sources += files(component + '.c') 28 | endforeach 29 | main_sources = sources + 'main.c' 30 | 31 | executable( 32 | 'klunok', 33 | main_sources, 34 | dependencies: [lua, fts], 35 | include_directories: inc, 36 | install: true 37 | ) 38 | -------------------------------------------------------------------------------- /tests/journal.c: -------------------------------------------------------------------------------- 1 | #include "journal.h" 2 | #include "trace.h" 3 | #include 4 | #include 5 | #include 6 | 7 | void test_journal(struct trace *trace) { 8 | const char *journal_path = "./var/journal"; 9 | const char *timestamp_pattern = "1"; 10 | struct journal *journal = 11 | open_journal(journal_path, timestamp_pattern, trace); 12 | assert(ok(trace)); 13 | note(NULL, 0, "", journal, trace); 14 | assert(ok(trace)); 15 | note("3", 5, "7", journal, trace); 16 | assert(ok(trace)); 17 | 18 | struct stat journal_stat; 19 | assert(stat(journal_path, &journal_stat) >= 0); 20 | assert(journal_stat.st_size == 8); 21 | 22 | free_journal(journal); 23 | journal = open_journal(NULL, "", trace); 24 | assert(ok(trace)); 25 | note("blah", 0, "/", journal, trace); 26 | assert(ok(trace)); 27 | } 28 | -------------------------------------------------------------------------------- /web/src/asciinema.jsx: -------------------------------------------------------------------------------- 1 | import BrowserOnly from "@docusaurus/BrowserOnly"; 2 | import "asciinema-player/dist/bundle/asciinema-player.css"; 3 | import React, {useEffect, useRef, useState} from "react"; 4 | 5 | export default ({ src, children }) => { 6 | const ref = useRef(); 7 | const [player, setPlayer] = useState(); 8 | 9 | useEffect(() => { 10 | import("asciinema-player").then(setPlayer); 11 | }, []); 12 | useEffect(() => { 13 | if (player) { 14 | const instance = player.create(src, ref.current, { 15 | idleTimeLimit: 3, 16 | autoPlay: true, 17 | loop: true, 18 | }); 19 | return () => instance.dispose(); 20 | } 21 | }, [src, player]); 22 | 23 | return ( 24 | 25 | {() => { 26 | return
; 27 | }} 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { lib 2 | , stdenv 3 | , meson 4 | , ninja 5 | , pkg-config 6 | , lua 7 | , valgrind-light 8 | , musl-fts 9 | , doCheckThoroughly ? ( 10 | lib.meta.availableOn stdenv.hostPlatform valgrind-light && !valgrind-light.meta.broken 11 | ) 12 | }: stdenv.mkDerivation { 13 | pname = "klunok"; 14 | version = builtins.readFile ./version 15 | + (if lua == null then "no-lua" else "lua-${lua.version}"); 16 | src = ./.; 17 | 18 | doCheck = !stdenv.targetPlatform.isStatic; 19 | mesonFlags = lib.optionals (!stdenv.targetPlatform.isStatic) [ 20 | "-Dwatch_nix_store=true" 21 | ]; 22 | 23 | nativeBuildInputs = [ 24 | meson 25 | ninja 26 | pkg-config 27 | ]; 28 | buildInputs = [ 29 | lua 30 | ] ++ lib.optionals stdenv.targetPlatform.isMusl [ 31 | musl-fts 32 | ]; 33 | checkInputs = lib.optionals doCheckThoroughly [ 34 | valgrind-light 35 | ]; 36 | } 37 | -------------------------------------------------------------------------------- /tests/list.c: -------------------------------------------------------------------------------- 1 | #include "list.h" 2 | #include "trace.h" 3 | #include 4 | #include 5 | #include 6 | 7 | void test_list(struct trace *trace) { 8 | struct list *list = create_list(trace); 9 | assert(ok(trace)); 10 | assert(!peek(list)); 11 | 12 | const char *head = "HEAD"; 13 | join(head, list, trace); 14 | assert(ok(trace)); 15 | 16 | const struct list_item *head_item = peek(list); 17 | assert(head_item); 18 | assert(!get_next(head_item)); 19 | assert(get_value(head_item) != head); 20 | assert(!strcmp(get_value(head_item), head)); 21 | 22 | const char *next_head = "NEXT"; 23 | join(next_head, list, trace); 24 | assert(ok(trace)); 25 | assert(peek(list)); 26 | assert(get_next(peek(list)) == head_item); 27 | assert(get_value(peek(list)) != next_head); 28 | assert(!strcmp(get_value(peek(list)), next_head)); 29 | 30 | free_list(list); 31 | } 32 | -------------------------------------------------------------------------------- /web/docs/mounts.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 7 3 | --- 4 | 5 | # Mounts 6 | 7 | Klunok uses the Linux fanotify API to monitor entire subtrees of the file system. 8 | This feature works only when the subtree is a mount point. 9 | `/` is always a mount point. 10 | 11 | If Klunok is requested to monitor a directory that is not a mount point, 12 | it bind-mounts the directory to itself. 13 | Such a mount point is completely transparent for most use cases. 14 | If you would like to avoid the automatic bind-mounting for some reason, 15 | mount the directory yourself before launching Klunok. 16 | 17 | Klunok does not monitor nested mount points by default. 18 | For example, if `/home/nazar/mnt` is a mount point, 19 | `klunok -w /home/nazar` won't monitor files within `/home/nazar/mnt`. 20 | If this is not the desired behavior, list nested mount points explicitly: 21 | `klunok -w /home/nazar/mnt -w /home/nazar`. 22 | -------------------------------------------------------------------------------- /tests/storepath.c: -------------------------------------------------------------------------------- 1 | #include "storepath.h" 2 | #include "trace.h" 3 | #include 4 | #include 5 | 6 | #define ROOT "/abcd/root/efg" 7 | #define EXTENSION ".build" 8 | #define PATH "src/klunok/meson" EXTENSION 9 | #define VERSION "v12-34-56" 10 | 11 | void test_storepath(struct trace *trace) { 12 | struct store_path *store_path = create_store_path(ROOT, PATH, VERSION, trace); 13 | assert(ok(trace)); 14 | assert(!strcmp(get_current_path(store_path), 15 | ROOT "/" PATH "/" VERSION EXTENSION)); 16 | 17 | increment(store_path, trace); 18 | assert(ok(trace)); 19 | assert(!strcmp(get_current_path(store_path), 20 | ROOT "/" PATH "/" VERSION "-1" EXTENSION)); 21 | 22 | increment(store_path, trace); 23 | assert(ok(trace)); 24 | assert(!strcmp(get_current_path(store_path), 25 | ROOT "/" PATH "/" VERSION "-2" EXTENSION)); 26 | 27 | free_store_path(store_path); 28 | } 29 | -------------------------------------------------------------------------------- /web/docs/projects.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # Projects 6 | 7 | Projects let you track the history 8 | of directories as a whole in addition to the history of individual files. 9 | Add a directory to [the `project_roots` setting](./configuration.md#project_roots) 10 | to track it. 11 | Backed up versions of the directory will then appear at `klunok/projects` by default. 12 | Files within the versions are hard links to files in the ordinary store 13 | (`klunok/store` by default). 14 | 15 | [The `debounce_seconds` setting](./configuration.md#debounce_seconds) applies to 16 | the projects as well. 17 | A new version of a project is stored if no files within the project are edited 18 | for this amount of seconds. 19 | 20 | If you have a common directory for projects, 21 | for example `~/Projects` or `~/src`, 22 | you can add the common directory to 23 | [the `project_parents` setting](./configuration.md#project_parents) 24 | instead of adding its children to `project_roots`. 25 | -------------------------------------------------------------------------------- /tests/timestamp.c: -------------------------------------------------------------------------------- 1 | #include "timestamp.h" 2 | #include "messages.h" 3 | #include "trace.h" 4 | #include 5 | #include 6 | #include 7 | 8 | void test_timestamp(struct trace *trace) { 9 | char *timestamp = get_timestamp("abc", 7, trace); 10 | assert(ok(trace)); 11 | assert(!strcmp(timestamp, "abc")); 12 | free(timestamp); 13 | 14 | timestamp = get_timestamp("abc", 3, trace); 15 | assert(ok(trace)); 16 | assert(!strcmp(timestamp, "abc")); 17 | free(timestamp); 18 | 19 | timestamp = get_timestamp("%C", 2, trace); 20 | assert(ok(trace)); 21 | assert(strlen(timestamp) == 2); 22 | free(timestamp); 23 | 24 | timestamp = get_timestamp("%Y%m%d%H%M%S", 14, trace); 25 | assert(ok(trace)); 26 | assert(strlen(timestamp) == 14); 27 | free(timestamp); 28 | 29 | try(trace); 30 | timestamp = get_timestamp("%Y", 3, trace); 31 | assert(catch_static(messages.timestamp.overflow, trace)); 32 | finally(trace); 33 | 34 | free(timestamp); 35 | } 36 | -------------------------------------------------------------------------------- /src/logstep.c: -------------------------------------------------------------------------------- 1 | #include "logstep.h" 2 | #include 3 | #include 4 | 5 | static void write_full(int fd, const char *data, size_t size) { 6 | while (size) { 7 | ssize_t written_size = write(fd, data, size); 8 | if (written_size <= 0) { 9 | return; 10 | } 11 | data += written_size; 12 | size -= written_size; 13 | } 14 | } 15 | 16 | void logstep(int fd, const char *prefix, const char *message, size_t depth) { 17 | for (size_t i = 1; i < depth; ++i) { 18 | write_full(fd, " ", 2); 19 | } 20 | if (depth) { 21 | write_full(fd, "└─", 6); 22 | } 23 | if (prefix) { 24 | if (depth) { 25 | write_full(fd, "┤", 3); 26 | } else { 27 | write_full(fd, "│", 3); 28 | } 29 | write_full(fd, prefix, strlen(prefix)); 30 | write_full(fd, "│", 3); 31 | } 32 | if (message) { 33 | if (prefix) { 34 | write_full(fd, " ", 1); 35 | } 36 | write_full(fd, message, strlen(message)); 37 | } 38 | write_full(fd, "\n", 1); 39 | } 40 | -------------------------------------------------------------------------------- /tests/main.c: -------------------------------------------------------------------------------- 1 | #include "buffer.h" 2 | #include "logstep.h" 3 | #include "trace.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | int main(int argc, const char **argv) { 10 | assert(argc > 1); 11 | void *self = dlopen(0, RTLD_LAZY); 12 | assert(self); 13 | void (*test_function)(struct trace *) = dlsym(self, argv[1]); 14 | assert(test_function); 15 | 16 | const char *xdg_run_dir = getenv("XDG_RUNTIME_DIR"); 17 | if (!xdg_run_dir) { 18 | xdg_run_dir = "."; 19 | } 20 | 21 | struct trace *trace = create_trace(); 22 | assert(trace); 23 | struct buffer *cwd_buffer = create_buffer(trace); 24 | concat_string(xdg_run_dir, cwd_buffer, trace); 25 | concat_string("/klunok-test-XXXXXX", cwd_buffer, trace); 26 | assert(ok(trace)); 27 | 28 | char *cwd = free_outer_buffer(cwd_buffer); 29 | assert(mkdtemp(cwd)); 30 | logstep(2, "CWD", cwd, 0); 31 | assert(chdir(cwd) >= 0); 32 | free(cwd); 33 | 34 | test_function(trace); 35 | free(trace); 36 | } 37 | -------------------------------------------------------------------------------- /inc/params.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct trace; 4 | 5 | struct params *parse_params(int argc, const char **argv, struct trace *trace) 6 | __attribute__((warn_unused_result)); 7 | bool is_help_requested(const struct params *params) 8 | __attribute__((pure, nonnull, warn_unused_result)); 9 | bool is_version_requested(const struct params *params) 10 | __attribute__((pure, nonnull, warn_unused_result)); 11 | const char *get_config_path(const struct params *params) 12 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 13 | const char *get_privilege_dropping_path(const struct params *params) 14 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 15 | const struct list *get_write_mounts(const struct params *params) 16 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 17 | const struct list *get_exec_mounts(const struct params *params) 18 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 19 | void free_params(struct params *params); 20 | -------------------------------------------------------------------------------- /src/deref.c: -------------------------------------------------------------------------------- 1 | #include "deref.h" 2 | #include "buffer.h" 3 | #include "trace.h" 4 | #include 5 | #include 6 | #include 7 | 8 | char *deref_fd(int fd, size_t length_guess, struct trace *trace) { 9 | struct buffer *link_buffer = create_buffer(trace); 10 | concat_string("/proc/self/fd/", link_buffer, trace); 11 | concat_size(fd, link_buffer, trace); 12 | if (!ok(trace)) { 13 | free_buffer(link_buffer); 14 | return NULL; 15 | } 16 | 17 | size_t max_size = length_guess + 1; 18 | 19 | for (;;) { 20 | char *target = TNULL(malloc(max_size), trace); 21 | int length = TNEG( 22 | readlink(get_string(get_view(link_buffer)), target, max_size), trace); 23 | 24 | if (!ok(trace)) { 25 | free_buffer(link_buffer); 26 | free(target); 27 | return NULL; 28 | } 29 | if (length < max_size) { 30 | free_buffer(link_buffer); 31 | target[length] = 0; 32 | return target; 33 | } 34 | 35 | free(target); 36 | max_size *= 2; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /inc/linq.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct trace; 4 | 5 | struct linq *load_linq(const char *path, time_t debounce_seconds, 6 | size_t entry_count_guess, size_t entry_length_guess, 7 | struct trace *trace) __attribute__((warn_unused_result)); 8 | void push(const char *path, size_t metadata, struct linq *linq, 9 | struct trace *trace); 10 | void pop_head(struct linq *linq, struct trace *trace); 11 | void redebounce(time_t debounce_seconds, struct linq *linq) 12 | __attribute__((nonnull)); 13 | void free_linq(struct linq *linq); 14 | 15 | struct linq_head *get_head(struct linq *linq, struct trace *trace) 16 | __attribute__((warn_unused_result)); 17 | const char *get_path(const struct linq_head *head) 18 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 19 | time_t get_pause(const struct linq_head *head) 20 | __attribute__((pure, nonnull, warn_unused_result)); 21 | size_t get_metadata(const struct linq_head *head) 22 | __attribute__((pure, warn_unused_result)); 23 | void free_linq_head(struct linq_head *head); 24 | -------------------------------------------------------------------------------- /tests/meson.build: -------------------------------------------------------------------------------- 1 | add_test_setup('fast') 2 | add_test_setup('gdb', gdb: true) 3 | 4 | valgrind = find_program('valgrind', required: false) 5 | 6 | if valgrind.found() 7 | add_test_setup( 8 | 'valgrind', 9 | exe_wrapper: [valgrind, '--error-exitcode=1', '--leak-check=full'], 10 | is_default: true 11 | ) 12 | endif 13 | 14 | test_constants = configuration_data() 15 | test_constants.set_quoted('TEST_ROOT', meson.current_source_dir()) 16 | configure_file(output: 'test-constants.h', configuration: test_constants) 17 | 18 | test_sources = main_sources 19 | foreach component: components + 'config' 20 | test_sources += files(component + '.c') 21 | endforeach 22 | 23 | test_executable = executable( 24 | 'klunok', 25 | test_sources, 26 | dependencies: [lua, fts, dl], 27 | link_args: '-Wl,--export-dynamic', 28 | include_directories: inc, 29 | ) 30 | 31 | foreach component: components 32 | test( 33 | component, 34 | test_executable, 35 | args: 'test_' + component.underscorify(), 36 | should_fail: component == 'handler' and not lua.found(), 37 | ) 38 | endforeach 39 | -------------------------------------------------------------------------------- /inc/set.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | struct buffer_view; 5 | struct trace; 6 | 7 | struct set *create_set(size_t size_guess, struct trace *trace) 8 | __attribute__((warn_unused_result)); 9 | 10 | bool is_empty(const struct set *set) 11 | __attribute__((pure, nonnull, warn_unused_result)); 12 | 13 | void add(const char *value, struct set *set, struct trace *trace); 14 | void add_with_metadata(const char *value, unsigned metadata, struct set *set, 15 | struct trace *trace); 16 | 17 | bool is_within(const struct buffer_view *value, const struct set *set) 18 | __attribute__((pure, nonnull, warn_unused_result)); 19 | void pop(const struct buffer_view *value, struct set *set) 20 | __attribute__((nonnull)); 21 | size_t get_count(const struct buffer_view *value, const struct set *set) 22 | __attribute__((pure, nonnull, warn_unused_result)); 23 | unsigned get_last_metadata(const struct buffer_view *value, 24 | const struct set *set) 25 | __attribute__((pure, nonnull, warn_unused_result)); 26 | 27 | void free_set(struct set *set); 28 | -------------------------------------------------------------------------------- /tests/logstep.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include "logstep.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | static void assert_fd_content(int fd, const char *content) { 10 | char c; 11 | assert(!lseek(fd, 0, SEEK_SET)); 12 | for (const char *cursor = content; *cursor; ++cursor) { 13 | assert(read(fd, &c, 1) == 1); 14 | assert(c == *cursor); 15 | } 16 | assert(!read(fd, &c, 1)); 17 | assert(!ftruncate(fd, 0)); 18 | assert(!lseek(fd, 0, SEEK_SET)); 19 | } 20 | 21 | void test_logstep(void *unused) { 22 | int fd = memfd_create("", O_RDWR); 23 | logstep(fd, "prefix", "message", 0); 24 | assert_fd_content(fd, "│prefix│ message\n"); 25 | logstep(fd, "prefix", "message", 1); 26 | assert_fd_content(fd, "└─┤prefix│ message\n"); 27 | logstep(fd, "prefix", "message", 2); 28 | assert_fd_content(fd, " └─┤prefix│ message\n"); 29 | logstep(fd, "prefix", "message", 3); 30 | assert_fd_content(fd, " └─┤prefix│ message\n"); 31 | logstep(fd, "prefix", NULL, 3); 32 | assert_fd_content(fd, " └─┤prefix│\n"); 33 | } 34 | -------------------------------------------------------------------------------- /web/docs/editors.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 7 3 | --- 4 | 5 | # Editors 6 | 7 | If you want to let Klunok back up files edited with an application that is not recognized 8 | as an editor by default, 9 | add the name of the executable that is normally used to start the application 10 | to [the `editors` setting](./configuration.md#editors). 11 | For example, if you type `awesomeeditor file.txt` in the terminal 12 | when you want to edit `file.txt` with `awesomeeditor`, add `editors.awesomeeditor = true` 13 | to the configuration file. 14 | 15 | This works for many applications on most Linux distributions. 16 | If it doesn't work, 17 | some investigation is needed to find out what executable 18 | writes to the file on behalf of the application. 19 | Please [open a GitHub issue](https://github.com/Kharacternyk/klunok/issues/new/choose) 20 | or ping nazar@vinnich.uk and we'll look into it. 21 | 22 | Unfortunately, some applications are not fully compatible with Klunok. 23 | These include: 24 | 25 | - some interpreted applications, for example Python scripts; 26 | - applications that use `mmap` to edit files; 27 | - applications that write to a temporary file and then replace the edited file with `rename`. 28 | -------------------------------------------------------------------------------- /misc/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Klunok: smart versioning and automatic backup for Linux" 3 | sidebar_position: 1 4 | sidebar_label: Introduction 5 | --- 6 | 7 | import Logo from '../static/logo.svg'; 8 | 9 | # Klunok 10 | 11 | Klunok is a smart versioning and automatic backup daemon for Linux. 12 | It keeps a versioned history of files that you edit, 13 | doing so in the background without any effort required from you. 14 | It picks up everything that matters (sources, …) and nothing that doesn't (binaries, …) 15 | automatically. 16 | 17 | Klunok works well with most text and graphics editors, IDEs, and office suites, 18 | including Vim, Visual Studio Code, LibreOffice, and Inkscape. 19 | Also, Klunok is free and open source: https://github.com/Kharacternyk/klunok. 20 | 21 | ```mermaid 22 | graph TD; 23 | subgraph "Work as you usually do…" 24 | w[Edit your files.]; 25 | s[Hit save.]; 26 | end 27 | q[The files are scheduled for backup.]; 28 | b[The files are backed up.]; 29 | w --> s; 30 | s --> w; 31 | s -.-> |"…and Klunok will take care of the rest."| q; 32 | q -.-> |"A minute passes with no further edits."| b; 33 | ``` 34 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "klunok", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids" 15 | }, 16 | "dependencies": { 17 | "@docusaurus/core": "^3.0.1", 18 | "@docusaurus/preset-classic": "^3.0.1", 19 | "@docusaurus/theme-mermaid": "^3.0.1", 20 | "@mdx-js/react": "^3.0.0", 21 | "asciinema-player": "^3.6.3", 22 | "clsx": "^2.0.0", 23 | "prism-react-renderer": "^2.3.0", 24 | "react": "^18.2.0", 25 | "react-dom": "^18.2.0" 26 | }, 27 | "devDependencies": { 28 | "@docusaurus/module-type-aliases": "^3.0.1" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.5%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "engines": { 43 | "node": ">=16.14" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /web/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | const prism = require("prism-react-renderer"); 2 | 3 | /** @type {import('@docusaurus/types').Config} */ 4 | const config = { 5 | title: "Klunok", 6 | url: "https://klunok.org/", 7 | favicon: "/favicon.ico", 8 | baseUrl: "/", 9 | trailingSlash: false, 10 | presets: [ 11 | [ 12 | "classic", 13 | { 14 | docs: { 15 | routeBasePath: "/", 16 | breadcrumbs: false, 17 | }, 18 | theme: { 19 | customCss: "./custom.css", 20 | }, 21 | }, 22 | ], 23 | ], 24 | plugins: [ 25 | () => ({ 26 | configureWebpack: () => ({ 27 | resolve: { 28 | symlinks: false, 29 | }, 30 | }), 31 | }), 32 | ], 33 | themeConfig: { 34 | navbar: { 35 | title: "Klunok", 36 | logo: { 37 | src: "logo.svg", 38 | }, 39 | }, 40 | colorMode: { 41 | respectPrefersColorScheme: true, 42 | }, 43 | prism: { 44 | theme: prism.themes.okaidia, 45 | additionalLanguages: ["lua"], 46 | }, 47 | algolia: { 48 | appId: "S86UJLB8SZ", 49 | apiKey: "fbaa1ec90ff857f010dc66e07adfef1f", 50 | indexName: "klunok", 51 | }, 52 | }, 53 | themes: ["@docusaurus/theme-mermaid"], 54 | markdown: { 55 | mermaid: true, 56 | }, 57 | }; 58 | 59 | module.exports = config; 60 | -------------------------------------------------------------------------------- /src/list.c: -------------------------------------------------------------------------------- 1 | #include "list.h" 2 | #include "trace.h" 3 | #include 4 | #include 5 | 6 | struct list_item { 7 | char *value; 8 | struct list_item *next; 9 | }; 10 | 11 | struct list { 12 | struct list_item *head; 13 | }; 14 | 15 | struct list *create_list(struct trace *trace) { 16 | struct list *list = TNULL(malloc(sizeof(struct list)), trace); 17 | if (!ok(trace)) { 18 | return NULL; 19 | } 20 | list->head = NULL; 21 | return list; 22 | } 23 | 24 | void join(const char *head, struct list *tail, struct trace *trace) { 25 | struct list_item *item = TNULL(malloc(sizeof(struct list_item)), trace); 26 | char *value = TNULL(strdup(head), trace); 27 | if (!ok(trace)) { 28 | return free(item); 29 | } 30 | item->value = value; 31 | item->next = tail->head; 32 | tail->head = item; 33 | } 34 | 35 | const struct list_item *peek(const struct list *list) { return list->head; } 36 | 37 | const struct list_item *get_next(const struct list_item *item) { 38 | return item->next; 39 | } 40 | 41 | const char *get_value(const struct list_item *item) { return item->value; } 42 | 43 | void free_list(struct list *list) { 44 | if (list) { 45 | while (list->head) { 46 | struct list_item *item = list->head; 47 | list->head = item->next; 48 | free(item->value); 49 | free(item); 50 | } 51 | free(list); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /inc/buffer.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct trace; 4 | 5 | struct buffer *create_buffer(struct trace *trace) 6 | __attribute__((warn_unused_result)); 7 | void concat_string(const char *string, struct buffer *buffer, 8 | struct trace *trace); 9 | void concat_bytes(const char *bytes, size_t byte_count, struct buffer *buffer, 10 | struct trace *trace); 11 | void concat_char(char c, struct buffer *buffer, struct trace *trace); 12 | void concat_size(size_t size, struct buffer *buffer, struct trace *trace); 13 | void set_length(size_t length, struct buffer *buffer) __attribute__((nonnull)); 14 | 15 | const struct buffer_view *get_view(const struct buffer *buffer) 16 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 17 | const char *get_string(const struct buffer_view *view) 18 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 19 | size_t get_length(const struct buffer_view *view) 20 | __attribute__((pure, nonnull, warn_unused_result)); 21 | size_t get_hash(const struct buffer_view *view) 22 | __attribute__((pure, nonnull, warn_unused_result)); 23 | 24 | struct buffer_view *create_buffer_view(const char *string, struct trace *trace) 25 | __attribute__((warn_unused_result)); 26 | void free_buffer_view(struct buffer_view *view); 27 | 28 | char *free_outer_buffer(struct buffer *buffer) 29 | __attribute__((warn_unused_result)); 30 | void free_buffer(struct buffer *buffer); 31 | -------------------------------------------------------------------------------- /tests/config-lua.c: -------------------------------------------------------------------------------- 1 | #include "buffer.h" 2 | #include "config.h" 3 | #include "set.h" 4 | #include "test-constants.h" 5 | #include "trace.h" 6 | #include 7 | #include 8 | #include 9 | 10 | void check_default_config(struct config *config); 11 | 12 | void test_config_lua(struct trace *trace) { 13 | struct config *config = load_config(NULL, trace); 14 | assert(ok(trace)); 15 | check_default_config(config); 16 | 17 | config = load_config(TEST_ROOT "/lua/empty.lua", trace); 18 | assert(ok(trace)); 19 | check_default_config(config); 20 | 21 | config = load_config(TEST_ROOT "/lua/override.lua", trace); 22 | assert(ok(trace)); 23 | 24 | const struct set *editors = get_editors(config); 25 | struct buffer_view *cat = create_buffer_view("cat", trace); 26 | struct buffer_view *vi = create_buffer_view("vi", trace); 27 | assert(ok(trace)); 28 | assert(is_within(cat, editors)); 29 | assert(!is_within(vi, editors)); 30 | free_buffer_view(cat); 31 | free_buffer_view(vi); 32 | 33 | const char *version_pattern = get_version_pattern(config); 34 | assert(!strcmp(version_pattern, "override")); 35 | 36 | free_config(config); 37 | 38 | try(trace); 39 | config = load_config(TEST_ROOT "/lua/broken-semantics.lua", trace); 40 | assert(!ok(trace)); 41 | finally_catch_all(trace); 42 | 43 | try(trace); 44 | config = load_config(TEST_ROOT "/lua/broken-syntax.lua", trace); 45 | assert(!ok(trace)); 46 | finally_catch_all(trace); 47 | } 48 | -------------------------------------------------------------------------------- /tests/buffer.c: -------------------------------------------------------------------------------- 1 | #include "buffer.h" 2 | #include "trace.h" 3 | #include 4 | #include 5 | #include 6 | 7 | #define S1 "abc" 8 | #define S2 " 1 2 3" 9 | #define N 1230 10 | #define SN "1230" 11 | 12 | void test_buffer(struct trace *trace) { 13 | struct buffer *buffer = create_buffer(trace); 14 | assert(ok(trace)); 15 | const struct buffer_view *view = get_view(buffer); 16 | assert(*get_string(view) == 0); 17 | assert(get_hash(view) == 0); 18 | 19 | concat_string(S1, buffer, trace); 20 | assert(ok(trace)); 21 | assert(!strcmp(S1, get_string(view))); 22 | size_t hash = get_hash(view); 23 | 24 | concat_string(S2, buffer, trace); 25 | assert(ok(trace)); 26 | assert(!strcmp(S1 S2, get_string(view))); 27 | assert(hash != get_hash(view)); 28 | hash = get_hash(view); 29 | 30 | size_t saved_length = get_length(view); 31 | 32 | concat_size(N, buffer, trace); 33 | assert(ok(trace)); 34 | assert(!strcmp(S1 S2 SN, get_string(view))); 35 | assert(hash != get_hash(view)); 36 | 37 | set_length(saved_length, buffer); 38 | assert(!strcmp(S1 S2, get_string(view))); 39 | assert(hash == get_hash(view)); 40 | 41 | concat_size(0, buffer, trace); 42 | assert(ok(trace)); 43 | assert(!strcmp(S1 S2 "0", get_string(view))); 44 | assert(hash != get_hash(view)); 45 | 46 | concat_bytes("ABCDEF", 4, buffer, trace); 47 | assert(ok(trace)); 48 | assert(!strcmp(S1 S2 "0ABCD", get_string(view))); 49 | assert(hash != get_hash(view)); 50 | 51 | free_buffer(buffer); 52 | } 53 | -------------------------------------------------------------------------------- /tests/parents.c: -------------------------------------------------------------------------------- 1 | #include "parents.h" 2 | #include "trace.h" 3 | #include 4 | #include 5 | #include 6 | 7 | #define DIRECTORY "parents/abc/defgh/123" 8 | 9 | void test_parents(struct trace *trace) { 10 | assert(access(DIRECTORY, F_OK) != 0); 11 | create_parents(DIRECTORY "/file", trace); 12 | assert(ok(trace)); 13 | assert(access(DIRECTORY, F_OK) == 0); 14 | remove_empty_parents(DIRECTORY "/file", trace); 15 | assert(ok(trace)); 16 | assert(access(DIRECTORY, F_OK) != 0); 17 | 18 | assert(get_common_parent_path_length("/abc/def", "/") == 1); 19 | assert(get_common_parent_path_length("/abc/def", "/ab") == 1); 20 | assert(get_common_parent_path_length("/abc/def", "/abc/de") == 5); 21 | assert(get_common_parent_path_length("/abc/def", "/abc/defgh") == 5); 22 | assert(get_common_parent_path_length("/abc/def", "/abc/def") == 9); 23 | assert(get_common_parent_path_length("/abc/def", "/abc/def/ghi") == 9); 24 | assert(get_common_parent_path_length("/abc/def", "/ayc/def") == 1); 25 | 26 | assert(get_common_parent_path_length("/", "/abc/def") == 1); 27 | assert(get_common_parent_path_length("/ab", "/abc/def") == 1); 28 | assert(get_common_parent_path_length("/abc/de", "/abc/def") == 5); 29 | assert(get_common_parent_path_length("/abc/defgh", "/abc/def") == 5); 30 | assert(get_common_parent_path_length("/abc/def", "/abc/def") == 9); 31 | assert(get_common_parent_path_length("/abc/def/ghi", "/abc/def") == 9); 32 | assert(get_common_parent_path_length("/ayc/def", "/abc/def") == 1); 33 | } 34 | -------------------------------------------------------------------------------- /src/bitmap.c: -------------------------------------------------------------------------------- 1 | #include "bitmap.h" 2 | #include "trace.h" 3 | #include 4 | #include 5 | 6 | struct bitmap { 7 | bool *array; 8 | size_t size; 9 | }; 10 | 11 | struct bitmap *create_bitmap(size_t size_guess, struct trace *trace) { 12 | struct bitmap *bitmap = TNULL(malloc(sizeof(struct bitmap)), trace); 13 | bool *array = TNULL(calloc(size_guess, sizeof(bool)), trace); 14 | if (!ok(trace)) { 15 | free(bitmap); 16 | free(array); 17 | return NULL; 18 | } 19 | bitmap->array = array; 20 | bitmap->size = size_guess; 21 | return bitmap; 22 | } 23 | 24 | void set_bit(size_t bit, struct bitmap *bitmap, struct trace *trace) { 25 | if (!ok(trace)) { 26 | return; 27 | } 28 | 29 | if (bit >= bitmap->size) { 30 | size_t new_size = bit * 2; 31 | bool *new_array = TNULL(calloc(new_size, sizeof(bool)), trace); 32 | if (!ok(trace)) { 33 | return; 34 | } 35 | memcpy(new_array, bitmap->array, bitmap->size); 36 | free(bitmap->array); 37 | bitmap->array = new_array; 38 | bitmap->size = new_size; 39 | } 40 | 41 | bitmap->array[bit] = true; 42 | } 43 | 44 | void unset_bit(size_t bit, struct bitmap *bitmap) { 45 | if (bit < bitmap->size) { 46 | bitmap->array[bit] = false; 47 | } 48 | } 49 | 50 | bool get_bit(size_t bit, const struct bitmap *bitmap) { 51 | if (bit < bitmap->size) { 52 | return bitmap->array[bit]; 53 | } 54 | return false; 55 | } 56 | 57 | void free_bitmap(struct bitmap *bitmap) { 58 | if (bitmap) { 59 | free(bitmap->array); 60 | free(bitmap); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/counter.c: -------------------------------------------------------------------------------- 1 | #include "counter.h" 2 | #include "buffer.h" 3 | #include "parents.h" 4 | #include "trace.h" 5 | #include 6 | #include 7 | #include 8 | 9 | /* FIXME avoid one-char IO */ 10 | 11 | size_t read_counter(const char *path, struct trace *trace) { 12 | if (!ok(trace)) { 13 | return 0; 14 | } 15 | 16 | int fd = open(path, O_RDONLY); 17 | if (fd < 0) { 18 | if (errno != ENOENT) { 19 | throw_errno(trace); 20 | } 21 | return 0; 22 | } 23 | 24 | size_t counter = 0; 25 | 26 | for (;;) { 27 | char digit = 0; 28 | if (TNEG(read(fd, &digit, 1), trace) <= 0 || digit < '0' || digit > '9') { 29 | break; 30 | } 31 | counter *= 10; 32 | counter += digit - '0'; 33 | } 34 | 35 | close(fd); 36 | 37 | return counter; 38 | } 39 | 40 | void write_counter(const char *path, size_t counter, struct trace *trace) { 41 | if (counter == 0) { 42 | if (!unlink(path)) { 43 | remove_empty_parents(path, trace); 44 | } else if (errno != ENOENT) { 45 | throw_errno(trace); 46 | } 47 | return; 48 | } 49 | 50 | create_parents(path, trace); 51 | int fd = TNEG(open(path, O_WRONLY | O_CREAT, 0644), trace); 52 | TNEG(ftruncate(fd, 0), trace); 53 | 54 | struct buffer *buffer = create_buffer(trace); 55 | concat_size(counter, buffer, trace); 56 | 57 | for (size_t i = 0; ok(trace) && i < get_length(get_view(buffer)); ++i) { 58 | TNEG(write(fd, get_string(get_view(buffer)) + i, 1), trace); 59 | } 60 | 61 | if (fd >= 0) { 62 | close(fd); 63 | } 64 | 65 | free_buffer(buffer); 66 | } 67 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "id": "flake-utils", 17 | "type": "indirect" 18 | } 19 | }, 20 | "nixpkgs": { 21 | "locked": { 22 | "lastModified": 1756787288, 23 | "narHash": "sha256-rw/PHa1cqiePdBxhF66V7R+WAP8WekQ0mCDG4CFqT8Y=", 24 | "owner": "NixOS", 25 | "repo": "nixpkgs", 26 | "rev": "d0fc30899600b9b3466ddb260fd83deb486c32f1", 27 | "type": "github" 28 | }, 29 | "original": { 30 | "id": "nixpkgs", 31 | "type": "indirect" 32 | } 33 | }, 34 | "root": { 35 | "inputs": { 36 | "flake-utils": "flake-utils", 37 | "nixpkgs": "nixpkgs" 38 | } 39 | }, 40 | "systems": { 41 | "locked": { 42 | "lastModified": 1681028828, 43 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 44 | "owner": "nix-systems", 45 | "repo": "default", 46 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "nix-systems", 51 | "repo": "default", 52 | "type": "github" 53 | } 54 | } 55 | }, 56 | "root": "root", 57 | "version": 7 58 | } 59 | -------------------------------------------------------------------------------- /tests/sieve.c: -------------------------------------------------------------------------------- 1 | #include "sieve.h" 2 | #include "set.h" 3 | #include "trace.h" 4 | #include 5 | #include 6 | 7 | void test_sieve(struct trace *trace) { 8 | struct set *sets[] = { 9 | create_set(0, trace), 10 | create_set(0, trace), 11 | create_set(0, trace), 12 | create_set(0, trace), 13 | }; 14 | 15 | add("/home/nazar", sets[0], trace); 16 | add("/home/nazar/src/klunok", sets[0], trace); 17 | add("/home/nazar/src/klunok/src", sets[0], trace); 18 | 19 | add("/", sets[1], trace); 20 | add("/etc", sets[1], trace); 21 | 22 | add("/usr/bin", sets[2], trace); 23 | add("/tmp", sets[2], trace); 24 | 25 | add("/home", sets[3], trace); 26 | add("nazar", sets[3], trace); 27 | add("nazar/src", sets[3], trace); 28 | add("klunok", sets[3], trace); 29 | 30 | assert(ok(trace)); 31 | 32 | const char *path = "/home/nazar/src/klunok/tests/sieve.c"; 33 | 34 | struct sieved_path *sieved_path = 35 | sieve(path, 6, (const struct set **)sets, 4, trace); 36 | assert(ok(trace)); 37 | 38 | assert(!get_hiding_dot(sieved_path)); 39 | assert(get_sieved_ends(sieved_path)[0] == path + 22); 40 | assert(get_sieved_ends(sieved_path)[1] == path + 1); 41 | assert(get_sieved_ends(sieved_path)[2] == NULL); 42 | assert(get_sieved_ends(sieved_path)[3] == path + 15); 43 | free_sieved_path(sieved_path); 44 | 45 | path = "/home/nazar/.config/pacwall/pacwall.conf"; 46 | sieved_path = sieve(path, 1, NULL, 0, trace); 47 | assert(ok(trace)); 48 | assert(get_hiding_dot(sieved_path) == path + 12); 49 | free_sieved_path(sieved_path); 50 | 51 | for (size_t i = 0; i < 4; ++i) { 52 | free_set(sets[i]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/params.c: -------------------------------------------------------------------------------- 1 | #include "params.h" 2 | #include "buffer.h" 3 | #include "list.h" 4 | #include "trace.h" 5 | #include 6 | #include 7 | #include 8 | 9 | void test_params(struct trace *trace) { 10 | const char *config_path = "config.lua"; 11 | const char *drop_path = "/home/nazar"; 12 | const char *write_mount = "/home/nazar/src"; 13 | const char *exec_mount = "/nix/store"; 14 | const char *both_mount = "/home/nazar/.local/bin"; 15 | const char *argv[] = {"klunok", "-c", config_path, "-d", drop_path, "-w", 16 | write_mount, "-e", exec_mount, "-e", both_mount, "-w", 17 | both_mount}; 18 | int argc = sizeof argv / sizeof *argv; 19 | 20 | struct params *params = parse_params(argc, argv, trace); 21 | assert(ok(trace)); 22 | assert(!strcmp(get_config_path(params), config_path)); 23 | assert(!strcmp(get_privilege_dropping_path(params), drop_path)); 24 | 25 | /*FIXME we shouldn't test the order*/ 26 | 27 | assert(!strcmp(get_value(peek(get_write_mounts(params))), both_mount)); 28 | assert(!strcmp(get_value(get_next(peek(get_write_mounts(params)))), 29 | write_mount)); 30 | 31 | assert(!strcmp(get_value(peek(get_exec_mounts(params))), both_mount)); 32 | assert( 33 | !strcmp(get_value(get_next(peek(get_exec_mounts(params)))), exec_mount)); 34 | 35 | free_params(params); 36 | 37 | const char *empty_argv[] = {"klunok"}; 38 | params = parse_params(1, empty_argv, trace); 39 | assert(ok(trace)); 40 | assert(peek(get_write_mounts(params))); 41 | assert(peek(get_exec_mounts(params))); 42 | assert(get_privilege_dropping_path(params)); 43 | free_params(params); 44 | } 45 | -------------------------------------------------------------------------------- /web/docs/security.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 8 3 | --- 4 | 5 | # Security 6 | 7 | ## Overview 8 | 9 | 1. Klunok starts as root. 10 | 2. Klunok parses command-line parameters. 11 | 3. Klunok initializes a fanotify API handle. 12 | 4. Klunok performs [bind mounts](./mounts.md) if necessary. 13 | 5. Klunok [drops privileges](./cli.md#-d-path-to-a-file-or-directory-which-owners-identity-will-be-used-for-running-klunok), exits if cannot drop. 14 | 6. Klunok parses [Lua configuration](./configuration.md). 15 | 7. Klunok listens for fanotify events. 16 | 17 | The only sensitive thing that is held by Klunok after it drops privileges is 18 | the stream of fanotify events. 19 | The events contain read-only open file descriptors of edited files 20 | and executable files of newly started applications. 21 | Therefore, Klunok monitors only the current working directory by default. 22 | This avoids receiving read-only file descriptors of, for example, `/etc/shadow`. 23 | 24 | ## Best practices 25 | 26 | - Avoid `klunok -w /`. 27 | - Do use [the `-e` command-line option](./cli.md#-e-path-to-a-directory-that-contains-executable-files). 28 | 29 | ## Security policy 30 | 31 | Please see https://github.com/Kharacternyk/klunok/blob/master/SECURITY.md. 32 | 33 | ## Static binary reproducibility 34 | 35 | You can check that the distributed binary 36 | has been built from the source without modifications 37 | by reproducing the build locally with [Nix](https://nixos.org/). 38 | For example, let's verify that the `v0.1.1` release has not been tampered with: 39 | 40 | ```bash 41 | nix build github:Kharacternyk/klunok/v0.1.1#static 42 | curl -Lo binary https://github.com/Kharacternyk/klunok/releases/download/v0.1.1/klunok 43 | cmp binary ./result/bin/klunok 44 | ``` 45 | 46 | The output of `cmp` must be empty. 47 | -------------------------------------------------------------------------------- /misc/fuzz.playbook.yaml: -------------------------------------------------------------------------------- 1 | - hosts: fuzz 2 | tasks: 3 | - become: true 4 | apt: 5 | update_cache: yes 6 | pkg: 7 | - git 8 | - ninja-build 9 | - pip 10 | - clang 11 | - make 12 | - binutils-dev 13 | - libunwind-dev 14 | - libblocksruntime-dev 15 | - become: true 16 | pip: 17 | name: meson 18 | - register: honggfuzz 19 | git: 20 | repo: https://github.com/google/honggfuzz 21 | dest: honggfuzz 22 | - when: honggfuzz is changed 23 | command: 24 | chdir: honggfuzz 25 | argv: 26 | - make 27 | - when: honggfuzz is changed 28 | become: true 29 | command: 30 | chdir: honggfuzz 31 | argv: 32 | - make 33 | - install 34 | - git: 35 | repo: https://github.com/Kharacternyk/klunok 36 | dest: klunok 37 | - command: 38 | chdir: klunok 39 | creates: build 40 | argv: 41 | - meson 42 | - setup 43 | - build 44 | environment: 45 | CC: hfuzz-clang 46 | - register: service 47 | become: true 48 | copy: 49 | src: fuzz.service.ini 50 | dest: /etc/systemd/system/fuzz.service 51 | - file: 52 | path: /home/admin/klunok/build/corpus/elfinterp 53 | state: directory 54 | - copy: 55 | remote_src: true 56 | src: /bin/true 57 | dest: /home/admin/klunok/build/corpus/elfinterp/true 58 | - when: service is changed 59 | become: true 60 | command: 61 | argv: 62 | - systemctl 63 | - daemon-reload 64 | - become: true 65 | service: 66 | name: fuzz 67 | enabled: yes 68 | state: restarted 69 | -------------------------------------------------------------------------------- /web/docs/usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | import Asciinema from '../src/asciinema.jsx'; 6 | 7 | # Usage 8 | 9 | Klunok uses the Linux kernel fanotify API, which as of now requires root privileges. 10 | Therefore, Klunok should be invoked, for example, with `sudo`: 11 | 12 | ```bash 13 | sudo -b klunok 14 | ``` 15 | 16 | The `-b` option of `sudo` makes the command run in the background, 17 | which lets you continue using the terminal for other commands. 18 | 19 | If Klunok is invoked without the necessary privileges, you will face an error: 20 | 21 | ``` 22 | Cannot initialize fanotify 23 | └─┤because of│ Operation not permitted 24 | ``` 25 | 26 | Klunok will drop privileges to the owner of the current working directory 27 | after initializing the fanotify API. 28 | You can use 29 | [the `-d` command line option](./cli.md#-d-path-to-a-file-or-directory-which-owners-identity-will-be-used-for-running-klunok) 30 | to customize this. 31 | 32 | Once Klunok successfully starts, 33 | it watches for file edits in the current working directory and its descendants. 34 | When it thinks that the current version of a file is more or less stable 35 | (the file hasn't been edited for one minute), 36 | it copies the file to the `klunok/store` directory. 37 | 38 | Klunok considers only files that are edited by applications that humans use to edit files, 39 | for example, Vim and LibreOffice. 40 | Please note that Klunok must be launched before these applications are to recognize them. 41 | 42 | Here is a simple demo: 43 | 44 | 45 |
46 | $ ls
47 | $ sudo -b klunok
48 | $ ls
49 | klunok/
50 | $ ls klunok
51 | var/
52 | $ nano hello.txt
53 | $ ls
54 | hello.txt  klunok/
55 | $ ls klunok
56 | var/
57 | $ sleep 60
58 | $ ls klunok
59 | store/  var/
60 | $ cat klunok/store/hello.txt/v2023-06-17-15-07.txt
61 | Hello, World!
62 |   
63 |
64 | -------------------------------------------------------------------------------- /src/elfinterp.c: -------------------------------------------------------------------------------- 1 | #include "elfinterp.h" 2 | #include "trace.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #ifdef __LP64__ 9 | #define ElfW(type) Elf64_##type 10 | #else 11 | #define ElfW(type) Elf32_##type 12 | #endif 13 | 14 | char *get_elf_interpreter(int exe_fd, struct trace *trace) { 15 | ElfW(Ehdr) elf_header; 16 | size_t total_read = 0; 17 | 18 | while (total_read < sizeof elf_header) { 19 | ssize_t iter_read = 20 | TNEG(read(exe_fd, &elf_header, sizeof elf_header), trace); 21 | if (!ok(trace) || !iter_read) { 22 | return NULL; 23 | } 24 | total_read += iter_read; 25 | } 26 | 27 | if (memcmp(elf_header.e_ident, ELFMAG, SELFMAG) || !elf_header.e_phoff) { 28 | return NULL; 29 | } 30 | 31 | if (lseek(exe_fd, elf_header.e_phoff, SEEK_SET) < 0) { 32 | return NULL; 33 | } 34 | 35 | for (size_t i = 0; i < elf_header.e_phnum; ++i) { 36 | ElfW(Phdr) program_header; 37 | size_t total_read = 0; 38 | while (total_read < sizeof program_header) { 39 | ssize_t iter_read = 40 | TNEG(read(exe_fd, &program_header, sizeof program_header), trace); 41 | if (!ok(trace) || !iter_read) { 42 | return NULL; 43 | } 44 | total_read += iter_read; 45 | } 46 | 47 | if (program_header.p_type == PT_INTERP) { 48 | if (lseek(exe_fd, program_header.p_offset, SEEK_SET) < 0) { 49 | return NULL; 50 | } 51 | char *result = malloc(program_header.p_filesz); 52 | if (!result) { 53 | return NULL; 54 | } 55 | 56 | size_t total_read = 0; 57 | while (total_read < program_header.p_filesz) { 58 | ssize_t iter_read = 59 | TNEG(read(exe_fd, result, program_header.p_filesz), trace); 60 | if (!ok(trace) || !iter_read) { 61 | return NULL; 62 | } 63 | total_read += iter_read; 64 | } 65 | 66 | char *resolved = TNULL(realpath(result, NULL), trace); 67 | free(result); 68 | return resolved; 69 | } 70 | } 71 | 72 | return NULL; 73 | } 74 | -------------------------------------------------------------------------------- /tests/linq.c: -------------------------------------------------------------------------------- 1 | #include "linq.h" 2 | #include "test-constants.h" 3 | #include "trace.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define F1 TEST_ROOT "/meson.build" 10 | #define F2 TEST_ROOT "/linq.c" 11 | 12 | void test_linq(struct trace *trace) { 13 | struct linq *linq = load_linq("linq/instant", 0, 0, 0, trace); 14 | assert(ok(trace)); 15 | 16 | struct linq_head *head = get_head(linq, trace); 17 | assert(ok(trace)); 18 | assert(get_pause(head) < 0); 19 | free_linq_head(head); 20 | 21 | push(F1, 0, linq, trace); 22 | assert(ok(trace)); 23 | head = get_head(linq, trace); 24 | assert(ok(trace)); 25 | assert(!get_pause(head)); 26 | assert(!strcmp(get_path(head), F1)); 27 | assert(!get_metadata(head)); 28 | free_linq_head(head); 29 | 30 | pop_head(linq, trace); 31 | assert(ok(trace)); 32 | 33 | push(F1, 123, linq, trace); 34 | assert(ok(trace)); 35 | push(F2, 1234, linq, trace); 36 | assert(ok(trace)); 37 | push(F1, 12345, linq, trace); 38 | assert(ok(trace)); 39 | 40 | head = get_head(linq, trace); 41 | assert(ok(trace)); 42 | assert(!get_pause(head)); 43 | assert(!strcmp(get_path(head), F2)); 44 | assert(get_metadata(head) == 1234); 45 | free_linq_head(head); 46 | 47 | pop_head(linq, trace); 48 | assert(ok(trace)); 49 | 50 | head = get_head(linq, trace); 51 | assert(ok(trace)); 52 | assert(!get_pause(head)); 53 | assert(!strcmp(get_path(head), F1)); 54 | assert(get_metadata(head) == 12345); 55 | free_linq_head(head); 56 | 57 | pop_head(linq, trace); 58 | assert(ok(trace)); 59 | 60 | head = get_head(linq, trace); 61 | assert(ok(trace)); 62 | assert(get_pause(head) < 0); 63 | free_linq_head(head); 64 | 65 | free_linq(linq); 66 | 67 | linq = load_linq("linq/lag", 3600, 0, 0, trace); 68 | push(F1, 0, linq, trace); 69 | assert(ok(trace)); 70 | head = get_head(linq, trace); 71 | assert(ok(trace)); 72 | assert(get_pause(head) > 0); 73 | free_linq_head(head); 74 | pop_head(linq, trace); 75 | assert(ok(trace)); 76 | 77 | free_linq(linq); 78 | } 79 | -------------------------------------------------------------------------------- /src/parents.c: -------------------------------------------------------------------------------- 1 | #include "parents.h" 2 | #include "messages.h" 3 | #include "trace.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | void create_parents(const char *original_path, struct trace *trace) { 12 | char *path = TNULL(strdup(original_path), trace); 13 | if (!ok(trace)) { 14 | return; 15 | } 16 | 17 | char *slash = strchr(path, '/'); 18 | 19 | if (slash == path) { 20 | slash = strchr(slash + 1, '/'); 21 | } 22 | 23 | while (slash) { 24 | *slash = 0; 25 | if (mkdir(path, 0755) < 0 && errno != EEXIST) { 26 | throw_errno(trace); 27 | throw_context(path, trace); 28 | throw_static(messages.parents.cannot_create_ancestor, trace); 29 | free(path); 30 | return; 31 | } 32 | *slash = '/'; 33 | ++slash; 34 | slash = strchr(slash, '/'); 35 | } 36 | 37 | free(path); 38 | } 39 | 40 | void remove_empty_parents(const char *original_path, struct trace *trace) { 41 | char *path = TNULL(strdup(original_path), trace); 42 | if (!ok(trace)) { 43 | return; 44 | } 45 | 46 | char *slash = strrchr(path, '/'); 47 | 48 | while (slash && slash != path) { 49 | *slash = 0; 50 | if (rmdir(path) < 0) { 51 | if (errno != ENOTEMPTY) { 52 | throw_errno(trace); 53 | throw_context(path, trace); 54 | throw_static(messages.parents.cannot_remove_ancestor, trace); 55 | } 56 | free(path); 57 | return; 58 | } 59 | char *old_slash = slash; 60 | slash = strrchr(path, '/'); 61 | *old_slash = '/'; 62 | } 63 | 64 | free(path); 65 | } 66 | 67 | static bool is_separator(char c) { return !c || c == '/'; } 68 | 69 | size_t get_common_parent_path_length(const char *first, const char *second) { 70 | assert(*first == '/'); 71 | assert(*first == *second); 72 | 73 | for (size_t i = 1, result = 1; /*keep going*/; ++i) { 74 | if (is_separator(first[i]) && is_separator(second[i])) { 75 | result = i + 1; 76 | if (!first[i] || !second[i]) { 77 | return result; 78 | } 79 | } else if (first[i] != second[i]) { 80 | return result; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/sync.c: -------------------------------------------------------------------------------- 1 | #include "sync.h" 2 | #include "messages.h" 3 | #include "test-constants.h" 4 | #include "trace.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | static void create_file(const char *path) { 13 | int fd = creat(path, 0644); 14 | assert(fd >= 0 && !close(fd)); 15 | } 16 | 17 | void test_sync(struct trace *trace) { 18 | const char *destination = "path/to/dest"; 19 | const char *source = TEST_ROOT "/meson.build"; 20 | 21 | assert(access(destination, F_OK) != 0); 22 | 23 | sync_file(destination, source, 0, trace); 24 | assert(ok(trace)); 25 | 26 | assert(!access(destination, F_OK)); 27 | 28 | try(trace); 29 | sync_file(destination, source, 0, trace); 30 | assert(catch_static(messages.sync.destination_already_exists, trace)); 31 | finally(trace); 32 | 33 | try(trace); 34 | sync_file("won't-exist", TEST_ROOT "/I-do-not-exist", 0, trace); 35 | assert(catch_static(messages.sync.source_does_not_exist, trace)); 36 | finally(trace); 37 | 38 | const char *log_destination = "path/to/log.txt"; 39 | size_t offset = 12; 40 | size_t new_offset = sync_file(log_destination, source, offset, trace); 41 | assert(ok(trace)); 42 | 43 | struct stat source_stat; 44 | assert(stat(source, &source_stat) >= 0); 45 | 46 | assert(new_offset == source_stat.st_size); 47 | 48 | struct stat log_destination_stat; 49 | assert(stat(log_destination, &log_destination_stat) >= 0); 50 | 51 | assert(new_offset == log_destination_stat.st_size + offset); 52 | 53 | assert(!mkdir("a", 0755)); 54 | assert(!mkdir("a/b", 0755)); 55 | create_file("a/b/c"); 56 | create_file("a/b/d"); 57 | create_file("a/b/e"); 58 | 59 | assert(!mkdir("f", 0755)); 60 | assert(!mkdir("f/b", 0755)); 61 | create_file("f/b/c"); 62 | create_file("f/b/e"); 63 | 64 | assert(access("g", F_OK)); 65 | 66 | sync_shallow_tree("g", "a", "f", trace); 67 | assert(ok(trace)); 68 | assert(!access("g/b/c", F_OK)); 69 | assert(access("g/b/d", F_OK)); 70 | assert(!access("g/b/e", F_OK)); 71 | assert(!access("a/b/c", F_OK)); 72 | assert(access("a/b/d", F_OK)); 73 | assert(!access("a/b/e", F_OK)); 74 | } 75 | -------------------------------------------------------------------------------- /tests/set.c: -------------------------------------------------------------------------------- 1 | #include "set.h" 2 | #include "buffer.h" 3 | #include "messages.h" 4 | #include "trace.h" 5 | #include 6 | 7 | void test_set(struct trace *trace) { 8 | struct set *set = create_set(0, trace); 9 | assert(ok(trace)); 10 | assert(is_empty(set)); 11 | 12 | const char *s1 = "/home/nazar"; 13 | struct buffer_view *v1 = create_buffer_view(s1, trace); 14 | assert(ok(trace)); 15 | 16 | assert(!is_within(v1, set)); 17 | assert(get_count(v1, set) == 0); 18 | add(s1, set, trace); 19 | assert(ok(trace)); 20 | assert(is_within(v1, set)); 21 | assert(!is_empty(set)); 22 | 23 | const char *s2 = "my-data.txt"; 24 | struct buffer_view *v2 = create_buffer_view(s2, trace); 25 | struct buffer_view *v3 = create_buffer_view(s2, trace); 26 | assert(ok(trace)); 27 | 28 | assert(!is_within(v2, set)); 29 | assert(!is_within(v3, set)); 30 | add(s2, set, trace); 31 | assert(ok(trace)); 32 | assert(is_within(v2, set)); 33 | assert(is_within(v3, set)); 34 | assert(is_within(v1, set)); 35 | 36 | const char *s4 = "/"; 37 | struct buffer_view *v4 = create_buffer_view(s4, trace); 38 | assert(ok(trace)); 39 | 40 | assert(!is_within(v4, set)); 41 | add(s4, set, trace); 42 | assert(ok(trace)); 43 | assert(is_within(v4, set)); 44 | assert(is_within(v2, set)); 45 | assert(is_within(v1, set)); 46 | 47 | pop(v1, set); 48 | assert(is_within(v4, set)); 49 | assert(is_within(v2, set)); 50 | assert(!is_within(v1, set)); 51 | 52 | pop(v1, set); 53 | assert(is_within(v4, set)); 54 | assert(is_within(v2, set)); 55 | assert(!is_within(v1, set)); 56 | 57 | assert(get_count(v4, set) == 1); 58 | add_with_metadata(s4, 11, set, trace); 59 | add_with_metadata(s4, 22, set, trace); 60 | assert(ok(trace)); 61 | assert(get_count(v4, set) == 3); 62 | assert(get_last_metadata(v4, set) == 22); 63 | assert(is_within(v4, set)); 64 | 65 | pop(v4, set); 66 | assert(ok(trace)); 67 | assert(is_within(v4, set)); 68 | 69 | pop(v4, set); 70 | pop(v4, set); 71 | assert(ok(trace)); 72 | assert(!is_within(v4, set)); 73 | 74 | free_buffer_view(v1); 75 | free_buffer_view(v2); 76 | free_buffer_view(v3); 77 | free_buffer_view(v4); 78 | 79 | free_set(set); 80 | } 81 | -------------------------------------------------------------------------------- /tests/config.c: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | #include "buffer.h" 3 | #include "set.h" 4 | #include "trace.h" 5 | #include 6 | #include 7 | #include 8 | 9 | void check_default_config(struct config *config) { 10 | struct trace *trace = create_trace(); 11 | const struct set *editors = get_editors(config); 12 | struct buffer_view *cat = create_buffer_view("cat", trace); 13 | struct buffer_view *vi = create_buffer_view("vi", trace); 14 | assert(ok(trace)); 15 | assert(!is_within(cat, editors)); 16 | assert(is_within(vi, editors)); 17 | free_buffer_view(cat); 18 | free_buffer_view(vi); 19 | free(trace); 20 | 21 | assert(is_empty(get_project_roots(config))); 22 | assert(is_empty(get_project_parents(config))); 23 | assert(is_empty(get_history_paths(config))); 24 | assert(is_empty(get_excluded_paths(config))); 25 | assert(is_empty(get_included_paths(config))); 26 | assert(is_empty(get_cluded_paths(config))); 27 | 28 | assert(!strcmp(get_store_root(config), "klunok/store")); 29 | assert(!strcmp(get_project_store_root(config), "klunok/projects")); 30 | assert( 31 | !strcmp(get_unstable_project_store_root(config), "klunok/var/projects")); 32 | assert(!strcmp(get_queue_path(config), "klunok/var/queue")); 33 | assert(!strcmp(get_journal_path(config), "klunok/var/journal")); 34 | assert(!strcmp(get_offset_store_root(config), "klunok/var/offsets")); 35 | assert(!strcmp(get_journal_timestamp_pattern(config), "%Y-%m-%d-%H-%M")); 36 | assert(!strcmp(get_version_pattern(config), "v%Y-%m-%d-%H-%M")); 37 | 38 | assert(get_debounce_seconds(config) == 60); 39 | assert(get_path_length_guess(config) == 1024); 40 | assert(get_max_pid_guess(config) == 1 << 15); 41 | assert(get_elf_interpreter_count_guess(config) == 1); 42 | assert(get_queue_size_guess(config) == get_debounce_seconds(config) * 2); 43 | 44 | assert(!get_event_open_exec_not_editor(config)); 45 | assert(!get_event_open_exec_editor(config)); 46 | assert(!get_event_close_write_not_by_editor(config)); 47 | assert(!get_event_close_write_by_editor(config)); 48 | assert(!get_event_queue_head_deleted(config)); 49 | assert(!get_event_queue_head_forbidden(config)); 50 | assert(!strcmp(get_event_queue_head_stored(config), "")); 51 | 52 | free_config(config); 53 | } 54 | -------------------------------------------------------------------------------- /inc/messages.h: -------------------------------------------------------------------------------- 1 | struct translation { 2 | struct { 3 | const char *because_of; 4 | const char *which_is; 5 | const char *message_dropped; 6 | } trace; 7 | struct { 8 | const char *overflow; 9 | } timestamp; 10 | struct { 11 | const char *cannot_create_ancestor; 12 | const char *cannot_remove_ancestor; 13 | } parents; 14 | struct { 15 | const char *source_is_not_regular_file; 16 | const char *source_does_not_exist; 17 | const char *source_permission_denied; 18 | const char *destination_already_exists; 19 | } sync; 20 | struct { 21 | const char *is_static; 22 | } config; 23 | struct { 24 | const char *invalid_entry; 25 | } linq; 26 | struct { 27 | const char *unknown_option; 28 | const char *stray_option; 29 | const char *redefined_option; 30 | } params; 31 | struct { 32 | struct { 33 | const char *has_slashes; 34 | } version; 35 | struct { 36 | const char *cannot_load; 37 | const char *cannot_reload; 38 | } config; 39 | struct { 40 | const char *cannot_load; 41 | const char *cannot_reload; 42 | const char *cannot_get_head; 43 | const char *cannot_push; 44 | } linq; 45 | struct { 46 | const char *cannot_copy; 47 | } store; 48 | struct { 49 | const char *cannot_open; 50 | const char *cannot_write_to; 51 | } journal; 52 | } handler; 53 | struct { 54 | const char *cannot_bootstrap; 55 | const char *out_of_memory; 56 | const char *cli_usage_violated; 57 | const char *cannot_parse_cli; 58 | const char *cannot_drop_privileges; 59 | const char *cannot_load_handler; 60 | const char *cannot_handle_exec; 61 | const char *cannot_handle_write; 62 | const char *cannot_handle_timeout; 63 | struct { 64 | const char *cannot_init; 65 | const char *cannot_poll; 66 | const char *cannot_read_event; 67 | const char *version_mismatch; 68 | const char *queue_overflow; 69 | } fanotify; 70 | struct { 71 | const char *cannot_list; 72 | const char *cannot_watch; 73 | } mount; 74 | struct { 75 | const char *version; 76 | const char *usage; 77 | const char *help; 78 | } info; 79 | } main; 80 | }; 81 | 82 | extern const struct translation messages; 83 | -------------------------------------------------------------------------------- /src/storepath.c: -------------------------------------------------------------------------------- 1 | #include "storepath.h" 2 | #include "buffer.h" 3 | #include "extension.h" 4 | #include "timestamp.h" 5 | #include "trace.h" 6 | #include 7 | #include 8 | 9 | struct store_path { 10 | struct buffer *buffer; 11 | char *extension; 12 | size_t extensionless_length; 13 | size_t duplicate_count; 14 | }; 15 | 16 | struct store_path *create_store_path(const char *root, 17 | const char *relative_path, 18 | const char *version, struct trace *trace) { 19 | struct store_path *store_path = 20 | TNULL(malloc(sizeof(struct store_path)), trace); 21 | struct buffer *buffer = create_buffer(trace); 22 | concat_string(root, buffer, trace); 23 | concat_char('/', buffer, trace); 24 | concat_string(relative_path, buffer, trace); 25 | concat_char('/', buffer, trace); 26 | concat_string(version, buffer, trace); 27 | 28 | if (!ok(trace)) { 29 | free_buffer(buffer); 30 | free(store_path); 31 | return NULL; 32 | } 33 | 34 | store_path->buffer = buffer; 35 | store_path->duplicate_count = 0; 36 | store_path->extensionless_length = get_length(get_view(buffer)); 37 | store_path->extension = 38 | TNULL(strdup(get_file_extension(relative_path)), trace); 39 | concat_string(store_path->extension, buffer, trace); 40 | 41 | if (!ok(trace)) { 42 | free_store_path(store_path); 43 | return NULL; 44 | } 45 | 46 | return store_path; 47 | } 48 | 49 | const char *get_current_path(const struct store_path *store_path) { 50 | return get_string(get_view(store_path->buffer)); 51 | } 52 | 53 | void increment(struct store_path *store_path, struct trace *trace) { 54 | if (!ok(trace)) { 55 | return; 56 | } 57 | 58 | set_length(store_path->extensionless_length, store_path->buffer); 59 | /*FIXME configure me*/ 60 | concat_char('-', store_path->buffer, trace); 61 | concat_size(store_path->duplicate_count + 1, store_path->buffer, trace); 62 | concat_string(store_path->extension, store_path->buffer, trace); 63 | 64 | if (ok(trace)) { 65 | ++store_path->duplicate_count; 66 | } 67 | } 68 | 69 | void free_store_path(struct store_path *store_path) { 70 | if (store_path) { 71 | free_buffer(store_path->buffer); 72 | free(store_path->extension); 73 | free(store_path); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/journal.c: -------------------------------------------------------------------------------- 1 | #include "journal.h" 2 | #include "buffer.h" 3 | #include "parents.h" 4 | #include "timestamp.h" 5 | #include "trace.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | struct journal { 12 | int fd; 13 | char *timestamp_pattern; 14 | }; 15 | 16 | struct journal *open_journal(const char *path, const char *timestamp_pattern, 17 | struct trace *trace) { 18 | if (!path) { 19 | return NULL; 20 | } 21 | create_parents(path, trace); 22 | struct journal *journal = TNULL(malloc(sizeof(struct journal)), trace); 23 | if (!ok(trace)) { 24 | return NULL; 25 | } 26 | journal->fd = TNEG(open(path, O_CREAT | O_WRONLY | O_APPEND, 0644), trace); 27 | journal->timestamp_pattern = TNULL(strdup(timestamp_pattern), trace); 28 | if (!ok(trace)) { 29 | if (journal->fd > 0) { 30 | close(journal->fd); 31 | } 32 | free(journal->timestamp_pattern); 33 | free(journal); 34 | return NULL; 35 | } 36 | return journal; 37 | } 38 | 39 | void note(const char *event, pid_t pid, const char *path, 40 | const struct journal *journal, struct trace *trace) { 41 | if (!journal || !event || !ok(trace)) { 42 | return; 43 | } 44 | char *timestamp = 45 | get_timestamp(journal->timestamp_pattern, /*FIXME*/ NAME_MAX, trace); 46 | struct buffer *buffer = create_buffer(trace); 47 | if (!ok(trace)) { 48 | free(timestamp); 49 | free_buffer(buffer); 50 | return; 51 | } 52 | if (*timestamp) { 53 | concat_string(timestamp, buffer, trace); 54 | concat_char('\t', buffer, trace); 55 | } 56 | if (*event) { 57 | concat_string(event, buffer, trace); 58 | concat_char('\t', buffer, trace); 59 | } 60 | if (pid) { 61 | concat_size(pid, buffer, trace); 62 | concat_char('\t', buffer, trace); 63 | } 64 | concat_string(path, buffer, trace); 65 | concat_char('\n', buffer, trace); 66 | 67 | size_t size_written = 0; 68 | while (ok(trace) && get_length(get_view(buffer)) > size_written) { 69 | size_written += TNEG(write(journal->fd, get_string(get_view(buffer)), 70 | get_length(get_view(buffer))), 71 | trace); 72 | } 73 | 74 | free(timestamp); 75 | free_buffer(buffer); 76 | } 77 | 78 | void free_journal(struct journal *journal) { 79 | if (journal) { 80 | close(journal->fd); 81 | free(journal->timestamp_pattern); 82 | free(journal); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/trace.c: -------------------------------------------------------------------------------- 1 | #include "trace.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | void test_trace(struct trace *trace) { 8 | const char *a = "abc"; 9 | const char *b = "XYZ"; 10 | const char *c = "///"; 11 | 12 | try(trace); 13 | throw_static(a, trace); 14 | assert(!catch_static(b, trace)); 15 | assert(catch_static(a, trace)); 16 | finally(trace); 17 | 18 | try(trace); 19 | throw_static(a, trace); 20 | try(trace); 21 | assert(!catch_static(a, trace)); 22 | finally(trace); 23 | assert(catch_static(a, trace)); 24 | finally(trace); 25 | 26 | try(trace); 27 | throw_dynamic(b, trace); 28 | assert(!catch_static(b, trace)); 29 | finally_catch_all(trace); 30 | 31 | try(trace); 32 | throw_context(c, trace); 33 | assert(!catch_static(c, trace)); 34 | finally_catch_all(trace); 35 | 36 | try(trace); 37 | assert(ok(trace)); 38 | errno = ENOMEM; 39 | throw_errno(trace); 40 | assert(!ok(trace)); 41 | finally_catch_all(trace); 42 | 43 | try(trace); 44 | throw_static(a, trace); 45 | assert(!catch_static(b, trace)); 46 | throw_static(b, trace); 47 | assert(!catch_static(a, trace)); 48 | throw_static(a, trace); 49 | assert(!catch_static(b, trace)); 50 | assert(catch_static(a, trace)); 51 | finally_catch_all(trace); 52 | assert(ok(trace)); 53 | 54 | try(trace); 55 | try(trace); 56 | finally_rethrow_static(a, trace); 57 | finally_rethrow_static(b, trace); 58 | assert(ok(trace)); 59 | 60 | try(trace); 61 | try(trace); 62 | try(trace); 63 | throw_static(a, trace); 64 | finally_rethrow_static(b, trace); 65 | finally_rethrow_static(c, trace); 66 | assert(!catch_static(a, trace)); 67 | assert(!catch_static(b, trace)); 68 | assert(catch_static(c, trace)); 69 | finally(trace); 70 | assert(ok(trace)); 71 | 72 | try(trace); 73 | try(trace); 74 | throw_static(a, trace); 75 | try(trace); 76 | finally_rethrow_static(b, trace); 77 | finally_rethrow_static(c, trace); 78 | assert(!catch_static(a, trace)); 79 | assert(!catch_static(b, trace)); 80 | assert(catch_static(c, trace)); 81 | finally(trace); 82 | assert(ok(trace)); 83 | 84 | try(trace); 85 | throw_static(a, trace); 86 | try(trace); 87 | try(trace); 88 | finally_rethrow_static(b, trace); 89 | finally_rethrow_static(c, trace); 90 | assert(!catch_static(b, trace)); 91 | assert(!catch_static(c, trace)); 92 | assert(catch_static(a, trace)); 93 | finally(trace); 94 | assert(ok(trace)); 95 | } 96 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A smart versioning and automatic backup daemon"; 3 | outputs = { self, nixpkgs, flake-utils }: 4 | let 5 | utils = flake-utils.lib; 6 | system = utils.system; 7 | supportedSystems = [ 8 | system.aarch64-linux 9 | system.x86_64-linux 10 | system.i686-linux 11 | system.armv7l-linux 12 | ]; 13 | in 14 | utils.eachSystem supportedSystems (system: 15 | let 16 | inherit (builtins) mapAttrs; 17 | pkgs = import nixpkgs { inherit system; }; 18 | 19 | packages = 20 | let 21 | mkPackage = pkgs': pkgs'.callPackage ./. { 22 | doCheckThoroughly = false; 23 | lua = pkgs'.lua5_4; 24 | }; 25 | in 26 | { 27 | default = mkPackage pkgs; 28 | static = mkPackage pkgs.pkgsStatic; 29 | }; 30 | 31 | checks = 32 | let 33 | mkCheck = { callPackage, lua }: callPackage ./. { 34 | inherit lua; 35 | }; 36 | in 37 | { 38 | glibc = mapAttrs 39 | (_: lua: mkCheck { 40 | inherit (pkgs) callPackage; 41 | inherit lua; 42 | }) 43 | { 44 | inherit (pkgs) lua5_4 lua5_3 lua5_2; 45 | withoutLua = null; 46 | }; 47 | musl = pkgs.lib.optionalAttrs pkgs.stdenv.targetPlatform.is64bit { 48 | muslWithoutLua = mkCheck { 49 | inherit (pkgs.pkgsMusl) callPackage; 50 | lua = null; 51 | }; 52 | }; 53 | }; 54 | in 55 | { 56 | inherit packages; 57 | checks = checks.glibc // checks.musl; 58 | devShells = 59 | let 60 | devShells = 61 | let 62 | mkShell = pkgs': package: pkgs'.mkShell { 63 | inputsFrom = [ 64 | package 65 | ]; 66 | packages = [ 67 | pkgs.gdb 68 | ]; 69 | }; 70 | in 71 | { 72 | glibc = mapAttrs (_: mkShell pkgs) checks.glibc; 73 | musl = mapAttrs (_: mkShell pkgs.pkgsMusl) checks.musl; 74 | default = { 75 | default = mkShell pkgs (packages.default.override { 76 | doCheckThoroughly = true; 77 | }); 78 | static = mkShell pkgs.pkgsStatic packages.static; 79 | }; 80 | }; 81 | in 82 | devShells.glibc // devShells.musl // devShells.default; 83 | } 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/sieve.c: -------------------------------------------------------------------------------- 1 | #include "sieve.h" 2 | #include "buffer.h" 3 | #include "set.h" 4 | #include "trace.h" 5 | #include 6 | #include 7 | 8 | struct sieved_path { 9 | size_t set_count; 10 | const char **ends; 11 | const char *hiding_dot; 12 | }; 13 | 14 | struct sieved_path *sieve(const char *path, size_t relative_path_offset, 15 | const struct set **sets, size_t set_count, 16 | struct trace *trace) { 17 | assert(*path == '/'); 18 | assert(relative_path_offset); 19 | 20 | struct sieved_path *sieved_path = 21 | TNULL(malloc(sizeof(struct sieved_path)), trace); 22 | const char **ends = TNULL(calloc(set_count, sizeof(char *)), trace); 23 | struct buffer *absolute_path_buffer = create_buffer(trace); 24 | struct buffer *relative_path_buffer = create_buffer(trace); 25 | 26 | if (!ok(trace)) { 27 | free(sieved_path); 28 | free(ends); 29 | free_buffer(absolute_path_buffer); 30 | free_buffer(relative_path_buffer); 31 | return NULL; 32 | } 33 | 34 | sieved_path->set_count = set_count; 35 | sieved_path->ends = ends; 36 | sieved_path->hiding_dot = NULL; 37 | 38 | const struct buffer_view *absolute_path_view = get_view(absolute_path_buffer); 39 | const struct buffer_view *relative_path_view = get_view(relative_path_buffer); 40 | 41 | for (const char *this = path, *next = path + 1; *this; ++this, ++next) { 42 | concat_char(*this, absolute_path_buffer, trace); 43 | if (this >= path + relative_path_offset) { 44 | concat_char(*this, relative_path_buffer, trace); 45 | } 46 | if (!ok(trace)) { 47 | free_buffer(absolute_path_buffer); 48 | free_buffer(relative_path_buffer); 49 | free_sieved_path(sieved_path); 50 | return NULL; 51 | } 52 | if (!*next || *next == '/' || this == path) { 53 | for (size_t i = 0; i < set_count; ++i) { 54 | if (is_within(absolute_path_view, sets[i]) || 55 | (this >= path + relative_path_offset && 56 | is_within(relative_path_view, sets[i]))) { 57 | sieved_path->ends[i] = next; 58 | } 59 | } 60 | } 61 | if (*this == '/' && *next == '.') { 62 | sieved_path->hiding_dot = next; 63 | } 64 | } 65 | 66 | free_buffer(absolute_path_buffer); 67 | free_buffer(relative_path_buffer); 68 | 69 | return sieved_path; 70 | } 71 | 72 | const char *get_hiding_dot(const struct sieved_path *sieved_path) { 73 | return sieved_path->hiding_dot; 74 | }; 75 | 76 | const char *const *get_sieved_ends(const struct sieved_path *sieved_path) { 77 | return sieved_path->ends; 78 | } 79 | 80 | void free_sieved_path(struct sieved_path *sieved_path) { 81 | if (sieved_path) { 82 | free(sieved_path->ends); 83 | free(sieved_path); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/handler.c: -------------------------------------------------------------------------------- 1 | #include "handler.h" 2 | #include "test-constants.h" 3 | #include "trace.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define CONFIG_BASE "lua/handler.lua" 11 | #define CONFIG TEST_ROOT "/" CONFIG_BASE 12 | #define F1 CONFIG 13 | #define F2 TEST_ROOT "/lua/empty.lua" 14 | #define EMPTY "empty" 15 | #define IN_STORE(PATH) "./klunok/store/" PATH "/version" 16 | 17 | void test_handler(struct trace *trace) { 18 | struct handler *handler = load_handler(CONFIG, 1, trace); 19 | assert(ok(trace)); 20 | 21 | time_t pause = handle_timeout(handler, trace); 22 | assert(ok(trace)); 23 | assert(pause < 0); 24 | 25 | int fd = open(CONFIG, O_RDONLY); 26 | assert(fd >= 0); 27 | handle_open_exec(getpid(), fd, handler, trace); 28 | assert(ok(trace)); 29 | 30 | handle_close_write(getpid(), fd, handler, trace); 31 | assert(ok(trace)); 32 | 33 | close(fd); 34 | fd = open(F2, O_RDONLY); 35 | assert(fd >= 0); 36 | handle_close_write(getpid(), fd, handler, trace); 37 | assert(ok(trace)); 38 | 39 | pause = handle_timeout(handler, trace); 40 | assert(ok(trace)); 41 | assert(pause < 0); 42 | 43 | assert(access(IN_STORE(F1) ".lua", F_OK) == 0); 44 | assert(access(IN_STORE(F2) ".lua", F_OK) == 0); 45 | 46 | handle_close_write(getpid(), fd, handler, trace); 47 | assert(ok(trace)); 48 | 49 | pause = handle_timeout(handler, trace); 50 | assert(ok(trace)); 51 | assert(pause < 0); 52 | 53 | assert(access(IN_STORE(F2) "-1.lua", F_OK) == 0); 54 | 55 | close(fd); 56 | fd = open(EMPTY, O_CREAT, S_IRWXU); 57 | 58 | handle_close_write(getpid(), fd, handler, trace); 59 | assert(ok(trace)); 60 | 61 | assert(unlink(EMPTY) >= 0); 62 | 63 | pause = handle_timeout(handler, trace); 64 | assert(ok(trace)); 65 | assert(pause < 0); 66 | assert(access(IN_STORE(EMPTY), F_OK) != 0); 67 | 68 | close(fd); 69 | fd = open(EMPTY, O_CREAT, S_IWOTH); 70 | 71 | handle_close_write(getpid(), fd, handler, trace); 72 | assert(ok(trace)); 73 | 74 | pause = handle_timeout(handler, trace); 75 | assert(ok(trace)); 76 | assert(pause < 0); 77 | assert(access(IN_STORE(EMPTY), F_OK) != 0); 78 | 79 | close(fd); 80 | free_handler(handler); 81 | 82 | handler = load_handler(CONFIG, sizeof TEST_ROOT, trace); 83 | assert(ok(trace)); 84 | fd = open(CONFIG, O_RDONLY); 85 | assert(fd >= 0); 86 | handle_open_exec(getpid(), fd, handler, trace); 87 | assert(ok(trace)); 88 | handle_close_write(getpid(), fd, handler, trace); 89 | assert(ok(trace)); 90 | pause = handle_timeout(handler, trace); 91 | assert(pause < 0); 92 | assert(ok(trace)); 93 | assert(access(IN_STORE(CONFIG_BASE) ".lua", F_OK) == 0); 94 | 95 | close(fd); 96 | free_handler(handler); 97 | } 98 | -------------------------------------------------------------------------------- /src/mountinfo.c: -------------------------------------------------------------------------------- 1 | #include "mountinfo.h" 2 | #include "buffer.h" 3 | #include "set.h" 4 | #include "trace.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | struct mountinfo { 12 | struct set *mounts; 13 | }; 14 | 15 | static size_t count_lines(const char *string) { 16 | size_t count = 0; 17 | for (const char *cursor = string; *cursor; ++cursor) { 18 | if (*cursor == '\n') { 19 | ++count; 20 | } 21 | } 22 | return count; 23 | } 24 | 25 | static char *read_proc_file(const char *path, struct trace *trace) { 26 | long page_size = TNEG(sysconf(_SC_PAGE_SIZE), trace); 27 | int fd = TNEG(open(path, O_RDONLY), trace); 28 | char *page = TNULL(malloc(page_size + 1), trace); 29 | struct buffer *buffer = create_buffer(trace); 30 | 31 | ssize_t ssize; 32 | while ((ssize = TNEG(read(fd, page, page_size), trace)) > 0) { 33 | page[ssize] = 0; 34 | concat_string(page, buffer, trace); 35 | } 36 | 37 | free(page); 38 | if (fd >= 0) { 39 | close(fd); 40 | } 41 | return free_outer_buffer(buffer); 42 | } 43 | 44 | struct mountinfo *load_mountinfo(struct trace *trace) { 45 | struct mountinfo *mountinfo = TNULL(malloc(sizeof(struct mountinfo)), trace); 46 | char *proc_mounts_content = read_proc_file("/proc/self/mounts", trace); 47 | if (!ok(trace)) { 48 | free(mountinfo); 49 | return NULL; 50 | } 51 | 52 | char *cursor = proc_mounts_content; 53 | mountinfo->mounts = create_set(count_lines(proc_mounts_content), trace); 54 | 55 | for (char *record = strsep(&cursor, "\n"); ok(trace) && record; 56 | record = strsep(&cursor, "\n")) { 57 | strsep(&record, " "); 58 | if (record) { 59 | add(strsep(&record, " "), mountinfo->mounts, trace); 60 | } 61 | } 62 | 63 | free(proc_mounts_content); 64 | 65 | if (!ok(trace)) { 66 | free_mountinfo(mountinfo); 67 | return NULL; 68 | } 69 | 70 | return mountinfo; 71 | } 72 | 73 | char *make_mount(const char *path, const struct mountinfo *mountinfo, 74 | struct trace *trace) { 75 | char *resolved_path = TNULL(realpath(path, NULL), trace); 76 | struct buffer_view *view = create_buffer_view(resolved_path, trace); 77 | if (!ok(trace)) { 78 | free(resolved_path); 79 | return NULL; 80 | } 81 | if (is_within(view, mountinfo->mounts)) { 82 | free_buffer_view(view); 83 | return resolved_path; 84 | } 85 | free_buffer_view(view); 86 | TNEG(mount(resolved_path, resolved_path, NULL, MS_BIND, NULL), trace); 87 | add(resolved_path, mountinfo->mounts, trace); 88 | return resolved_path; 89 | } 90 | 91 | void free_mountinfo(struct mountinfo *mountinfo) { 92 | if (mountinfo) { 93 | free_set(mountinfo->mounts); 94 | free(mountinfo); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /web/docs/cli.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # CLI 6 | 7 | ## `-c`: path to the configuration file 8 | 9 | See [the configuration section](./configuration.md) for more details. 10 | Defaults to no configuration. 11 | Example: 12 | 13 | ```bash 14 | klunok -c ~/.config/klunok/config.lua 15 | ``` 16 | 17 | ## `-w`: path to a directory that should be monitored for edited files 18 | 19 | Can be specified more than once. 20 | Files in subdirectories, subsubdirectories, etc. are also included. 21 | The directory will be bind-mounted to itself if it's not already a mount point, 22 | a detailed explanation of the mechanism is in 23 | [the mounts section](./mounts.md). 24 | Defaults to the current working directory. 25 | Example: 26 | 27 | ```bash 28 | klunok -w /home/nazar -w /etc/nixos 29 | ``` 30 | 31 | ## `-e`: path to a directory that contains executable files 32 | 33 | Klunok can recognize a text editor, office suite, vector graphics editor, and so on 34 | only if the executable file of the text editor (office suite, …) 35 | is within one of these directories (including subdirectories, subsubdirectories, …). 36 | 37 | Can be specified more than once. 38 | The directory will be bind-mounted to itself if it's not already a mount point, 39 | a detailed explanation of the mechanism is in 40 | [the mounts section](./mounts.md). 41 | 42 | Defaults to `/` and, if you have 43 | [installed Klunok with the Nix package manager](./installation.md?method=nix), `/nix/store`. 44 | 45 | Usually, Linux distributions place all the executable files in several well-known 46 | directories, such as `/bin` or `/usr/bin`. 47 | If you know for sure which directories contain executable files of text editors 48 | (office suites, …), it's a good security practice to list them explicitly, as in the example: 49 | 50 | ```bash 51 | klunok -e /usr/bin -e /nix/store 52 | ``` 53 | 54 | ## `-d`: path to a file or directory, which owner's identity will be used for running Klunok 55 | 56 | Even though Klunok must be started as root to properly initialize, 57 | Klunok will not continue running as root after initializing. 58 | Instead, it will assume identity of the owner of the provided file or directory. 59 | This way, most of the code of Klunok doesn't run as root, which is a good security practice. 60 | 61 | Defaults to the current working directory. 62 | Example: 63 | 64 | ```bash 65 | klunok -d /home/nazar 66 | ``` 67 | 68 | ## `-h`: print help 69 | 70 | Example: 71 | 72 | ```bash 73 | klunok -h 74 | ``` 75 | 76 | ``` 77 | Klunok 0.1.1, Lua 5.4.6, compiled with Nix support 78 | Usage: klunok [-h | -v | -c PATH | -d PATH | -w PATH | -e PATH]... 79 | ``` 80 | 81 | ## `-v`: print version 82 | 83 | Example: 84 | 85 | ```bash 86 | klunok -v 87 | ``` 88 | 89 | ``` 90 | Klunok 0.1.1, Lua 5.4.6, compiled with Nix support 91 | ``` 92 | -------------------------------------------------------------------------------- /inc/trace.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | struct trace *create_trace() __attribute__((warn_unused_result)); 5 | 6 | bool ok(const struct trace *trace) 7 | __attribute__((pure, nonnull, warn_unused_result)); 8 | void try(struct trace *trace) __attribute__((nonnull)); 9 | 10 | void throw_static(const char *message, struct trace *trace) 11 | __attribute__((nonnull)); 12 | void throw_dynamic(const char *message, struct trace *trace) 13 | __attribute__((nonnull)); 14 | void throw_context(const char *message, struct trace *trace) 15 | __attribute__((nonnull)); 16 | void throw_errno(struct trace *trace) __attribute__((nonnull)); 17 | 18 | bool catch_static(const char *message, struct trace *trace) 19 | __attribute__((nonnull)); 20 | 21 | void finally(struct trace *trace) __attribute__((nonnull)); 22 | void finally_catch_all(struct trace *trace) __attribute__((nonnull)); 23 | void finally_rethrow_static(const char *message, struct trace *trace) 24 | __attribute__((nonnull)); 25 | 26 | void rethrow_context(const char *message, struct trace *trace) 27 | __attribute__((nonnull)); 28 | 29 | void unwind(int fd, const struct trace *trace) __attribute__((nonnull)); 30 | 31 | #define TNEG(call, trace) \ 32 | ({ \ 33 | typeof(call) _tneg_result = -1; \ 34 | if (ok(trace)) { \ 35 | _tneg_result = call; \ 36 | if (_tneg_result < 0) { \ 37 | throw_errno(trace); \ 38 | } \ 39 | } \ 40 | _tneg_result; \ 41 | }) 42 | 43 | #define TNULL(call, trace) \ 44 | ({ \ 45 | typeof(call) _tnull_result = NULL; \ 46 | if (ok(trace)) { \ 47 | _tnull_result = call; \ 48 | if (!_tnull_result) { \ 49 | throw_errno(trace); \ 50 | } \ 51 | } \ 52 | _tnull_result; \ 53 | }) 54 | -------------------------------------------------------------------------------- /web/docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | import Tabs from '@theme/Tabs'; 6 | import TabItem from '@theme/TabItem'; 7 | 8 | # Installation 9 | 10 | You can choose from three installation methods. 11 | Please navigate to the one you prefer via the tabs below. 12 | 13 | 14 | 15 | 16 | You can download a self-contained binary from 17 | https://github.com/Kharacternyk/klunok/releases/latest/download/klunok. 18 | You can also browse past releases at https://github.com/Kharacternyk/klunok/releases. 19 | 20 | Once the file is downloaded, mark it as executable: 21 | 22 | ```bash 23 | chmod +x ~/Downloads/klunok 24 | ``` 25 | 26 | Also, you can copy the binary to somewhere in you `$PATH`, for example: 27 | 28 | ```bash 29 | sudo cp ~/Downloads/klunok /bin/ 30 | ``` 31 | 32 | If you skip this step, 33 | do not forget to specify the full path to the binary when invoking Klunok, for example 34 | `~/Downloads/klunok`. 35 | 36 | The binaries should work on all x86-64 Linux systems. 37 | They will not work on architectures other than x86-64, 38 | for example ARM on Raspberry Pi or 32-bit x86. 39 | Please use alternative installation methods for other architectures. 40 | 41 | [The binaries are reproducible.](./security.md#static-binary-reproducibility) 42 | 43 | 44 | 45 | 46 | 47 | You can install Klunok with a flake-enabled version of the Nix package manager: 48 | 49 | ```bash 50 | nix profile install github:Kharacternyk/klunok/v1 51 | ``` 52 | 53 | Avoid installing `github:Kharacternyk/klunok` (without a version tag), 54 | as this is the development version that is more likely to have bugs. 55 | Instead use `github:Kharacternyk/klunok/v1` for the latest stable version. 56 | 57 | 58 | 59 | 60 | Installing from source requires a C compiler and the Meson build system. 61 | Meson also depends on the Ninja build system. 62 | You can install them, for example, with `apt`: 63 | 64 | ```bash 65 | sudo apt install gcc meson ninja-build 66 | ``` 67 | 68 | The version of Meson installed with `apt` may not be new enough to build Klunok. 69 | In this case, you can install Meson with `pip`: 70 | 71 | ```bash 72 | sudo apt install pip 73 | sudo pip install meson 74 | ``` 75 | 76 | Lua is an optional but recommended dependency. 77 | It allows configuring Klunok without recompiling it. 78 | Klunok should work with Lua version 5.2 or newer. 79 | Installing Lua with `apt` looks like this: 80 | 81 | ```bash 82 | sudo apt install lua5.4 83 | ``` 84 | 85 | Once the dependencies are installed, get the source from 86 | [GitHub](https://github.com/Kharacternyk/klunok) and install Klunok with Meson. 87 | The whole process may look like this: 88 | 89 | ```bash 90 | git clone https://github.com/Kharacternyk/klunok 91 | cd klunok 92 | git checkout v1 93 | meson setup build -Dbuildtype=release 94 | cd build 95 | sudo meson install 96 | ``` 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /inc/config.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | struct trace; 5 | 6 | struct config *load_config(const char *path, struct trace *trace) 7 | __attribute__((warn_unused_result)); 8 | 9 | const struct set *get_editors(const struct config *config) 10 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 11 | const struct set *get_project_roots(const struct config *config) 12 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 13 | const struct set *get_project_parents(const struct config *config) 14 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 15 | const struct set *get_history_paths(const struct config *config) 16 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 17 | const struct set *get_excluded_paths(const struct config *config) 18 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 19 | const struct set *get_included_paths(const struct config *config) 20 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 21 | const struct set *get_cluded_paths(const struct config *config) 22 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 23 | const char *get_store_root(const struct config *config) 24 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 25 | const char *get_project_store_root(const struct config *config) 26 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 27 | const char *get_unstable_project_store_root(const struct config *config) 28 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 29 | const char *get_queue_path(const struct config *config) 30 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 31 | const char *get_journal_path(const struct config *config) 32 | __attribute__((pure, nonnull, warn_unused_result)); 33 | const char *get_offset_store_root(const struct config *config) 34 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 35 | const char *get_journal_timestamp_pattern(const struct config *config) 36 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 37 | const char *get_version_pattern(const struct config *config) 38 | __attribute__((pure, returns_nonnull, nonnull, warn_unused_result)); 39 | size_t get_debounce_seconds(const struct config *config) 40 | __attribute__((pure, nonnull, warn_unused_result)); 41 | size_t get_path_length_guess(const struct config *config) 42 | __attribute__((pure, nonnull, warn_unused_result)); 43 | size_t get_elf_interpreter_count_guess(const struct config *config) 44 | __attribute__((pure, nonnull, warn_unused_result)); 45 | size_t get_queue_size_guess(const struct config *config) 46 | __attribute__((pure, nonnull, warn_unused_result)); 47 | pid_t get_max_pid_guess(const struct config *config) 48 | __attribute__((pure, nonnull, warn_unused_result)); 49 | const char *get_event_open_exec_not_editor(const struct config *config) 50 | __attribute__((pure, nonnull, warn_unused_result)); 51 | const char *get_event_open_exec_editor(const struct config *config) 52 | __attribute__((pure, nonnull, warn_unused_result)); 53 | const char *get_event_close_write_not_by_editor(const struct config *config) 54 | __attribute__((pure, nonnull, warn_unused_result)); 55 | const char *get_event_close_write_by_editor(const struct config *config) 56 | __attribute__((pure, nonnull, warn_unused_result)); 57 | const char *get_event_queue_head_deleted(const struct config *config) 58 | __attribute__((pure, nonnull, warn_unused_result)); 59 | const char *get_event_queue_head_forbidden(const struct config *config) 60 | __attribute__((pure, nonnull, warn_unused_result)); 61 | const char *get_event_queue_head_stored(const struct config *config) 62 | __attribute__((pure, nonnull, warn_unused_result)); 63 | 64 | void free_config(struct config *config); 65 | -------------------------------------------------------------------------------- /src/messages.c: -------------------------------------------------------------------------------- 1 | #include "messages.h" 2 | #include "constants.h" 3 | 4 | #define BARE_VERSION_MESSAGE "Klunok " VERSION ", Lua " LUA_VERSION 5 | #define USAGE "klunok [-h | -v | -c PATH | -d PATH | -w PATH | -e PATH]..." 6 | 7 | #ifdef WATCH_NIX_STORE 8 | #define VERSION_MESSAGE BARE_VERSION_MESSAGE ", compiled with Nix support" 9 | #else 10 | #define VERSION_MESSAGE BARE_VERSION_MESSAGE 11 | #endif 12 | 13 | const struct translation messages = { 14 | .trace = {.because_of = "because of", 15 | .which_is = "which is", 16 | .message_dropped = "message dropped"}, 17 | .timestamp = {.overflow = "A version string exceeds the size limit"}, 18 | .parents = {.cannot_remove_ancestor = "Cannot remove an ancestor directory", 19 | .cannot_create_ancestor = 20 | "Cannot create an ancestor directory"}, 21 | .sync = {.source_is_not_regular_file = 22 | "Cannot copy a file that is not a regular file", 23 | .source_does_not_exist = "The file does not exist", 24 | .source_permission_denied = "Permission denied", 25 | .destination_already_exists = "The destination already exists"}, 26 | .config = {.is_static = "Configuration files are not supported"}, 27 | .linq = {.invalid_entry = "The queue contains an invalid entry"}, 28 | .params = {.unknown_option = "An unknown option has been passed", 29 | .stray_option = 30 | "An option without a required value has been passed", 31 | .redefined_option = 32 | "An option has been passed more than once, but " 33 | "it cannot have multiple values"}, 34 | .handler = {.version = {.has_slashes = 35 | "Slash characters found in a version string"}, 36 | .config = {.cannot_load = "Cannot load the configuration file", 37 | .cannot_reload = 38 | "Cannot reload the configuration file"}, 39 | .linq = {.cannot_load = "Cannot load the debouncing link queue", 40 | .cannot_reload = 41 | "Cannot reload the debouncing link queue", 42 | .cannot_get_head = 43 | "Cannot get the head of the debouncing link queue", 44 | .cannot_push = 45 | "Cannot push a file to the debouncing link queue"}, 46 | .store = {.cannot_copy = "Cannot copy a file to the store"}, 47 | .journal = {.cannot_open = "Cannot open the journal", 48 | .cannot_write_to = "Cannot write to the journal"}}, 49 | .main = { 50 | .cannot_bootstrap = "Cannot bootstrap", 51 | .out_of_memory = "There is not enough memory", 52 | .cli_usage_violated = 53 | "Some of the command line arguments do not fit the usage", 54 | .cannot_parse_cli = "Cannot parse the command line arguments", 55 | .cannot_drop_privileges = 56 | "Cannot drop privileges by assuming identity of " 57 | "the owner of a path", 58 | .cannot_load_handler = "Cannot load the event handler", 59 | .cannot_handle_exec = "Cannot handle a file execution event", 60 | .cannot_handle_write = "Cannot handle a file write event", 61 | .cannot_handle_timeout = "Cannot handle the periodical tasks", 62 | .fanotify = {.cannot_init = "Cannot initialize fanotify", 63 | .cannot_poll = "Cannot poll the fanotify file descriptor", 64 | .cannot_read_event = "Cannot read a fanotify event", 65 | .version_mismatch = 66 | "The version of a fanotify event is unsupported", 67 | .queue_overflow = 68 | "The fanotify event queue has overflowed"}, 69 | .mount = {.cannot_list = "Cannot list mount points", 70 | .cannot_watch = "Cannot watch a mount point"}, 71 | .info = {.version = VERSION_MESSAGE, 72 | .usage = USAGE, 73 | .help = VERSION_MESSAGE "\nUsage: " USAGE}}}; 74 | -------------------------------------------------------------------------------- /src/params.c: -------------------------------------------------------------------------------- 1 | #include "params.h" 2 | #include "constants.h" 3 | #include "list.h" 4 | #include "messages.h" 5 | #include "trace.h" 6 | #include 7 | #include 8 | 9 | struct params { 10 | bool is_version_requested; 11 | bool is_help_requested; 12 | const char *config_path; 13 | const char *privilege_dropping_path; 14 | struct list *write_mounts; 15 | struct list *exec_mounts; 16 | }; 17 | 18 | struct params *parse_params(int argc, const char **argv, struct trace *trace) { 19 | struct params *params = TNULL(calloc(1, sizeof(struct params)), trace); 20 | struct list *write_mounts = create_list(trace); 21 | struct list *exec_mounts = create_list(trace); 22 | char opt = 0; 23 | 24 | for (int i = 1; i < argc && ok(trace) && opt != 'h' && opt != 'v'; ++i) { 25 | switch (opt) { 26 | case 0: 27 | if (argv[i][0] != '-' || !argv[i][1] || argv[i][2]) { 28 | throw_context(argv[i], trace); 29 | throw_static(messages.params.unknown_option, trace); 30 | } else { 31 | opt = argv[i][1]; 32 | } 33 | break; 34 | case 'c': 35 | if (params->config_path) { 36 | throw_context(argv[i - 1], trace); 37 | throw_static(messages.params.redefined_option, trace); 38 | } else { 39 | params->config_path = argv[i]; 40 | opt = 0; 41 | } 42 | break; 43 | case 'd': 44 | if (params->privilege_dropping_path) { 45 | throw_context(argv[i - 1], trace); 46 | throw_static(messages.params.redefined_option, trace); 47 | } else { 48 | params->privilege_dropping_path = argv[i]; 49 | opt = 0; 50 | } 51 | break; 52 | case 'w': 53 | join(argv[i], write_mounts, trace); 54 | opt = 0; 55 | break; 56 | case 'e': 57 | join(argv[i], exec_mounts, trace); 58 | opt = 0; 59 | break; 60 | default: 61 | throw_context(argv[i - 1], trace); 62 | throw_static(messages.params.unknown_option, trace); 63 | } 64 | } 65 | 66 | if (opt && ok(trace)) { 67 | switch (opt) { 68 | case 'h': 69 | params->is_help_requested = true; 70 | break; 71 | case 'v': 72 | params->is_version_requested = true; 73 | break; 74 | case 'c': 75 | case 'd': 76 | case 'w': 77 | case 'e': 78 | throw_context(argv[argc - 1], trace); 79 | throw_static(messages.params.stray_option, trace); 80 | break; 81 | default: 82 | throw_context(argv[argc - 1], trace); 83 | throw_static(messages.params.unknown_option, trace); 84 | } 85 | } 86 | 87 | if (ok(trace) && !peek(exec_mounts)) { 88 | join("/", exec_mounts, trace); 89 | #ifdef WATCH_NIX_STORE 90 | join("/nix/store", exec_mounts, trace); 91 | #endif 92 | } 93 | if (ok(trace) && !peek(write_mounts)) { 94 | join(".", write_mounts, trace); 95 | } 96 | if (ok(trace) && !params->privilege_dropping_path) { 97 | params->privilege_dropping_path = "."; 98 | } 99 | 100 | if (!ok(trace)) { 101 | free_list(exec_mounts); 102 | free_list(write_mounts); 103 | free(params); 104 | return NULL; 105 | } 106 | 107 | params->exec_mounts = exec_mounts; 108 | params->write_mounts = write_mounts; 109 | return params; 110 | } 111 | 112 | bool is_help_requested(const struct params *params) { 113 | return params->is_help_requested; 114 | } 115 | 116 | bool is_version_requested(const struct params *params) { 117 | return params->is_version_requested; 118 | } 119 | 120 | const char *get_config_path(const struct params *params) { 121 | return params->config_path; 122 | } 123 | 124 | const char *get_privilege_dropping_path(const struct params *params) { 125 | return params->privilege_dropping_path; 126 | } 127 | 128 | const struct list *get_write_mounts(const struct params *params) { 129 | return params->write_mounts; 130 | } 131 | 132 | const struct list *get_exec_mounts(const struct params *params) { 133 | return params->exec_mounts; 134 | } 135 | 136 | void free_params(struct params *params) { 137 | if (params) { 138 | free_list(params->write_mounts); 139 | free_list(params->exec_mounts); 140 | free(params); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/trace.c: -------------------------------------------------------------------------------- 1 | #include "trace.h" 2 | #include "logstep.h" 3 | #include "messages.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | struct frame { 10 | union { 11 | char *dynamic_message; 12 | const char *static_message; 13 | }; 14 | bool is_dynamic; 15 | bool is_context; 16 | struct frame *next; 17 | }; 18 | 19 | struct trace { 20 | size_t dropped_frame_count; 21 | struct frame *head; 22 | size_t pre_throw_depth; 23 | size_t post_throw_depth; 24 | }; 25 | 26 | struct trace *create_trace() { return calloc(1, sizeof(struct trace)); } 27 | 28 | static void pop_trace_message(struct trace *trace) { 29 | struct frame *new_head = trace->head->next; 30 | if (trace->head->is_dynamic) { 31 | free(trace->head->dynamic_message); 32 | } 33 | free(trace->head); 34 | trace->head = new_head; 35 | } 36 | 37 | static void throw_common(const char *message, bool is_dynamic, bool is_context, 38 | struct trace *trace) { 39 | assert(is_dynamic || !is_context); 40 | struct frame *frame = malloc(sizeof(struct frame)); 41 | if (!frame) { 42 | ++trace->dropped_frame_count; 43 | return; 44 | } 45 | if (is_dynamic) { 46 | frame->dynamic_message = strdup(message); 47 | if (!frame->dynamic_message) { 48 | ++trace->dropped_frame_count; 49 | return free(frame); 50 | } 51 | } else { 52 | frame->static_message = message; 53 | } 54 | frame->is_dynamic = is_dynamic; 55 | frame->is_context = is_context; 56 | frame->next = trace->head; 57 | trace->head = frame; 58 | } 59 | 60 | void throw_static(const char *message, struct trace *trace) { 61 | throw_common(message, false, false, trace); 62 | } 63 | 64 | void throw_dynamic(const char *message, struct trace *trace) { 65 | throw_common(message, true, false, trace); 66 | } 67 | 68 | void throw_context(const char *message, struct trace *trace) { 69 | throw_common(message, true, true, trace); 70 | } 71 | 72 | void throw_errno(struct trace *trace) { throw_dynamic(strerror(errno), trace); } 73 | 74 | bool ok(const struct trace *trace) { 75 | return !trace->head && !trace->dropped_frame_count; 76 | } 77 | 78 | void try(struct trace *trace) { 79 | if (ok(trace)) { 80 | ++trace->pre_throw_depth; 81 | } else { 82 | ++trace->post_throw_depth; 83 | } 84 | } 85 | 86 | bool catch_static(const char *message, struct trace *trace) { 87 | assert(trace->pre_throw_depth || trace->post_throw_depth); 88 | if (trace->dropped_frame_count || trace->post_throw_depth) { 89 | return false; 90 | } 91 | if (trace->head && !trace->head->is_dynamic && 92 | trace->head->static_message == message) { 93 | while (trace->head) { 94 | pop_trace_message(trace); 95 | } 96 | return true; 97 | } 98 | return false; 99 | } 100 | 101 | static bool decrement_depth(struct trace *trace) { 102 | if (trace->post_throw_depth) { 103 | --trace->post_throw_depth; 104 | return false; 105 | } else { 106 | assert(trace->pre_throw_depth); 107 | --trace->pre_throw_depth; 108 | return true; 109 | } 110 | } 111 | 112 | void finally_catch_all(struct trace *trace) { 113 | assert(trace->pre_throw_depth || trace->post_throw_depth); 114 | trace->dropped_frame_count = 0; 115 | while (trace->head) { 116 | pop_trace_message(trace); 117 | } 118 | decrement_depth(trace); 119 | } 120 | 121 | void finally(struct trace *trace) { decrement_depth(trace); } 122 | 123 | void finally_rethrow_static(const char *message, struct trace *trace) { 124 | if (decrement_depth(trace) && !ok(trace)) { 125 | throw_static(message, trace); 126 | } 127 | } 128 | 129 | void rethrow_context(const char *message, struct trace *trace) { 130 | assert(trace->post_throw_depth || trace->pre_throw_depth); 131 | if (!trace->post_throw_depth && !ok(trace)) { 132 | throw_context(message, trace); 133 | } 134 | } 135 | 136 | void unwind(int fd, const struct trace *trace) { 137 | size_t depth = 0; 138 | 139 | for (struct frame *frame = trace->head; frame; frame = frame->next) { 140 | const char *prefix = !depth ? NULL 141 | : frame->is_context ? messages.trace.which_is 142 | : messages.trace.because_of; 143 | logstep(fd, prefix, frame->static_message, depth); 144 | ++depth; 145 | } 146 | 147 | for (size_t i = 0; i < trace->dropped_frame_count; ++i) { 148 | logstep(fd, messages.trace.message_dropped, NULL, depth); 149 | ++depth; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/set.c: -------------------------------------------------------------------------------- 1 | #include "set.h" 2 | #include "buffer.h" 3 | #include "messages.h" 4 | #include "trace.h" 5 | #include 6 | #include 7 | #include 8 | 9 | struct entry { 10 | struct buffer *value; 11 | size_t count; 12 | /* TODO use unsigned instead of size_t in a lot of places */ 13 | unsigned metadata; 14 | struct entry *next; 15 | }; 16 | 17 | struct set { 18 | size_t size; 19 | size_t empty_head_count; 20 | struct entry **heads; 21 | }; 22 | 23 | struct set *create_set(size_t size_guess, struct trace *trace) { 24 | size_t size = size_guess * 2 + 2; 25 | struct entry **entries = TNULL(calloc(size, sizeof(struct entry *)), trace); 26 | struct set *set = TNULL(malloc(sizeof(struct set)), trace); 27 | if (!ok(trace)) { 28 | free(entries); 29 | return NULL; 30 | } 31 | set->size = size; 32 | set->empty_head_count = size; 33 | set->heads = entries; 34 | return set; 35 | } 36 | 37 | bool is_empty(const struct set *set) { 38 | assert(set->empty_head_count <= set->size); 39 | return set->empty_head_count == set->size; 40 | } 41 | 42 | static struct entry **get_head(const struct buffer_view *value, 43 | const struct set *set) { 44 | /* TODO if the size is always a power of two, we can replace modulo with 45 | * bitwise stuff */ 46 | return set->heads + get_hash(value) % set->size; 47 | } 48 | 49 | static struct entry **find_entry(const struct buffer_view *value, 50 | struct entry **head) { 51 | struct entry **entry = head; 52 | 53 | while (*entry) { 54 | assert((*entry)->count > 0); 55 | const struct buffer_view *candidate = get_view((*entry)->value); 56 | 57 | if (get_hash(value) == get_hash(candidate) && 58 | get_length(value) == get_length(candidate) && 59 | !strcmp(get_string(value), get_string(candidate))) { 60 | return entry; 61 | } 62 | 63 | entry = &((*entry)->next); 64 | } 65 | 66 | return entry; 67 | } 68 | 69 | enum attribute { 70 | metadata, 71 | count, 72 | }; 73 | 74 | static size_t get_attribute(enum attribute attribute, 75 | const struct buffer_view *value, 76 | const struct set *set) { 77 | if (is_empty(set)) { 78 | return 0; 79 | } 80 | struct entry **entry = find_entry(value, get_head(value, set)); 81 | if (*entry) { 82 | return attribute == count ? (*entry)->count : (*entry)->metadata; 83 | } 84 | return 0; 85 | } 86 | 87 | size_t get_count(const struct buffer_view *value, const struct set *set) { 88 | return get_attribute(count, value, set); 89 | } 90 | 91 | unsigned get_last_metadata(const struct buffer_view *value, 92 | const struct set *set) { 93 | return get_attribute(metadata, value, set); 94 | } 95 | 96 | bool is_within(const struct buffer_view *value, const struct set *set) { 97 | return get_count(value, set); 98 | } 99 | 100 | void add_with_metadata(const char *value, unsigned metadata, struct set *set, 101 | struct trace *trace) { 102 | struct buffer *buffer = create_buffer(trace); 103 | concat_string(value, buffer, trace); 104 | if (!ok(trace)) { 105 | free_buffer(buffer); 106 | return; 107 | } 108 | 109 | struct entry **head = get_head(get_view(buffer), set); 110 | struct entry **entry = find_entry(get_view(buffer), head); 111 | if (*entry) { 112 | ++(*entry)->count; 113 | (*entry)->metadata = metadata; 114 | free_buffer(buffer); 115 | return; 116 | } 117 | 118 | struct entry *new_entry = TNULL(malloc(sizeof(struct entry)), trace); 119 | if (!ok(trace)) { 120 | free_buffer(buffer); 121 | free(new_entry); 122 | return; 123 | } 124 | 125 | if (!*head) { 126 | --set->empty_head_count; 127 | } 128 | 129 | new_entry->next = *head; 130 | new_entry->value = buffer; 131 | new_entry->metadata = metadata; 132 | new_entry->count = 1; 133 | 134 | *head = new_entry; 135 | } 136 | 137 | void add(const char *value, struct set *set, struct trace *trace) { 138 | add_with_metadata(value, 0, set, trace); 139 | } 140 | 141 | void pop(const struct buffer_view *value, struct set *set) { 142 | struct entry **head = get_head(value, set); 143 | struct entry **entry = find_entry(value, head); 144 | if (!*entry) { 145 | return; 146 | } 147 | 148 | --(*entry)->count; 149 | 150 | if (!(*entry)->count) { 151 | struct entry *new_next = (*entry)->next; 152 | free_buffer((*entry)->value); 153 | free(*entry); 154 | *entry = new_next; 155 | 156 | if (!*head) { 157 | ++set->empty_head_count; 158 | } 159 | } 160 | } 161 | 162 | void free_set(struct set *set) { 163 | if (!set) { 164 | return; 165 | } 166 | for (size_t i = 0; i < set->size; ++i) { 167 | struct entry *entry = set->heads[i]; 168 | while (entry) { 169 | struct entry *next = entry->next; 170 | free_buffer(entry->value); 171 | free(entry); 172 | entry = next; 173 | } 174 | } 175 | free(set->heads); 176 | free(set); 177 | } 178 | -------------------------------------------------------------------------------- /src/buffer.c: -------------------------------------------------------------------------------- 1 | #include "buffer.h" 2 | #include "trace.h" 3 | #include 4 | #include 5 | #include 6 | 7 | struct buffer_view { 8 | const char *string; 9 | size_t size; 10 | struct hash *hash; 11 | }; 12 | 13 | struct buffer { 14 | char *string; 15 | size_t capacity; 16 | struct buffer_view view; 17 | }; 18 | 19 | struct hash { 20 | size_t value; 21 | size_t seen_length; 22 | }; 23 | 24 | static const struct hash empty_hash; 25 | 26 | struct buffer_view *create_buffer_view(const char *string, 27 | struct trace *trace) { 28 | struct buffer_view *view = TNULL(malloc(sizeof(struct buffer_view)), trace); 29 | struct hash *hash = TNULL(calloc(1, sizeof(struct hash)), trace); 30 | 31 | if (!ok(trace)) { 32 | free(view); 33 | free(hash); 34 | return NULL; 35 | } 36 | 37 | view->string = string; 38 | view->hash = hash; 39 | view->size = strlen(string) + 1; 40 | 41 | return view; 42 | } 43 | 44 | struct buffer *create_buffer(struct trace *trace) { 45 | struct buffer *buffer = TNULL(malloc(sizeof(struct buffer)), trace); 46 | char *string = TNULL(calloc(1, sizeof(char)), trace); 47 | struct hash *hash = TNULL(calloc(1, sizeof(struct hash)), trace); 48 | 49 | if (!ok(trace)) { 50 | free(buffer); 51 | free(string); 52 | free(hash); 53 | return NULL; 54 | } 55 | 56 | buffer->string = string; 57 | buffer->capacity = 1; 58 | buffer->view.hash = hash; 59 | buffer->view.size = 1; 60 | buffer->view.string = buffer->string; 61 | 62 | return buffer; 63 | } 64 | 65 | static void ensure_capacity(size_t capacity, struct buffer *buffer, 66 | struct trace *trace) { 67 | if (ok(trace) && capacity > buffer->capacity) { 68 | size_t new_capacity = capacity * 2; 69 | char *new_string = TNULL(realloc(buffer->string, new_capacity), trace); 70 | if (ok(trace)) { 71 | buffer->capacity = new_capacity; 72 | buffer->view.string = buffer->string = new_string; 73 | } 74 | } 75 | } 76 | 77 | void concat_string(const char *string, struct buffer *buffer, 78 | struct trace *trace) { 79 | if (!ok(trace)) { 80 | return; 81 | } 82 | 83 | concat_bytes(string, strlen(string), buffer, trace); 84 | } 85 | 86 | void concat_bytes(const char *bytes, size_t byte_count, struct buffer *buffer, 87 | struct trace *trace) { 88 | if (!ok(trace)) { 89 | return; 90 | } 91 | 92 | size_t new_size = buffer->view.size + byte_count; 93 | ensure_capacity(new_size, buffer, trace); 94 | 95 | if (ok(trace)) { 96 | strncat(buffer->string, bytes, byte_count); 97 | buffer->view.size = new_size; 98 | } 99 | } 100 | 101 | void concat_char(char c, struct buffer *buffer, struct trace *trace) { 102 | ensure_capacity(buffer->view.size + 1, buffer, trace); 103 | if (ok(trace)) { 104 | buffer->string[buffer->view.size] = 0; 105 | buffer->string[buffer->view.size - 1] = c; 106 | ++buffer->view.size; 107 | } 108 | } 109 | 110 | void concat_size(size_t size, struct buffer *buffer, struct trace *trace) { 111 | if (!ok(trace)) { 112 | return; 113 | } 114 | if (!size) { 115 | return concat_char('0', buffer, trace); 116 | } 117 | size_t saved_length = get_length(&buffer->view); 118 | 119 | size_t power_of_ten = 1; 120 | while (power_of_ten <= size / 10) { 121 | power_of_ten *= 10; 122 | } 123 | 124 | char digit; 125 | while (power_of_ten) { 126 | digit = '0' + size / power_of_ten; 127 | concat_char(digit, buffer, trace); 128 | if (!ok(trace)) { 129 | set_length(saved_length, buffer); 130 | return; 131 | } 132 | size %= power_of_ten; 133 | power_of_ten /= 10; 134 | } 135 | } 136 | 137 | const struct buffer_view *get_view(const struct buffer *buffer) { 138 | return &buffer->view; 139 | } 140 | 141 | const char *get_string(const struct buffer_view *view) { return view->string; } 142 | 143 | size_t get_length(const struct buffer_view *view) { return view->size - 1; } 144 | 145 | void set_length(size_t length, struct buffer *buffer) { 146 | assert(length < buffer->view.size); 147 | buffer->string[length] = 0; 148 | buffer->view.size = length + 1; 149 | *buffer->view.hash = empty_hash; 150 | } 151 | 152 | size_t get_hash(const struct buffer_view *view) { 153 | while (view->hash->seen_length < get_length(view)) { 154 | char character = view->string[view->hash->seen_length]; 155 | size_t hash = view->hash->value; 156 | 157 | hash = character + (hash << 6) + (hash << 16) - hash; 158 | 159 | view->hash->value = hash; 160 | ++view->hash->seen_length; 161 | } 162 | 163 | return view->hash->value; 164 | } 165 | 166 | void free_buffer_view(struct buffer_view *view) { 167 | if (view) { 168 | free(view->hash); 169 | free(view); 170 | } 171 | } 172 | 173 | char *free_outer_buffer(struct buffer *buffer) { 174 | if (!buffer) { 175 | return NULL; 176 | } 177 | char *result = buffer->string; 178 | free(buffer->view.hash); 179 | free(buffer); 180 | return result; 181 | } 182 | 183 | void free_buffer(struct buffer *buffer) { 184 | if (buffer) { 185 | free(buffer->string); 186 | free(buffer->view.hash); 187 | free(buffer); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/sync.c: -------------------------------------------------------------------------------- 1 | #include "sync.h" 2 | #include "buffer.h" 3 | #include "messages.h" 4 | #include "parents.h" 5 | #include "trace.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | static void clean_up(const char *destination) { 17 | /*FIXME cleanup error reporting*/ 18 | struct trace *cleanup_trace = create_trace(); 19 | if (cleanup_trace) { 20 | try(cleanup_trace); 21 | remove_empty_parents(destination, cleanup_trace); 22 | finally_catch_all(cleanup_trace); 23 | free(cleanup_trace); 24 | } 25 | } 26 | 27 | off_t sync_file(const char *destination, const char *source, 28 | off_t source_offset, struct trace *trace) { 29 | if (!ok(trace)) { 30 | return 0; 31 | } 32 | 33 | create_parents(destination, trace); 34 | if (!ok(trace)) { 35 | clean_up(destination); 36 | return 0; 37 | } 38 | 39 | int in_fd = open(source, O_RDONLY); 40 | if (in_fd < 0) { 41 | if (errno == ENOENT) { 42 | throw_static(messages.sync.source_does_not_exist, trace); 43 | } else if (errno == EACCES) { 44 | throw_static(messages.sync.source_permission_denied, trace); 45 | } else { 46 | throw_errno(trace); 47 | } 48 | clean_up(destination); 49 | return 0; 50 | } 51 | 52 | int out_fd = open(destination, O_CREAT | O_WRONLY | O_EXCL, 0444); 53 | if (out_fd < 0) { 54 | if (errno == EEXIST) { 55 | throw_static(messages.sync.destination_already_exists, trace); 56 | } else { 57 | throw_errno(trace); 58 | } 59 | close(in_fd); 60 | clean_up(destination); 61 | return 0; 62 | } 63 | 64 | struct stat in_fd_stat; 65 | if (fstat(in_fd, &in_fd_stat) < 0) { 66 | throw_errno(trace); 67 | close(out_fd); 68 | close(in_fd); 69 | clean_up(destination); 70 | return 0; 71 | } 72 | 73 | if (!S_ISREG(in_fd_stat.st_mode)) { 74 | throw_static(messages.sync.source_is_not_regular_file, trace); 75 | close(out_fd); 76 | close(in_fd); 77 | clean_up(destination); 78 | return 0; 79 | } 80 | 81 | while (in_fd_stat.st_size) { 82 | ssize_t written_size = 83 | sendfile(out_fd, in_fd, &source_offset, in_fd_stat.st_size); 84 | if (written_size > 0) { 85 | in_fd_stat.st_size -= written_size; 86 | } else if (written_size == 0) { 87 | break; 88 | } else { 89 | throw_errno(trace); 90 | close(out_fd); 91 | close(in_fd); 92 | clean_up(destination); 93 | return 0; 94 | } 95 | } 96 | 97 | if (close(out_fd) < 0) { 98 | throw_errno(trace); 99 | close(in_fd); 100 | clean_up(destination); 101 | return 0; 102 | } 103 | 104 | close(in_fd); 105 | return source_offset; 106 | } 107 | 108 | void sync_shallow_tree(const char *destination, const char *source, 109 | const char *existence_filter_root, struct trace *trace) { 110 | create_parents(destination, trace); 111 | 112 | if (ok(trace) && mkdir(destination, 0755)) { 113 | if (errno == EEXIST) { 114 | throw_static(messages.sync.destination_already_exists, trace); 115 | } else { 116 | throw_errno(trace); 117 | } 118 | } 119 | 120 | int destination_fd = TNEG(open(destination, O_RDONLY), trace); 121 | char *paths[] = {TNULL(strdup(source), trace), NULL}; 122 | 123 | if (!ok(trace)) { 124 | if (destination_fd > 0) { 125 | close(destination_fd); 126 | } 127 | free(*paths); 128 | clean_up(destination); 129 | return; 130 | } 131 | 132 | FTS *fts = fts_open(paths, FTS_PHYSICAL | FTS_NOSTAT | FTS_NOCHDIR, NULL); 133 | 134 | if (!fts) { 135 | if (errno == ENOENT) { 136 | throw_static(messages.sync.source_does_not_exist, trace); 137 | } else if (errno == EACCES) { 138 | throw_static(messages.sync.source_permission_denied, trace); 139 | } else { 140 | throw_errno(trace); 141 | } 142 | } 143 | 144 | assert(!strstr(destination, "//")); 145 | size_t source_length = strlen(source); 146 | 147 | struct buffer *filter_path = create_buffer(trace); 148 | concat_string(existence_filter_root, filter_path, trace); 149 | concat_char('/', filter_path, trace); 150 | size_t filter_root_length = get_length(get_view(filter_path)); 151 | 152 | for (FTSENT *entry = fts_read(fts); entry && ok(trace); 153 | entry = fts_read(fts)) { 154 | const char *relative_path = entry->fts_path + source_length; 155 | 156 | if (!*relative_path) { 157 | continue; 158 | } 159 | if (*relative_path == '/') { 160 | ++relative_path; 161 | } 162 | 163 | set_length(filter_root_length, filter_path); 164 | concat_string(relative_path, filter_path, trace); 165 | if (!ok(trace)) { 166 | break; 167 | } 168 | 169 | switch (entry->fts_info) { 170 | case FTS_D: 171 | if (!access(get_string(get_view(filter_path)), F_OK)) { 172 | TNEG(mkdirat(destination_fd, relative_path, 0755), trace); 173 | } 174 | break; 175 | case FTS_DP: 176 | if (access(get_string(get_view(filter_path)), F_OK)) { 177 | TNEG(rmdir(entry->fts_path), trace); 178 | } 179 | break; 180 | case FTS_NSOK: 181 | if (!access(get_string(get_view(filter_path)), F_OK)) { 182 | TNEG( 183 | linkat(AT_FDCWD, entry->fts_path, destination_fd, relative_path, 0), 184 | trace); 185 | } else { 186 | TNEG(unlink(entry->fts_path), trace); 187 | } 188 | break; 189 | case FTS_DNR: 190 | case FTS_ERR: 191 | throw_dynamic(strerror(entry->fts_errno), trace); 192 | break; 193 | } 194 | } 195 | 196 | if (ok(trace) && errno) { 197 | throw_errno(trace); 198 | } 199 | 200 | if (fts) { 201 | fts_close(fts); 202 | } 203 | 204 | close(destination_fd); 205 | free(*paths); 206 | free_buffer(filter_path); 207 | 208 | if (!ok(trace)) { 209 | clean_up(destination); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include "buffer.h" 2 | #include "handler.h" 3 | #include "list.h" 4 | #include "logstep.h" 5 | #include "messages.h" 6 | #include "mountinfo.h" 7 | #include "params.h" 8 | #include "parents.h" 9 | #include "set.h" 10 | #include "trace.h" 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | static int fail(const struct trace *trace) { 22 | unwind(2, trace); 23 | return EXIT_FAILURE; 24 | } 25 | 26 | int main(int argc, const char **argv) { 27 | struct trace *trace = create_trace(); 28 | if (!trace) { 29 | logstep(2, NULL, messages.main.cannot_bootstrap, 0); 30 | logstep(2, messages.trace.because_of, messages.main.out_of_memory, 1); 31 | return EXIT_FAILURE; 32 | } 33 | 34 | struct params *params = parse_params(argc, argv, trace); 35 | if (!ok(trace)) { 36 | throw_context(messages.main.info.usage, trace); 37 | throw_static(messages.main.cli_usage_violated, trace); 38 | throw_static(messages.main.cannot_parse_cli, trace); 39 | return fail(trace); 40 | } 41 | if (is_version_requested(params)) { 42 | logstep(2, NULL, messages.main.info.version, 0); 43 | return EXIT_SUCCESS; 44 | } 45 | if (is_help_requested(params)) { 46 | logstep(2, NULL, messages.main.info.help, 0); 47 | return EXIT_SUCCESS; 48 | } 49 | assert(peek(get_write_mounts(params))); 50 | assert(peek(get_exec_mounts(params))); 51 | assert(get_privilege_dropping_path(params)); 52 | 53 | int fanotify_fd = fanotify_init(FAN_CLASS_NOTIF, O_RDONLY); 54 | if (fanotify_fd < 0) { 55 | throw_errno(trace); 56 | throw_static(messages.main.fanotify.cannot_init, trace); 57 | return fail(trace); 58 | } 59 | 60 | struct mountinfo *mountinfo = load_mountinfo(trace); 61 | if (!ok(trace)) { 62 | throw_static(messages.main.mount.cannot_list, trace); 63 | return fail(trace); 64 | } 65 | 66 | char *previous_mount = NULL; 67 | size_t common_parent_path_length = 0; 68 | 69 | for (const struct list_item *write_mount = peek(get_write_mounts(params)); 70 | write_mount; write_mount = get_next(write_mount)) { 71 | char *mount = make_mount(get_value(write_mount), mountinfo, trace); 72 | if (TNEG(fanotify_mark(fanotify_fd, FAN_MARK_ADD | FAN_MARK_MOUNT, 73 | FAN_CLOSE_WRITE, 0, mount), 74 | trace) < 0) { 75 | throw_context(get_value(write_mount), trace); 76 | throw_static(messages.main.mount.cannot_watch, trace); 77 | return fail(trace); 78 | } 79 | 80 | if (previous_mount) { 81 | size_t length = get_common_parent_path_length(previous_mount, mount); 82 | if (length < common_parent_path_length) { 83 | common_parent_path_length = length; 84 | } 85 | } else { 86 | common_parent_path_length = strlen(mount) + 1; 87 | } 88 | 89 | free(previous_mount); 90 | previous_mount = mount; 91 | } 92 | 93 | free(previous_mount); 94 | 95 | for (const struct list_item *exec_mount = peek(get_exec_mounts(params)); 96 | exec_mount; exec_mount = get_next(exec_mount)) { 97 | char *mount = make_mount(get_value(exec_mount), mountinfo, trace); 98 | if (TNEG(fanotify_mark(fanotify_fd, FAN_MARK_ADD | FAN_MARK_MOUNT, 99 | FAN_OPEN_EXEC, 0, mount), 100 | trace) < 0) { 101 | throw_context(get_value(exec_mount), trace); 102 | throw_static(messages.main.mount.cannot_watch, trace); 103 | return fail(trace); 104 | } 105 | free(mount); 106 | } 107 | 108 | free_mountinfo(mountinfo); 109 | 110 | struct stat drop_stat; 111 | if (stat(get_privilege_dropping_path(params), &drop_stat) >= 0 && 112 | drop_stat.st_gid && drop_stat.st_uid) { 113 | TNEG(setgroups(0, NULL), trace); 114 | TNEG(setgid(drop_stat.st_gid), trace); 115 | TNEG(setuid(drop_stat.st_uid), trace); 116 | } 117 | 118 | if (!ok(trace) || !getuid() || !getgid()) { 119 | throw_context(get_privilege_dropping_path(params), trace); 120 | throw_static(messages.main.cannot_drop_privileges, trace); 121 | return fail(trace); 122 | } 123 | 124 | struct handler *handler = 125 | load_handler(get_config_path(params), common_parent_path_length, trace); 126 | if (!ok(trace)) { 127 | throw_static(messages.main.cannot_load_handler, trace); 128 | return fail(trace); 129 | } 130 | 131 | pid_t self = getpid(); 132 | time_t pause = 0; 133 | struct pollfd pollfd = { 134 | .fd = fanotify_fd, 135 | .events = POLLIN, 136 | .revents = 0, 137 | }; 138 | 139 | for (;;) { 140 | int status = poll(&pollfd, 1, pause * 1000); 141 | if (status < 0 || (status > 0 && pollfd.revents ^ POLLIN)) { 142 | throw_errno(trace); 143 | throw_static(messages.main.fanotify.cannot_poll, trace); 144 | } else if (status > 0) { 145 | struct fanotify_event_metadata event; 146 | try(trace); 147 | TNEG(read(fanotify_fd, &event, sizeof event) - sizeof event, trace); 148 | finally_rethrow_static(messages.main.fanotify.cannot_read_event, trace); 149 | if (ok(trace)) { 150 | if (event.vers != FANOTIFY_METADATA_VERSION) { 151 | throw_static(messages.main.fanotify.version_mismatch, trace); 152 | } else if (event.mask & FAN_Q_OVERFLOW) { 153 | throw_static(messages.main.fanotify.queue_overflow, trace); 154 | } else { 155 | if (event.mask & FAN_OPEN_EXEC) { 156 | try(trace); 157 | handle_open_exec(event.pid, event.fd, handler, trace); 158 | finally_rethrow_static(messages.main.cannot_handle_exec, trace); 159 | } else if (event.mask & FAN_CLOSE_WRITE && event.pid != self) { 160 | try(trace); 161 | handle_close_write(event.pid, event.fd, handler, trace); 162 | finally_rethrow_static(messages.main.cannot_handle_write, trace); 163 | } 164 | close(event.fd); 165 | } 166 | } 167 | } 168 | 169 | try(trace); 170 | pause = handle_timeout(handler, trace); 171 | finally_rethrow_static(messages.main.cannot_handle_timeout, trace); 172 | 173 | if (!ok(trace)) { 174 | return fail(trace); 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/config-static.c: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | #include "set.h" 3 | #include "trace.h" 4 | #include 5 | #include 6 | #include 7 | 8 | static const char *const store_root = "klunok/store"; 9 | static const char *const project_store_root = "klunok/projects"; 10 | static const char *const unstable_project_store_root = "klunok/var/projects"; 11 | static const char *const queue_path = "klunok/var/queue"; 12 | static const char *const journal_path = "klunok/var/journal"; 13 | static const char *const offset_store_root = "klunok/var/offsets"; 14 | static const char *const journal_timestamp_pattern = "%Y-%m-%d-%H-%M"; 15 | static const char *const version_pattern = "v%Y-%m-%d-%H-%M"; 16 | static const size_t debounce_seconds = 60; 17 | static const size_t path_length_guess = 1024; 18 | static const pid_t max_pid_guess = 1 << 15; 19 | static const size_t elf_interpreter_count_guess = 1; 20 | static const size_t queue_size_guess = 2 * debounce_seconds; 21 | static const char *const editors[] = { 22 | "atom", 23 | "code", 24 | "codium", 25 | "gedit", 26 | "gnome-text-editor", 27 | "howl", 28 | "hx", 29 | "inkscape", 30 | "kak", 31 | "kate", 32 | "kwrite", 33 | "micro", 34 | "nano", 35 | "notepadqq-bin", 36 | "nvim", 37 | "pluma", 38 | "rsession", 39 | "soffice.bin", 40 | "sublime_text", 41 | "vi", 42 | "vim", 43 | "vim.basic", 44 | "vim.tiny", 45 | "xed", 46 | ".gedit-wrapped", 47 | ".gnome-text-editor-wrapped", 48 | ".howl-wrapped", 49 | ".hx-wrapped", 50 | ".inkscape-wrapped", 51 | ".kate-wrapped", 52 | ".kwrite-wrapped", 53 | ".pluma-wrapped", 54 | ".xed-wrapped", 55 | }; 56 | static const char *const project_roots[] = {}; 57 | static const char *const project_parents[] = {}; 58 | static const char *const history_paths[] = {}; 59 | static const char *const excluded_paths[] = {}; 60 | static const char *const included_paths[] = {}; 61 | static const char *const cluded_paths[] = {}; 62 | static const char *const event_open_exec_not_editor; 63 | static const char *const event_open_exec_editor; 64 | static const char *const event_close_write_not_by_editor; 65 | static const char *const event_close_write_by_editor; 66 | static const char *const event_queue_head_deleted; 67 | static const char *const event_queue_head_forbidden; 68 | static const char *const event_queue_head_stored = ""; 69 | 70 | struct config { 71 | struct set *editors; 72 | struct set *project_roots; 73 | struct set *project_parents; 74 | struct set *history_paths; 75 | struct set *excluded_paths; 76 | struct set *included_paths; 77 | struct set *cluded_paths; 78 | }; 79 | 80 | static struct set *load_set(const char *const *array, size_t size, 81 | struct trace *trace) { 82 | struct set *set = create_set(size / sizeof(char *), trace); 83 | for (size_t i = 0; ok(trace) && i < size / sizeof(char *); ++i) { 84 | add(editors[i], set, trace); 85 | } 86 | return set; 87 | } 88 | 89 | struct config *load_config(const char *path, struct trace *trace) { 90 | if (ok(trace) && path) { 91 | throw_static(messages.config.is_static, trace); 92 | return NULL; 93 | } 94 | 95 | struct config *config = TNULL(calloc(1, sizeof(struct config)), trace); 96 | if (!ok(trace)) { 97 | return NULL; 98 | } 99 | 100 | config->editors = load_set(editors, sizeof editors, trace); 101 | config->project_roots = load_set(project_roots, sizeof project_roots, trace); 102 | config->project_parents = 103 | load_set(project_parents, sizeof project_parents, trace); 104 | config->history_paths = load_set(history_paths, sizeof history_paths, trace); 105 | config->excluded_paths = 106 | load_set(excluded_paths, sizeof excluded_paths, trace); 107 | config->included_paths = 108 | load_set(included_paths, sizeof included_paths, trace); 109 | config->cluded_paths = load_set(cluded_paths, sizeof cluded_paths, trace); 110 | 111 | if (!ok(trace)) { 112 | free_config(config); 113 | return NULL; 114 | } 115 | return config; 116 | } 117 | 118 | const struct set *get_editors(const struct config *config) { 119 | return config->editors; 120 | } 121 | 122 | const struct set *get_project_roots(const struct config *config) { 123 | return config->project_roots; 124 | } 125 | 126 | const struct set *get_project_parents(const struct config *config) { 127 | return config->project_parents; 128 | } 129 | 130 | const struct set *get_history_paths(const struct config *config) { 131 | return config->history_paths; 132 | } 133 | 134 | const struct set *get_excluded_paths(const struct config *config) { 135 | return config->excluded_paths; 136 | } 137 | 138 | const struct set *get_included_paths(const struct config *config) { 139 | return config->included_paths; 140 | } 141 | 142 | const struct set *get_cluded_paths(const struct config *config) { 143 | return config->cluded_paths; 144 | } 145 | 146 | const char *get_store_root(const struct config *config) { return store_root; } 147 | 148 | const char *get_project_store_root(const struct config *config) { 149 | return project_store_root; 150 | } 151 | 152 | const char *get_unstable_project_store_root(const struct config *config) { 153 | return unstable_project_store_root; 154 | } 155 | 156 | const char *get_queue_path(const struct config *config) { return queue_path; } 157 | 158 | const char *get_journal_path(const struct config *config) { 159 | return journal_path; 160 | } 161 | 162 | const char *get_journal_timestamp_pattern(const struct config *config) { 163 | return journal_timestamp_pattern; 164 | } 165 | 166 | const char *get_version_pattern(const struct config *config) { 167 | return version_pattern; 168 | } 169 | 170 | const char *get_offset_store_root(const struct config *config) { 171 | return offset_store_root; 172 | } 173 | 174 | size_t get_debounce_seconds(const struct config *config) { 175 | return debounce_seconds; 176 | } 177 | 178 | size_t get_path_length_guess(const struct config *config) { 179 | return path_length_guess; 180 | } 181 | 182 | pid_t get_max_pid_guess(const struct config *config) { return max_pid_guess; } 183 | 184 | size_t get_elf_interpreter_count_guess(const struct config *config) { 185 | return elf_interpreter_count_guess; 186 | } 187 | 188 | size_t get_queue_size_guess(const struct config *config) { 189 | return queue_size_guess; 190 | } 191 | 192 | const char *get_event_open_exec_not_editor(const struct config *config) { 193 | return event_open_exec_not_editor; 194 | } 195 | 196 | const char *get_event_open_exec_editor(const struct config *config) { 197 | return event_open_exec_editor; 198 | } 199 | 200 | const char *get_event_close_write_not_by_editor(const struct config *config) { 201 | return event_close_write_not_by_editor; 202 | } 203 | 204 | const char *get_event_close_write_by_editor(const struct config *config) { 205 | return event_close_write_by_editor; 206 | } 207 | 208 | const char *get_event_queue_head_deleted(const struct config *config) { 209 | return event_queue_head_deleted; 210 | } 211 | 212 | const char *get_event_queue_head_forbidden(const struct config *config) { 213 | return event_queue_head_forbidden; 214 | } 215 | 216 | const char *get_event_queue_head_stored(const struct config *config) { 217 | return event_queue_head_stored; 218 | } 219 | 220 | void free_config(struct config *config) { 221 | if (config) { 222 | free_set(config->editors); 223 | free_set(config->project_roots); 224 | free_set(config->project_parents); 225 | free_set(config->history_paths); 226 | free_set(config->excluded_paths); 227 | free_set(config->included_paths); 228 | free_set(config->cluded_paths); 229 | free(config); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /misc/demo.cast: -------------------------------------------------------------------------------- 1 | {"version": 2, "width": 94, "height": 24, "timestamp": 1687003555, "env": {"SHELL": "/run/current-system/sw/bin/bash", "TERM": "alacritty"}} 2 | [0.134229, "o", "\u001b[?2004h"] 3 | [0.135462, "o", "\u001b[6 q\u001b[1m\u001b[36m$ \u001b(B\u001b[m"] 4 | [0.686838, "o", "l"] 5 | [0.727769, "o", "s"] 6 | [1.00486, "o", "\r\n"] 7 | [1.006227, "o", "\u001b[?2004l\r"] 8 | [1.06814, "o", "\u001b[?2004h\u001b[6 q\u001b[1m\u001b[36m$ \u001b(B\u001b[m"] 9 | [2.235806, "o", "s"] 10 | [2.315741, "o", "u"] 11 | [2.433755, "o", "d"] 12 | [2.546745, "o", "o"] 13 | [2.666733, "o", " "] 14 | [2.81969, "o", "-"] 15 | [2.937101, "o", "b"] 16 | [3.010737, "o", " "] 17 | [3.234457, "o", "k"] 18 | [3.353754, "o", "l"] 19 | [3.544763, "o", "u"] 20 | [3.764784, "o", "n"] 21 | [3.864026, "o", "o"] 22 | [3.962776, "o", "k"] 23 | [4.278754, "o", "\r\n"] 24 | [4.2801, "o", "\u001b[?2004l\r"] 25 | [5.568274, "o", "\u001b[?2004h"] 26 | [5.569756, "o", "\u001b[6 q\u001b[1m\u001b[36m$ \u001b(B\u001b[m"] 27 | [6.211703, "o", "l"] 28 | [6.321692, "o", "s"] 29 | [6.737694, "o", "\r\n"] 30 | [6.739052, "o", "\u001b[?2004l\r"] 31 | [6.747025, "o", "\u001b[0m\u001b[01;34mklunok\u001b[0m/\r\n"] 32 | [6.752362, "o", "\u001b[?2004h\u001b[6 q\u001b[1m\u001b[36m$ \u001b(B\u001b[m"] 33 | [7.029679, "o", "l"] 34 | [7.070605, "o", "s"] 35 | [7.203562, "o", " "] 36 | [7.353618, "o", "k"] 37 | [7.451635, "o", "l"] 38 | [7.618608, "o", "u"] 39 | [7.838628, "o", "n"] 40 | [7.914561, "o", "o"] 41 | [8.037662, "o", "k"] 42 | [8.352635, "o", "\r\n"] 43 | [8.354097, "o", "\u001b[?2004l\r"] 44 | [8.361712, "o", "\u001b[0m\u001b[01;34mvar\u001b[0m/\r\n"] 45 | [8.367096, "o", "\u001b[?2004h\u001b[6 q\u001b[1m\u001b[36m$ \u001b(B\u001b[m"] 46 | [9.658611, "o", "n"] 47 | [9.74755, "o", "a"] 48 | [9.856544, "o", "n"] 49 | [10.125547, "o", "o"] 50 | [10.268523, "o", " "] 51 | [10.762363, "o", "h"] 52 | [10.858533, "o", "e"] 53 | [10.943514, "o", "l"] 54 | [11.090494, "o", "l"] 55 | [11.260494, "o", "o"] 56 | [11.500489, "o", "."] 57 | [11.641498, "o", "t"] 58 | [11.808455, "o", "x"] 59 | [11.907472, "o", "t"] 60 | [12.046509, "o", "\r\n"] 61 | [12.047924, "o", "\u001b[?2004l\r"] 62 | [12.062092, "o", "\u001b[?2004h"] 63 | [12.062608, "o", "\u001b[?1049h\u001b[22;0;0t\u001b[1;24r\u001b(B\u001b[m\u001b[4l\u001b[?7h\u001b[39;49m\u001b[?1h\u001b=\u001b[?1h\u001b=\u001b[?25l"] 64 | [12.062766, "o", "\u001b[39;49m\u001b(B\u001b[m\u001b[H\u001b[2J\u001b[22;42H\u001b(B\u001b[0;7m[ New File ]\u001b(B\u001b[m"] 65 | [12.063247, "o", "\u001b[H\u001b(B\u001b[0;7m GNU nano 7.2 hello.txt \u001b[1;93H\u001b(B\u001b[m"] 66 | [12.063466, "o", "\r\u001b[23d\u001b(B\u001b[0;7m^G\u001b(B\u001b[m Help\u001b[23;16H\u001b(B\u001b[0;7m^O\u001b(B\u001b[m Write Out \u001b(B\u001b[0;7m^W\u001b(B\u001b[m Where Is \u001b(B\u001b[0;7m^K\u001b(B\u001b[m Cut\u001b[23;61H\u001b(B\u001b[0;7m^T\u001b(B\u001b[m Execute \u001b(B\u001b[0;7m^C\u001b(B\u001b[m Location\r\u001b[24d\u001b(B\u001b[0;7m^X\u001b(B\u001b[m Exit\u001b[24;16H\u001b(B\u001b[0;7m^R\u001b(B\u001b[m Read File \u001b(B\u001b[0;7m^\\\u001b(B\u001b[m Replace \u001b(B\u001b[0;7m^U\u001b(B\u001b[m Paste\u001b[61G\u001b(B\u001b[0;7m^J\u001b(B\u001b[m Justify \u001b(B\u001b[0;7m^/\u001b(B\u001b[m Go To Line"] 67 | [12.06361, "o", "\r\u001b[2d\u001b[?12l\u001b[?25h"] 68 | [12.649511, "o", "\u001b[?25l"] 69 | [12.650066, "o", "\u001b[1;85H\u001b(B\u001b[0;7mModified\u001b(B\u001b[m\u001b[?12l\u001b[?25h\r\u001b[2dH"] 70 | [12.8655, "o", "\u001b[?25l"] 71 | [12.866024, "o", "\u001b[?12l\u001b[?25he"] 72 | [12.950398, "o", "\u001b[?25l"] 73 | [12.950466, "o", "\u001b[?12l\u001b[?25h"] 74 | [12.950611, "o", "l"] 75 | [13.120426, "o", "\u001b[?25l"] 76 | [13.120652, "o", "\u001b[?12l\u001b[?25h"] 77 | [13.120693, "o", "l"] 78 | [13.452468, "o", "\u001b[?25l"] 79 | [13.452654, "o", "\u001b[?12l\u001b[?25ho"] 80 | [13.737475, "o", "\u001b[?25l"] 81 | [13.73772, "o", "\u001b[?12l\u001b[?25h,"] 82 | [13.9266, "o", "\u001b[?25l\u001b[?12l\u001b[?25h"] 83 | [13.927126, "o", " "] 84 | [14.326443, "o", "\u001b[?25l"] 85 | [14.326621, "o", "\u001b[?12l\u001b[?25hW"] 86 | [14.555464, "o", "\u001b[?25l"] 87 | [14.555967, "o", "\u001b[?12l\u001b[?25ho"] 88 | [14.672439, "o", "\u001b[?25l"] 89 | [14.672914, "o", "\u001b[?12l\u001b[?25hr"] 90 | [14.803359, "o", "\u001b[?25l"] 91 | [14.803829, "o", "\u001b[?12l\u001b[?25hl"] 92 | [14.918367, "o", "\u001b[?25l"] 93 | [14.918609, "o", "\u001b[?12l\u001b[?25hd"] 94 | [15.652372, "o", "\u001b[?25l"] 95 | [15.652444, "o", "\u001b[?12l\u001b[?25h"] 96 | [15.652619, "o", "!"] 97 | [16.711321, "o", "\u001b[?25l"] 98 | [16.711833, "o", "\r\u001b[22d\u001b(B\u001b[0;7mSave modified buffer? \u001b[23;1H"] 99 | [16.712103, "o", " Y\u001b(B\u001b[m Yes\u001b[K\r\u001b[24d\u001b(B\u001b[0;7m N\u001b(B\u001b[m No \u001b[24;16H \u001b(B\u001b[0;7m^C\u001b(B\u001b[m Cancel\u001b[K\u001b[22;23H\u001b[?12l\u001b[?25h"] 100 | [17.351332, "o", "\u001b[?25l"] 101 | [17.351792, "o", "\r\u001b[23d\u001b(B\u001b[0;7m^G\u001b(B\u001b[m Help\u001b[23;24H"] 102 | [17.352111, "o", "\u001b(B\u001b[0;7mM-D\u001b(B\u001b[m DOS Format\u001b[23;47H\u001b(B\u001b[0;7mM-A\u001b(B\u001b[m Append\u001b[23;70H\u001b(B\u001b[0;7mM-B\u001b(B\u001b[m Backup File\r\u001b[24d\u001b(B\u001b[0;7m^C\u001b(B\u001b[m Cancel\u001b[17G \u001b(B\u001b[0;7mM-M\u001b(B\u001b[m Mac Format\u001b[24;47H\u001b(B\u001b[0;7mM-P\u001b(B\u001b[m Prepend\u001b[24;70H"] 103 | [17.352386, "o", "\u001b(B\u001b[0;7m^T\u001b(B\u001b[m Browse\r\u001b[22d\u001b(B\u001b[0;7mFile Name to Write: hello.txt\u001b(B\u001b[m\u001b[?12l\u001b[?25h"] 104 | [18.247259, "o", "\u001b[?25l"] 105 | [18.247818, "o", "\u001b[22;40H\u001b[1K \u001b(B\u001b[0;7m[ Writing... ]\u001b(B\u001b[m\u001b[K"] 106 | [18.252527, "o", "\u001b[1;85H"] 107 | [18.253196, "o", "\u001b(B\u001b[0;7m \u001b(B\u001b[m\u001b[22;40H\u001b(B\u001b[0;7m[ Wrote 1 line ]\u001b(B\u001b[m\r\u001b[J\u001b[24d\u001b[?12l\u001b[?25h\u001b[24;1H\u001b[?1049l\u001b[23;0;0t\r\u001b[?1l\u001b>\u001b[?2004l"] 108 | [18.258734, "o", "\u001b[?2004h\u001b[6 q\u001b[1m\u001b[36m$ \u001b(B\u001b[m"] 109 | [19.405329, "o", "l"] 110 | [19.566267, "o", "s"] 111 | [19.866315, "o", "\r\n"] 112 | [19.867631, "o", "\u001b[?2004l\r"] 113 | [19.877509, "o", "hello.txt \u001b[0m\u001b[01;34mklunok\u001b[0m/\r\n"] 114 | [19.882902, "o", "\u001b[?2004h\u001b[6 q\u001b[1m\u001b[36m$ \u001b(B\u001b[m"] 115 | [21.093254, "o", "l"] 116 | [21.157207, "o", "s"] 117 | [21.263188, "o", " "] 118 | [21.417164, "o", "k"] 119 | [21.561235, "o", "l"] 120 | [21.755198, "o", "u"] 121 | [21.971193, "o", "n"] 122 | [22.047214, "o", "o"] 123 | [22.170215, "o", "k"] 124 | [22.462213, "o", "\r\n"] 125 | [22.463655, "o", "\u001b[?2004l\r"] 126 | [22.4714, "o", "\u001b[0m\u001b[01;34mvar\u001b[0m/\r\n"] 127 | [22.478117, "o", "\u001b[?2004h"] 128 | [22.478547, "o", "\u001b[6 q\u001b[1m\u001b[36m$ \u001b(B\u001b[m"] 129 | [23.253167, "o", "s"] 130 | [23.363063, "o", "l"] 131 | [23.502147, "o", "e"] 132 | [23.718112, "o", "e"] 133 | [23.967148, "o", "p"] 134 | [24.085082, "o", " "] 135 | [24.694108, "o", "6"] 136 | [24.893099, "o", "0"] 137 | [25.182128, "o", "\r\n"] 138 | [25.183497, "o", "\u001b[?2004l\r"] 139 | [85.199811, "o", "\u001b[?2004h\u001b[6 q\u001b[1m\u001b[36m$ \u001b(B\u001b[m"] 140 | [85.637288, "o", "l"] 141 | [85.747254, "o", "s"] 142 | [85.853255, "o", " "] 143 | [86.029162, "o", "k"] 144 | [86.127226, "o", "l"] 145 | [86.322161, "o", "u"] 146 | [86.538257, "o", "n"] 147 | [86.614179, "o", "o"] 148 | [86.759206, "o", "k"] 149 | [87.121243, "o", "\r\n"] 150 | [87.122704, "o", "\u001b[?2004l\r"] 151 | [87.130624, "o", "\u001b[0m\u001b[01;34mstore\u001b[0m/ \u001b[01;34mvar\u001b[0m/\r\n"] 152 | [87.136449, "o", "\u001b[?2004h\u001b[6 q\u001b[1m\u001b[36m$ \u001b(B\u001b[m"] 153 | [88.331251, "o", "c"] 154 | [88.400171, "o", "a"] 155 | [88.579099, "o", "t"] 156 | [88.654162, "o", " "] 157 | [89.062324, "o", "k"] 158 | [89.160163, "o", "l"] 159 | [89.285874, "o", "unok/"] 160 | [89.749146, "o", "s"] 161 | [89.923082, "o", "t"] 162 | [90.005032, "o", "o"] 163 | [90.150686, "o", "re/"] 164 | [90.411751, "o", "hello.txt/"] 165 | [90.997924, "o", "v2023-06-17-15-07.txt "] 166 | [92.119066, "o", "\r\n"] 167 | [92.120532, "o", "\u001b[?2004l\r"] 168 | [92.131118, "o", "Hello, World!\r\n"] 169 | [92.137614, "o", "\u001b[?2004h\u001b[6 q\u001b[1m\u001b[36m$ \u001b(B\u001b[m"] 170 | [94.232026, "o", "\u001b[?2004l\r\r\n"] 171 | [94.232134, "o", "exit\r\n"] 172 | -------------------------------------------------------------------------------- /src/linq.c: -------------------------------------------------------------------------------- 1 | #include "linq.h" 2 | #include "buffer.h" 3 | #include "messages.h" 4 | #include "parents.h" 5 | #include "set.h" 6 | #include "trace.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | struct linq { 19 | int dirfd; 20 | time_t debounce_seconds; 21 | size_t length_guess; 22 | size_t head_index; 23 | size_t size; 24 | struct set *set; 25 | }; 26 | 27 | struct linq_head { 28 | char *target; 29 | char *path; 30 | size_t metadata; 31 | time_t pause; 32 | }; 33 | 34 | static int dot_filter(const struct dirent *dirent) { 35 | const char *name = dirent->d_name; 36 | return !(name[0] == '.' && 37 | (name[1] == 0 || (name[1] == '.' && name[2] == 0))); 38 | } 39 | 40 | static int compare(const struct dirent **first, const struct dirent **second) { 41 | return strtol((*first)->d_name, NULL, 10) - 42 | strtol((*second)->d_name, NULL, 10); 43 | } 44 | 45 | static void create_linq_path(const char *path, struct trace *trace) { 46 | create_parents(path, trace); 47 | TNEG(mkdir(path, 0755), trace); 48 | } 49 | 50 | static void free_entries(struct dirent **entries, size_t entry_count) { 51 | for (size_t i = 0; i < entry_count; ++i) { 52 | free(entries[i]); 53 | } 54 | free(entries); 55 | } 56 | 57 | static char *strip_metadata(char *target) { 58 | while (target[1] == '/' || (target[1] == '.' && target[2] == '/')) { 59 | ++target; 60 | } 61 | return target; 62 | } 63 | 64 | static char *read_entry(const char *entry, const struct linq *linq, 65 | struct trace *trace) { 66 | if (!ok(trace)) { 67 | return NULL; 68 | } 69 | size_t max_size = linq->length_guess + 1; 70 | for (;;) { 71 | try(trace); 72 | char *target = TNULL(malloc(max_size), trace); 73 | int length = TNEG(readlinkat(linq->dirfd, entry, target, max_size), trace); 74 | rethrow_context(entry, trace); 75 | finally_rethrow_static(messages.linq.invalid_entry, trace); 76 | 77 | if (!ok(trace)) { 78 | free(target); 79 | return NULL; 80 | } 81 | if (length < max_size) { 82 | target[length] = 0; 83 | return target; 84 | } 85 | 86 | free(target); 87 | max_size *= 2; 88 | } 89 | } 90 | 91 | static struct linq * 92 | load_or_create_linq(const char *path, time_t debounce_seconds, 93 | size_t entry_count_guess, size_t entry_length_guess, 94 | bool try_to_create, struct trace *trace) { 95 | if (!ok(trace)) { 96 | return NULL; 97 | } 98 | struct dirent **entries; 99 | int entry_count = scandir(path, &entries, dot_filter, compare); 100 | if (entry_count < 0) { 101 | if (errno == ENOENT && try_to_create) { 102 | create_linq_path(path, trace); 103 | return load_or_create_linq(path, debounce_seconds, entry_count_guess, 104 | entry_length_guess, false, trace); 105 | } 106 | throw_errno(trace); 107 | return NULL; 108 | } 109 | 110 | size_t set_size = 111 | entry_count_guess > entry_count ? entry_count_guess : entry_count; 112 | 113 | struct linq *linq = TNULL(malloc(sizeof(struct linq)), trace); 114 | int dirfd = TNEG(open(path, O_DIRECTORY), trace); 115 | struct set *set = create_set(set_size, trace); 116 | if (ok(trace)) { 117 | linq->dirfd = dirfd; 118 | linq->size = entry_count; 119 | linq->debounce_seconds = debounce_seconds; 120 | linq->length_guess = entry_length_guess; 121 | linq->set = set; 122 | if (entry_count > 0) { 123 | linq->head_index = strtol(entries[0]->d_name, NULL, 10); 124 | } else { 125 | linq->head_index = 0; 126 | } 127 | } 128 | for (size_t i = 0; i < entry_count && ok(trace); ++i) { 129 | char *entry_target = read_entry(entries[i]->d_name, linq, trace); 130 | if (ok(trace)) { 131 | add(strip_metadata(entry_target), set, trace); 132 | free(entry_target); 133 | } 134 | } 135 | if (!ok(trace)) { 136 | free(linq); 137 | free_entries(entries, entry_count); 138 | free_set(set); 139 | return NULL; 140 | } 141 | 142 | free_entries(entries, entry_count); 143 | 144 | return linq; 145 | } 146 | 147 | struct linq *load_linq(const char *path, time_t debounce_seconds, 148 | size_t entry_count_guess, size_t entry_length_guess, 149 | struct trace *trace) { 150 | return load_or_create_linq(path, debounce_seconds, entry_count_guess, 151 | entry_length_guess, true, trace); 152 | } 153 | 154 | static void concat_metadata(struct buffer *buffer, size_t metadata, 155 | struct trace *trace) { 156 | size_t bit_length = 0; 157 | 158 | while (metadata >= 1 << bit_length) { 159 | ++bit_length; 160 | } 161 | 162 | for (size_t i = 1; ok(trace) && i <= bit_length; ++i) { 163 | concat_char('/', buffer, trace); 164 | if (metadata & (1 << (bit_length - i))) { 165 | concat_char('.', buffer, trace); 166 | } 167 | } 168 | } 169 | 170 | void push(const char *path, size_t metadata, struct linq *linq, 171 | struct trace *trace) { 172 | if (!ok(trace)) { 173 | return; 174 | } 175 | assert(*path == '/'); 176 | 177 | struct buffer *link_buffer = create_buffer(trace); 178 | concat_size(linq->head_index + linq->size, link_buffer, trace); 179 | 180 | struct buffer *target_buffer = create_buffer(trace); 181 | concat_metadata(target_buffer, metadata, trace); 182 | concat_string(path, target_buffer, trace); 183 | 184 | TNEG(symlinkat(get_string(get_view(target_buffer)), linq->dirfd, 185 | get_string(get_view(link_buffer))), 186 | trace); 187 | add(path, linq->set, trace); 188 | if (ok(trace)) { 189 | ++linq->size; 190 | } 191 | free_buffer(link_buffer); 192 | free_buffer(target_buffer); 193 | } 194 | 195 | struct linq_head *get_head(struct linq *linq, struct trace *trace) { 196 | struct linq_head *head = TNULL(malloc(sizeof(struct linq_head)), trace); 197 | if (!ok(trace)) { 198 | return NULL; 199 | } 200 | 201 | if (!linq->size) { 202 | head->pause = -1; 203 | return head; 204 | } 205 | 206 | struct buffer *link = create_buffer(trace); 207 | concat_size(linq->head_index, link, trace); 208 | 209 | struct stat link_stat; 210 | TNEG(fstatat(linq->dirfd, get_string(get_view(link)), &link_stat, 211 | AT_SYMLINK_NOFOLLOW), 212 | trace); 213 | if (!ok(trace)) { 214 | free_buffer(link); 215 | free(head); 216 | return NULL; 217 | } 218 | 219 | time_t link_age = time(NULL) - link_stat.st_mtime; 220 | if (link_age < linq->debounce_seconds) { 221 | free_buffer(link); 222 | head->pause = linq->debounce_seconds - link_age; 223 | return head; 224 | } 225 | 226 | char *target = read_entry(get_string(get_view(link)), linq, trace); 227 | free_buffer(link); 228 | 229 | char *path = target; 230 | head->metadata = 0; 231 | while (path[1] == '/' || (path[1] == '.' && path[2] == '/')) { 232 | head->metadata *= 2; 233 | ++path; 234 | if (*path == '.') { 235 | head->metadata += 1; 236 | ++path; 237 | } 238 | } 239 | 240 | struct buffer_view *path_view = create_buffer_view(path, trace); 241 | 242 | if (ok(trace) && get_count(path_view, linq->set) > 1) { 243 | free(target); 244 | free(head); 245 | free_buffer_view(path_view); 246 | pop_head(linq, trace); 247 | return get_head(linq, trace); 248 | } 249 | 250 | free_buffer_view(path_view); 251 | 252 | if (!ok(trace)) { 253 | free(head); 254 | return NULL; 255 | } 256 | 257 | head->pause = 0; 258 | head->target = target; 259 | head->path = path; 260 | 261 | return head; 262 | } 263 | 264 | void pop_head(struct linq *linq, struct trace *trace) { 265 | if (!ok(trace)) { 266 | return; 267 | } 268 | assert(linq->size); 269 | struct buffer *link = create_buffer(trace); 270 | concat_size(linq->head_index, link, trace); 271 | char *target = read_entry(get_string(get_view(link)), linq, trace); 272 | 273 | TNEG(unlinkat(linq->dirfd, get_string(get_view(link)), 0), trace); 274 | 275 | if (ok(trace)) { 276 | struct buffer_view *path_view = 277 | create_buffer_view(strip_metadata(target), trace); 278 | 279 | if (ok(trace)) { 280 | pop(path_view, linq->set); 281 | --linq->size; 282 | if (linq->size) { 283 | ++linq->head_index; 284 | } else { 285 | linq->head_index = 0; 286 | } 287 | } 288 | 289 | free_buffer_view(path_view); 290 | } 291 | 292 | free(target); 293 | free_buffer(link); 294 | } 295 | 296 | const char *get_path(const struct linq_head *head) { return head->path; } 297 | 298 | size_t get_metadata(const struct linq_head *head) { return head->metadata; } 299 | 300 | time_t get_pause(const struct linq_head *head) { return head->pause; } 301 | 302 | void redebounce(time_t debounce_seconds, struct linq *linq) { 303 | linq->debounce_seconds = debounce_seconds; 304 | } 305 | 306 | void free_linq(struct linq *linq) { 307 | if (linq) { 308 | close(linq->dirfd); 309 | free_set(linq->set); 310 | free(linq); 311 | } 312 | } 313 | 314 | void free_linq_head(struct linq_head *head) { 315 | if (head) { 316 | if (!head->pause) { 317 | free(head->target); 318 | } 319 | free(head); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/config-lua.c: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | #include "constants.h" 3 | #include "set.h" 4 | #include "trace.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | extern const char _binary_lua_pre_config_lua_start; 12 | extern const char _binary_lua_pre_config_lua_end; 13 | extern const char _binary_lua_post_config_lua_start; 14 | extern const char _binary_lua_post_config_lua_end; 15 | 16 | struct config { 17 | struct set *editors; 18 | struct set *project_roots; 19 | struct set *project_parents; 20 | struct set *history_paths; 21 | struct set *excluded_paths; 22 | struct set *included_paths; 23 | struct set *cluded_paths; 24 | char *store_root; 25 | char *project_store_root; 26 | char *unstable_project_store_root; 27 | char *queue_path; 28 | char *journal_path; 29 | char *journal_timestamp_pattern; 30 | char *version_pattern; 31 | char *offset_store_root; 32 | size_t debounce_seconds; 33 | size_t path_length_guess; 34 | size_t elf_interpreter_count_guess; 35 | size_t queue_size_guess; 36 | pid_t max_pid_guess; 37 | char *event_open_exec_not_editor; 38 | char *event_open_exec_editor; 39 | char *event_close_write_not_by_editor; 40 | char *event_close_write_by_editor; 41 | char *event_queue_head_deleted; 42 | char *event_queue_head_forbidden; 43 | char *event_queue_head_stored; 44 | }; 45 | 46 | static size_t read_lua_size(lua_State *lua, const char *name) { 47 | lua_getglobal(lua, name); 48 | return lua_tointeger(lua, -1); 49 | } 50 | 51 | static char *read_lua_string(lua_State *lua, const char *name, 52 | struct trace *trace) { 53 | if (!ok(trace)) { 54 | return NULL; 55 | } 56 | lua_getglobal(lua, name); 57 | const char *string = lua_tostring(lua, -1); 58 | if (!string) { 59 | return NULL; 60 | } 61 | return TNULL(strdup(string), trace); 62 | } 63 | 64 | static struct set *read_lua_set(lua_State *lua, const char *name, 65 | struct trace *trace) { 66 | if (!ok(trace)) { 67 | return NULL; 68 | } 69 | lua_getglobal(lua, name); 70 | struct set *set = create_set(lua_rawlen(lua, -1), trace); 71 | if (!ok(trace)) { 72 | return NULL; 73 | } 74 | 75 | lua_pushnil(lua); 76 | while (lua_next(lua, -2)) { 77 | add(lua_tostring(lua, -2), set, trace); 78 | if (!ok(trace)) { 79 | free_set(set); 80 | return NULL; 81 | } 82 | 83 | lua_pop(lua, 1); 84 | } 85 | return set; 86 | } 87 | 88 | struct config *load_config(const char *path, struct trace *trace) { 89 | struct config *config = TNULL(calloc(1, sizeof(struct config)), trace); 90 | if (!ok(trace)) { 91 | return NULL; 92 | } 93 | 94 | lua_State *lua = TNULL(luaL_newstate(), trace); 95 | 96 | if (ok(trace)) { 97 | luaL_openlibs(lua); 98 | if (luaL_loadbuffer(lua, &_binary_lua_pre_config_lua_start, 99 | &_binary_lua_pre_config_lua_end - 100 | &_binary_lua_pre_config_lua_start, 101 | "pre-config") || 102 | lua_pcall(lua, 0, 0, 0)) { 103 | throw_dynamic(lua_tostring(lua, -1), trace); 104 | } 105 | } 106 | 107 | if (ok(trace)) { 108 | if (path) { 109 | if (luaL_loadfile(lua, path) || lua_pcall(lua, 0, 0, 0) || 110 | luaL_loadbuffer(lua, &_binary_lua_post_config_lua_start, 111 | &_binary_lua_post_config_lua_end - 112 | &_binary_lua_post_config_lua_start, 113 | "post-config") || 114 | lua_pcall(lua, 0, 0, 0)) { 115 | throw_dynamic(lua_tostring(lua, -1), trace); 116 | } 117 | } else if (luaL_loadbuffer(lua, &_binary_lua_post_config_lua_start, 118 | &_binary_lua_post_config_lua_end - 119 | &_binary_lua_post_config_lua_start, 120 | "post-config") || 121 | lua_pcall(lua, 0, 0, 0)) { 122 | throw_dynamic(lua_tostring(lua, -1), trace); 123 | } 124 | } 125 | 126 | config->editors = read_lua_set(lua, "editors", trace); 127 | config->project_roots = read_lua_set(lua, "project_roots", trace); 128 | config->project_parents = read_lua_set(lua, "project_parents", trace); 129 | config->history_paths = read_lua_set(lua, "history_paths", trace); 130 | config->excluded_paths = read_lua_set(lua, "excluded_paths", trace); 131 | config->included_paths = read_lua_set(lua, "included_paths", trace); 132 | config->cluded_paths = read_lua_set(lua, "cluded_paths", trace); 133 | config->store_root = read_lua_string(lua, "store_root", trace); 134 | config->project_store_root = 135 | read_lua_string(lua, "project_store_root", trace); 136 | config->unstable_project_store_root = 137 | read_lua_string(lua, "unstable_project_store_root", trace); 138 | config->queue_path = read_lua_string(lua, "queue_path", trace); 139 | config->journal_path = read_lua_string(lua, "journal_path", trace); 140 | config->journal_timestamp_pattern = 141 | read_lua_string(lua, "journal_timestamp_pattern", trace); 142 | config->version_pattern = read_lua_string(lua, "version_pattern", trace); 143 | config->offset_store_root = read_lua_string(lua, "offset_store_root", trace); 144 | config->event_open_exec_not_editor = 145 | read_lua_string(lua, "event_open_exec_not_editor", trace); 146 | config->event_open_exec_editor = 147 | read_lua_string(lua, "event_open_exec_editor", trace); 148 | config->event_close_write_not_by_editor = 149 | read_lua_string(lua, "event_close_write_not_by_editor", trace); 150 | config->event_close_write_by_editor = 151 | read_lua_string(lua, "event_close_write_by_editor", trace); 152 | config->event_queue_head_deleted = 153 | read_lua_string(lua, "event_queue_head_deleted", trace); 154 | config->event_queue_head_forbidden = 155 | read_lua_string(lua, "event_queue_head_forbidden", trace); 156 | config->event_queue_head_stored = 157 | read_lua_string(lua, "event_queue_head_stored", trace); 158 | 159 | if (!ok(trace)) { 160 | if (lua) { 161 | lua_close(lua); 162 | } 163 | free_config(config); 164 | return NULL; 165 | } 166 | 167 | config->debounce_seconds = read_lua_size(lua, "debounce_seconds"); 168 | config->path_length_guess = read_lua_size(lua, "path_length_guess"); 169 | config->max_pid_guess = read_lua_size(lua, "max_pid_guess"); 170 | config->elf_interpreter_count_guess = 171 | read_lua_size(lua, "elf_interpreter_count_guess"); 172 | config->queue_size_guess = read_lua_size(lua, "queue_size_guess"); 173 | 174 | lua_close(lua); 175 | return config; 176 | } 177 | 178 | const struct set *get_editors(const struct config *config) { 179 | return config->editors; 180 | } 181 | 182 | const struct set *get_project_roots(const struct config *config) { 183 | return config->project_roots; 184 | } 185 | 186 | const struct set *get_project_parents(const struct config *config) { 187 | return config->project_parents; 188 | } 189 | 190 | const struct set *get_history_paths(const struct config *config) { 191 | return config->history_paths; 192 | } 193 | 194 | const struct set *get_excluded_paths(const struct config *config) { 195 | return config->excluded_paths; 196 | } 197 | 198 | const struct set *get_included_paths(const struct config *config) { 199 | return config->included_paths; 200 | } 201 | 202 | const struct set *get_cluded_paths(const struct config *config) { 203 | return config->cluded_paths; 204 | } 205 | 206 | const char *get_store_root(const struct config *config) { 207 | return config->store_root; 208 | } 209 | 210 | const char *get_project_store_root(const struct config *config) { 211 | return config->project_store_root; 212 | } 213 | 214 | const char *get_unstable_project_store_root(const struct config *config) { 215 | return config->unstable_project_store_root; 216 | } 217 | 218 | const char *get_queue_path(const struct config *config) { 219 | return config->queue_path; 220 | } 221 | 222 | const char *get_journal_path(const struct config *config) { 223 | return config->journal_path; 224 | } 225 | 226 | const char *get_journal_timestamp_pattern(const struct config *config) { 227 | return config->journal_timestamp_pattern; 228 | } 229 | 230 | const char *get_version_pattern(const struct config *config) { 231 | return config->version_pattern; 232 | } 233 | 234 | const char *get_offset_store_root(const struct config *config) { 235 | return config->offset_store_root; 236 | } 237 | 238 | size_t get_debounce_seconds(const struct config *config) { 239 | return config->debounce_seconds; 240 | } 241 | 242 | size_t get_path_length_guess(const struct config *config) { 243 | return config->path_length_guess; 244 | } 245 | 246 | pid_t get_max_pid_guess(const struct config *config) { 247 | return config->max_pid_guess; 248 | } 249 | 250 | size_t get_elf_interpreter_count_guess(const struct config *config) { 251 | return config->elf_interpreter_count_guess; 252 | } 253 | 254 | size_t get_queue_size_guess(const struct config *config) { 255 | return config->queue_size_guess; 256 | } 257 | 258 | const char *get_event_open_exec_not_editor(const struct config *config) { 259 | return config->event_open_exec_not_editor; 260 | } 261 | 262 | const char *get_event_open_exec_editor(const struct config *config) { 263 | return config->event_open_exec_editor; 264 | } 265 | 266 | const char *get_event_close_write_not_by_editor(const struct config *config) { 267 | return config->event_close_write_not_by_editor; 268 | } 269 | 270 | const char *get_event_close_write_by_editor(const struct config *config) { 271 | return config->event_close_write_by_editor; 272 | } 273 | 274 | const char *get_event_queue_head_deleted(const struct config *config) { 275 | return config->event_queue_head_deleted; 276 | } 277 | 278 | const char *get_event_queue_head_forbidden(const struct config *config) { 279 | return config->event_queue_head_forbidden; 280 | } 281 | 282 | const char *get_event_queue_head_stored(const struct config *config) { 283 | return config->event_queue_head_stored; 284 | } 285 | 286 | void free_config(struct config *config) { 287 | if (config) { 288 | free_set(config->editors); 289 | free_set(config->project_roots); 290 | free_set(config->project_parents); 291 | free_set(config->history_paths); 292 | free_set(config->excluded_paths); 293 | free_set(config->included_paths); 294 | free_set(config->cluded_paths); 295 | free(config->store_root); 296 | free(config->project_store_root); 297 | free(config->unstable_project_store_root); 298 | free(config->queue_path); 299 | free(config->journal_path); 300 | free(config->journal_timestamp_pattern); 301 | free(config->version_pattern); 302 | free(config->offset_store_root); 303 | free(config->event_open_exec_not_editor); 304 | free(config->event_open_exec_editor); 305 | free(config->event_close_write_not_by_editor); 306 | free(config->event_close_write_by_editor); 307 | free(config->event_queue_head_deleted); 308 | free(config->event_queue_head_forbidden); 309 | free(config->event_queue_head_stored); 310 | free(config); 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /lua/config.lua.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Configuration 6 | 7 | Klunok can be configured with the Lua programming language. 8 | Settings are global Lua variables assigned in a file. 9 | (If you want to use a custom variable with a name that is guaranteed not to clash 10 | with any future settings, begin the name with `custom`). 11 | If the file is within directories that are monitored by Klunok, 12 | the settings are automatically reloaded when the file is written to. 13 | Here is an example of the content of the configuration file: 14 | 15 | ```lua 16 | debounce_seconds = 5 17 | editors['emacs-28.2'] = true 18 | ``` 19 | 20 | The file is passed to Klunok via the `-c` command-line option, 21 | like this: 22 | 23 | ```bash 24 | klunok -c ~/.config/klunok.lua 25 | ``` 26 | 27 | This page documents the available settings, their types, 28 | and default values. 29 | 30 | Configuration parsing code is extracted from this page 31 | to ensure that the documentation is always up to date. 32 | This imposes a certain structure on this page. 33 | Lua code blocks titled `pre-config` are executed before the passed configuration file. 34 | This blocks assign static default values to some settings. 35 | You can use this values in your configuration file, like this: 36 | 37 | ```lua 38 | debounce_seconds = debounce_seconds * 2 39 | prefix_var = prefix .. '/volatile' 40 | ``` 41 | 42 | Lua code blocks titled `post-config` are executed after the passed configuration file. 43 | This blocks invoke the `declare` function. 44 | The invocations specify the name of a setting, 45 | its dynamically computed default value and its type. 46 | Here is a sample invocation: 47 | 48 | ```lua 49 | declare('prefix_var', prefix .. '/var', is_string) 50 | ``` 51 | 52 | This invocation means that a setting called `prefix_var` must be a string, 53 | and if it's not set in the configuration file, 54 | its value is dynamically computed as string `/var` concatenated to 55 | the value of the `prefix` setting. 56 | The `declare` function is defined below: 57 | 58 | ```lua title="post-config" 59 | function declare(name, default, assertion) 60 | if _G[name] == nil and default ~= nil then 61 | _G[name] = default 62 | else 63 | assertion(name) 64 | end 65 | end 66 | ``` 67 | 68 | ## Setting types 69 | 70 | The functions below check that a setting is of the right type. 71 | They are used as the third argument to the `declare` function. 72 | 73 | `is_string` accepts only strings. 74 | Strings in Lua are enclosed in double or single quotes, like `"this"` or `'this'`. 75 | 76 | ```lua title="post-config" 77 | function is_string(name) 78 | assert(type(_G[name]) == 'string', name .. ' must be a string') 79 | end 80 | ``` 81 | 82 | `is_nil_or_string` accepts `nil` and strings. 83 | `nil` in Lua is a representation of absence of value. 84 | `nil` is not the same as an empty string `''`. 85 | The description of a setting of this type will specify what exactly `nil` represents in the 86 | context of the setting. 87 | 88 | ```lua title="post-config" 89 | function is_nil_or_string(name) 90 | assert(_G[name] == nil or type(_G[name]) == 'string', name .. ' must be nil or a string') 91 | end 92 | ``` 93 | 94 | `is_positive` accepts integers greater than or equal to zero. 95 | 96 | ```lua title="post-config" 97 | function is_positive(name) 98 | local value = _G[name] 99 | assert( 100 | type(value) == 'number' and math.floor(value) == value and value >= 0, 101 | name .. ' must be a positive integer' 102 | ) 103 | end 104 | ``` 105 | 106 | `is_set_of_strings` accepts Lua tables with string keys. 107 | Working with settings of this type usually means setting individual keys to `true` or `nil`. 108 | For example, to add string `'abc'` to set `xyz`, you can write `xyz.abc = true`. 109 | To remove `'abc'` from `xyz`, you can write `xyz.abc = nil`. 110 | If the key contains characters that are not letters, for example string `'/@'`, 111 | you can add the key as `xyz['/@'] = true` and remove it as `xyz['/@'] = nil`. 112 | 113 | ```lua title="post-config" 114 | function is_set_of_strings(name) 115 | local value = _G[name] 116 | assert(type(value) == 'table', name .. ' must be a table') 117 | for key, _ in pairs(value) do 118 | assert(type(key) == 'string', name .. ' must contain only string keys') 119 | end 120 | end 121 | ``` 122 | 123 | ## Changing the paths that Klunok writes to 124 | 125 | ### `prefix` 126 | 127 | Prefix used by default for all of the paths that Klunok writes to. 128 | 129 | ```lua title="example" 130 | prefix = '/var/klunok' 131 | ``` 132 | 133 | ```lua title="pre-config" 134 | prefix = 'klunok' 135 | ``` 136 | 137 | ```lua title="post-config" 138 | declare('prefix', nil, is_string) 139 | ``` 140 | 141 | ### `prefix_var` 142 | 143 | Prefix used by default for non-store paths that Klunok writes to. 144 | 145 | ```lua title="post-config" 146 | declare('prefix_var', prefix .. '/var', is_string) 147 | ``` 148 | 149 | ### `store_root` 150 | 151 | Root of the store. 152 | This is where the backed up versions of files that you edit are placed. 153 | 154 | ```lua title="post-config" 155 | declare('store_root', prefix .. '/store', is_string) 156 | ``` 157 | 158 | ### `queue_path` 159 | 160 | Path to the queue. 161 | The queue is a directory that contains symbolic links to files that you edit. 162 | Klunok uses the queue for [debouncing](#debouncing). 163 | 164 | ```lua title="post-config" 165 | declare('queue_path', prefix_var .. '/queue', is_string) 166 | ``` 167 | 168 | ### `journal_path` 169 | 170 | Path to the journal. 171 | The journal is a file where Klunok records various events, 172 | for example a file being backed up. 173 | [See the other events here.](#events) 174 | If `nil`, Klunok does not write journal events anywhere. 175 | Specifying `nil` is more efficient than `'/dev/null'`. 176 | 177 | ```lua title="example" 178 | journal_path = '/dev/stderr' -- write the events to the terminal 179 | ``` 180 | 181 | ```lua title="post-config" 182 | declare('journal_path', prefix_var .. '/journal', is_nil_or_string) 183 | ``` 184 | 185 | ### `offset_store_root` 186 | 187 | Root of an auxiliary store used for keeping track of offsets of 188 | [`history_paths`](#history_paths). 189 | 190 | ```lua title="post-config" 191 | declare('offset_store_root', prefix_var .. '/offsets', is_string) 192 | ``` 193 | 194 | ## Debouncing 195 | 196 | Debouncing means delaying copying until some time passes 197 | without any further modifications. 198 | 199 | ### `debounce_seconds` 200 | 201 | The delay in seconds of copying a file to the store after the last modification. 202 | 203 | ```lua title="pre-config" 204 | debounce_seconds = 60 205 | ``` 206 | 207 | ```lua title="post-config" 208 | declare('debounce_seconds', nil, is_positive) 209 | ``` 210 | 211 | ## Timestamps and versions 212 | 213 | These settings use 214 | [the `strftime` special characters](https://man.archlinux.org/man/strftime.3). 215 | 216 | ### `journal_timestamp_pattern` 217 | 218 | Pattern of timestamps in the journal. 219 | 220 | ```lua title="pre-config" 221 | journal_timestamp_pattern = '%Y-%m-%d-%H-%M' 222 | ``` 223 | 224 | ```lua title="post-config" 225 | declare('journal_timestamp_pattern', nil, is_string) 226 | ``` 227 | 228 | ### `version_pattern` 229 | 230 | Pattern of file versions in the store. 231 | 232 | ```lua title="post-config" 233 | declare('version_pattern', 'v' .. journal_timestamp_pattern, is_string) 234 | ``` 235 | 236 | ## Controlling which files are copied to the store and how 237 | 238 | By default, a file is copied to the store only if it's written to by 239 | an editor application and it's not hidden. 240 | A file is hidden if its name or name of one of its ancestor directories begins with a dot, 241 | for example `.config`. 242 | 243 | Relative paths are interpreted relative to the common parent of directories 244 | monitored via 245 | [the `-w` command line option](./cli.md#-w-path-to-a-directory-that-should-be-monitored-for-edited-files). 246 | For example, if Klunok is invoked as 247 | `klunok -w /home/nazar/src -w /home/nazar/.config/nvim -w /home/nazar/.config/klunok`, 248 | relative paths are interpreted relative to `/home/nazar`. 249 | 250 | `history_paths`, `excluded_paths`, `included_paths` and `cluded_paths` 251 | can be paths not only to files, but also to directories. 252 | If a path is a directory, 253 | the setting applies to each file in the directory and its descendants. 254 | More specific paths override less specific ones. 255 | For example, let's consider this configuration: 256 | 257 | ```lua 258 | excluded_paths['/home/nazar'] = true 259 | included_paths['/home/nazar/src'] = true 260 | excluded_paths['/home/nazar/src/secret.txt'] = true 261 | included_paths['/home/nazar/.config/klunok'] = true 262 | ``` 263 | 264 | With this configuration: 265 | 266 | - `/home/nazar/file.txt` is excluded; 267 | - `/home/nazar/src/file.txt` is included; 268 | - `/home/nazar/src/project/file.txt` is included; 269 | - `/home/nazar/src/secret.txt` is excluded; 270 | - `/home/nazar/.config/file.txt` is excluded because the `.config` directory is hidden; 271 | - `/home/nazar/.config/klunok/file.txt` is included; 272 | - `/home/nazar/.config/klunok/.file.txt` is excluded. 273 | 274 | ### `editors` 275 | 276 | Filenames of executables that are considered editors. 277 | By default, only files edited by this applications are copied to the store. 278 | If you have problems registering an application as an editor, 279 | please read [the editors section](./editors.md). 280 | 281 | ```lua title="example" 282 | editors.ed = true 283 | editors['emacs-28.3'] = true 284 | editors.code = nil -- do not treat "code" as an editor 285 | ``` 286 | 287 | ```lua title="pre-config" 288 | editors = { 289 | atom = true, 290 | code = true, 291 | codium = true, 292 | gedit = true, 293 | howl = true, 294 | hx = true, 295 | inkscape = true, 296 | kak = true, 297 | kate = true, 298 | kwrite = true, 299 | micro = true, 300 | nano = true, 301 | nvim = true, 302 | pluma = true, 303 | rsession = true, 304 | sublime_text = true, 305 | vi = true, 306 | vim = true, 307 | xed = true, 308 | ['gnome-text-editor'] = true, 309 | ['notepadqq-bin'] = true, 310 | ['soffice.bin'] = true, 311 | ['vim.basic'] = true, 312 | ['vim.tiny'] = true, 313 | ['.gedit-wrapped'] = true, 314 | ['.gnome-text-editor-wrapped'] = true, 315 | ['.howl-wrapped'] = true, 316 | ['.hx-wrapped'] = true, 317 | ['.inkscape-wrapped'] = true, 318 | ['.kate-wrapped'] = true, 319 | ['.kwrite-wrapped'] = true, 320 | ['.pluma-wrapped'] = true, 321 | ['.xed-wrapped'] = true, 322 | } 323 | ``` 324 | 325 | ```lua title="post-config" 326 | declare('editors', nil, is_set_of_strings) 327 | ``` 328 | 329 | ### `history_paths` 330 | 331 | Paths that are assumed to be always appended to. 332 | Only changes will be stored as new versions. 333 | These paths are copied to the store regardless of the application that writes to them 334 | and hence regardless of [the `editors` setting](#editors). 335 | 336 | ```lua title="example" 337 | history_paths['/home/nazar/.bash_history'] = true 338 | ``` 339 | 340 | ```lua title="pre-config" 341 | history_paths = {} 342 | ``` 343 | 344 | ```lua title="post-config" 345 | declare('history_paths', nil, is_set_of_strings) 346 | ``` 347 | 348 | ### `excluded_paths` 349 | 350 | Paths that are never copied to the store. 351 | 352 | ```lua title="pre-config" 353 | excluded_paths = {} 354 | ``` 355 | 356 | ```lua title="post-config" 357 | declare('excluded_paths', nil, is_set_of_strings) 358 | ``` 359 | 360 | ### `included_paths` 361 | 362 | Paths that are copied to the store regardless of the application that writes to them, 363 | and hence regardless of [the `editors` setting](#editors). 364 | 365 | ```lua title="pre-config" 366 | included_paths = {} 367 | ``` 368 | 369 | ```lua title="post-config" 370 | declare('included_paths', nil, is_set_of_strings) 371 | ``` 372 | 373 | ### `cluded_paths` 374 | 375 | Paths that are copied to the store only if they are written to by an editor application. 376 | Editor applications are defined in [the `editors` setting](#editors). 377 | 378 | This is the default, so this setting is mainly useful to: 379 | 380 | - override `history_paths`, `excluded_paths` and `included_paths`; 381 | - include files in hidden directories if the files themselves are not hidden, for example 382 | specifying `cluded_paths['/home/nazar/.config'] = true` will allow 383 | `/home/nazar/.config/klunok.lua` to be copied when written to by an editor application, 384 | but `/home/nazar/.config/.klunok.lua` and `/home/nazar/.config/.klunok/config.lua` 385 | will not be copied. 386 | 387 | ```lua title="pre-config" 388 | cluded_paths = {} 389 | ``` 390 | 391 | ```lua title="post-config" 392 | declare('cluded_paths', nil, is_set_of_strings) 393 | ``` 394 | 395 | ## Projects 396 | 397 | See [the projects section](./projects.md). 398 | 399 | ### `project_roots` 400 | 401 | Roots of projects. 402 | 403 | ```lua title="example" 404 | project_roots['/home/nazar/src/klunok'] = true 405 | ``` 406 | 407 | ```lua title="pre-config" 408 | project_roots = {} 409 | ``` 410 | 411 | ```lua title="post-config" 412 | declare('project_roots', nil, is_set_of_strings) 413 | ``` 414 | 415 | ### `project_parents` 416 | 417 | Directories that contain roots of projects. 418 | 419 | ```lua title="example" 420 | project_parents['/home/nazar/src/'] = true 421 | ``` 422 | 423 | ```lua title="pre-config" 424 | project_parents = {} 425 | ``` 426 | 427 | ```lua title="post-config" 428 | declare('project_parents', nil, is_set_of_strings) 429 | ``` 430 | 431 | ### `project_store_root` 432 | 433 | Root of the project store. 434 | 435 | ```lua title="post-config" 436 | declare('project_store_root', prefix .. '/projects', is_string) 437 | ``` 438 | 439 | ### `unstable_project_store_root` 440 | 441 | Root of the unstable project store. 442 | 443 | ```lua title="post-config" 444 | declare('unstable_project_store_root', prefix_var .. '/projects', is_string) 445 | ``` 446 | 447 | ## Performance tuning 448 | 449 | These settings control the trade-off between RAM usage and performance. 450 | Tinkering with these settings cannot impact Klunok in any other way. 451 | 452 | ### `queue_size_guess` 453 | 454 | Guess of the maximum queue size. 455 | 456 | ```lua title="post-config" 457 | declare('queue_size_guess', debounce_seconds * 2, is_positive) 458 | ``` 459 | 460 | ### `path_length_guess` 461 | 462 | Guess of the maximum length of the majority of the paths in the system. 463 | 464 | ```lua title="pre-config" 465 | path_length_guess = 1024 466 | ``` 467 | 468 | ```lua title="post-config" 469 | declare('path_length_guess', nil, is_positive) 470 | ``` 471 | 472 | ### `max_pid_guess` 473 | 474 | Guess of the maximum PID (process ID) in the system while Klunok is running. 475 | 476 | ```lua title="pre-config" 477 | max_pid_guess = 2^15 478 | ``` 479 | 480 | ```lua title="post-config" 481 | declare('max_pid_guess', nil, is_positive) 482 | ``` 483 | 484 | ### `elf_interpreter_count_guess` 485 | 486 | Guess of how many ELF iterpreters are there in the system. 487 | 488 | ```lua title="pre-config" 489 | elf_interpreter_count_guess = 1 490 | ``` 491 | 492 | ```lua title="post-config" 493 | declare('elf_interpreter_count_guess', nil, is_positive) 494 | ``` 495 | 496 | ## Events 497 | 498 | If a setting from this section is `nil`, the corresponding event is not logged to 499 | the journal. 500 | Otherwise, the corresponding event is logged to the journal with 501 | the provided prefix. 502 | If prefix is not an empty string, it is separated from the 503 | rest of the logged line by a tab. 504 | 505 | ```lua title="pre-config" 506 | event_queue_head_stored = '' 507 | ``` 508 | 509 | ```lua title="post-config" 510 | declare('event_open_exec_not_editor', nil, is_nil_or_string) 511 | declare('event_open_exec_editor', nil, is_nil_or_string) 512 | declare('event_close_write_not_by_editor', nil, is_nil_or_string) 513 | declare('event_close_write_by_editor', nil, is_nil_or_string) 514 | declare('event_queue_head_deleted', nil, is_nil_or_string) 515 | declare('event_queue_head_forbidden', nil, is_nil_or_string) 516 | declare('event_queue_head_stored', nil, is_nil_or_string) 517 | ``` 518 | -------------------------------------------------------------------------------- /src/handler.c: -------------------------------------------------------------------------------- 1 | #include "handler.h" 2 | #include "bitmap.h" 3 | #include "buffer.h" 4 | #include "config.h" 5 | #include "counter.h" 6 | #include "deref.h" 7 | #include "elfinterp.h" 8 | #include "journal.h" 9 | #include "linq.h" 10 | #include "messages.h" 11 | #include "parents.h" 12 | #include "set.h" 13 | #include "sieve.h" 14 | #include "storepath.h" 15 | #include "sync.h" 16 | #include "timestamp.h" 17 | #include "trace.h" 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | struct handler { 25 | char *config_path; 26 | size_t common_parent_path_length; 27 | struct config *config; 28 | struct journal *journal; 29 | struct linq *linq; 30 | struct bitmap *editor_pid_bitmap; 31 | struct set *elf_interpreters; 32 | }; 33 | 34 | struct handler *load_handler(const char *config_path, 35 | size_t common_parent_path_length, 36 | struct trace *trace) { 37 | struct handler *handler = TNULL(calloc(1, sizeof(struct handler)), trace); 38 | if (!ok(trace)) { 39 | return NULL; 40 | } 41 | 42 | handler->common_parent_path_length = common_parent_path_length; 43 | 44 | try(trace); 45 | if (config_path) { 46 | handler->config_path = TNULL(realpath(config_path, NULL), trace); 47 | } else { 48 | handler->config_path = NULL; 49 | } 50 | handler->config = load_config(config_path, trace); 51 | if (config_path) { 52 | rethrow_context(config_path, trace); 53 | } 54 | finally_rethrow_static(messages.handler.config.cannot_load, trace); 55 | 56 | if (ok(trace)) { 57 | handler->elf_interpreters = 58 | create_set(get_elf_interpreter_count_guess(handler->config), trace); 59 | handler->editor_pid_bitmap = 60 | create_bitmap(get_max_pid_guess(handler->config), trace); 61 | 62 | try(trace); 63 | handler->linq = load_linq(get_queue_path(handler->config), 64 | get_debounce_seconds(handler->config), 65 | get_queue_size_guess(handler->config), 66 | get_path_length_guess(handler->config), trace); 67 | rethrow_context(get_queue_path(handler->config), trace); 68 | finally_rethrow_static(messages.handler.linq.cannot_load, trace); 69 | 70 | try(trace); 71 | handler->journal = 72 | open_journal(get_journal_path(handler->config), 73 | get_journal_timestamp_pattern(handler->config), trace); 74 | if (get_journal_path(handler->config)) { 75 | rethrow_context(get_journal_path(handler->config), trace); 76 | } 77 | finally_rethrow_static(messages.handler.journal.cannot_open, trace); 78 | } 79 | 80 | if (!ok(trace)) { 81 | free_handler(handler); 82 | return NULL; 83 | } 84 | 85 | return handler; 86 | } 87 | 88 | static void record_event(const char *event, pid_t pid, const char *path, 89 | const struct handler *handler, struct trace *trace) { 90 | try(trace); 91 | note(event, pid, path, handler->journal, trace); 92 | rethrow_context(get_journal_path(handler->config), trace); 93 | finally_rethrow_static(messages.handler.journal.cannot_write_to, trace); 94 | } 95 | 96 | void handle_open_exec(pid_t pid, int fd, struct handler *handler, 97 | struct trace *trace) { 98 | if (!ok(trace)) { 99 | return; 100 | } 101 | char *file_path = deref_fd(fd, get_path_length_guess(handler->config), trace); 102 | if (!ok(trace)) { 103 | return; 104 | } 105 | char *exe_filename = strrchr(file_path, '/'); 106 | assert(exe_filename); 107 | ++exe_filename; 108 | 109 | const char *event = get_event_open_exec_not_editor(handler->config); 110 | 111 | struct buffer_view *exe_filename_view = 112 | create_buffer_view(exe_filename, trace); 113 | 114 | if (!ok(trace)) { 115 | free_buffer_view(exe_filename_view); 116 | return; 117 | } 118 | 119 | if (is_within(exe_filename_view, get_editors(handler->config))) { 120 | event = get_event_open_exec_editor(handler->config); 121 | set_bit(pid, handler->editor_pid_bitmap, trace); 122 | 123 | char *interpreter = get_elf_interpreter(fd, trace); 124 | if (interpreter) { 125 | add(interpreter, handler->elf_interpreters, trace); 126 | free(interpreter); 127 | } 128 | } else if (get_bit(pid, handler->editor_pid_bitmap)) { 129 | struct buffer_view *file_path_view = create_buffer_view(file_path, trace); 130 | if (ok(trace) && !is_within(file_path_view, handler->elf_interpreters)) { 131 | unset_bit(pid, handler->editor_pid_bitmap); 132 | } 133 | free_buffer_view(file_path_view); 134 | } 135 | 136 | record_event(event, pid, file_path, handler, trace); 137 | 138 | free(file_path); 139 | free_buffer_view(exe_filename_view); 140 | } 141 | 142 | static const size_t linq_meta_is_project = 1; 143 | static const size_t linq_meta_is_history = 2; 144 | static const size_t linq_meta_project_offset = 2; 145 | 146 | enum status { 147 | cluded, 148 | included, 149 | excluded, 150 | history, 151 | project, 152 | project_parent, 153 | }; 154 | 155 | static bool push_to_linq(pid_t pid, char *path, struct handler *handler, 156 | struct trace *trace) { 157 | if (!ok(trace)) { 158 | return false; 159 | } 160 | const struct set *sets[] = { 161 | get_cluded_paths(handler->config), get_included_paths(handler->config), 162 | get_excluded_paths(handler->config), get_history_paths(handler->config), 163 | get_project_roots(handler->config), get_project_parents(handler->config), 164 | }; 165 | struct sieved_path *sieved_path = 166 | sieve(path, handler->common_parent_path_length, sets, 167 | sizeof sets / sizeof sets[0], trace); 168 | 169 | if (!ok(trace)) { 170 | return false; 171 | } 172 | 173 | const char *const *ends = get_sieved_ends(sieved_path); 174 | 175 | const char *farthest_end = get_hiding_dot(sieved_path); 176 | bool is_written_by_editor = get_bit(pid, handler->editor_pid_bitmap); 177 | bool is_pushed = !get_hiding_dot(sieved_path) && is_written_by_editor; 178 | bool is_history = false; 179 | 180 | for (size_t i = 0; i < sizeof sets / sizeof sets[0]; ++i) { 181 | if (ends[i] > farthest_end) { 182 | farthest_end = ends[i]; 183 | is_history = i == history; 184 | switch (i) { 185 | case cluded: 186 | is_pushed = is_written_by_editor; 187 | break; 188 | case included: 189 | case history: 190 | is_pushed = true; 191 | break; 192 | case excluded: 193 | is_pushed = false; 194 | break; 195 | } 196 | } 197 | } 198 | 199 | const char *project_root_end = NULL; 200 | 201 | if (ends[project_parent] > ends[project] && *ends[project_parent]) { 202 | project_root_end = strchr(ends[project_parent] + 1, '/'); 203 | } else if (ends[project] && *ends[project]) { 204 | project_root_end = ends[project]; 205 | } 206 | 207 | free_sieved_path(sieved_path); 208 | 209 | if (!is_pushed) { 210 | return false; 211 | } 212 | 213 | size_t metadata = 0; 214 | if (is_history) { 215 | metadata |= linq_meta_is_history; 216 | } 217 | if (project_root_end) { 218 | metadata |= (project_root_end - path) << linq_meta_project_offset; 219 | } 220 | 221 | try(trace); 222 | push(path, metadata, handler->linq, trace); 223 | rethrow_context(path, trace); 224 | finally_rethrow_static(messages.handler.linq.cannot_push, trace); 225 | 226 | if (project_root_end) { 227 | char original_end = *project_root_end; 228 | path[project_root_end - path] = 0; 229 | 230 | try(trace); 231 | push(path, linq_meta_is_project, handler->linq, trace); 232 | rethrow_context(path, trace); 233 | finally_rethrow_static(messages.handler.linq.cannot_push, trace); 234 | 235 | path[project_root_end - path] = original_end; 236 | } 237 | 238 | return true; 239 | } 240 | 241 | void handle_close_write(pid_t pid, int fd, struct handler *handler, 242 | struct trace *trace) { 243 | if (!ok(trace)) { 244 | return; 245 | } 246 | char *file_path = deref_fd(fd, get_path_length_guess(handler->config), trace); 247 | if (!ok(trace)) { 248 | return; 249 | } 250 | 251 | const char *event = get_event_close_write_not_by_editor(handler->config); 252 | 253 | if (push_to_linq(pid, file_path, handler, trace)) { 254 | event = get_event_close_write_by_editor(handler->config); 255 | } 256 | 257 | record_event(event, pid, file_path, handler, trace); 258 | 259 | if (ok(trace) && handler->config_path && 260 | !strcmp(file_path, handler->config_path)) { 261 | try(trace); 262 | struct config *new_config = load_config(handler->config_path, trace); 263 | rethrow_context(handler->config_path, trace); 264 | finally_rethrow_static(messages.handler.config.cannot_reload, trace); 265 | 266 | struct linq *new_linq = NULL; 267 | if (ok(trace) && 268 | strcmp(get_queue_path(handler->config), get_queue_path(new_config))) { 269 | try(trace); 270 | new_linq = load_linq(get_queue_path(new_config), 271 | get_debounce_seconds(new_config), 272 | get_queue_size_guess(new_config), 273 | get_path_length_guess(new_config), trace); 274 | rethrow_context(get_queue_path(new_config), trace); 275 | finally_rethrow_static(messages.handler.linq.cannot_reload, trace); 276 | } 277 | 278 | struct journal *new_journal = NULL; 279 | if (ok(trace)) { 280 | try(trace); 281 | new_journal = 282 | open_journal(get_journal_path(new_config), 283 | get_journal_timestamp_pattern(new_config), trace); 284 | rethrow_context(get_journal_path(new_config), trace); 285 | finally_rethrow_static(messages.handler.journal.cannot_open, trace); 286 | } 287 | 288 | if (ok(trace)) { 289 | free_config(handler->config); 290 | handler->config = new_config; 291 | free_journal(handler->journal); 292 | handler->journal = new_journal; 293 | if (new_linq) { 294 | free_linq(handler->linq); 295 | handler->linq = new_linq; 296 | } 297 | redebounce(get_debounce_seconds(new_config), handler->linq); 298 | } 299 | } 300 | 301 | free(file_path); 302 | } 303 | 304 | time_t handle_timeout(struct handler *handler, struct trace *trace) { 305 | while (ok(trace)) { 306 | try(trace); 307 | struct linq_head *head = get_head(handler->linq, trace); 308 | finally_rethrow_static(messages.handler.linq.cannot_get_head, trace); 309 | if (!ok(trace)) { 310 | return 0; 311 | } 312 | 313 | time_t pause = get_pause(head); 314 | if (pause) { 315 | free_linq_head(head); 316 | return pause; 317 | } 318 | 319 | char *version = 320 | get_timestamp(get_version_pattern(handler->config), NAME_MAX, trace); 321 | if (ok(trace) && strchr(version, '/')) { 322 | throw_context(version, trace); 323 | throw_static(messages.handler.version.has_slashes, trace); 324 | } 325 | if (!ok(trace)) { 326 | free_linq_head(head); 327 | free(version); 328 | return 0; 329 | } 330 | 331 | const char *event = get_event_queue_head_stored(handler->config); 332 | const char *relative_path = 333 | get_path(head) + handler->common_parent_path_length; 334 | 335 | if (get_metadata(head) & linq_meta_is_project) { 336 | const char *project_name = strrchr(get_path(head), '/') + 1; 337 | struct store_path *store_path = 338 | create_store_path(get_project_store_root(handler->config), 339 | project_name, version, trace); 340 | struct buffer *unstable_path = create_buffer(trace); 341 | concat_string(get_unstable_project_store_root(handler->config), 342 | unstable_path, trace); 343 | concat_char('/', unstable_path, trace); 344 | concat_string(project_name, unstable_path, trace); 345 | 346 | while (ok(trace)) { 347 | try(trace); 348 | sync_shallow_tree(get_current_path(store_path), 349 | get_string(get_view(unstable_path)), get_path(head), 350 | trace); 351 | if (catch_static(messages.sync.destination_already_exists, trace)) { 352 | finally(trace); 353 | increment(store_path, trace); 354 | } else { 355 | if (catch_static(messages.sync.source_does_not_exist, trace)) { 356 | event = get_event_queue_head_deleted(handler->config); 357 | } else if (catch_static(messages.sync.source_permission_denied, 358 | trace)) { 359 | event = get_event_queue_head_forbidden(handler->config); 360 | }; 361 | finally(trace); 362 | break; 363 | } 364 | } 365 | 366 | record_event(event, 0, relative_path, handler, trace); 367 | pop_head(handler->linq, trace); 368 | free_store_path(store_path); 369 | free_buffer(unstable_path); 370 | free_linq_head(head); 371 | continue; 372 | } 373 | 374 | struct store_path *store_path = create_store_path( 375 | get_store_root(handler->config), relative_path, version, trace); 376 | free(version); 377 | 378 | struct buffer *offset_path = create_buffer(trace); 379 | concat_string(get_offset_store_root(handler->config), offset_path, trace); 380 | concat_char('/', offset_path, trace); 381 | concat_string(relative_path, offset_path, trace); 382 | 383 | bool is_history_path = get_metadata(head) & linq_meta_is_history; 384 | size_t offset = is_history_path 385 | ? read_counter(get_string(get_view(offset_path)), trace) 386 | : 0; 387 | 388 | /* TODO: just recompute metadata on pop. 389 | * history_paths can be appended to included_path in Lua.*/ 390 | size_t project_root_end_offset = 391 | get_metadata(head) >> linq_meta_project_offset; 392 | 393 | if (ok(trace) && project_root_end_offset && 394 | (project_root_end_offset > strlen(get_path(head)) || 395 | get_path(head)[project_root_end_offset] != '/')) { 396 | throw_context(get_path(head), trace); 397 | throw_static(messages.linq.invalid_entry, trace); 398 | } 399 | 400 | if (!ok(trace)) { 401 | free_linq_head(head); 402 | free_store_path(store_path); 403 | free_buffer(offset_path); 404 | return 0; 405 | } 406 | 407 | bool is_stored = false; 408 | 409 | for (;;) { 410 | try(trace); 411 | size_t new_offset = sync_file(get_current_path(store_path), 412 | get_path(head), offset, trace); 413 | if (!is_history_path) { 414 | new_offset = 0; 415 | } 416 | if (catch_static(messages.sync.source_does_not_exist, trace) || 417 | catch_static(messages.sync.source_is_not_regular_file, trace)) { 418 | event = get_event_queue_head_deleted(handler->config); 419 | } else if (catch_static(messages.sync.source_permission_denied, trace)) { 420 | event = get_event_queue_head_forbidden(handler->config); 421 | } else if (catch_static(messages.sync.destination_already_exists, 422 | trace)) { 423 | increment(store_path, trace); 424 | continue; 425 | } else { 426 | write_counter(get_string(get_view(offset_path)), new_offset, trace); 427 | is_stored = ok(trace); 428 | } 429 | finally(trace); 430 | 431 | pop_head(handler->linq, trace); 432 | 433 | if (!ok(trace)) { 434 | throw_context(get_path(head), trace); 435 | throw_static(messages.handler.store.cannot_copy, trace); 436 | free_linq_head(head); 437 | free_store_path(store_path); 438 | free_buffer(offset_path); 439 | return 0; 440 | } 441 | 442 | break; 443 | } 444 | 445 | if (ok(trace) && project_root_end_offset && is_stored) { 446 | size_t project_name_length = 0; 447 | while (*(get_path(head) + project_root_end_offset - 1 - 448 | project_name_length) != '/') { 449 | ++project_name_length; 450 | } 451 | 452 | struct buffer *project_path = create_buffer(trace); 453 | concat_string(get_unstable_project_store_root(handler->config), 454 | project_path, trace); 455 | concat_char('/', project_path, trace); 456 | concat_bytes(get_path(head) + project_root_end_offset - 457 | project_name_length, 458 | project_name_length, project_path, trace); 459 | concat_string(get_path(head) + project_root_end_offset, project_path, 460 | trace); 461 | 462 | if (ok(trace) && unlink(get_string(get_view(project_path))) < 0 && 463 | errno != ENOENT) { 464 | throw_errno(trace); 465 | } 466 | create_parents(get_string(get_view(project_path)), trace); 467 | TNEG(link(get_current_path(store_path), 468 | get_string(get_view(project_path))), 469 | trace); 470 | } 471 | 472 | record_event(event, 0, relative_path, handler, trace); 473 | 474 | free_linq_head(head); 475 | free_store_path(store_path); 476 | free_buffer(offset_path); 477 | } 478 | 479 | return 0; 480 | } 481 | 482 | void free_handler(struct handler *handler) { 483 | if (handler) { 484 | free(handler->config_path); 485 | free_config(handler->config); 486 | free_journal(handler->journal); 487 | free_linq(handler->linq); 488 | free_bitmap(handler->editor_pid_bitmap); 489 | free_set(handler->elf_interpreters); 490 | free(handler); 491 | } 492 | } 493 | --------------------------------------------------------------------------------