├── .github └── workflows │ └── flake_check.yml ├── .gitignore ├── LICENSE ├── README.md ├── debugrun.sh ├── flake.lock ├── flake.nix ├── nixfs ├── CMakeLists.txt ├── include │ ├── base64.h │ ├── debug.h │ ├── nixfs.h │ ├── urldec.h │ └── version.h.in └── src │ ├── base64.c │ ├── main.c │ ├── nixfs.c │ └── urldec.c └── utils └── update-release.sh /.github/workflows/flake_check.yml: -------------------------------------------------------------------------------- 1 | name: flake check 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | types: [ "opened" ] 8 | branches: [ "master" ] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Install Nix 16 | uses: cachix/install-nix-action@v18 17 | with: 18 | extra_nix_config: "experimental-features = nix-command flakes\nsystem-features = nixos-test benchmark big-parallel kvm" 19 | 20 | - uses: actions/checkout@v3 21 | 22 | - name: flake check 23 | run: nix flake check 24 | - name: flake metadata 25 | run: nix flake metadata 26 | - name: flake show 27 | run: nix flake show 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | result 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 illustris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NixFS 2 | 3 | Access any Nix derivation through filesystem paths with NixFS. 4 | 5 | NixFS is a FUSE-based filesystem that allows you to access any Nix derivation by simply accessing a corresponding path. This can be helpful for exploring the Nix store and easily accessing the derivations without having to explicitly build them first. 6 | 7 | ## How it works 8 | 9 | ``` 10 | /nixfs 11 | ├── expr 12 | │   ├── b64 13 | │   ├── str 14 | │   └── urlenc 15 | └── flake 16 | ├── b64 17 | ├── str 18 | └── urlenc 19 | ``` 20 | 21 | ### Flakes 22 | 23 | When you attempt to access /nixfs/flake/str/nixpkgs#hello or any subpaths of it, NixFS will automatically trigger a nix-build of nixpkgs#hello. The path will then appear as a symlink to the store path of the build result. 24 | 25 | For flakes with / in their URL, you can use `/nixfs/flake/urlenc/` to access them. 26 | You can also use `base64url` encoding (i.e. b64 with `+/` replaced by `-_`) at `/nixfs/flake/b64/`. 27 | 28 | Example: 29 | ``` 30 | $ /nixfs/flake/str/nixpkgs#hello/bin/hello 31 | Hello, world! 32 | 33 | ``` 34 | 35 | ### Expressions 36 | 37 | There is no easy way to build arbitrary expressions in the context of a flake right now. See [here](https://github.com/NixOS/nix/issues/5567). You can build and access nix expressions using the `/nixfs/expr` path. However, the build will be impure because of the above limitation. 38 | 39 | Example: 40 | ``` 41 | $ /tmp/nixfs/expr/str/'(import { }).hello'/bin/hello 42 | Hello, world! 43 | ``` 44 | 45 | See the usage section below for more flake and expr examples. 46 | 47 | ## Passing flags and args to nix build 48 | 49 | You can pass flags to the nix build by appending them to the path just before the flake url or expression. Flags starting with a dash (`-`) should be added directly, while arguments not starting with a dash should be prefixed with a hash (`#`). For example: 50 | 51 | ``` 52 | /nixfs/flake/str/--optional-flag-starting-with-dash/#optional-flag-not-starting-with-dash/nixpkgs#hello 53 | ``` 54 | 55 | For example: 56 | 57 | ``` 58 | /nixfs/flake/str/--refresh/nixpkgs#hello 59 | ``` 60 | 61 | ## Usage 62 | 63 | ``` 64 | nixfs [--debug] [] /mount/path 65 | ``` 66 | 67 | This will mount the NixFS filesystem to the specified mount path. 68 | 69 | ``` 70 | $ ls /tmp/nixfs/flake/urlenc/$(echo -n github:illustris/nixfs | jq -sRr @uri)/bin 71 | mount.fuse.nixfs mount.nixfs nixfs 72 | $ ls /tmp/nixfs/flake/b64/$(echo -n github:illustris/nixfs | base64 -w0 | tr '+/' '-_')/bin 73 | mount.fuse.nixfs mount.nixfs nixfs 74 | $ /nixfs/expr/str/'(import { }).python3.withPackages (p: [p.requests])'/bin/python -c 'import requests; print(requests.__version__)' 75 | 2.32.3 76 | $ /nixfs/expr/b64/$(echo -n '(import { }).python3.withPackages (p: [p.requests])' | base64 -w0 | tr '+/' '-_')/bin/python -c 'import requests; print(requests.__version__)' 77 | 2.32.3 78 | $ /nixfs/expr/urlenc/$(echo -n '(import { }).python3.withPackages (p: [p.requests])' | jq -sRr @uri)/bin/python -c 'import requests; print(requests.__version__)' 79 | 2.32.3 80 | ``` 81 | 82 | ## NixOS module 83 | 84 | You can also integrate NixFS into your NixOS configuration as a module. 85 | 86 | To do this, add the following to your flake.nix: 87 | 88 | ``` 89 | { 90 | inputs.nixfs.url = "github:illustris/nixfs"; 91 | 92 | outputs = {nixpkgs, nixfs, ...}: { 93 | nixosConfigurations.my_machine = { 94 | imports = [ nixfs.nixosModules.nixfs ]; 95 | services.nixfs.enable = true; 96 | }; 97 | }; 98 | } 99 | ``` 100 | 101 | This will enable the NixFS service on your NixOS machine, and the filesystem will be automatically mounted on startup. 102 | 103 | ## Contributing 104 | 105 | If you'd like to contribute to the development of NixFS, please feel free to submit issues or pull requests on the GitHub repository. 106 | -------------------------------------------------------------------------------- /debugrun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MOUNT_POINT="/tmp/nixfs/" 4 | FUSE_COMMAND="nix run -L .# -- ${MOUNT_POINT} --debug -f -s" 5 | SRC_DIR="nixfs/" 6 | 7 | function cleanup { 8 | fusermount -u ${MOUNT_POINT} 9 | echo "Unmounted ${MOUNT_POINT}" 10 | rmdir $MOUNT_POINT 11 | } 12 | 13 | trap cleanup EXIT 14 | 15 | while true; do 16 | # Ensure the dir exists 17 | mkdir -p $MOUNT_POINT 18 | # Run the FUSE command in the background 19 | ${FUSE_COMMAND} & 20 | FUSE_PID=$! 21 | 22 | # Wait for any change in the src directory, excluding filenames ending with ~ 23 | inotifywait -r -e modify,move,create,delete --exclude '~$' --exclude '#' "${SRC_DIR}" 24 | 25 | # Kill the FUSE process 26 | kill ${FUSE_PID} 27 | wait ${FUSE_PID} 2>/dev/null 28 | 29 | # Unmount the FUSE filesystem 30 | cleanup 31 | done 32 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1728018373, 6 | "narHash": "sha256-NOiTvBbRLIOe5F6RbHaAh6++BNjsb149fGZd1T4+KBg=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "bc947f541ae55e999ffdb4013441347d83b00feb", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "NixFS: Every derivation, everywhere"; 3 | 4 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 5 | 6 | outputs = { self, nixpkgs }: with nixpkgs.lib; let 7 | archs = [ 8 | "x86_64-linux" 9 | "aarch64-linux" 10 | "riscv64-linux" 11 | ]; 12 | in { 13 | packages = genAttrs archs (system: with nixpkgs.legacyPackages.${system}; rec { 14 | nixfs = stdenv.mkDerivation { 15 | pname = "nixfs"; 16 | version = "0.2.1"; 17 | src = ./nixfs; 18 | nativeBuildInputs = [ 19 | cmake 20 | pkg-config 21 | ]; 22 | buildInputs = [ 23 | fuse 24 | openssl 25 | ]; 26 | }; 27 | default = nixfs; 28 | # to speed up nixos test 29 | inherit hello; 30 | updateRelease = writeScriptBin "update-release" (builtins.readFile ./utils/update-release.sh); 31 | }); 32 | devShells = genAttrs archs (system: with nixpkgs.legacyPackages.${system}; rec { 33 | default = mkShell { 34 | buildInputs = [ inotify-tools ]; 35 | shellHook = "alias debug='bash ${./debugrun.sh}'"; 36 | }; 37 | }); 38 | nixosModules.nixfs = { config, pkgs, lib, ... }: with lib; { 39 | options.services.nixfs = { 40 | enable = mkEnableOption "NixFS service"; 41 | mountPath = mkOption { 42 | type = types.path; 43 | default = "/nixfs"; 44 | description = "Path to mount the NixFS filesystem."; 45 | }; 46 | }; 47 | 48 | config = mkIf config.services.nixfs.enable { 49 | system.fsPackages = [ self.packages.${pkgs.system}.nixfs ]; 50 | systemd.mounts = [{ 51 | what = "none"; 52 | where = config.services.nixfs.mountPath; 53 | type = "fuse.nixfs"; 54 | options = "allow_other"; 55 | wantedBy = [ "multi-user.target" ]; 56 | }]; 57 | }; 58 | }; 59 | checks = genAttrs archs (system: with nixpkgs.legacyPackages.${system}; { 60 | default = nixosTest { 61 | name = "nixfs-test"; 62 | nodes.n = { pkgs, ... }: { 63 | imports = [ self.nixosModules.nixfs ]; 64 | services.nixfs.enable = true; 65 | # ensure the build result nixfs will access is already present in the VM 66 | system.extraDependencies = [ 67 | self.inputs.nixpkgs 68 | self 69 | self.packages.${system}.hello 70 | ]; 71 | # useful for debugging 72 | systemd.services.execsnoop = { 73 | script = "execsnoop"; 74 | path = with pkgs; [ 75 | bcc 76 | gnutar 77 | kmod 78 | xz 79 | ]; 80 | wantedBy = [ "multi-user.target" ]; 81 | }; 82 | }; 83 | # use the store path of the nixpkgs flake to avoid downloading from the internet 84 | testScript = concatStringsSep "\n" [ 85 | "n.wait_for_unit('execsnoop.service')" 86 | "assert 'Hello, world!' in n.succeed('set -x; /nixfs/flake/b64/--offline/$(printf ${self}#hello | base64 -w0)/bin/hello')" 87 | ]; 88 | }; 89 | }); 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /nixfs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0) 2 | project(nixfs VERSION 0.2.1 LANGUAGES C) 3 | 4 | configure_file( 5 | "${PROJECT_SOURCE_DIR}/include/version.h.in" 6 | "${PROJECT_BINARY_DIR}/include/version.h" 7 | ) 8 | 9 | include_directories("${PROJECT_BINARY_DIR}/include") 10 | 11 | set(CMAKE_C_STANDARD 99) 12 | set(CMAKE_C_STANDARD_REQUIRED ON) 13 | 14 | include_directories(include) 15 | 16 | file(GLOB_RECURSE SOURCES LIST_DIRECTORIES false src/*.c) 17 | add_executable(nixfs ${SOURCES}) 18 | 19 | # Add the -D_FILE_OFFSET_BITS=64 compile flag 20 | set_property(TARGET nixfs PROPERTY COMPILE_DEFINITIONS _FILE_OFFSET_BITS=64) 21 | 22 | # Find and link the Fuse library 23 | find_package(PkgConfig REQUIRED) 24 | pkg_check_modules(FUSE REQUIRED fuse) 25 | include_directories(${FUSE_INCLUDE_DIRS}) 26 | target_link_libraries(nixfs ${FUSE_LIBRARIES}) 27 | 28 | # Find and link OpenSSL 29 | find_package(OpenSSL REQUIRED) 30 | include_directories(${OPENSSL_INCLUDE_DIR}) 31 | target_link_libraries(nixfs ${OPENSSL_LIBRARIES}) 32 | 33 | install(TARGETS nixfs DESTINATION bin) 34 | install(CODE "execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink nixfs \$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/bin/mount.nixfs)") 35 | install(CODE "execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink nixfs \$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/bin/mount.fuse.nixfs)") 36 | -------------------------------------------------------------------------------- /nixfs/include/base64.h: -------------------------------------------------------------------------------- 1 | #ifndef B64_H 2 | #define B64_H 3 | 4 | int base64_decode(const char *in, size_t inlen, char *out, size_t *outlen); 5 | 6 | #endif // B64_H 7 | 8 | -------------------------------------------------------------------------------- /nixfs/include/debug.h: -------------------------------------------------------------------------------- 1 | #ifndef DEBUG_H 2 | #define DEBUG_H 3 | 4 | #include 5 | 6 | extern int debug_enabled; 7 | 8 | #define log_debug(...) do { if (debug_enabled) fprintf(stderr, __VA_ARGS__); } while(0) 9 | 10 | #endif // DEBUG_H 11 | -------------------------------------------------------------------------------- /nixfs/include/nixfs.h: -------------------------------------------------------------------------------- 1 | #ifndef NIXFS_H 2 | #define NIXFS_H 3 | 4 | int nixfs_getattr(const char *path, struct stat *stbuf); 5 | int nixfs_readlink(const char *path, char *buf, size_t size); 6 | int nixfs_readdir(const char *path, void *buf, fuse_fill_dir_t filler, 7 | off_t offset, struct fuse_file_info *fi); 8 | int nixfs_open(const char *path, struct fuse_file_info *fi); 9 | int nixfs_read(const char *path, char *buf, size_t size, off_t offset, 10 | struct fuse_file_info *fi); 11 | 12 | #define MAX_PATH_LENGTH 256 13 | 14 | typedef struct { 15 | char *path; 16 | mode_t mode; 17 | off_t size; 18 | } fs_node; 19 | 20 | 21 | #endif // NIXFS_H 22 | -------------------------------------------------------------------------------- /nixfs/include/urldec.h: -------------------------------------------------------------------------------- 1 | #ifndef URLDEC_H 2 | #define URLDEC_H 3 | 4 | int urldecode(const char *in, size_t inlen, char *out, size_t *outlen); 5 | 6 | #endif // URLDEC_H 7 | -------------------------------------------------------------------------------- /nixfs/include/version.h.in: -------------------------------------------------------------------------------- 1 | #define NIXFS_VERSION "@PROJECT_VERSION@" 2 | -------------------------------------------------------------------------------- /nixfs/src/base64.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int base64_decode(char *in, size_t inlen, char *out, size_t *outlen) { 7 | BIO *bio, *b64; 8 | BUF_MEM *buffer_ptr; 9 | 10 | for (int i = 0; i < inlen; i++) { 11 | if (in[i] == '-') 12 | in[i] = '+'; 13 | if (in[i] == '_') 14 | in[i] = '/'; 15 | } 16 | 17 | b64 = BIO_new(BIO_f_base64()); 18 | bio = BIO_new_mem_buf(in, inlen); 19 | bio = BIO_push(b64, bio); 20 | 21 | BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); // Ignore newlines 22 | 23 | *outlen = BIO_read(bio, out, inlen); 24 | out[*outlen] = '\0'; 25 | 26 | BIO_free_all(bio); 27 | 28 | return 0; 29 | } 30 | -------------------------------------------------------------------------------- /nixfs/src/main.c: -------------------------------------------------------------------------------- 1 | #define FUSE_USE_VERSION 26 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "debug.h" 9 | #include "nixfs.h" 10 | #include "version.h" 11 | 12 | int debug_enabled = 0; 13 | 14 | typedef struct { 15 | int debug_enabled; 16 | int print_version; 17 | const char *mount_point; 18 | // Add other custom flags as needed 19 | } nixfs_options; 20 | 21 | nixfs_options options; 22 | 23 | #define NIXFS_OPT(t, p) { t, offsetof(nixfs_options, p), 1 } 24 | static struct fuse_opt nixfs_opts[] = { 25 | NIXFS_OPT("--debug", debug_enabled), 26 | NIXFS_OPT("--version", print_version), 27 | // Add more custom flags here 28 | FUSE_OPT_END 29 | }; 30 | 31 | static int nixfs_opt_proc(void *data, const char *arg, int key, struct fuse_args *outargs) { 32 | nixfs_options *opts = (nixfs_options *)data; 33 | 34 | switch (key) { 35 | case FUSE_OPT_KEY_NONOPT: 36 | if (opts->mount_point == NULL) { 37 | if (strcmp(arg, "none") == 0) { 38 | // Ignore "none" for compatibility with fstab/mount syntax 39 | return 0; // Do not add this to outargs 40 | } else { 41 | opts->mount_point = arg; 42 | return 0; // Do not add this to outargs 43 | } 44 | } 45 | // Pass non-option arguments to fuse 46 | return 1; 47 | default: 48 | // Pass options not defined in nixfs_opts to fuse 49 | return 1; 50 | } 51 | } 52 | 53 | static struct fuse_operations nixfs_oper = { 54 | .getattr = nixfs_getattr, 55 | .readdir = nixfs_readdir, 56 | .open = nixfs_open, 57 | .read = nixfs_read, 58 | .readlink = nixfs_readlink, 59 | }; 60 | 61 | int main(int argc, char *argv[]) { 62 | struct fuse_args args = FUSE_ARGS_INIT(argc, argv); 63 | memset(&options, 0, sizeof(options)); 64 | 65 | // Parse options and set mount point if available 66 | fuse_opt_parse(&args, &options, nixfs_opts, nixfs_opt_proc); 67 | 68 | if (options.print_version) { 69 | printf("nixfs version %s\n", NIXFS_VERSION); 70 | return 0; 71 | } 72 | 73 | if (options.debug_enabled) { 74 | debug_enabled = 1; 75 | log_debug("Debug logging enabled\n"); 76 | } 77 | 78 | // If no mount point was specified, show usage and exit 79 | if (options.mount_point == NULL) { 80 | fprintf(stderr, "Usage: %s [none] /path/to/mount/point [--options]\n", argv[0]); 81 | return 1; 82 | } 83 | 84 | // Add the mount point back to args if it was filtered out 85 | if (strcmp(options.mount_point, "none") != 0) { 86 | // Re-add the mount point to args for FUSE 87 | fuse_opt_add_arg(&args, options.mount_point); 88 | } 89 | 90 | return fuse_main(args.argc, args.argv, &nixfs_oper, NULL); 91 | } 92 | -------------------------------------------------------------------------------- /nixfs/src/nixfs.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "base64.h" 10 | #include "debug.h" 11 | #include "nixfs.h" 12 | #include "urldec.h" 13 | 14 | // define the fixed parts of the tree 15 | fs_node fs_nodes[] = { 16 | { "/", S_IFDIR | 0755, 0 }, 17 | { "/flake", S_IFDIR | 0755, 0 }, 18 | { "/flake/b64", S_IFDIR | 0755, 0 }, 19 | { "/flake/str", S_IFDIR | 0755, 0 }, 20 | { "/flake/urlenc", S_IFDIR | 0755, 0 }, 21 | { "/expr", S_IFDIR | 0755, 0 }, 22 | { "/expr/b64", S_IFDIR | 0755, 0 }, 23 | { "/expr/str", S_IFDIR | 0755, 0 }, 24 | { "/expr/urlenc", S_IFDIR | 0755, 0 }, 25 | }; 26 | 27 | 28 | 29 | #define N_FS_NODES sizeof(fs_nodes) / sizeof(fs_nodes[0]) 30 | #define STR_PREFIX(path, match) (strncmp(path, match, strlen(match)) == 0) 31 | 32 | char** tokenize_path(const char *path) { 33 | // Copy the input path since strtok modifies the string 34 | char *path_copy = strdup(path); 35 | if (path_copy == NULL) { 36 | return NULL; // Memory allocation failed 37 | } 38 | 39 | // Count the number of tokens 40 | int token_count = 0; 41 | for (int i = 0; path_copy[i] != '\0'; i++) { 42 | if (path_copy[i] == '/') { 43 | token_count++; 44 | } 45 | } 46 | 47 | // Allocate memory for the token pointers 48 | char **tokens = malloc(sizeof(char*) * (token_count + 2)); // +1 for the last token and +1 for NULL 49 | if (tokens == NULL) { 50 | free(path_copy); 51 | return NULL; // Memory allocation failed 52 | } 53 | 54 | // Tokenize the path 55 | int index = 0; 56 | char *token = strtok(path_copy, "/"); 57 | while (token != NULL) { 58 | tokens[index++] = strdup(token); // Duplicate token 59 | token = strtok(NULL, "/"); 60 | } 61 | tokens[index] = NULL; // Null-terminate the array 62 | 63 | free(path_copy); // Free the copy as it's no longer needed 64 | return tokens; 65 | } 66 | 67 | void free_tokens(char **tokens) { 68 | if (tokens != NULL) { 69 | for (int i = 0; tokens[i] != NULL; i++) { 70 | free(tokens[i]); 71 | } 72 | free(tokens); 73 | } 74 | } 75 | 76 | int validate_path(const char *valid_paths[], int num_valid_paths, const char *token) { 77 | int is_valid_path = 0; 78 | for (int i = 0; i < num_valid_paths; i++) { 79 | if (strcmp(token, valid_paths[i]) == 0) { 80 | is_valid_path = 1; 81 | break; 82 | } 83 | } 84 | return is_valid_path; 85 | } 86 | 87 | int nixfs_getattr(const char *path, struct stat *stbuf) { 88 | log_debug("nixfs_getattr: path='%s'\n", path); 89 | 90 | memset(stbuf, 0, sizeof(struct stat)); 91 | 92 | // iterate through fs_nodes till a matching path is found 93 | for (size_t i = 0; i < N_FS_NODES; i++) { 94 | if (strcmp(path, fs_nodes[i].path) == 0) { 95 | stbuf->st_mode = fs_nodes[i].mode; 96 | stbuf->st_nlink = (fs_nodes[i].mode & S_IFDIR) ? 2 : 1; 97 | stbuf->st_size = fs_nodes[i].size; 98 | return 0; 99 | } 100 | } 101 | 102 | char **tokens = tokenize_path(path); 103 | int token_count = 0; 104 | if (tokens != NULL) { 105 | for (int i = 0; tokens[i] != NULL; i++) { 106 | log_debug("nixfs_getattr: tokens[%d] = %s\n", i, tokens[i]); 107 | token_count++; 108 | } 109 | } 110 | 111 | // Handle dynamic paths under /flake/ and /expr/ 112 | // TODO: avoid hardcoding each valid subpath 113 | if (validate_path((const char*[]){ "flake", "expr" }, 2, tokens[0])) { 114 | if (!validate_path((const char*[]){ "str", "b64", "urlenc" }, 3, tokens[1])) { 115 | log_debug("nixfs_getattr: invalid path\n"); 116 | free_tokens(tokens); 117 | return -ENOENT; 118 | } 119 | log_debug("nixfs_getattr: path passed basic validation\n"); 120 | 121 | // if the final token starts with a hash or dash, 122 | // assume it is a flag and say it is a dir 123 | if (tokens[token_count-1][0] == '#' || tokens[token_count-1][0] == '-') { 124 | stbuf->st_mode = S_IFDIR; 125 | stbuf->st_nlink = 2; 126 | stbuf->st_size = 0; 127 | free_tokens(tokens); 128 | return 0; 129 | } 130 | 131 | // if it matches none of the above, assume it is a flake path or expression 132 | stbuf->st_mode = S_IFLNK | 0777; 133 | stbuf->st_nlink = 1; 134 | stbuf->st_size = 0; 135 | free_tokens(tokens); 136 | return 0; 137 | } 138 | 139 | free_tokens(tokens); 140 | return -ENOENT; 141 | } 142 | 143 | int nixfs_readdir(const char *path, void *buf, fuse_fill_dir_t filler, 144 | off_t offset, struct fuse_file_info *fi) { 145 | log_debug("nixfs_readdir: path='%s'\n", path); 146 | 147 | fs_node *parent = NULL; 148 | 149 | for (size_t i = 0; i < N_FS_NODES; i++) { 150 | if (strcmp(path, fs_nodes[i].path) == 0) { 151 | parent = &fs_nodes[i]; 152 | break; 153 | } 154 | } 155 | 156 | if (!parent || !(parent->mode & S_IFDIR)) { 157 | log_debug("nixfs_readdir: parent not found\n"); 158 | return -ENOENT; 159 | } 160 | 161 | filler(buf, ".", NULL, 0); 162 | filler(buf, "..", NULL, 0); 163 | 164 | // find all child dirs of parent 165 | for (size_t i = 0; i < N_FS_NODES; i++) { 166 | // match the start of the path with parent 167 | if (fs_nodes[i].path != parent->path && STR_PREFIX(fs_nodes[i].path, parent->path)) { 168 | const char *child_name = fs_nodes[i].path + strlen(parent->path); 169 | // pass only up to the next / to filler() 170 | if (child_name[0] != '\0' && strchr(child_name + 1, '/') == NULL) { 171 | filler(buf, child_name + (child_name[0] == '/' ? 1 : 0), NULL, 0); 172 | } 173 | } 174 | } 175 | 176 | return 0; 177 | } 178 | 179 | int nixfs_open(const char *path, struct fuse_file_info *fi) { 180 | log_debug("nixfs_open: path='%s'\n", path); 181 | 182 | // only allow read-only open() 183 | if (strcmp(path, "/flake/b64") == 0 || strcmp(path, "/flake/str") == 0) { 184 | if ((fi->flags & O_ACCMODE) != O_RDONLY) { 185 | return -EACCES; 186 | } 187 | } else { 188 | return -ENOENT; 189 | } 190 | 191 | return 0; 192 | } 193 | 194 | // minimal implementation of read() 195 | int nixfs_read(const char *path, char *buf, size_t size, off_t offset, 196 | struct fuse_file_info *fi) { 197 | return -ENOENT; 198 | } 199 | 200 | void exec_nix_command(char **tokens, const char *spec, int is_expr) { 201 | // Count the number of tokens 202 | int token_count; 203 | for (token_count = 0; tokens[token_count] != NULL; token_count++); 204 | 205 | // Prepare the arguments array 206 | const int FIXED_ARGS = is_expr ? 9 : 8; // Number of fixed arguments before tokens 207 | char **args = malloc((FIXED_ARGS + token_count + 2) * sizeof(char*)); 208 | 209 | if (args == NULL) { 210 | perror("malloc"); 211 | _exit(1); 212 | } 213 | 214 | // Add fixed arguments 215 | args[0] = "nix"; 216 | args[1] = "--extra-experimental-features"; 217 | args[2] = "nix-command"; 218 | args[3] = "--extra-experimental-features"; 219 | args[4] = "flakes"; 220 | args[5] = "build"; 221 | args[6] = "--no-link"; 222 | args[7] = "--print-out-paths"; 223 | 224 | if (is_expr) { 225 | args[8] = "--impure"; 226 | } 227 | 228 | // Add tokens 229 | for (int i = 0; i < token_count; i++) { 230 | if (tokens[i][0] == '#') tokens[i]++; 231 | log_debug("exec_nix_command: tokens[%d] = %s\n", i, tokens[i]); 232 | args[FIXED_ARGS + i] = tokens[i]; 233 | } 234 | 235 | // Add spec and NULL 236 | args[FIXED_ARGS + token_count] = is_expr ? "--expr" : (char*)spec; 237 | args[FIXED_ARGS + token_count + 1] = is_expr ? (char*)spec : NULL; 238 | args[FIXED_ARGS + token_count + 2] = NULL; 239 | 240 | for(int i = 0; args[i] != NULL; i++) { 241 | log_debug("exec_nix_command: args[%d] = %s\n", i, args[i]); 242 | } 243 | 244 | // Execute the command 245 | execvp("nix", args); 246 | perror("exec"); 247 | _exit(1); // If execvp fails, exit the child process 248 | } 249 | 250 | int nixfs_readlink(const char *path, char *buf, size_t size) { 251 | log_debug("nixfs_readlink: path='%s'\n", path); 252 | 253 | char **tokens = tokenize_path(path); 254 | int token_count = 0; 255 | if (tokens != NULL) { 256 | for (int i = 0; tokens[i] != NULL; i++) { 257 | log_debug("nixfs_readlink: tokens[%d] = %s\n", i, tokens[i]); 258 | token_count++; 259 | } 260 | } 261 | 262 | if (strcmp(tokens[0], "flake") != 0 && strcmp(tokens[0], "expr") != 0) { 263 | log_debug("nixfs_readlink: invalid path\n"); 264 | free_tokens(tokens); 265 | return -ENOENT; 266 | } 267 | int is_expr = (strcmp(tokens[0], "expr") == 0); 268 | 269 | if (!validate_path((const char*[]){ "str", "b64", "urlenc" }, 3, tokens[1])) { 270 | log_debug("nixfs_readlink: invalid path\n"); 271 | free_tokens(tokens); 272 | return -ENOENT; 273 | } 274 | log_debug("nixfs_readlink: path passed basic validation\n"); 275 | 276 | const char *encoded_spec; 277 | const char *spec; 278 | size_t decoded_len; 279 | char *decoded_spec; 280 | 281 | if (strcmp(tokens[1], "str") == 0) { 282 | spec = strdup(tokens[token_count-1]); 283 | log_debug("nixfs_readlink: type str\n"); 284 | } else { 285 | int (*decode_func)(const char*, size_t, char*, size_t*); 286 | if (strcmp(tokens[1], "b64") == 0) { 287 | log_debug("nixfs_readlink: type b64\n"); 288 | decode_func = base64_decode; 289 | } else if (strcmp(tokens[1], "urlenc") == 0) { 290 | log_debug("nixfs_readlink: type urlenc\n"); 291 | decode_func = urldecode; 292 | } else { 293 | free_tokens(tokens); 294 | return -ENOENT; 295 | } 296 | encoded_spec = tokens[token_count-1]; 297 | log_debug("nixfs_readlink: encoded spec = %s\n", encoded_spec); 298 | char *decoded_spec = malloc(strlen(encoded_spec) + 1); 299 | if (decoded_spec == NULL) { 300 | free_tokens(tokens); 301 | return -ENOENT; 302 | }; 303 | if (decode_func(encoded_spec, strlen(encoded_spec), decoded_spec, &decoded_len) == -1) { 304 | free(decoded_spec); 305 | free_tokens(tokens); 306 | return -ENOENT; 307 | } 308 | decoded_spec[decoded_len] = '\0'; 309 | spec = strdup(decoded_spec); 310 | free(decoded_spec); 311 | } 312 | log_debug("nixfs_readlink: spec = %s\n", spec); 313 | 314 | int pipe_fd[2]; 315 | 316 | if (pipe(pipe_fd) == -1) { 317 | perror("pipe"); 318 | free_tokens(tokens); 319 | free((void *)spec); 320 | return -EIO; 321 | } 322 | 323 | pid_t pid = fork(); 324 | if (pid == -1) { 325 | perror("fork"); 326 | free_tokens(tokens); 327 | free((void *)spec); 328 | return -EIO; 329 | } 330 | 331 | if (pid == 0) { // Child process 332 | dup2(pipe_fd[1], STDOUT_FILENO); 333 | close(pipe_fd[0]); 334 | close(pipe_fd[1]); 335 | 336 | tokens[token_count-1] = NULL; 337 | exec_nix_command(tokens+2, spec, is_expr); 338 | } else { // Parent process 339 | close(pipe_fd[1]); 340 | 341 | ssize_t nread; 342 | ssize_t total_read = 0; 343 | while (total_read < size - 1) { 344 | nread = read(pipe_fd[0], buf + total_read, size - 1 - total_read); 345 | if (nread == -1) { 346 | if (errno == EAGAIN || errno == EWOULDBLOCK) { 347 | continue; 348 | } 349 | perror("read"); 350 | free_tokens(tokens); 351 | log_debug("nixfs_readlink: failed to read from child process\n"); 352 | free((void *)spec); 353 | return -EIO; 354 | } else if (nread == 0) { 355 | break; 356 | } 357 | total_read += nread; 358 | log_debug("nixfs_readlink: partial read buf = %s\n", buf); 359 | } 360 | 361 | buf[total_read] = '\0'; 362 | buf[strcspn(buf, "\n")] = '\0'; // Remove newline character 363 | 364 | close(pipe_fd[0]); 365 | 366 | int wstatus; 367 | waitpid(pid, &wstatus, 0); 368 | if (WEXITSTATUS(wstatus) != 0) { 369 | log_debug("nix command exited with status %d\n", WEXITSTATUS(wstatus)); 370 | free_tokens(tokens); 371 | free((void *)spec); 372 | return -ENOENT; 373 | } 374 | 375 | free_tokens(tokens); 376 | free((void *)spec); 377 | return 0; 378 | } 379 | 380 | free_tokens(tokens); 381 | free((void *)spec); 382 | return -ENOENT; 383 | } 384 | -------------------------------------------------------------------------------- /nixfs/src/urldec.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "urldec.h" 5 | 6 | int hex_to_decimal(int c) { 7 | if ('0' <= c && c <= '9') { 8 | return c - '0'; 9 | } else if ('a' <= c && c <= 'f') { 10 | return c - 'a' + 10; 11 | } else if ('A' <= c && c <= 'F') { 12 | return c - 'A' + 10; 13 | } 14 | return -1; 15 | } 16 | 17 | int urldecode(const char* in, size_t inlen, char* out, size_t* outlen) { 18 | size_t i = 0; 19 | size_t j = 0; 20 | 21 | while (i < inlen) { 22 | if (in[i] == '%') { 23 | if (i + 2 < inlen) { 24 | int high = hex_to_decimal(in[i+1]); 25 | int low = hex_to_decimal(in[i+2]); 26 | if (high == -1 || low == -1) { 27 | return -1; // invalid URL-encoded string 28 | } 29 | out[j++] = (char)((high << 4) | low); 30 | i += 3; 31 | } else { 32 | return -1; // invalid URL-encoded string 33 | } 34 | } else if (in[i] == '+') { 35 | out[j++] = ' '; 36 | i++; 37 | } else { 38 | out[j++] = in[i++]; 39 | } 40 | } 41 | 42 | out[j] = '\0'; // null-terminate the output string 43 | *outlen = j; // set output length 44 | 45 | return 0; 46 | } 47 | -------------------------------------------------------------------------------- /utils/update-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | execute() { 4 | if [ "$dry_run" == "1" ]; then 5 | echo "Dry run: $*" 6 | else 7 | "$@" 8 | fi 9 | } 10 | 11 | dry_run=0 12 | [ "$1" == "--dry-run" ] && dry_run=1 13 | 14 | read -p "Enter new version number: " new_version 15 | 16 | execute sed -i "s/project(nixfs VERSION [0-9.]* LANGUAGES C)/project(nixfs VERSION $new_version LANGUAGES C)/" nixfs/CMakeLists.txt 17 | execute sed -i "s/version = \"[0-9.]*\";/version = \"$new_version\";/" flake.nix 18 | 19 | git diff 20 | 21 | read -p "Confirm changes and continue? (y/n): " confirm 22 | [ "$confirm" != "y" ] && echo "Aborting." && exit 1 23 | 24 | execute git add nixfs/CMakeLists.txt flake.nix 25 | 26 | git diff --cached 27 | 28 | read -p "Confirm commit and continue? (y/n): " confirm_commit 29 | [ "$confirm_commit" != "y" ] && echo "Aborting." && git reset && exit 1 30 | 31 | execute git commit -m "Bump version to $new_version" 32 | execute git tag "v$new_version" 33 | 34 | read -p "Push changes and tag to remote? (y/n): " confirm_push 35 | [ "$confirm_push" != "y" ] && echo "Aborting." && exit 1 36 | 37 | execute git push origin master 38 | execute git push origin "v$new_version" 39 | 40 | echo "Done!" 41 | --------------------------------------------------------------------------------