Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8711fa13a2 | |||
| 25b58aad4d | |||
| c71eea8e68 | |||
| afbdf92fa6 | |||
| daac34f7c8 | |||
| 88692b79eb | |||
| 035dcde9ba | |||
| cc14ae1c79 | |||
| 78f807b3dd | |||
| fd885f023c | |||
| 033a1d48f5 | |||
| 82285f9c28 | |||
| 43b2708186 | |||
| d749988c80 | |||
| 7921230f83 | |||
| ea60e3466e | |||
| 033a88349c | |||
| 1cd6995713 | |||
| 9e35c39ba8 | |||
| bf540dd68b | |||
| 882d3a0185 | |||
| 113882adda | |||
| ccb6821019 | |||
| ed0825440c | |||
| a9eb71cb1c | |||
| b1715a5a62 | |||
| 21fdcad6fb | |||
| ef94977c5a | |||
| d78a624756 | |||
| ddf1e42ce7 | |||
| 1a52814a4e | |||
| c9bf8ad451 | |||
| 5f767dec5b | |||
| 8bb9d33d16 | |||
| eadf7f104a |
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
@@ -0,0 +1,149 @@
|
||||
name: platform builds
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
linux-compat:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: ubuntu-latest
|
||||
runner: ubuntu-latest
|
||||
run_tests: true
|
||||
- name: ubuntu-22.04
|
||||
runner: ubuntu-22.04
|
||||
run_tests: true
|
||||
- name: ubuntu-24.04-arm
|
||||
runner: ubuntu-24.04-arm
|
||||
run_tests: true
|
||||
- name: ubuntu-24.04-release
|
||||
runner: ubuntu-24.04
|
||||
buildtype: release
|
||||
- name: ubuntu-24.04-debug
|
||||
runner: ubuntu-24.04
|
||||
buildtype: debug
|
||||
- name: ubuntu-24.04-hardened
|
||||
runner: ubuntu-24.04
|
||||
extra_cflags: "-D_FORTIFY_SOURCE=3 -fstack-protector-strong"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc ninja-build pkg-config libglib2.0-dev libfuse3-dev ${{ matrix.run_tests && 'openssh-server openssh-client fuse3' || '' }}
|
||||
pip3 install meson ${{ matrix.run_tests && 'pytest pytest-timeout' || '' }}
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
CFLAGS: ${{ matrix.extra_cflags || '' }}
|
||||
run: |
|
||||
meson setup build ${{ matrix.buildtype && format('--buildtype={0}', matrix.buildtype) || '' }}
|
||||
ninja -C build
|
||||
|
||||
- name: Setup SSH
|
||||
if: matrix.run_tests
|
||||
run: |
|
||||
chmod go-w ~
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -q -N ""
|
||||
cat ~/.ssh/id_ed25519.pub > ~/.ssh/authorized_keys
|
||||
chmod 600 ~/.ssh/authorized_keys
|
||||
sudo sed -i 's/^#*PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config
|
||||
sudo systemctl restart ssh || sudo service ssh restart
|
||||
ssh -o StrictHostKeyChecking=no -o BatchMode=yes localhost true
|
||||
|
||||
- name: Check FUSE availability
|
||||
if: matrix.run_tests
|
||||
run: |
|
||||
test -e /dev/fuse
|
||||
command -v fusermount3
|
||||
|
||||
- name: Run tests
|
||||
if: matrix.run_tests
|
||||
timeout-minutes: 20
|
||||
run: |
|
||||
cd build
|
||||
python3 -m pytest test/ --timeout=300 --maxfail=99 --junitxml=test-results.xml
|
||||
|
||||
- name: Upload test results
|
||||
if: matrix.run_tests && always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: test-results-${{ matrix.name }}
|
||||
path: |
|
||||
build/test-results.xml
|
||||
build/meson-logs/
|
||||
|
||||
- name: Upload build logs
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: failure()
|
||||
with:
|
||||
name: build-logs-${{ matrix.name }}
|
||||
path: build/meson-logs/
|
||||
|
||||
alpine-musl:
|
||||
name: alpine-musl
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Build in Alpine container
|
||||
run: |
|
||||
docker run --rm -v "$PWD:/work" -w /work alpine:3.21@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d sh -euxc '
|
||||
apk add --no-cache gcc musl-dev meson ninja pkgconf glib-dev fuse3-dev
|
||||
meson setup build-alpine
|
||||
ninja -C build-alpine
|
||||
'
|
||||
|
||||
- name: Upload build logs
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: failure()
|
||||
with:
|
||||
name: build-logs-alpine-musl
|
||||
path: build-alpine/meson-logs/
|
||||
|
||||
freebsd:
|
||||
name: freebsd-14
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Build on FreeBSD
|
||||
uses: vmactions/freebsd-vm@d1e65811565151536c0c894fff74f06351ed26e6 # v1.4.5
|
||||
with:
|
||||
usesh: true
|
||||
prepare: |
|
||||
pkg install -y fusefs-libs3 glib meson ninja pkgconf
|
||||
run: |
|
||||
meson setup build-freebsd
|
||||
ninja -C build-freebsd
|
||||
|
||||
- name: Upload build logs
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: failure()
|
||||
with:
|
||||
name: build-logs-freebsd
|
||||
path: build-freebsd/meson-logs/
|
||||
@@ -7,45 +7,137 @@ on:
|
||||
workflow_dispatch: # this is a nice option that will enable a button w/ inputs
|
||||
inputs:
|
||||
git-ref:
|
||||
description: Git Ref (Optional)
|
||||
description: Git Ref (Optional)
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
name: Build and test
|
||||
runs-on: ubuntu-latest
|
||||
name: ${{ matrix.compiler }} / ${{ matrix.buildtype }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
compiler: [gcc, clang]
|
||||
buildtype: [debugoptimized, release]
|
||||
include:
|
||||
- compiler: gcc
|
||||
cc: gcc
|
||||
- compiler: clang
|
||||
cc: clang
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install valgrind gcc ninja-build meson libglib2.0-dev libfuse3-dev
|
||||
sudo apt-get install -y gcc clang ninja-build libglib2.0-dev libfuse3-dev openssh-server openssh-client fuse3
|
||||
|
||||
- name: Install meson
|
||||
run: pip3 install meson pytest
|
||||
run: pip3 install meson pytest pytest-timeout
|
||||
|
||||
- name: build
|
||||
- name: Print tool versions
|
||||
run: |
|
||||
mkdir build; cd build
|
||||
meson ..
|
||||
ninja
|
||||
${{ matrix.cc }} --version
|
||||
meson --version
|
||||
|
||||
# cd does not persist across steps
|
||||
- name: upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Setup SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -q -N ""
|
||||
cat ~/.ssh/id_ed25519.pub > ~/.ssh/authorized_keys
|
||||
chmod 600 ~/.ssh/authorized_keys
|
||||
sudo systemctl start ssh || sudo service ssh start
|
||||
ssh -o StrictHostKeyChecking=no -o BatchMode=yes localhost true
|
||||
|
||||
- name: Check FUSE availability
|
||||
run: |
|
||||
test -e /dev/fuse
|
||||
command -v fusermount3
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
CC: ${{ matrix.cc }}
|
||||
run: |
|
||||
meson setup build --buildtype=${{ matrix.buildtype }} -Dwerror=true
|
||||
ninja -C build
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: sshfs
|
||||
name: sshfs-${{ matrix.compiler }}-${{ matrix.buildtype }}
|
||||
path: build/sshfs
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: make ssh into localhost without prompt possible for tests
|
||||
run: |
|
||||
ssh-keygen -b 2048 -t rsa -f ~/.ssh/id_rsa -q -N ""
|
||||
cat ~/.ssh/id_rsa.pub > ~/.ssh/authorized_keys
|
||||
|
||||
- name: run tests
|
||||
- name: Run tests
|
||||
timeout-minutes: 20
|
||||
run: |
|
||||
cd build
|
||||
python3 -m pytest test/
|
||||
python3 -m pytest --maxfail=99 --timeout=300 --junitxml=test-results.xml test/
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: always()
|
||||
with:
|
||||
name: test-results-${{ matrix.compiler }}-${{ matrix.buildtype }}
|
||||
path: |
|
||||
build/test-results.xml
|
||||
build/meson-logs/
|
||||
|
||||
strict-warnings:
|
||||
name: ${{ matrix.compiler }} / strict warnings
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- compiler: gcc
|
||||
cc: gcc
|
||||
extra_cflags: "-Wformat=2 -Wformat-security -Wundef -Wstrict-prototypes -Wold-style-definition -Wmissing-prototypes -Wnull-dereference"
|
||||
- compiler: clang
|
||||
cc: clang
|
||||
extra_cflags: "-Wformat=2 -Wformat-security -Wundef -Wstrict-prototypes -Wold-style-definition -Wmissing-prototypes -Wnull-dereference"
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc clang ninja-build libglib2.0-dev libfuse3-dev
|
||||
|
||||
- name: Install meson
|
||||
run: pip3 install meson
|
||||
|
||||
- name: Print tool versions
|
||||
run: |
|
||||
${{ matrix.cc }} --version
|
||||
meson --version
|
||||
|
||||
- name: Build with strict warnings
|
||||
env:
|
||||
CC: ${{ matrix.cc }}
|
||||
CFLAGS: ${{ matrix.extra_cflags }}
|
||||
run: |
|
||||
meson setup build -Dwerror=true
|
||||
ninja -C build
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
Release 3.7.5 (2025-11-11)
|
||||
--------------------------
|
||||
|
||||
* New maintainers added
|
||||
* Main documentation updates to using Markdown
|
||||
* Updated Windows and macOS support
|
||||
* Added IPv6 support in -o directport
|
||||
* Added -o vsock option
|
||||
* Minor bugfixes
|
||||
|
||||
Release 3.7.3 (2022-05-26)
|
||||
--------------------------
|
||||
|
||||
|
||||
@@ -55,6 +55,39 @@ On BSD and macOS, to unmount the filesystem:
|
||||
umount mountpoint
|
||||
```
|
||||
|
||||
### Bypassing SSH
|
||||
|
||||
#### Using directport
|
||||
|
||||
Using direct connections to sftp-server to bypass SSH for performance is also possible. To do this, start a network service using sftp-server (part of OpenSSH) on a server, then connect directly using `-o directport=PORT` option.
|
||||
|
||||
On server (listen on port 1234 using socat):
|
||||
|
||||
`socat tcp-listen:1234,reuseaddr,fork exec:/usr/lib/openssh/sftp-server`
|
||||
|
||||
On client:
|
||||
|
||||
`sshfs -o directport=1234 127.0.0.1:/tmp /tmp/mnt`
|
||||
|
||||
Note that this is insecure as connection will happen without encryption. Only use this on localhost or trusted networks. This option is sometimes used by other projects to mount folders inside VMs.
|
||||
|
||||
IPv6 is also possible:
|
||||
|
||||
`socat tcp6-listen:1234,reuseaddr,fork exec:/usr/lib/openssh/sftp-server`
|
||||
|
||||
`sshfs -o directport=1234 [::1]:/tmp /tmp/mnt`
|
||||
|
||||
#### Using vsock
|
||||
|
||||
Similarly to above, Linux [vsock](https://man7.org/linux/man-pages/man7/vsock.7.html) can be used to connect directly to sockets within VMs using `-o vsock=CID:PORT`.
|
||||
|
||||
```
|
||||
# on the host
|
||||
socat VSOCK-LISTEN:12345 EXEC:"/usr/lib/openssh/sftp-server",nofork
|
||||
# on the clientside
|
||||
sshfs -o vsock=2:12345 unused_host: ./tmp
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <errno.h>
|
||||
#include <glib.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define DEFAULT_CACHE_TIMEOUT_SECS 20
|
||||
#define DEFAULT_MAX_CACHE_SIZE 10000
|
||||
@@ -40,7 +41,7 @@ static struct cache cache;
|
||||
struct node {
|
||||
struct stat stat;
|
||||
time_t stat_valid;
|
||||
char **dir;
|
||||
GPtrArray *dir;
|
||||
time_t dir_valid;
|
||||
char *link;
|
||||
time_t link_valid;
|
||||
@@ -63,13 +64,28 @@ struct file_handle {
|
||||
unsigned long fs_fh;
|
||||
};
|
||||
|
||||
struct cache_dirent {
|
||||
char *name;
|
||||
struct stat stat;
|
||||
};
|
||||
|
||||
static void free_node(gpointer node_)
|
||||
{
|
||||
struct node *node = (struct node *) node_;
|
||||
g_strfreev(node->dir);
|
||||
if (node->dir != NULL) {
|
||||
g_ptr_array_free(node->dir, TRUE);
|
||||
}
|
||||
g_free(node);
|
||||
}
|
||||
|
||||
static void free_cache_dirent(gpointer data) {
|
||||
struct cache_dirent *cache_dirent = (struct cache_dirent *) data;
|
||||
if (cache_dirent != NULL) {
|
||||
g_free(cache_dirent->name);
|
||||
g_free(cache_dirent);
|
||||
}
|
||||
}
|
||||
|
||||
static int cache_clean_entry(void *key_, struct node *node, time_t *now)
|
||||
{
|
||||
(void) key_;
|
||||
@@ -186,13 +202,15 @@ void cache_add_attr(const char *path, const struct stat *stbuf, uint64_t wrctr)
|
||||
pthread_mutex_unlock(&cache.lock);
|
||||
}
|
||||
|
||||
static void cache_add_dir(const char *path, char **dir)
|
||||
static void cache_add_dir(const char *path, GPtrArray *dir)
|
||||
{
|
||||
struct node *node;
|
||||
|
||||
pthread_mutex_lock(&cache.lock);
|
||||
node = cache_get(path);
|
||||
g_strfreev(node->dir);
|
||||
if (node->dir != NULL) {
|
||||
g_ptr_array_free(node->dir, TRUE);
|
||||
}
|
||||
node->dir = dir;
|
||||
node->dir_valid = time(NULL) + cache.dir_timeout_secs;
|
||||
if (node->dir_valid > node->valid)
|
||||
@@ -341,7 +359,10 @@ static int cache_dirfill (void *buf, const char *name,
|
||||
ch = (struct readdir_handle*) buf;
|
||||
err = ch->filler(ch->buf, name, stbuf, off, flags);
|
||||
if (!err) {
|
||||
g_ptr_array_add(ch->dir, g_strdup(name));
|
||||
struct cache_dirent *cdent = g_malloc(sizeof(struct cache_dirent));
|
||||
cdent->name = g_strdup(name);
|
||||
cdent->stat = *stbuf;
|
||||
g_ptr_array_add(ch->dir, cdent);
|
||||
if (stbuf->st_mode & S_IFMT) {
|
||||
char *fullpath;
|
||||
const char *basepath = !ch->path[1] ? "" : ch->path;
|
||||
@@ -361,8 +382,9 @@ static int cache_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
|
||||
struct readdir_handle ch;
|
||||
struct file_handle *cfi;
|
||||
int err;
|
||||
char **dir;
|
||||
GPtrArray *dir;
|
||||
struct node *node;
|
||||
struct cache_dirent **cdent;
|
||||
|
||||
assert(offset == 0);
|
||||
|
||||
@@ -371,9 +393,9 @@ static int cache_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
|
||||
if (node != NULL && node->dir != NULL) {
|
||||
time_t now = time(NULL);
|
||||
if (node->dir_valid - now >= 0) {
|
||||
for(dir = node->dir; *dir != NULL; dir++)
|
||||
// FIXME: What about st_mode?
|
||||
filler(buf, *dir, NULL, 0, 0);
|
||||
for(cdent = (struct cache_dirent**)node->dir->pdata; *cdent != NULL; cdent++) {
|
||||
filler(buf, (*cdent)->name, &(*cdent)->stat, 0, 0);
|
||||
}
|
||||
pthread_mutex_unlock(&cache.lock);
|
||||
return 0;
|
||||
}
|
||||
@@ -397,16 +419,16 @@ static int cache_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
|
||||
ch.buf = buf;
|
||||
ch.filler = filler;
|
||||
ch.dir = g_ptr_array_new();
|
||||
g_ptr_array_set_free_func(ch.dir, free_cache_dirent);
|
||||
ch.wrctr = cache_get_write_ctr();
|
||||
err = cache.next_oper->readdir(path, &ch, cache_dirfill, offset, fi, flags);
|
||||
g_ptr_array_add(ch.dir, NULL);
|
||||
dir = (char **) ch.dir->pdata;
|
||||
dir = ch.dir;
|
||||
if (!err) {
|
||||
cache_add_dir(path, dir);
|
||||
} else {
|
||||
g_strfreev(dir);
|
||||
g_ptr_array_free(dir, TRUE);
|
||||
}
|
||||
g_ptr_array_free(ch.dir, FALSE);
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -1,364 +0,0 @@
|
||||
/*
|
||||
FUSE: Filesystem in Userspace
|
||||
Copyright (C) 2001-2006 Miklos Szeredi <miklos@szeredi.hu>
|
||||
|
||||
This program can be distributed under the terms of the GNU LGPL.
|
||||
See the file COPYING.LIB
|
||||
*/
|
||||
|
||||
#include "fuse_opt.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
struct fuse_opt_context {
|
||||
void *data;
|
||||
const struct fuse_opt *opt;
|
||||
fuse_opt_proc_t proc;
|
||||
int argctr;
|
||||
int argc;
|
||||
char **argv;
|
||||
struct fuse_args outargs;
|
||||
char *opts;
|
||||
int nonopt;
|
||||
};
|
||||
|
||||
void fuse_opt_free_args(struct fuse_args *args)
|
||||
{
|
||||
if (args && args->argv && args->allocated) {
|
||||
int i;
|
||||
for (i = 0; i < args->argc; i++)
|
||||
free(args->argv[i]);
|
||||
free(args->argv);
|
||||
args->argv = NULL;
|
||||
args->allocated = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int alloc_failed(void)
|
||||
{
|
||||
fprintf(stderr, "fuse: memory allocation failed\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int fuse_opt_add_arg(struct fuse_args *args, const char *arg)
|
||||
{
|
||||
char **newargv;
|
||||
char *newarg;
|
||||
|
||||
assert(!args->argv || args->allocated);
|
||||
|
||||
newargv = realloc(args->argv, (args->argc + 2) * sizeof(char *));
|
||||
newarg = newargv ? strdup(arg) : NULL;
|
||||
if (!newargv || !newarg)
|
||||
return alloc_failed();
|
||||
|
||||
args->argv = newargv;
|
||||
args->allocated = 1;
|
||||
args->argv[args->argc++] = newarg;
|
||||
args->argv[args->argc] = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fuse_opt_insert_arg(struct fuse_args *args, int pos, const char *arg)
|
||||
{
|
||||
assert(pos <= args->argc);
|
||||
if (fuse_opt_add_arg(args, arg) == -1)
|
||||
return -1;
|
||||
|
||||
if (pos != args->argc - 1) {
|
||||
char *newarg = args->argv[args->argc - 1];
|
||||
memmove(&args->argv[pos + 1], &args->argv[pos],
|
||||
sizeof(char *) * (args->argc - pos - 1));
|
||||
args->argv[pos] = newarg;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int next_arg(struct fuse_opt_context *ctx, const char *opt)
|
||||
{
|
||||
if (ctx->argctr + 1 >= ctx->argc) {
|
||||
fprintf(stderr, "fuse: missing argument after `%s'\n", opt);
|
||||
return -1;
|
||||
}
|
||||
ctx->argctr++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int add_arg(struct fuse_opt_context *ctx, const char *arg)
|
||||
{
|
||||
return fuse_opt_add_arg(&ctx->outargs, arg);
|
||||
}
|
||||
|
||||
int fuse_opt_add_opt(char **opts, const char *opt)
|
||||
{
|
||||
char *newopts;
|
||||
if (!*opts)
|
||||
newopts = strdup(opt);
|
||||
else {
|
||||
unsigned oldlen = strlen(*opts);
|
||||
newopts = realloc(*opts, oldlen + 1 + strlen(opt) + 1);
|
||||
if (newopts) {
|
||||
newopts[oldlen] = ',';
|
||||
strcpy(newopts + oldlen + 1, opt);
|
||||
}
|
||||
}
|
||||
if (!newopts)
|
||||
return alloc_failed();
|
||||
|
||||
*opts = newopts;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int add_opt(struct fuse_opt_context *ctx, const char *opt)
|
||||
{
|
||||
return fuse_opt_add_opt(&ctx->opts, opt);
|
||||
}
|
||||
|
||||
static int call_proc(struct fuse_opt_context *ctx, const char *arg, int key,
|
||||
int iso)
|
||||
{
|
||||
if (key == FUSE_OPT_KEY_DISCARD)
|
||||
return 0;
|
||||
|
||||
if (key != FUSE_OPT_KEY_KEEP && ctx->proc) {
|
||||
int res = ctx->proc(ctx->data, arg, key, &ctx->outargs);
|
||||
if (res == -1 || !res)
|
||||
return res;
|
||||
}
|
||||
if (iso)
|
||||
return add_opt(ctx, arg);
|
||||
else
|
||||
return add_arg(ctx, arg);
|
||||
}
|
||||
|
||||
static int match_template(const char *t, const char *arg, unsigned *sepp)
|
||||
{
|
||||
int arglen = strlen(arg);
|
||||
const char *sep = strchr(t, '=');
|
||||
sep = sep ? sep : strchr(t, ' ');
|
||||
if (sep && (!sep[1] || sep[1] == '%')) {
|
||||
int tlen = sep - t;
|
||||
if (sep[0] == '=')
|
||||
tlen ++;
|
||||
if (arglen >= tlen && strncmp(arg, t, tlen) == 0) {
|
||||
*sepp = sep - t;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (strcmp(t, arg) == 0) {
|
||||
*sepp = 0;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct fuse_opt *find_opt(const struct fuse_opt *opt,
|
||||
const char *arg, unsigned *sepp)
|
||||
{
|
||||
for (; opt && opt->templ; opt++)
|
||||
if (match_template(opt->templ, arg, sepp))
|
||||
return opt;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int fuse_opt_match(const struct fuse_opt *opts, const char *opt)
|
||||
{
|
||||
unsigned dummy;
|
||||
return find_opt(opts, opt, &dummy) ? 1 : 0;
|
||||
}
|
||||
|
||||
static int process_opt_param(void *var, const char *format, const char *param,
|
||||
const char *arg)
|
||||
{
|
||||
assert(format[0] == '%');
|
||||
if (format[1] == 's') {
|
||||
char *copy = strdup(param);
|
||||
if (!copy)
|
||||
return alloc_failed();
|
||||
|
||||
*(char **) var = copy;
|
||||
} else {
|
||||
if (sscanf(param, format, var) != 1) {
|
||||
fprintf(stderr, "fuse: invalid parameter in option `%s'\n", arg);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int process_opt(struct fuse_opt_context *ctx,
|
||||
const struct fuse_opt *opt, unsigned sep,
|
||||
const char *arg, int iso)
|
||||
{
|
||||
if (opt->offset == -1U) {
|
||||
if (call_proc(ctx, arg, opt->value, iso) == -1)
|
||||
return -1;
|
||||
} else {
|
||||
void *var = ctx->data + opt->offset;
|
||||
if (sep && opt->templ[sep + 1]) {
|
||||
const char *param = arg + sep;
|
||||
if (opt->templ[sep] == '=')
|
||||
param ++;
|
||||
if (process_opt_param(var, opt->templ + sep + 1,
|
||||
param, arg) == -1)
|
||||
return -1;
|
||||
} else
|
||||
*(int *)var = opt->value;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int process_opt_sep_arg(struct fuse_opt_context *ctx,
|
||||
const struct fuse_opt *opt, unsigned sep,
|
||||
const char *arg, int iso)
|
||||
{
|
||||
int res;
|
||||
char *newarg;
|
||||
char *param;
|
||||
|
||||
if (next_arg(ctx, arg) == -1)
|
||||
return -1;
|
||||
|
||||
param = ctx->argv[ctx->argctr];
|
||||
newarg = malloc(sep + strlen(param) + 1);
|
||||
if (!newarg)
|
||||
return alloc_failed();
|
||||
|
||||
memcpy(newarg, arg, sep);
|
||||
strcpy(newarg + sep, param);
|
||||
res = process_opt(ctx, opt, sep, newarg, iso);
|
||||
free(newarg);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static int process_gopt(struct fuse_opt_context *ctx, const char *arg, int iso)
|
||||
{
|
||||
unsigned sep;
|
||||
const struct fuse_opt *opt = find_opt(ctx->opt, arg, &sep);
|
||||
if (opt) {
|
||||
for (; opt; opt = find_opt(opt + 1, arg, &sep)) {
|
||||
int res;
|
||||
if (sep && opt->templ[sep] == ' ' && !arg[sep])
|
||||
res = process_opt_sep_arg(ctx, opt, sep, arg, iso);
|
||||
else
|
||||
res = process_opt(ctx, opt, sep, arg, iso);
|
||||
if (res == -1)
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
} else
|
||||
return call_proc(ctx, arg, FUSE_OPT_KEY_OPT, iso);
|
||||
}
|
||||
|
||||
static int process_real_option_group(struct fuse_opt_context *ctx, char *opts)
|
||||
{
|
||||
char *sep;
|
||||
|
||||
do {
|
||||
int res;
|
||||
sep = strchr(opts, ',');
|
||||
if (sep)
|
||||
*sep = '\0';
|
||||
res = process_gopt(ctx, opts, 1);
|
||||
if (res == -1)
|
||||
return -1;
|
||||
opts = sep + 1;
|
||||
} while (sep);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int process_option_group(struct fuse_opt_context *ctx, const char *opts)
|
||||
{
|
||||
int res;
|
||||
char *copy;
|
||||
const char *sep = strchr(opts, ',');
|
||||
if (!sep)
|
||||
return process_gopt(ctx, opts, 1);
|
||||
|
||||
copy = strdup(opts);
|
||||
if (!copy) {
|
||||
fprintf(stderr, "fuse: memory allocation failed\n");
|
||||
return -1;
|
||||
}
|
||||
res = process_real_option_group(ctx, copy);
|
||||
free(copy);
|
||||
return res;
|
||||
}
|
||||
|
||||
static int process_one(struct fuse_opt_context *ctx, const char *arg)
|
||||
{
|
||||
if (ctx->nonopt || arg[0] != '-')
|
||||
return call_proc(ctx, arg, FUSE_OPT_KEY_NONOPT, 0);
|
||||
else if (arg[1] == 'o') {
|
||||
if (arg[2])
|
||||
return process_option_group(ctx, arg + 2);
|
||||
else {
|
||||
if (next_arg(ctx, arg) == -1)
|
||||
return -1;
|
||||
|
||||
return process_option_group(ctx, ctx->argv[ctx->argctr]);
|
||||
}
|
||||
} else if (arg[1] == '-' && !arg[2]) {
|
||||
if (add_arg(ctx, arg) == -1)
|
||||
return -1;
|
||||
ctx->nonopt = ctx->outargs.argc;
|
||||
return 0;
|
||||
} else
|
||||
return process_gopt(ctx, arg, 0);
|
||||
}
|
||||
|
||||
static int opt_parse(struct fuse_opt_context *ctx)
|
||||
{
|
||||
if (ctx->argc) {
|
||||
if (add_arg(ctx, ctx->argv[0]) == -1)
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (ctx->argctr = 1; ctx->argctr < ctx->argc; ctx->argctr++)
|
||||
if (process_one(ctx, ctx->argv[ctx->argctr]) == -1)
|
||||
return -1;
|
||||
|
||||
if (ctx->opts) {
|
||||
if (fuse_opt_insert_arg(&ctx->outargs, 1, "-o") == -1 ||
|
||||
fuse_opt_insert_arg(&ctx->outargs, 2, ctx->opts) == -1)
|
||||
return -1;
|
||||
}
|
||||
if (ctx->nonopt && ctx->nonopt == ctx->outargs.argc) {
|
||||
free(ctx->outargs.argv[ctx->outargs.argc - 1]);
|
||||
ctx->outargs.argv[--ctx->outargs.argc] = NULL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fuse_opt_parse(struct fuse_args *args, void *data,
|
||||
const struct fuse_opt opts[], fuse_opt_proc_t proc)
|
||||
{
|
||||
int res;
|
||||
struct fuse_opt_context ctx = {
|
||||
.data = data,
|
||||
.opt = opts,
|
||||
.proc = proc,
|
||||
};
|
||||
|
||||
if (!args || !args->argv || !args->argc)
|
||||
return 0;
|
||||
|
||||
ctx.argc = args->argc;
|
||||
ctx.argv = args->argv;
|
||||
|
||||
res = opt_parse(&ctx);
|
||||
if (res != -1) {
|
||||
struct fuse_args tmp = *args;
|
||||
*args = ctx.outargs;
|
||||
ctx.outargs = tmp;
|
||||
}
|
||||
free(ctx.opts);
|
||||
fuse_opt_free_args(&ctx.outargs);
|
||||
return res;
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
/*
|
||||
FUSE: Filesystem in Userspace
|
||||
Copyright (C) 2001-2006 Miklos Szeredi <miklos@szeredi.hu>
|
||||
|
||||
This program can be distributed under the terms of the GNU GPL.
|
||||
See the file COPYING.
|
||||
*/
|
||||
|
||||
#ifndef _FUSE_OPT_H_
|
||||
#define _FUSE_OPT_H_
|
||||
|
||||
/* This file defines the option parsing interface of FUSE */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Option description
|
||||
*
|
||||
* This structure describes a single option, and an action associated
|
||||
* with it, in case it matches.
|
||||
*
|
||||
* More than one such match may occur, in which case the action for
|
||||
* each match is executed.
|
||||
*
|
||||
* There are three possible actions in case of a match:
|
||||
*
|
||||
* i) An integer (int or unsigned) variable determined by 'offset' is
|
||||
* set to 'value'
|
||||
*
|
||||
* ii) The processing function is called, with 'value' as the key
|
||||
*
|
||||
* iii) An integer (any) or string (char *) variable determined by
|
||||
* 'offset' is set to the value of an option parameter
|
||||
*
|
||||
* 'offset' should normally be either set to
|
||||
*
|
||||
* - 'offsetof(struct foo, member)' actions i) and iii)
|
||||
*
|
||||
* - -1 action ii)
|
||||
*
|
||||
* The 'offsetof()' macro is defined in the <stddef.h> header.
|
||||
*
|
||||
* The template determines which options match, and also have an
|
||||
* effect on the action. Normally the action is either i) or ii), but
|
||||
* if a format is present in the template, then action iii) is
|
||||
* performed.
|
||||
*
|
||||
* The types of templates are:
|
||||
*
|
||||
* 1) "-x", "-foo", "--foo", "--foo-bar", etc. These match only
|
||||
* themselves. Invalid values are "--" and anything beginning
|
||||
* with "-o"
|
||||
*
|
||||
* 2) "foo", "foo-bar", etc. These match "-ofoo", "-ofoo-bar" or
|
||||
* the relevant option in a comma separated option list
|
||||
*
|
||||
* 3) "bar=", "--foo=", etc. These are variations of 1) and 2)
|
||||
* which have a parameter
|
||||
*
|
||||
* 4) "bar=%s", "--foo=%lu", etc. Same matching as above but perform
|
||||
* action iii).
|
||||
*
|
||||
* 5) "-x ", etc. Matches either "-xparam" or "-x param" as
|
||||
* two separate arguments
|
||||
*
|
||||
* 6) "-x %s", etc. Combination of 4) and 5)
|
||||
*
|
||||
* If the format is "%s", memory is allocated for the string unlike
|
||||
* with scanf().
|
||||
*/
|
||||
struct fuse_opt {
|
||||
/** Matching template and optional parameter formatting */
|
||||
const char *templ;
|
||||
|
||||
/**
|
||||
* Offset of variable within 'data' parameter of fuse_opt_parse()
|
||||
* or -1
|
||||
*/
|
||||
unsigned long offset;
|
||||
|
||||
/**
|
||||
* Value to set the variable to, or to be passed as 'key' to the
|
||||
* processing function. Ignored if template has a format
|
||||
*/
|
||||
int value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Key option. In case of a match, the processing function will be
|
||||
* called with the specified key.
|
||||
*/
|
||||
#define FUSE_OPT_KEY(templ, key) { templ, -1U, key }
|
||||
|
||||
/**
|
||||
* Last option. An array of 'struct fuse_opt' must end with a NULL
|
||||
* template value
|
||||
*/
|
||||
#define FUSE_OPT_END { .templ = NULL }
|
||||
|
||||
/**
|
||||
* Argument list
|
||||
*/
|
||||
struct fuse_args {
|
||||
/** Argument count */
|
||||
int argc;
|
||||
|
||||
/** Argument vector. NULL terminated */
|
||||
char **argv;
|
||||
|
||||
/** Is 'argv' allocated? */
|
||||
int allocated;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializer for 'struct fuse_args'
|
||||
*/
|
||||
#define FUSE_ARGS_INIT(argc, argv) { argc, argv, 0 }
|
||||
|
||||
/**
|
||||
* Key value passed to the processing function if an option did not
|
||||
* match any template
|
||||
*/
|
||||
#define FUSE_OPT_KEY_OPT -1
|
||||
|
||||
/**
|
||||
* Key value passed to the processing function for all non-options
|
||||
*
|
||||
* Non-options are the arguments beginning with a charater other than
|
||||
* '-' or all arguments after the special '--' option
|
||||
*/
|
||||
#define FUSE_OPT_KEY_NONOPT -2
|
||||
|
||||
/**
|
||||
* Special key value for options to keep
|
||||
*
|
||||
* Argument is not passed to processing function, but behave as if the
|
||||
* processing function returned 1
|
||||
*/
|
||||
#define FUSE_OPT_KEY_KEEP -3
|
||||
|
||||
/**
|
||||
* Special key value for options to discard
|
||||
*
|
||||
* Argument is not passed to processing function, but behave as if the
|
||||
* processing function returned zero
|
||||
*/
|
||||
#define FUSE_OPT_KEY_DISCARD -4
|
||||
|
||||
/**
|
||||
* Processing function
|
||||
*
|
||||
* This function is called if
|
||||
* - option did not match any 'struct fuse_opt'
|
||||
* - argument is a non-option
|
||||
* - option did match and offset was set to -1
|
||||
*
|
||||
* The 'arg' parameter will always contain the whole argument or
|
||||
* option including the parameter if exists. A two-argument option
|
||||
* ("-x foo") is always converted to single arguemnt option of the
|
||||
* form "-xfoo" before this function is called.
|
||||
*
|
||||
* Options of the form '-ofoo' are passed to this function without the
|
||||
* '-o' prefix.
|
||||
*
|
||||
* The return value of this function determines whether this argument
|
||||
* is to be inserted into the output argument vector, or discarded.
|
||||
*
|
||||
* @param data is the user data passed to the fuse_opt_parse() function
|
||||
* @param arg is the whole argument or option
|
||||
* @param key determines why the processing function was called
|
||||
* @param outargs the current output argument list
|
||||
* @return -1 on error, 0 if arg is to be discarded, 1 if arg should be kept
|
||||
*/
|
||||
typedef int (*fuse_opt_proc_t)(void *data, const char *arg, int key,
|
||||
struct fuse_args *outargs);
|
||||
|
||||
/**
|
||||
* Option parsing function
|
||||
*
|
||||
* If 'args' was returned from a previous call to fuse_opt_parse() or
|
||||
* it was constructed from
|
||||
*
|
||||
* A NULL 'args' is equivalent to an empty argument vector
|
||||
*
|
||||
* A NULL 'opts' is equivalent to an 'opts' array containing a single
|
||||
* end marker
|
||||
*
|
||||
* A NULL 'proc' is equivalent to a processing function always
|
||||
* returning '1'
|
||||
*
|
||||
* @param args is the input and output argument list
|
||||
* @param data is the user data
|
||||
* @param opts is the option description array
|
||||
* @param proc is the processing function
|
||||
* @return -1 on error, 0 on success
|
||||
*/
|
||||
int fuse_opt_parse(struct fuse_args *args, void *data,
|
||||
const struct fuse_opt opts[], fuse_opt_proc_t proc);
|
||||
|
||||
/**
|
||||
* Add an option to a comma separated option list
|
||||
*
|
||||
* @param opts is a pointer to an option list, may point to a NULL value
|
||||
* @param opt is the option to add
|
||||
* @return -1 on allocation error, 0 on success
|
||||
*/
|
||||
int fuse_opt_add_opt(char **opts, const char *opt);
|
||||
|
||||
/**
|
||||
* Add an argument to a NULL terminated argument vector
|
||||
*
|
||||
* @param args is the structure containing the current argument list
|
||||
* @param arg is the new argument to add
|
||||
* @return -1 on allocation error, 0 on success
|
||||
*/
|
||||
int fuse_opt_add_arg(struct fuse_args *args, const char *arg);
|
||||
|
||||
/**
|
||||
* Add an argument at the specified position in a NULL terminated
|
||||
* argument vector
|
||||
*
|
||||
* Adds the argument to the N-th position. This is useful for adding
|
||||
* options at the beggining of the array which must not come after the
|
||||
* special '--' option.
|
||||
*
|
||||
* @param args is the structure containing the current argument list
|
||||
* @param pos is the position at which to add the argument
|
||||
* @param arg is the new argument to add
|
||||
* @return -1 on allocation error, 0 on success
|
||||
*/
|
||||
int fuse_opt_insert_arg(struct fuse_args *args, int pos, const char *arg);
|
||||
|
||||
/**
|
||||
* Free the contents of argument list
|
||||
*
|
||||
* The structure itself is not freed
|
||||
*
|
||||
* @param args is the structure containing the argument list
|
||||
*/
|
||||
void fuse_opt_free_args(struct fuse_args *args);
|
||||
|
||||
|
||||
/**
|
||||
* Check if an option matches
|
||||
*
|
||||
* @param opts is the option description array
|
||||
* @param opt is the option to match
|
||||
* @return 1 if a match is found, 0 if not
|
||||
*/
|
||||
int fuse_opt_match(const struct fuse_opt opts[], const char *opt);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _FUSE_OPT_H_ */
|
||||
+3
-2
@@ -1,4 +1,4 @@
|
||||
project('sshfs', 'c', version: '3.7.3',
|
||||
project('sshfs', 'c', version: '3.7.5',
|
||||
meson_version: '>= 0.40',
|
||||
default_options: [ 'buildtype=debugoptimized' ])
|
||||
|
||||
@@ -34,8 +34,9 @@ cfg.set_quoted('PACKAGE_VERSION', meson.project_version())
|
||||
include_dirs = [ include_directories('.') ]
|
||||
sshfs_sources = ['sshfs.c', 'cache.c']
|
||||
if target_machine.system() == 'darwin'
|
||||
add_global_arguments('-DFUSE_DARWIN_ENABLE_EXTENSIONS=0', language: 'c')
|
||||
cfg.set_quoted('IDMAP_DEFAULT', 'user')
|
||||
sshfs_sources += [ 'compat/fuse_opt.c', 'compat/darwin_compat.c' ]
|
||||
sshfs_sources += [ 'compat/darwin_compat.c' ]
|
||||
include_dirs += [ include_directories('compat') ]
|
||||
else
|
||||
cfg.set_quoted('IDMAP_DEFAULT', 'none')
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
#if !defined(__CYGWIN__)
|
||||
# include <fuse_lowlevel.h>
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
# include <fuse_darwin.h>
|
||||
#endif
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -283,6 +280,11 @@ struct read_chunk {
|
||||
struct sshfs_io sio;
|
||||
};
|
||||
|
||||
struct conntab_entry {
|
||||
unsigned refcount;
|
||||
struct conn *conn;
|
||||
};
|
||||
|
||||
struct sshfs_file {
|
||||
struct buffer handle;
|
||||
struct list_head write_reqs;
|
||||
@@ -292,15 +294,11 @@ struct sshfs_file {
|
||||
off_t next_pos;
|
||||
int is_seq;
|
||||
struct conn *conn;
|
||||
struct conntab_entry *ce;
|
||||
int connver;
|
||||
int modifver;
|
||||
};
|
||||
|
||||
struct conntab_entry {
|
||||
unsigned refcount;
|
||||
struct conn *conn;
|
||||
};
|
||||
|
||||
struct sshfs {
|
||||
char *directport;
|
||||
char *ssh_command;
|
||||
@@ -340,7 +338,6 @@ struct sshfs {
|
||||
int sync_readdir;
|
||||
int direct_io;
|
||||
int debug;
|
||||
int verbose;
|
||||
int foreground;
|
||||
int reconnect;
|
||||
int delay_connect;
|
||||
@@ -494,7 +491,6 @@ static struct fuse_opt sshfs_opts[] = {
|
||||
SSHFS_OPT("no_readahead", sync_read, 1),
|
||||
SSHFS_OPT("sync_readdir", sync_readdir, 1),
|
||||
SSHFS_OPT("sshfs_debug", debug, 1),
|
||||
SSHFS_OPT("sshfs_verbose", verbose, 1),
|
||||
SSHFS_OPT("reconnect", reconnect, 1),
|
||||
SSHFS_OPT("transform_symlinks", transform_symlinks, 1),
|
||||
SSHFS_OPT("follow_symlinks", follow_symlinks, 1),
|
||||
@@ -516,8 +512,6 @@ static struct fuse_opt sshfs_opts[] = {
|
||||
SSHFS_OPT("--version", show_version, 1),
|
||||
SSHFS_OPT("-d", debug, 1),
|
||||
SSHFS_OPT("debug", debug, 1),
|
||||
SSHFS_OPT("-v", verbose, 1),
|
||||
SSHFS_OPT("verbose", verbose, 1),
|
||||
SSHFS_OPT("-f", foreground, 1),
|
||||
SSHFS_OPT("-s", singlethread, 1),
|
||||
|
||||
@@ -1179,7 +1173,7 @@ static int start_ssh(struct conn *conn)
|
||||
perror("failed to redirect input/output");
|
||||
_exit(1);
|
||||
}
|
||||
if (!sshfs.verbose && !sshfs.foreground && devnull != -1)
|
||||
if (!sshfs.foreground && devnull != -1)
|
||||
dup2(devnull, 2);
|
||||
|
||||
close(devnull);
|
||||
@@ -1227,6 +1221,13 @@ static int start_ssh(struct conn *conn)
|
||||
fprintf(stderr, "\n");
|
||||
}
|
||||
|
||||
#if defined(__CYGWIN__)
|
||||
/*
|
||||
* Windows native OpenSSH stdio behavior. For details check
|
||||
* https://github.com/PowerShell/openssh-portable/pull/759
|
||||
*/
|
||||
putenv("OPENSSH_STDIO_MODE=nonsock");
|
||||
#endif
|
||||
execvp(sshfs.ssh_args.argv[0], sshfs.ssh_args.argv);
|
||||
fprintf(stderr, "failed to execute '%s': %s\n",
|
||||
sshfs.ssh_args.argv[0], strerror(errno));
|
||||
@@ -1254,6 +1255,10 @@ static int connect_to(struct conn *conn, char *host, char *port)
|
||||
|
||||
memset(&hint, 0, sizeof(hint));
|
||||
hint.ai_family = PF_INET;
|
||||
if (strstr(host, ":") != NULL) { // only ipv6 should have : in it, normal IP and domains do not.
|
||||
hint.ai_family = PF_INET6;
|
||||
DEBUG("using ipv6 to connect to host %s\n", host);
|
||||
}
|
||||
hint.ai_socktype = SOCK_STREAM;
|
||||
err = getaddrinfo(host, port, &hint, &ai);
|
||||
if (err) {
|
||||
@@ -1437,23 +1442,24 @@ static int do_read(struct conn *conn, struct buffer *buf)
|
||||
|
||||
static int sftp_read(struct conn *conn, uint8_t *type, struct buffer *buf)
|
||||
{
|
||||
int res;
|
||||
int res = -1;
|
||||
struct buffer buf2;
|
||||
uint32_t len;
|
||||
buf_init(&buf2, 5);
|
||||
res = do_read(conn, &buf2);
|
||||
if (res != -1) {
|
||||
if (buf_get_uint32(&buf2, &len) == -1)
|
||||
return -1;
|
||||
if (len > MAX_REPLY_LEN) {
|
||||
fprintf(stderr, "reply len too large: %u\n", len);
|
||||
return -1;
|
||||
}
|
||||
if (buf_get_uint8(&buf2, type) == -1)
|
||||
return -1;
|
||||
buf_init(buf, len - 1);
|
||||
res = do_read(conn, buf);
|
||||
if (do_read(conn, &buf2) == -1)
|
||||
goto out;
|
||||
if (buf_get_uint32(&buf2, &len) == -1)
|
||||
goto out;
|
||||
if (len < 1 || len > MAX_REPLY_LEN) {
|
||||
fprintf(stderr, "bad reply len: %u\n", len);
|
||||
goto out;
|
||||
}
|
||||
if (buf_get_uint8(&buf2, type) == -1)
|
||||
goto out;
|
||||
buf_init(buf, len - 1);
|
||||
res = do_read(conn, buf);
|
||||
|
||||
out:
|
||||
buf_free(&buf2);
|
||||
return res;
|
||||
}
|
||||
@@ -1526,10 +1532,14 @@ static int process_one_request(struct conn *conn)
|
||||
|
||||
buf_init(&buf, 0);
|
||||
res = sftp_read(conn, &type, &buf);
|
||||
if (res == -1)
|
||||
if (res == -1) {
|
||||
buf_free(&buf);
|
||||
return -1;
|
||||
if (buf_get_uint32(&buf, &id) == -1)
|
||||
}
|
||||
if (buf_get_uint32(&buf, &id) == -1) {
|
||||
buf_free(&buf);
|
||||
return -1;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&sshfs.lock);
|
||||
req = (struct request *)
|
||||
@@ -2759,6 +2769,12 @@ static int sshfs_utimens(const char *path, const struct timespec tv[2],
|
||||
return err;
|
||||
}
|
||||
|
||||
static gboolean conntab_entry_is(gpointer key, gpointer value, gpointer data)
|
||||
{
|
||||
(void) key;
|
||||
return value == data;
|
||||
}
|
||||
|
||||
static int sshfs_open_common(const char *path, mode_t mode,
|
||||
struct fuse_file_info *fi)
|
||||
{
|
||||
@@ -2819,12 +2835,13 @@ static int sshfs_open_common(const char *path, mode_t mode,
|
||||
g_hash_table_insert(sshfs.conntab, g_strdup(path), ce);
|
||||
}
|
||||
sf->conn = ce->conn;
|
||||
sf->ce = ce;
|
||||
ce->refcount++;
|
||||
sf->conn->file_count++;
|
||||
assert(sf->conn->file_count > 0);
|
||||
} else {
|
||||
sf->conn = &sshfs.conns[0];
|
||||
ce = NULL; // only to silence compiler warning
|
||||
sf->ce = NULL;
|
||||
}
|
||||
sf->connver = sf->conn->connver;
|
||||
pthread_mutex_unlock(&sshfs.lock);
|
||||
@@ -2864,10 +2881,12 @@ static int sshfs_open_common(const char *path, mode_t mode,
|
||||
if (sshfs.max_conns > 1) {
|
||||
pthread_mutex_lock(&sshfs.lock);
|
||||
sf->conn->file_count--;
|
||||
ce->refcount--;
|
||||
if(ce->refcount == 0) {
|
||||
g_hash_table_remove(sshfs.conntab, path);
|
||||
g_free(ce);
|
||||
sf->ce->refcount--;
|
||||
if (sf->ce->refcount == 0) {
|
||||
g_hash_table_foreach_remove(sshfs.conntab,
|
||||
conntab_entry_is,
|
||||
sf->ce);
|
||||
g_free(sf->ce);
|
||||
}
|
||||
pthread_mutex_unlock(&sshfs.lock);
|
||||
}
|
||||
@@ -2938,7 +2957,6 @@ static int sshfs_release(const char *path, struct fuse_file_info *fi)
|
||||
{
|
||||
struct sshfs_file *sf = get_sshfs_file(fi);
|
||||
struct buffer *handle = &sf->handle;
|
||||
struct conntab_entry *ce;
|
||||
if (sshfs_file_is_conn(sf)) {
|
||||
sshfs_flush(path, fi);
|
||||
sftp_request(sf->conn, SSH_FXP_CLOSE, handle, 0, NULL);
|
||||
@@ -2946,12 +2964,13 @@ static int sshfs_release(const char *path, struct fuse_file_info *fi)
|
||||
buf_free(handle);
|
||||
chunk_put_locked(sf->readahead);
|
||||
if (sshfs.max_conns > 1) {
|
||||
struct conntab_entry *ce = sf->ce;
|
||||
pthread_mutex_lock(&sshfs.lock);
|
||||
sf->conn->file_count--;
|
||||
ce = g_hash_table_lookup(sshfs.conntab, path);
|
||||
ce->refcount--;
|
||||
if(ce->refcount == 0) {
|
||||
g_hash_table_remove(sshfs.conntab, path);
|
||||
if (ce->refcount == 0) {
|
||||
g_hash_table_foreach_remove(sshfs.conntab,
|
||||
conntab_entry_is, ce);
|
||||
g_free(ce);
|
||||
}
|
||||
pthread_mutex_unlock(&sshfs.lock);
|
||||
@@ -3378,7 +3397,7 @@ static int sshfs_statfs(const char *path, struct statvfs *buf)
|
||||
return sshfs_ext_statvfs(path, buf);
|
||||
|
||||
buf->f_namemax = 255;
|
||||
buf->f_bsize = sshfs.blksize;
|
||||
buf->f_bsize = sshfs.blksize ? sshfs.blksize : 4096;
|
||||
/*
|
||||
* df seems to use f_bsize instead of f_frsize, so make them
|
||||
* the same
|
||||
@@ -3664,7 +3683,6 @@ static void usage(const char *progname)
|
||||
" -o no_readahead synchronous reads (no speculative readahead)\n"
|
||||
" -o sync_readdir synchronous readdir\n"
|
||||
" -d, --debug print some debugging information (implies -f)\n"
|
||||
" -v, --verbose print ssh replies and messages\n"
|
||||
" -o dir_cache=BOOL enable caching of directory contents (names,\n"
|
||||
" attributes, symlink targets) {yes,no} (default: yes)\n"
|
||||
" -o dcache_max_size=N sets the maximum size of the directory cache (default: 10000)\n"
|
||||
@@ -3794,6 +3812,16 @@ static int sshfs_opt_proc(void *data, const char *arg, int key,
|
||||
sshfs.mountpoint = strdup(arg);
|
||||
} else {
|
||||
sshfs.mountpoint = realpath(arg, NULL);
|
||||
#ifdef __APPLE__
|
||||
if (!sshfs.mountpoint) {
|
||||
/*
|
||||
* The mountpoint does not exist, yet.
|
||||
* macFUSE will try to create it before
|
||||
* mounting the volume.
|
||||
*/
|
||||
sshfs.mountpoint = strdup(arg);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
if (!sshfs.mountpoint) {
|
||||
@@ -3926,7 +3954,7 @@ static char *tokenize_on_space(char *str)
|
||||
|
||||
start = pos;
|
||||
|
||||
while (pos && *pos != '\0') {
|
||||
while (*pos != '\0') {
|
||||
// break on space, but not on '\ '
|
||||
if (*pos == ' ' && *(pos - 1) != '\\') {
|
||||
break;
|
||||
|
||||
+412
-8
@@ -135,7 +135,7 @@ def test_sshfs(
|
||||
try:
|
||||
wait_for_mount(mount_process, mnt_dir)
|
||||
|
||||
tst_statvfs(mnt_dir)
|
||||
tst_statvfs(src_dir, mnt_dir)
|
||||
tst_readdir(src_dir, mnt_dir)
|
||||
tst_open_read(src_dir, mnt_dir)
|
||||
tst_open_write(src_dir, mnt_dir)
|
||||
@@ -145,6 +145,10 @@ def test_sshfs(
|
||||
tst_passthrough(src_dir, mnt_dir, cache_timeout)
|
||||
tst_mkdir(mnt_dir)
|
||||
tst_rmdir(src_dir, mnt_dir, cache_timeout)
|
||||
tst_rename(mnt_dir)
|
||||
tst_rename_over(mnt_dir)
|
||||
tst_chmod(mnt_dir)
|
||||
tst_fsync(src_dir, mnt_dir)
|
||||
tst_unlink(src_dir, mnt_dir, cache_timeout)
|
||||
tst_symlink(mnt_dir)
|
||||
if os.getuid() == 0:
|
||||
@@ -159,6 +163,12 @@ def test_sshfs(
|
||||
tst_truncate_path(mnt_dir)
|
||||
tst_truncate_fd(mnt_dir)
|
||||
tst_open_unlink(mnt_dir)
|
||||
tst_open_writeonly_read(mnt_dir)
|
||||
tst_access(mnt_dir)
|
||||
tst_mkdir_exist(mnt_dir)
|
||||
tst_readdir_repeated(mnt_dir)
|
||||
tst_rename_sibling(mnt_dir)
|
||||
tst_rename_open_release(mnt_dir)
|
||||
except Exception as exc:
|
||||
cleanup(mount_process, mnt_dir)
|
||||
raise exc
|
||||
@@ -208,6 +218,83 @@ def tst_rmdir(src_dir, mnt_dir, cache_timeout):
|
||||
assert name not in os.listdir(src_dir)
|
||||
|
||||
|
||||
def tst_rename(mnt_dir):
|
||||
src_name = pjoin(mnt_dir, name_generator())
|
||||
dst_name = pjoin(mnt_dir, name_generator())
|
||||
data = b"rename test data\n"
|
||||
|
||||
with open(src_name, "wb") as fh:
|
||||
fh.write(data)
|
||||
assert os.path.exists(src_name)
|
||||
|
||||
os.rename(src_name, dst_name)
|
||||
|
||||
assert not os.path.exists(src_name)
|
||||
assert os.path.basename(src_name) not in os.listdir(mnt_dir)
|
||||
assert os.path.basename(dst_name) in os.listdir(mnt_dir)
|
||||
with open(dst_name, "rb") as fh:
|
||||
assert fh.read() == data
|
||||
|
||||
os.unlink(dst_name)
|
||||
|
||||
|
||||
def tst_rename_over(mnt_dir):
|
||||
src_name = pjoin(mnt_dir, name_generator())
|
||||
dst_name = pjoin(mnt_dir, name_generator())
|
||||
src_data = b"source content\n"
|
||||
dst_data = b"destination content\n"
|
||||
|
||||
with open(src_name, "wb") as fh:
|
||||
fh.write(src_data)
|
||||
with open(dst_name, "wb") as fh:
|
||||
fh.write(dst_data)
|
||||
|
||||
os.rename(src_name, dst_name)
|
||||
|
||||
assert not os.path.exists(src_name)
|
||||
assert os.path.basename(src_name) not in os.listdir(mnt_dir)
|
||||
with open(dst_name, "rb") as fh:
|
||||
assert fh.read() == src_data
|
||||
|
||||
os.unlink(dst_name)
|
||||
|
||||
|
||||
def tst_chmod(mnt_dir):
|
||||
filename = pjoin(mnt_dir, name_generator())
|
||||
with open(filename, "wb") as fh:
|
||||
fh.write(b"chmod test\n")
|
||||
|
||||
os.chmod(filename, 0o644)
|
||||
fstat = os.stat(filename)
|
||||
assert stat.S_IMODE(fstat.st_mode) == 0o644
|
||||
|
||||
os.chmod(filename, 0o755)
|
||||
fstat = os.stat(filename)
|
||||
assert stat.S_IMODE(fstat.st_mode) == 0o755
|
||||
|
||||
os.unlink(filename)
|
||||
|
||||
|
||||
def tst_fsync(src_dir, mnt_dir):
|
||||
name = name_generator()
|
||||
mnt_name = pjoin(mnt_dir, name)
|
||||
src_name = pjoin(src_dir, name)
|
||||
data = b"fsync test data\n"
|
||||
|
||||
fd = os.open(mnt_name, os.O_CREAT | os.O_WRONLY)
|
||||
try:
|
||||
os.write(fd, data)
|
||||
os.fsync(fd)
|
||||
# Read from backing store while fd is still open, before
|
||||
# close/release has a chance to flush
|
||||
with open(src_name, "rb") as fh:
|
||||
assert fh.read() == data
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
os.unlink(mnt_name)
|
||||
|
||||
|
||||
def tst_symlink(mnt_dir):
|
||||
linkname = name_generator()
|
||||
fullname = mnt_dir + "/" + linkname
|
||||
@@ -218,6 +305,9 @@ def tst_symlink(mnt_dir):
|
||||
assert fstat.st_nlink == 1
|
||||
assert linkname in os.listdir(mnt_dir)
|
||||
|
||||
os.unlink(fullname)
|
||||
assert linkname not in os.listdir(mnt_dir)
|
||||
|
||||
|
||||
def tst_create(mnt_dir):
|
||||
name = name_generator()
|
||||
@@ -314,15 +404,121 @@ def tst_open_unlink(mnt_dir):
|
||||
os.unlink(fullname)
|
||||
with pytest.raises(OSError) as exc_info:
|
||||
os.stat(fullname)
|
||||
assert exc_info.value.errno == errno.ENOENT
|
||||
assert exc_info.value.errno == errno.ENOENT
|
||||
assert name not in os.listdir(mnt_dir)
|
||||
fh.write(data2)
|
||||
fh.seek(0)
|
||||
assert fh.read() == data1 + data2
|
||||
|
||||
|
||||
def tst_statvfs(mnt_dir):
|
||||
os.statvfs(mnt_dir)
|
||||
def tst_statvfs(src_dir, mnt_dir):
|
||||
vfs = os.statvfs(mnt_dir)
|
||||
ref = os.statvfs(src_dir)
|
||||
# When the server supports statvfs@openssh.com, values should
|
||||
# match the backing store. Otherwise sshfs returns synthetic
|
||||
# values that still pass the loose checks.
|
||||
if vfs.f_bsize == ref.f_bsize:
|
||||
assert vfs.f_frsize == ref.f_frsize
|
||||
assert vfs.f_blocks == ref.f_blocks
|
||||
assert vfs.f_namemax == ref.f_namemax
|
||||
else:
|
||||
assert vfs.f_bsize > 0
|
||||
assert vfs.f_blocks > 0
|
||||
assert vfs.f_namemax > 0
|
||||
|
||||
|
||||
def tst_open_writeonly_read(mnt_dir):
|
||||
name = pjoin(mnt_dir, name_generator())
|
||||
fd = os.open(name, os.O_CREAT | os.O_WRONLY)
|
||||
try:
|
||||
os.write(fd, b"hello")
|
||||
with pytest.raises(OSError) as exc_info:
|
||||
os.read(fd, 10)
|
||||
assert exc_info.value.errno == errno.EBADF
|
||||
finally:
|
||||
os.close(fd)
|
||||
os.unlink(name)
|
||||
|
||||
|
||||
def tst_access(mnt_dir):
|
||||
filename = pjoin(mnt_dir, name_generator())
|
||||
with open(filename, "wb") as fh:
|
||||
fh.write(b"test")
|
||||
os.chmod(filename, 0o644)
|
||||
assert os.access(filename, os.R_OK)
|
||||
if os.getuid() != 0:
|
||||
assert not os.access(filename, os.X_OK)
|
||||
os.unlink(filename)
|
||||
|
||||
|
||||
def tst_mkdir_exist(mnt_dir):
|
||||
name = name_generator()
|
||||
fullname = pjoin(mnt_dir, name)
|
||||
os.mkdir(fullname)
|
||||
with pytest.raises(OSError) as exc_info:
|
||||
os.mkdir(fullname)
|
||||
assert exc_info.value.errno == errno.EEXIST
|
||||
os.rmdir(fullname)
|
||||
|
||||
|
||||
def tst_readdir_repeated(mnt_dir):
|
||||
dirname = pjoin(mnt_dir, name_generator())
|
||||
os.mkdir(dirname)
|
||||
names = []
|
||||
for i in range(5):
|
||||
n = name_generator()
|
||||
names.append(n)
|
||||
with open(pjoin(dirname, n), "wb") as fh:
|
||||
fh.write(b"x")
|
||||
|
||||
# Verify repeated directory listings return consistent results
|
||||
listing1 = sorted(os.listdir(dirname))
|
||||
listing2 = sorted(os.listdir(dirname))
|
||||
assert listing1 == sorted(names)
|
||||
assert listing1 == listing2
|
||||
|
||||
for n in names:
|
||||
os.unlink(pjoin(dirname, n))
|
||||
os.rmdir(dirname)
|
||||
|
||||
|
||||
def tst_rename_sibling(mnt_dir):
|
||||
# Verify renaming one file doesn't break access to a sibling
|
||||
name_a = pjoin(mnt_dir, name_generator())
|
||||
name_b = pjoin(mnt_dir, name_generator())
|
||||
name_c = pjoin(mnt_dir, name_generator())
|
||||
|
||||
with open(name_a, "wb") as fh:
|
||||
fh.write(b"aaa")
|
||||
with open(name_b, "wb") as fh:
|
||||
fh.write(b"bbb")
|
||||
|
||||
os.rename(name_a, name_c)
|
||||
|
||||
assert not os.path.exists(name_a)
|
||||
assert os.path.exists(name_b)
|
||||
with open(name_b, "rb") as fh:
|
||||
assert fh.read() == b"bbb"
|
||||
|
||||
os.unlink(name_b)
|
||||
os.unlink(name_c)
|
||||
|
||||
|
||||
def tst_rename_open_release(mnt_dir):
|
||||
src = pjoin(mnt_dir, name_generator())
|
||||
dst = pjoin(mnt_dir, name_generator())
|
||||
|
||||
fd = os.open(src, os.O_CREAT | os.O_RDWR)
|
||||
try:
|
||||
os.write(fd, b"data")
|
||||
os.rename(src, dst)
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
assert not os.path.exists(src)
|
||||
with open(dst, "rb") as fh:
|
||||
assert fh.read() == b"data"
|
||||
os.unlink(dst)
|
||||
|
||||
|
||||
def tst_link(mnt_dir, cache_timeout):
|
||||
@@ -365,6 +561,10 @@ def tst_link(mnt_dir, cache_timeout):
|
||||
assert os.path.basename(name2) not in os.listdir(mnt_dir)
|
||||
with pytest.raises(FileNotFoundError):
|
||||
os.lstat(name2)
|
||||
if cache_timeout:
|
||||
safe_sleep(cache_timeout + 1)
|
||||
fstat1 = os.lstat(name1)
|
||||
assert fstat1.st_nlink == 1
|
||||
|
||||
os.unlink(name1)
|
||||
|
||||
@@ -418,6 +618,10 @@ def tst_truncate_path(mnt_dir):
|
||||
with open(filename, "rb") as fh:
|
||||
assert fh.read(size) == TEST_DATA[: size - 1024]
|
||||
|
||||
# Truncate to zero
|
||||
os.truncate(filename, 0)
|
||||
assert os.stat(filename).st_size == 0
|
||||
|
||||
os.unlink(filename)
|
||||
|
||||
|
||||
@@ -443,6 +647,10 @@ def tst_truncate_fd(mnt_dir):
|
||||
fh.seek(0)
|
||||
assert fh.read(size) == TEST_DATA[: size - 1024]
|
||||
|
||||
# Truncate to zero via fd
|
||||
os.ftruncate(fd, 0)
|
||||
assert os.fstat(fd).st_size == 0
|
||||
|
||||
|
||||
def tst_utimens(mnt_dir, tol=0):
|
||||
filename = pjoin(mnt_dir, name_generator())
|
||||
@@ -483,7 +691,7 @@ def tst_utimens_now(mnt_dir):
|
||||
def tst_passthrough(src_dir, mnt_dir, cache_timeout):
|
||||
name = name_generator()
|
||||
src_name = pjoin(src_dir, name)
|
||||
mnt_name = pjoin(src_dir, name)
|
||||
mnt_name = pjoin(mnt_dir, name)
|
||||
assert name not in os.listdir(src_dir)
|
||||
assert name not in os.listdir(mnt_dir)
|
||||
with open(src_name, "w") as fh:
|
||||
@@ -492,11 +700,16 @@ def tst_passthrough(src_dir, mnt_dir, cache_timeout):
|
||||
if cache_timeout:
|
||||
safe_sleep(cache_timeout + 1)
|
||||
assert name in os.listdir(mnt_dir)
|
||||
assert os.stat(src_name) == os.stat(mnt_name)
|
||||
src_st = os.stat(src_name)
|
||||
mnt_st = os.stat(mnt_name)
|
||||
assert src_st.st_size == mnt_st.st_size
|
||||
assert src_st.st_uid == mnt_st.st_uid
|
||||
assert src_st.st_gid == mnt_st.st_gid
|
||||
assert abs(src_st.st_mtime - mnt_st.st_mtime) <= 1
|
||||
|
||||
name = name_generator()
|
||||
src_name = pjoin(src_dir, name)
|
||||
mnt_name = pjoin(src_dir, name)
|
||||
mnt_name = pjoin(mnt_dir, name)
|
||||
assert name not in os.listdir(src_dir)
|
||||
assert name not in os.listdir(mnt_dir)
|
||||
with open(mnt_name, "w") as fh:
|
||||
@@ -505,4 +718,195 @@ def tst_passthrough(src_dir, mnt_dir, cache_timeout):
|
||||
if cache_timeout:
|
||||
safe_sleep(cache_timeout + 1)
|
||||
assert name in os.listdir(mnt_dir)
|
||||
assert os.stat(src_name) == os.stat(mnt_name)
|
||||
src_st = os.stat(src_name)
|
||||
mnt_st = os.stat(mnt_name)
|
||||
assert src_st.st_size == mnt_st.st_size
|
||||
assert src_st.st_uid == mnt_st.st_uid
|
||||
assert src_st.st_gid == mnt_st.st_gid
|
||||
assert abs(src_st.st_mtime - mnt_st.st_mtime) <= 1
|
||||
|
||||
|
||||
def _check_ssh_localhost():
|
||||
try:
|
||||
res = subprocess.call(
|
||||
["ssh", "-o", "StrictHostKeyChecking=no",
|
||||
"-o", "KbdInteractiveAuthentication=no",
|
||||
"-o", "ChallengeResponseAuthentication=no",
|
||||
"-o", "PasswordAuthentication=no",
|
||||
"localhost", "--", "true"],
|
||||
stdin=subprocess.DEVNULL, timeout=10,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
res = 1
|
||||
if res != 0:
|
||||
pytest.fail("Unable to ssh into localhost without password prompt.")
|
||||
|
||||
|
||||
_mount_ctr = [0]
|
||||
|
||||
|
||||
def _mount_sshfs(tmpdir, extra_opts=None):
|
||||
"""Helper to mount sshfs with custom options. Returns (mount_process, mnt_dir, src_dir)."""
|
||||
_check_ssh_localhost()
|
||||
_mount_ctr[0] += 1
|
||||
mnt_dir = str(tmpdir.mkdir(f"mnt{_mount_ctr[0]}"))
|
||||
src_dir = str(tmpdir.mkdir(f"src{_mount_ctr[0]}"))
|
||||
|
||||
cmdline = base_cmdline + [
|
||||
pjoin(basename, "sshfs"),
|
||||
"-f",
|
||||
f"localhost:{src_dir}",
|
||||
mnt_dir,
|
||||
"-o", "entry_timeout=0",
|
||||
"-o", "attr_timeout=0",
|
||||
]
|
||||
if extra_opts:
|
||||
for opt in extra_opts:
|
||||
cmdline += ["-o", opt]
|
||||
|
||||
new_env = dict(os.environ)
|
||||
new_env["G_DEBUG"] = "fatal-warnings"
|
||||
|
||||
mount_process = subprocess.Popen(cmdline, env=new_env)
|
||||
try:
|
||||
wait_for_mount(mount_process, mnt_dir)
|
||||
except:
|
||||
cleanup(mount_process, mnt_dir)
|
||||
raise
|
||||
return mount_process, mnt_dir, src_dir
|
||||
|
||||
|
||||
def test_disable_hardlink(tmpdir, capfd):
|
||||
capfd.register_output(r"^Warning: Permanently added 'localhost' .+", count=0)
|
||||
|
||||
# Control: verify hardlinks work without disable_hardlink.
|
||||
# If the server lacks the extension, skip this test entirely.
|
||||
mount_process, mnt_dir, src_dir = _mount_sshfs(tmpdir, [])
|
||||
try:
|
||||
name1 = pjoin(mnt_dir, name_generator())
|
||||
name2 = pjoin(mnt_dir, name_generator())
|
||||
with open(name1, "wb") as fh:
|
||||
fh.write(b"test")
|
||||
try:
|
||||
os.link(name1, name2)
|
||||
except OSError:
|
||||
os.unlink(name1)
|
||||
pytest.skip("server does not support hardlink extension")
|
||||
os.unlink(name2)
|
||||
os.unlink(name1)
|
||||
except Exception:
|
||||
cleanup(mount_process, mnt_dir)
|
||||
raise
|
||||
else:
|
||||
umount(mount_process, mnt_dir)
|
||||
|
||||
# Now test with disable_hardlink — links should fail
|
||||
mount_process, mnt_dir, src_dir = _mount_sshfs(tmpdir, ["disable_hardlink"])
|
||||
try:
|
||||
name1 = pjoin(mnt_dir, name_generator())
|
||||
name2 = pjoin(mnt_dir, name_generator())
|
||||
with open(name1, "wb") as fh:
|
||||
fh.write(b"test")
|
||||
with pytest.raises(OSError) as exc_info:
|
||||
os.link(name1, name2)
|
||||
assert exc_info.value.errno in (errno.ENOSYS, errno.EPERM)
|
||||
os.unlink(name1)
|
||||
except Exception:
|
||||
cleanup(mount_process, mnt_dir)
|
||||
raise
|
||||
else:
|
||||
umount(mount_process, mnt_dir)
|
||||
|
||||
|
||||
def test_follow_symlinks(tmpdir, capfd):
|
||||
capfd.register_output(r"^Warning: Permanently added 'localhost' .+", count=0)
|
||||
mount_process, mnt_dir, src_dir = _mount_sshfs(tmpdir, ["follow_symlinks"])
|
||||
try:
|
||||
target_name = name_generator()
|
||||
target = pjoin(src_dir, target_name)
|
||||
with open(target, "wb") as fh:
|
||||
fh.write(b"symlink target data")
|
||||
|
||||
link = pjoin(src_dir, name_generator())
|
||||
os.symlink(target_name, link)
|
||||
|
||||
mnt_link = pjoin(mnt_dir, os.path.basename(link))
|
||||
# With follow_symlinks, stat should return the target's attributes
|
||||
# and the entry should appear as a regular file, not a symlink
|
||||
fstat = os.lstat(mnt_link)
|
||||
assert stat.S_ISREG(fstat.st_mode)
|
||||
with open(mnt_link, "rb") as fh:
|
||||
assert fh.read() == b"symlink target data"
|
||||
|
||||
os.unlink(link)
|
||||
os.unlink(target)
|
||||
except Exception:
|
||||
cleanup(mount_process, mnt_dir)
|
||||
raise
|
||||
else:
|
||||
umount(mount_process, mnt_dir)
|
||||
|
||||
|
||||
def test_direct_io(tmpdir, capfd):
|
||||
capfd.register_output(r"^Warning: Permanently added 'localhost' .+", count=0)
|
||||
mount_process, mnt_dir, src_dir = _mount_sshfs(tmpdir, ["direct_io"])
|
||||
try:
|
||||
name = name_generator()
|
||||
mnt_name = pjoin(mnt_dir, name)
|
||||
src_name = pjoin(src_dir, name)
|
||||
data = b"direct io test data\n" * 100
|
||||
|
||||
with open(mnt_name, "wb") as fh:
|
||||
fh.write(data)
|
||||
with open(mnt_name, "rb") as fh:
|
||||
assert fh.read() == data
|
||||
with open(src_name, "rb") as fh:
|
||||
assert fh.read() == data
|
||||
|
||||
os.unlink(mnt_name)
|
||||
except Exception:
|
||||
cleanup(mount_process, mnt_dir)
|
||||
raise
|
||||
else:
|
||||
umount(mount_process, mnt_dir)
|
||||
|
||||
|
||||
def test_bad_sftp_reply_len(tmpdir):
|
||||
"""sshfs must reject a zero-length SFTP reply instead of underflowing."""
|
||||
helper = tmpdir.join("bad_sftp.py")
|
||||
helper.write(
|
||||
'#!/usr/bin/env python3\n'
|
||||
'import os, struct, sys\n'
|
||||
'def read_pkt():\n'
|
||||
' hdr = os.read(0, 4)\n'
|
||||
' if len(hdr) < 4: sys.exit(0)\n'
|
||||
' n = struct.unpack(">I", hdr)[0]\n'
|
||||
' while n:\n'
|
||||
' c = os.read(0, n)\n'
|
||||
' if not c: sys.exit(0)\n'
|
||||
' n -= len(c)\n'
|
||||
'read_pkt()\n'
|
||||
'os.write(1, struct.pack(">IBI", 5, 2, 3))\n' # SSH_FXP_VERSION v3
|
||||
'read_pkt()\n'
|
||||
'os.write(1, struct.pack(">IB", 0, 0))\n' # len=0 reply (5 bytes on wire)
|
||||
)
|
||||
helper.chmod(0o755)
|
||||
|
||||
mnt_dir = str(tmpdir.mkdir("mnt"))
|
||||
cmdline = base_cmdline + [
|
||||
pjoin(basename, "sshfs"),
|
||||
"-f",
|
||||
"dummy:/",
|
||||
mnt_dir,
|
||||
"-o", f"ssh_command={helper}",
|
||||
]
|
||||
res = subprocess.run(
|
||||
cmdline,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=10,
|
||||
text=True,
|
||||
)
|
||||
assert res.returncode != 0
|
||||
assert "bad reply len: 0" in res.stderr
|
||||
|
||||
Reference in New Issue
Block a user